💖比 BeanUtils 更快的实体映射工具 MapStruct 原理
💓 BeanUtils类
BeanUtils 是 org.springframework.beans 包下的工具类,将给定源bean的属性值复制到目标bean中,源类和目标类不必匹配,甚至不必派生只要属性匹配就可以进行复制。任何bean属性源bean公开,但目标bean不公开,将被默认忽略。
💓BeanUtils测试
测试代码
public static void main(String[] args) {
User user = new User();
UserDto userDto = new UserDto();
user.setNickName("zcct");
user.setUsername("测试");
BeanUtils.copyProperties(user,userDto);
System.out.println(userDto);
}
打印结果
💓BeanUtils原理解析
简单来说 BeanUtils 其实就是利用反射获取bean的所有属性,循环通过setter方法 向target设置属性,他的代码实现也比较简单,如下所示
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
// 获取 target 属性
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
// 循环设置属性
for (PropertyDescriptor targetPd : targetPds) {
// 获取setter方法
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
// 获取 soure 属性
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
// 获取get方法
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
// 反射设置属性
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
💓 mapstruct
MapStruct 是一个代码生成器,它基于约定而不是配置方法,极大地简化了 Java Bean 类型之间映射的实现。生成的映射代码使用纯方法调用,因此快速、类型安全且易于理解。
💓Bmapstruct测试
1. 导入 mapstruct 依赖
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
2. 增加 mapper 映射器接口
注意这里的 @Mapper 注解 是 org.mapstruct包下的注解列
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserCopeMapper {
UserCopeMapper INSTANCE = Mappers.getMapper( UserCopeMapper.class );
@Mapping(source = "username", target = "username")
@Mapping(source = "nickName", target = "nickName")
UserDto userToUserDto(User user);
}
3. 测试使用
User user = new User();
user.setNickName("zcct");
user.setUsername("测试");
UserDto userDto = UserCopeMapper.INSTANCE.userToUserDto(user);
System.out.println(userDto);
}
打印结果
💓Mapstruct原理
在debug 中可以看到 这里获取的实例时 UserCpoerMapper 的实现类
这个实例是通过 Mappers.getMapper( UserCopeMapper.class ); 获取的
1. Mappers.getMapper方法
在这个方法中 获取一个 Mapper实例
首先获取所有类加载器,遍历类加载器通过doGetMapper生成 mapper对象
private static <T> T getMapper(Class<T> mapperType, Iterable<ClassLoader> classLoaders) throws ClassNotFoundException, NoSuchMethodException {
Iterator var2 = classLoaders.iterator();
Object mapper;
do {
if (!var2.hasNext()) {
throw new ClassNotFoundException("Cannot find implementation for " + mapperType.getName());
}
ClassLoader classLoader = (ClassLoader)var2.next();
mapper = doGetMapper(mapperType, classLoader);
} while(mapper == null);
return mapper;
}
doGetMapper
doGetMapper 加载一个 名字为 mapperName + “Impl” 的类 , 从这里并不能看出 impl 字类时怎样生成的,
private static <T> T doGetMapper(Class<T> clazz, ClassLoader classLoader) throws NoSuchMethodException {
try {
@SuppressWarnings( "unchecked" )
Class<T> implementation = (Class<T>) classLoader.loadClass( clazz.getName() + IMPLEMENTATION_SUFFIX );
Constructor<T> constructor = implementation.getDeclaredConstructor();
constructor.setAccessible( true );
return constructor.newInstance();
}
catch (ClassNotFoundException e) {
return getMapperFromServiceLoader( clazz, classLoader );
}
catch ( InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException( e );
}
}
Mapstruct 生成 Impl 过程
我们打开项目的 target 目录 可以看到这样的一个文件
public class UserCopeMapperImpl implements UserCopeMapper {
@Override
public UserDto userToUserDto(User user) {
if ( user == null ) {
return null;
}
UserDto userDto = new UserDto();
userDto.setNickName( user.getNickName() );
userDto.setUsername( user.getUsername() );
return userDto;
}
其实在编译阶段就生成了 mapper 子类 ,其原理在于JSR-269规范, lombok 也是基于此实现的
JDK 1.6中实现了JSR-269规范JSR-269:Pluggable Annotations Processing API(插入式注解处理API)。提供了一组插入式注解处理器的标准API在编译期间对注解进行处理。我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round,也就是第一张图中的回环过程。 有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。只要有足够的创意,程序员可以使用插入式注解处理器来实现许多原本只能在编码中完成的事情
在 org.mapstruct.ap; 包下有一个 MappingProcessor 类
@SupportedAnnotationTypes("org.mapstruct.Mapper")
@SupportedOptions({
MappingProcessor.SUPPRESS_GENERATOR_TIMESTAMP,
MappingProcessor.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT,
MappingProcessor.UNMAPPED_TARGET_POLICY,
MappingProcessor.DEFAULT_COMPONENT_MODEL
})
public class MappingProcessor extends AbstractProcessor {
}
AbstractProcessor 相关方法:
- init() 方法,会被注解处理工具调用
- process() 方法,这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
- getSupportedSourceVersion() 方法,指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6。
- getSupportedAnnotationTypes() 方法,这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称,即注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
MappingProcessor类
一个 JSR 269 注释Processor ,它生成映射器接口(使用@Mapper注释的接口)的实现。
实施说明:
生成是通过增量构建每个要生成的映射器(一个Mapper对象)的模型表示来进行的,然后将其写入生成的 Java 源文件。
通过应用一系列ModelElementProcessor ,模型实例化和处理发生在几个阶段/通道中。使用 Java 服务加载器机制检索要应用的处理器,并按照它们的priority顺序进行处理。一般的处理流程是这样的:
- 检索映射方法
- 创建Mapper模型
- 执行丰富和修改(例如为依赖注入添加注释)
- 如果没有发生错误,则将模型写入 Java 源文件
为了读取注释属性,使用在Hickory 工具的帮助下生成的棱镜。这些棱镜允许轻松访问注释及其属性,而不依赖于它们的类对象。
Java 源文件的创建是使用FreeMarker 模板引擎完成的。映射器模型的每个节点都有一个相应的 FreeMarker 模板文件,该文件提供该元素的 Java 表示,并且可以通过自定义 FreeMarker 指令包含子元素。这样写出模型的根节点( Mapper )将递归地包括所有包含的子元素(例如它的方法,它们的属性映射等)