Bean拷贝组件(注解驱动)方案设计与落地
一、背景
数据流转在各层之间的过程,应当是改头换面的,字段属性数量,属性名称(一般不变,但也有重构时出现变化的情况),类型名称(普遍变化例如BO、VO、DTO)。对于转换的业务对象,原始的做法时直接实例采用Getter与Setter方法进行逐一填充。这太低效了,那我们就先了解最简单的拷贝工具。
二、问题
业界采用BeanCopyUtils、Orika、ReflectionUtils等填充工具类实现字段的拷贝。默认的实现都是以Field.getName()的值进行比对拷贝。所以针对属性名发生变化的情况很容易在不注意的情况下拷贝成null值。一旦拷贝成null值,后续的业务就会受到不同程度的影响,所以我设想以下两种方案,解决字段变化,且字段耦合面比较广泛,无法直接修改字段名称的情况。
三、方案
方案一:二次封装Orkia组件,设计classMap字段映射配置类,使用ServiceLoader服务加载器加载配置类,自定义配置,随用随配。(缺点:需要维护Java类配置变化字段的映射,变化越多,类越重)
方案二:设计类型注解与字段注解,使用spring ApplicationContextAware接口设计统一快速注册classMap中的字段映射(性能高,快速装配)。
接下来的两种方案都有一些思路以及遇到的问题及其解决方法,加深相关技术理解。
四、实现
(1)方案一实现
核心工具类BeanCopyUtil.java
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.impl.DefaultMapperFactory;
import java.util.List;
import java.util.ServiceLoader;/*** @author : forestSpringH* @description:* @date : Created in 2023/9/14* @modified By:* @project: */
public class BeanCopyUtil {private static final MapperFactory MAPPER_FACTORY;private static final MapperFacade MAPPER_FACADE;static {MAPPER_FACTORY = new DefaultMapperFactory.Builder().build();MAPPER_FACADE = MAPPER_FACTORY.getMapperFacade();ServiceLoader<CopyInterface> serviceLoader = ServiceLoader.load(CopyInterface.class);for (CopyInterface beanCopyRules : serviceLoader) {beanCopyRules.register(MAPPER_FACTORY);}}public static <S, T> T map(S source, Class<T> targetClass) {return MAPPER_FACADE.map(source, targetClass);}public static <S, T> List<T> mapAsList(Iterable<S> source, Class<T> targetClass) {return MAPPER_FACADE.mapAsList(source, targetClass);}}
接口CopyInterface.java
import ma.glasnost.orika.MapperFactory;/*** @author : forestSpringH* @description:* @date : Created in 2023/9/14* @modified By:* @project: */
public interface CopyInterface {void register(MapperFactory mapperFactory);
}
变化字段配置类BeanCopyRules.java
import com.runjing.tms.domain.dto.applet.RiderWaybillsDistributionDetailsDto;
import com.runjing.tms.repository.model.TransportExpressWaybills;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ma.glasnost.orika.MapperFactory;/*** @author : forestSpringH* @description:* @date : Created in 2023/9/14* @modified By:* @project: */
@Slf4j
public class BeanCopyRules implements CopyInterface{@Overridepublic void register(MapperFactory mapperFactory) {log.info("加载字段映射工厂自定义字段映射");mapperFactory.classMap(TransportExpressWaybills.class, RiderWaybillsDistributionDetailsDto.class).field("expectTime","expectStartTime").field("id","waybillId").byDefault().register();}
}
注意点:
ServiceLoader服务加载器需要查找META-INF.services下的文件,加载对应的类路劲,所以如果文件中填写的也是接口CopyInterface.java的路径而不是其实现类BeanCopyRules.java的路径,就会加载出来ServiceLoader<CopyInterface>内部的实例为空,无法进入循环。
META-INF.service下的com.runjing.tms.util.orika.CopyInterface文件
com.runjing.tms.util.orika.BeanCopyRules
(2)方案二实现
代码分包结构:
类型注解EnableOpenFieldCopy.java
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;import java.lang.annotation.*;/*** @author : forestSpringH* @description:* @date : Created in 2023/9/14* @modified By:* @project: */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public @interface EnableOpenFieldCopy {boolean value() default true;boolean callSuper() default false;boolean callSoon() default false;
}
字段注解FieldCopyMapping.java
import java.lang.annotation.*;/*** @author : forestSpringH* @description: 字段映射注解* @date : Created in 2023/9/14* @modified By:* @project:*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface FieldCopyMapping {String targetFieldName() default "";Class<?>[] targetClass() default {};
}
SpringHolder.java关键代码段
public static List<Class<?>> getBeanByAnnotation(Class<? extends Annotation> annotationClazz){Assert.notNull(serviceApplicationContext, "容器上下文获取失败");Assert.notNull(annotationClazz,"注解字节码入参为空");List<String> collect = Arrays.stream(serviceApplicationContext.getBeanNamesForAnnotation(annotationClazz)).collect(Collectors.toList());List<Class<?>> classList = new LinkedList<>();if (!CollectionUtils.isEmpty(collect)){collect.forEach(s -> classList.add(getBeanByName(s).getClass()));}return classList;}
BeanCopyService.java核心代码段
@PostConstructpublic void init() {log.info("初始化BeanCopyService组件");mapperFactory = new DefaultMapperFactory.Builder().build();mapperFacade = mapperFactory.getMapperFacade();log.info("加载字段拷贝映射注解类");List<Class<?>> beanList = SpringHolder.getBeanByAnnotation(EnableOpenFieldCopy.class);register(beanList);}public <S, T> T copyBean(S source, Class<T> targetClass) {return mapperFacade.map(source, targetClass);}private void register(List<Class<?>> beanCopyList) {if (!CollectionUtils.isEmpty(beanCopyList)) {beanCopyList.forEach(clazz -> {//获取类的属性log.info("获取映射注解类:{}下字段集合", clazz.getName());List<Field> collect = Arrays.stream(clazz.getDeclaredFields()).collect(Collectors.toList());if (!CollectionUtils.isEmpty(collect)) {collect.forEach(field -> {//获取属性中打上映射注解的注解if (field.isAnnotationPresent(FieldCopyMapping.class)) {FieldCopyMapping annotation = field.getAnnotation(FieldCopyMapping.class);String sourceFieldName = field.getName();//获取注解上的目标字段名String targetFieldName = annotation.targetFieldName();log.info("配置字段:{} 映射 {}", sourceFieldName, targetFieldName);//获取注解上的目标拷贝对象字节码数组List<Class<?>> targetClazzList = Arrays.stream(annotation.targetClass()).collect(Collectors.toList());if (!CollectionUtils.isEmpty(targetClazzList)) {//逐一注册log.info("逐一注册字段映射模型列表");targetClazzList.forEach(targetClazz -> {MapperModel model = new MapperModel(clazz.getName() + targetClazz.getName(), clazz, targetClazz, sourceFieldName, targetFieldName);mapperModelList.add(model);});}}});}});Map<String, List<MapperModel>> group = groupByMapperKey(mapperModelList);if (!CollectionUtils.isEmpty(group)) {group.values().forEach(modelList -> {log.info("开始映射:{}", modelList);ClassMapBuilder<?, ?> classMapBuilder = mapperFactory.classMap(modelList.get(0).getSourceClass(), modelList.get(0).getTargetClass());for (MapperModel model : modelList) {if (Objects.equals(modelList.get(modelList.size() - 1), model)) {log.info("映射注册完毕:{}", model.getMapperKey());classMapBuilder.field(model.getSourceFieldName(), model.getTargetFieldName()).byDefault().register();} else {classMapBuilder.field(model.getSourceFieldName(), model.getTargetFieldName());}}});}}}private Map<String, List<MapperModel>> groupByMapperKey(List<MapperModel> modelList) {Map<String, List<MapperModel>> groupMap = new HashMap<>();if (CollectionUtils.isEmpty(modelList)) {return groupMap;}Set<String> keys = modelList.stream().map(MapperModel::getMapperKey).collect(Collectors.toSet());keys.forEach(key -> {List<MapperModel> mapperModels = new LinkedList<>();modelList.forEach(mapperModel -> {if (Objects.equals(mapperModel.getMapperKey(), key)) {mapperModels.add(mapperModel);}});groupMap.put(key, mapperModels);});return groupMap;}
五、测试
Person.java测试实体
@EnableOpenFieldCopy
@Data
public class Person {@FieldCopyMapping(targetFieldName = "id", targetClass = {PersonBo.class, PersonDto.class})private int age;@FieldCopyMapping(targetFieldName = "personName",targetClass = {PersonDto.class})private String name;
}
PersonBo.java测试实体
@Data
public class PersonBo {private int id;private String name;
}
PersonDto.java测试实体
@Data
public class PersonDto {private int id;private String personName;
}
单元测试代码
@Testpublic void copy(){Person person = new Person();person.setAge(1);person.setName("hlc");PersonBo personBo = beanCopyService.copyBean(person, PersonBo.class);PersonDto personDto = beanCopyService.copyBean(person, PersonDto.class);System.out.println(personBo);System.out.println(personDto);}
断点查看结果
代码逻辑还需要继续优化,方案二跑通之后将会将其设计成jar包。
导入使用。
相关文章:

Bean拷贝组件(注解驱动)方案设计与落地
一、背景 数据流转在各层之间的过程,应当是改头换面的,字段属性数量,属性名称(一般不变,但也有重构时出现变化的情况),类型名称(普遍变化例如BO、VO、DTO)。对于转换的业…...

hive的建表语句
hive建表语句CREATE TABLE ccwn_zh_event_push (customerid string,cardnumber string,accountnumber string,eventcode string,eventtime string,activities string,activityRefuseCode string,lables string)PARTITIONED BY(dt string)ROW FORMAT SERDE org.apache.hadoop.hi…...

提升效率:PostgreSQL准确且快速的数据对比方法
作为一款强大而广受欢迎的开源关系型数据库管理系统,PostgreSQL 在数据库领域拥有显著的市场份额。其出色的可扩展性、稳定性使其成为众多企业和项目的首选数据库。而在很多场景下(开发|生产环境同步、备份恢复验证、数据迁移、数据合并等)&a…...

【轻NAS】Windows搭建可道云私有云盘,并内网穿透公网访问
文章目录 1.前言2. Kodcloud网站搭建2.1. Kodcloud下载和安装2.2 Kodcloud网页测试 3. cpolar内网穿透的安装和注册4. 本地网页发布4.1 Cpolar云端设置4.2 Cpolar本地设置 5. 公网访问测试6.结语 1.前言 云存储作为近些年兴起的概念,成功吸引了各大互联网厂商下场&…...

计算机网络 第一章:概述
目录 一.因特网概述 1.1网络、互联网(互连网)和因特网 1.2internet与Internet的区别 1.3因特网服务提供者ISP(Internet Service Provider) 1.4因特网组成 二.三种交换方式 2.1电路交换 2.2分组交换(重点) 2.3报文交换 三.计算机网络的定义和分类 四.计算机网络的性能…...
centos7 firewalld ip转发设置、安装docker-compose出现错误、docker-compose部署Yapi
一 centos7 firewalld ip转发设置 #!/bin/bash #开启系统路由模式功能 vim /etc/sysctl.conf #添加下面一行 net.ipv4.ip_forward1 #运行这个命令会输出上面添加的那一行信息,意思是使内核修改生效 sysctl -p #开启firewalld systemctl start firewalld #防火墙开启…...

Cglib代理和JDK代理原理的区别
一、JDK Jdk动态代理,拿到目标类所继承的接口,生成代理类,并且代理类也会实现和目标类一样的接口。 二、Cglib Cglib代理功能更强,无论目标类是否实现接口都可以代理,他是基于继承的方式类代理目标类,如果…...

论文阅读-A General Language for Modeling Social Media Account Behavior
论文链接:https://arxiv.org/pdf/2211.00639v1.pdf 目录 摘要 1 Introduction 2 Related work 2.1 Automation 2.2 Coordination 3 Behavioral Language for Online Classification 3.1 BLOC alphabets 3.1.1 Action alphabet 3.1.2 Content alphabets 3.…...

Python中的异常处理4-3
在《Python中的异常处理4-2》中提到,except语句后面可以加上具体的异常类型。有时我们需要这个异常的其他细节,此时可以使用except...as语句。 1 except...as语句 except..as语句的格式为 except 异常类型 as 异常实例名 从以上格式中可以看到&#…...

Swift学习内容精选(一)
Swift 可选(Optionals)类型 Swift 的可选(Optional)类型,用于处理值缺失的情况。可选表示"那儿有一个值,并且它等于 x "或者"那儿没有值"。 Swfit语言定义后缀?作为命名类型Optional的简写&…...

Marin说PCB之封装设计系列---(02)--异形焊盘的封装设计总结
每天下班回家看电视本来是一件很美好的事情,可是正当我磕着瓜子看着异人之下的时候,手机突然响起来了,我以为是我们组哪个同事找我呢。一接电话居然是我的老朋友陈世美陈总,江湖人称少妇杀手。给我打电话主要是说他最近遇到一个异…...

SpringBoot使用AOP详解
目录 1 AOP是什么2 AOP概念3 Springboot中使用AOP4 AOP原理5 应用场景 1 AOP是什么 AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续&…...
【Qt】QGroundControl入门1:介绍
1、简介 1.1 QGroundControl QGroundControl是一款开源的无人机地面控制站软件,依赖Qt库,简称QGC。 QGroundControl为任何支持 MAVLink协议 的无人机提供完整的飞行控制和任务规划。QGroundControl为 PX4 和 ArduPilot 驱动的无人机提供驱动配置。 源码:https://github.co…...

第36章_瑞萨MCU零基础入门系列教程之步进电机控制实验
本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id728461040949 配套资料获取:https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总: ht…...
198.打家劫舍,213.打家劫舍II,337.打家劫舍III
代码随想录训练营第48天|198.打家劫舍,213.打家劫舍II,337.打家劫舍III 198.打家劫舍文章思路代码 213.打家劫舍III文章思路代码 337.打家劫舍III文章思路代码 总结 198.打家劫舍 文章 代码随想录|0198.打家劫舍 思路 d p [ i ] M a x ( d p [ i − …...

msvcp140.dll是什么东西,如何解决msvcp140.dll丢失的问题的方法分享
在现代生活中,电脑已经成为我们工作、学习和娱乐的重要工具。然而,电脑问题的出现往往会给我们的生活带来不便。其中,"msvcp140.dll丢失"是一个常见的电脑问题。本文将详细介绍这个问题的原因和解决方法,帮助大家更好地…...
音视频 SDL vs2017配置
一、首先我把SDL放在了C盘根目录下 二、新建空项目 三、添加main.cpp //main.cpp #include<iostream> #include <SDL.h>int main(int argc, char* argv[]) // main函数头必须这样写,因为SDL把main定义成了宏 {SDL_Delay(3000); // 让窗口在屏幕上保持…...

前端面试要点
0914 JScript深拷贝和浅拷贝(js解构赋值算哪个?) 深拷贝和浅拷贝 回流和重绘 回流和重绘 webpack打包流程 Webpack打包 虚拟DOM 虚拟DOM git合并分支 git合并分支 CSS盒子模型 CSS盒子模型 0911 WebPack分包 webpack分包 ts泛型 ts泛型 优化…...
shell字符串处理之字符串比较
引言 我们在使用shell编写脚本时,经常需要对字符串进行处理,如字符串大小比较、模式匹配、替换、截断等。本文将梳理字符串比较中常见的用法。 字符串比较 1. 直接比较字符串 a$1 b$2 c"" # 等于 if [ $a "abc" ];thenecho $a …...

怎么获取别人店铺的商品呢?
jd.item_search_shop(获得店铺的所有商品) 为了进行电商平台 的API开发,首先我们需要做下面几件事情。 1)开发者注册一个账号 2)然后为每个JD应用注册一个应用程序键(App Key) 。 3)下载JDAPI的SDK并掌握基本的API…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...

Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...
第7篇:中间件全链路监控与 SQL 性能分析实践
7.1 章节导读 在构建数据库中间件的过程中,可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中,必须做到: 🔍 追踪每一条 SQL 的生命周期(从入口到数据库执行)&#…...
中国政务数据安全建设细化及市场需求分析
(基于新《政务数据共享条例》及相关法规) 一、引言 近年来,中国政府高度重视数字政府建设和数据要素市场化配置改革。《政务数据共享条例》(以下简称“《共享条例》”)的发布,与《中华人民共和国数据安全法》(以下简称“《数据安全法》”)、《中华人民共和国个人信息…...

篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...