分享完希望能知道
- Java Annotation Processing以及使用方式
- 如何debug类似Mapstruct编译期生成代码的框架
- Mapstruct的生成代码的过程
- Javapoet框架
Mapstruct介绍
官网介绍:
MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach. The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.
翻译一下,就是Mapstruct是一个通过配置的方式来极大的简化Java对象之间转换的代码生成器。生成的转换代码使用的普通的方法调用,所以性能高、类型安全并且转换代码可读性高。
Mapstruct和BeanUtils的区别
Mapstruct:编译期生成转换代码,后续转换对象时,相当于调用转换代码的普通方法
BeanUtils:在运行时通过反射,对字段进行赋值
优缺点
Mapstruct
优点:
- 性能高
- 编译后可以查看转换代码,方便排查问题
- 转换灵活
- 转换错误,在编译的时候就可以发现
缺点:
-
有一定的上手成本
-
需要定义convert类
BeanUtils
优点:
- 使用简单,不需要创建convert类,做一些配置等等
缺点:
- 转换逻辑不灵活,针对一些不同名的字段,转换不方便
- 转换代码盲盒,不知道转换的逻辑是什么样的
- 因为转换用的是反射,性能相比于Mapstruct是比较低的(因为jvm优化在这种场景下有可能无效,所以在对性能要求很高或者经常被调用的程序中,尽量不要使用。)
数据量 | Apache BeanUtils | Spring BeanUtils | Mapstruct |
---|---|---|---|
10 | 2ms | 1ms | 1ms |
100 | 5ms | 3ms | 1ms |
1k | 15ms | 6ms | 1ms |
1w | 30ms | 19ms | 2ms |
Mapstruct源码
Java Annotation Processing
是什么
用于在编译期扫描关心的注解,再通过对应的processor处理逻辑,可以在编译期改动/新增java源码文件或者生成一些别的元素,比如其他类型的文件、注释等等,像Lombok、Mapstruct、AutoService都是使用了这个技术。平时我们大部分用到的都是运行时注解,比如@Service、@Autowired等等。后面方便称呼,统一叫APT(Annotation Processing Tool)。
ps:AutoService是google的一个自动生成Spi实现类的工具,不需要在META-INF/services中做配置。
APT关键的组件介绍
AbstractProcessor
java
package com.example;
// @SupportedAnnotationTypes("org.mapstruct.Mapper")
// @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
// 实现自己的逻辑
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
// 标明这个处理器用来处理哪些注解,也可以通过注解来设置@SupportedAnnotationTypes("org.mapstruct.Mapper")
@Override
public Set<String> getSupportedAnnotationTypes() { }
// 支持的JDK版本,也可以通过注解来设置,@SupportedSourceVersion(SourceVersion.RELEASE_8)
@Override
public SourceVersion getSupportedSourceVersion() { }
}
如果自定义一个编译期注解,并且需要实现这个注解处理逻辑,那么需要新建一个Processor继承AbstractProcessor,这个AbstractProcessor用到了模板方法的设计模式。
Element
Element: 一种元素的抽象,包、类、接口、方法、静态方法块、方法参数等都可以叫做Element TypeElement: 类、接口或者是方法的参数 PackageElement: 包 ExecutableElement: 方法、方法块、静态方法块 VariableElement: 类的字段
TypeParameterElement: 对应类、接口、方法或构造函数的类型参数,也就是定义泛型时的E、T之类的泛型通配符
TypeMirror
用来表明一个元素的类型,是一个接口,Element.asType()返回的对象,TypeMirror下有很多子类,比如ExecutableType(方法)、DeclaredType(类/接口)等等
AnnotationMirror
用来表明一个注解,可以获取注解上的值等信息。
RoundEnvironment
APT会多轮进行处理插入式注解的处理,每一轮会生成一个新的RoundEnvironment对象,根据这个可以知道该注解对应的处理类是否都处理完成、获取注解对应的Element
ProcessingEnvironment
获取一些中间的工具类
Filer
通过这个接口,可以在编译期创建/获取相关资源,比如.java、.class、.properties等文件。
Messager
相当于日志打印,有ERROR、WARNING、NOTE等
用法示例
java
processingEnv.getMessager().printMessage(
Kind.NOTE, "MapStruct: referred types not available (yet), deferring mapper: "
+ mapperElement );
Mapstruct一些类介绍
MappingProcessor
继承了AbstractProcessor,是Mapstruct处理的入口
ModelElementProcessor
Mapstruct生成代码的核心处理器,MappingProcessor中使用ModelElementProcessor来组装Freemarker模板需要的元数据、生成文件等动作。
ModelElementProcessor使用策略模式+责任链模式+模板模式来处理逻辑 ModelElementProcessor使用SPI方式加载 具体运行时的顺序,按照优先级运行,值越小优先级越高
Processor名称 | 优先级 |
---|---|
MethodRetrievalProcessor | 1 |
MapperCreationProcessor | 1000 |
AnnotationBasedComponentModelProcessor | 1100 |
MapperRenderingProcessor | 9999 |
MapperServiceProcessor | 10000 |
为什么这个值是跳跃式的,猜测是为了在中间可以让用户自定义策略。 |
Mapper
用来表示生成Mapper的元数据,存储了生成Mapper需要的各种元数据,渲染模板需要从Mapper对象里去取。
怎么debug Mapstruct源码
- 配置debug信息 端口配置8000
- 进入项目根目录,执行mvnDebug compile
- 在MappingProcessor中的process方法处打断点
- debug模式运行即可进入断点处
Mapstruct处理流程
从0到1实现一个简易版的mapstruct
背景
通过@MyMapper、@MyMapping注解实现类似于Mapstruct的转换。
准备工作
了解JavaPoet
github: github.com/square/java...
JavaPoet is a Java API for generating .java source files. Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.
简单概括一下:用于生成Java源文件的工具包。
如果正常生成Java源码文件,会比较麻烦,因为需要你做各种字符串的拼接,但是用这个可以通过调用它的API来生成响应代码,可读性和易用性大大提升。
我们需要用他来给我们生成源码文件。
新建一个项目
annotation-processing:处理我们的注解逻辑 yard:引用annotation-processing模块
maven插件配置
java
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
ps: 注意-proc:none,只有加上这个配置才能够运行,不然会报错。
创建注解
java
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.TYPE })
public @interface MyMapper {
}
java
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD })
public @interface MyMapping {
String target();
String source();
}
实现AbstractProcessor
java
@SupportedAnnotationTypes("org.example.mymapstruct.MyMapper")
public class MyMapstructProcessor extends AbstractProcessor {
Messager messager;
ProcessingEnvironment processingEnvironment;
Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Messager messager = processingEnv.getMessager();
this.messager = messager;
this.processingEnvironment = processingEnv;
this.elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotatedElement : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, annotatedElement.getSimpleName().toString());
Set<? extends Element> annotatedMappers = roundEnv.getElementsAnnotatedWith(annotatedElement);
for (Element annotatedMapper : annotatedMappers) {
TypeElement mapperTypeElement = TypeElement.class.cast(annotatedMapper);
List<MappingMethod> mappingMethodList = listAllNeedConvertMethodElement(mapperTypeElement);
// 生成Mapper实现类
generateMapperImpl(mapperTypeElement, mappingMethodList);
}
}
return true;
}
private void generateMapperImpl(TypeElement mapperTypeElement, List<MappingMethod> mappingMethodList) {
PackageElement interfacePackageElement = elementUtils.getPackageOf(mapperTypeElement);
String interfacePackageName = interfacePackageElement.toString();
String interfaceName = mapperTypeElement.getSimpleName().toString();
String implClassName = mapperTypeElement.getSimpleName().toString() + "Impl";
if (mappingMethodList == null || mappingMethodList.size() == 0) {
return;
}
List<MethodSpec> methodSpecList = mappingMethodList.stream().map(mappingMethod -> {
int lastOutputParamDotIndex = mappingMethod.getOutputParameter().getParameterTypeName().lastIndexOf(".");
String outputParameterPackageName = mappingMethod.getOutputParameter().getParameterTypeName().substring(0, lastOutputParamDotIndex);
String outputParameterClassName = mappingMethod.getOutputParameter().getParameterTypeName().substring(lastOutputParamDotIndex+1);
ClassName returnClassName = ClassName.get(outputParameterPackageName, outputParameterClassName);
String inputParamPackageName = mappingMethod.getInputParameter().getParameterTypeName().substring(0, mappingMethod.getInputParameter().getParameterTypeName().lastIndexOf("."));
String inputParamClassName = mappingMethod.getInputParameter().getParameterTypeName().substring(mappingMethod.getInputParameter().getParameterTypeName().lastIndexOf(".") + 1);
ClassName inputClass = ClassName.get(inputParamPackageName, inputParamClassName);
StringBuilder setResult = new StringBuilder();
for (MappingMethod.MappingItem mappingItem : mappingMethod.getMappingItemList()) {
setResult.append(String.format("result.set%s(%s.get%s());\n", StringUtils.capitalize(mappingItem.getTarget()), mappingMethod.getInputParameter().getName(), StringUtils.capitalize(mappingItem.getSource())));
}
MethodSpec methodSpec = MethodSpec.methodBuilder(mappingMethod.getMethodName())
.addParameter(inputClass, mappingMethod.getInputParameter().getName())
.addModifiers(Modifier.PUBLIC)
.returns(returnClassName)
.beginControlFlow("if ($N == null)", mappingMethod.getInputParameter().getName())
.addStatement("return null")
.endControlFlow()
.addStatement("$T result = new $T();", returnClassName, returnClassName)
.addCode(setResult.toString())
.addStatement("return result")
.build();
return methodSpec;
}).collect(Collectors.toList());
ClassName interfaceClass = ClassName.get(interfacePackageName, interfaceName);
TypeSpec generatedClass = TypeSpec.classBuilder(implClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(interfaceClass)
.addMethods(methodSpecList)
.build();
JavaFile javaFile = JavaFile.builder(interfacePackageName, generatedClass)
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private List<MappingMethod> listAllNeedConvertMethodElement(TypeElement typeElement) {
List<MappingMethod> result = new ArrayList<>();
List<? extends Element> enclosedElements = typeElement.getEnclosedElements();
for (Element element : enclosedElements) {
if (!element.getKind().equals(ElementKind.METHOD)) {
continue;
}
ExecutableElement methodElement = ExecutableElement.class.cast(element);
List<? extends VariableElement> parameterElementList = methodElement.getParameters();
MyMapping[] annotationsByType = methodElement.getAnnotationsByType(MyMapping.class);
MappingMethod mappingMethod = new MappingMethod();
mappingMethod.setMethodName(methodElement.getSimpleName().toString());
Parameter inputParameter = new Parameter();
inputParameter.setName(parameterElementList.get(0).getSimpleName().toString());
inputParameter.setParameterTypeName(parameterElementList.get(0).asType().toString());
mappingMethod.setInputParameter(inputParameter);
Parameter outputParameter = new Parameter();
outputParameter.setName(parameterElementList.get(0).getSimpleName().toString());
outputParameter.setParameterTypeName(methodElement.getReturnType().toString());
mappingMethod.setOutputParameter(outputParameter);
if (annotationsByType.length == 0) {
mappingMethod.setNeedConvert(false);
} else {
List<MappingMethod.MappingItem> mappingItemList = convert2MappingItemList(annotationsByType);
mappingMethod.setMappingItemList(mappingItemList);
mappingMethod.setNeedConvert(true);
}
result.add(mappingMethod);
}
return result;
}
private List<MappingMethod.MappingItem> convert2MappingItemList(MyMapping[] annotationsByType) {
return Arrays.stream(annotationsByType).map(myMappingAnno -> {
MappingMethod.MappingItem mappingItem = new MappingMethod.MappingItem();
mappingItem.setTarget(myMappingAnno.target());
mappingItem.setSource(myMappingAnno.source());
return mappingItem;
}).collect(Collectors.toList());
}
}
SPI配置
有两种方式,第一种:META-INF/services方式,第二种:google的AutoService。
第一种:
在annotation-processing模块下
第二种:
先引入google AutoService jar包
java
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.1.1</version>
</dependency>
然后在对应的processor上,加上注解
打包
mvn clean package或者mvn clean installl
引入
yard模块引入annotation-processing jar包
总结
从Mapstruct的源码学习中我们能学到的是:
- 责任链+策略模式+模板方法的使用
- 生成模板类代码的处理方式
- SPI的使用
- 框架的生命周期的各个阶段都应该支持钩子函数
- APT中对Java相关元素的抽象(一个类的所有东西都可以抽象成Element)