APT实战-MapStruct增强

500 阅读2分钟

我正在参加「掘金·启航计划」

APT介绍

在JDK6的时候引入了JSR269的标准,即 Pluggable Annotation Processing API (可插入注解处理API),简称APT。它提供了在编译时期改变一些行为的相关API,例如生成一些新的JAVA文件。

大致处理流程如下图,APT在编译成class文件时执行,在这个过程中可以自定义相关行为,例如生成新的源文件(MapStruct),修改AST语法树(Lombok),直到所有编译处理器都没有在对JAVA文件进行修改为止,才会走到后续的生成Class文件的流程。

img

APT实战

前言

在稍微了解一些APT的原理后,我们来尝试写一个基于MapStruct增强Demo,如果不了解MapStruct是啥的,可以去看看相关的文章

在MapStruct框架中实现对象之间的映射,总要手动创建一个Mapper类型,相对于我们常用的基于反射的BeanUtils来说,就还是不够简单。那么我们能不能自动创建一个Mapper类型呢?我们下面就使用APT来尝试一下把`

创建processor项目

环境版本
JDK11
MapStruct1.5.2
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>pers.orange</groupId>
    <artifactId>processor</artifactId>
    <version>1.0-SNAPSHOT</version>
​
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
    </properties>
​
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <!--Disable annotation processing for ourselves-->
                    <compilerArgument>-proc:none</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
目录结构
src
│  └─main
│      ├─java
│      │  └─pers
│      │      └─orange
│      │              BeanUtils.java
│      │              CommonMapper.java
│      │              MappingProcessor.java
│      │              To.java
│      │
│      └─resources
│          └─META-INF
│              └─services
│                      javax.annotation.processing.Processor
创建注解
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.TYPE )
public @interface To {
​
    Class target(); // 所要转换的类
}
创建通用Mapper模板
public interface CommonMapper<S,T> {
​
    T to(S source); 
​
    void to(S source, @MappingTarget T target);
}
​
创建核心Processor类
@SupportedAnnotationTypes("pers.orange.To") // 对被@To注解的类进行扫描
@SupportedSourceVersion( SourceVersion.RELEASE_11 ) // 支持的JDK版本
// 继承AbstractProcessor类,实现process方法
public class MappingProcessor extends AbstractProcessor {
    private Messager messager; // 编译输出日志
    private Filer filer; // 文件输出
​
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init( processingEnv );
        this.messager = processingEnv.getMessager();
        this.filer = processingEnv.getFiler();
    }
​
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith( To.class ); // 获得被@To注解修饰的元素集合
        for ( Element element : elements ) {
            messager.printMessage( Diagnostic.Kind.NOTE, element.getSimpleName().toString());
            To annotation = element.getAnnotation( To.class );
            TypeMirror sourceTypeMirror = element.asType(); // 获得源对象类型
            TypeMirror targetTypeMirror = getTargetTypeMirror( annotation ); // 获得目标对象类型
            createMapperFile( sourceTypeMirror, targetTypeMirror );
        }
        return false;
    }
​
    private TypeMirror getTargetTypeMirror(To annotation){
        TypeMirror typeMirror = null;
        try {
            annotation.target();
        }
        catch ( MirroredTypeException e ) {
            typeMirror = e.getTypeMirror();
        }
        return typeMirror;
    }
​
    private void createMapperFile(TypeMirror sourceTypeMirror,TypeMirror targetTypeMirror){
        String sourceName = getSimpleName(  sourceTypeMirror );
        String targetName = getSimpleName(targetTypeMirror );
        String mapperFileName = sourceName+"To"+targetName+"Mapper";
        String packageName = getPackageName( sourceTypeMirror );
        // 使用JavaPoet框架,简化Java文件的生成,也可以直接使用字符串拼接来实现
        TypeSpec typeSpec = TypeSpec.interfaceBuilder( mapperFileName )
            .addModifiers( Modifier.PUBLIC )
            .addAnnotation( Mapper.class )
            .addSuperinterface( ParameterizedTypeName.get( ClassName.get( CommonMapper.class ),ClassName.get( sourceTypeMirror ),ClassName.get( targetTypeMirror ) ))
            .build();
        JavaFile javaFile = JavaFile.builder( packageName, typeSpec)
            .build();
        try {
            javaFile.writeTo( filer );
        }
        catch ( IOException e ) {
            throw new RuntimeException( e );
        }
    }
​
    private String getSimpleName(TypeMirror typeMirror){
        String mirrorName = typeMirror.toString();
        return mirrorName.substring( mirrorName.lastIndexOf( "." )+1 );
    }
​
    private String getPackageName(TypeMirror typeMirror){
        String mirrorName = typeMirror.toString();
        return mirrorName.substring( 0,mirrorName.lastIndexOf( "." ) );
    }
}
创建BeanUtils工具类
public class BeanUtils {
​
    private static final Map<String,CommonMapper> commonMapperMap = new ConcurrentHashMap<>();
​
    private static final ClassLoader CLASS_LOADER = BeanUtils.class.getClassLoader();
​
    /**
     * 根据source对象更新target对象
     * @param source
     * @param target
     */
    public static void copyProperties(Object source, Object target){
        String mapperClassName = getMapperClassName( source.getClass(), target.getClass() );
        CommonMapper mapper = getMapper( mapperClassName );
        mapper.to( source, target );
    }
​
    /**
     * 创建T对象并赋值
     * @param source
     * @param targetClazz
     * @return
     * @param <T>
     */
    public static <T> T copProperties(Object source,Class<T> targetClazz){
        String mapperClassName = getMapperClassName( source.getClass(), targetClazz );
        CommonMapper mapper = getMapper( mapperClassName );
        return (T) mapper.to( source );
    }
​
    private static String getMapperClassName(Class source,Class target){
        String sourceName = source.getSimpleName();
        String targetName = target.getSimpleName();
        String packageName = source.getPackageName();
        String mapperClassName = packageName+"."+sourceName+"To"+targetName+"Mapper";
        return mapperClassName;
    }
​
    /**
     * 根据全限定类名获得对应的Mapper对象
     * @param mapperClassName
     * @return
     */
    private static CommonMapper getMapper(String mapperClassName){
        // 使用map缓存Mapper对象避免重复加载判断
        CommonMapper commonMapper = commonMapperMap.computeIfAbsent( mapperClassName, (className) -> {
            try {
                Class<?> mapperClass = CLASS_LOADER.loadClass( mapperClassName );
                CommonMapper mapper = (CommonMapper) Mappers.getMapper( mapperClass );
                return mapper;
            }
            catch ( ClassNotFoundException e ) {
                throw new RuntimeException( e );
            }
        } );
        if ( commonMapper == null ){
            throw new RuntimeException(mapperClassName+"不存在");
        }
        return commonMapper;
    }
}

processor项目的代码就开发完成啦,最后在javax.annotation.processing.Processor文件里指定一下类名就好啦

// javax.annotation.processing.Processor
pers.orange.MappingProcessor
​

测试一下

在新建一个新的test项目,并引入刚刚的processor项目和MapStruct项目,同时指定注解处理器

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>pers.orange</groupId>
    <artifactId>test</artifactId>
    <version>1.0-SNAPSHOT</version>
​
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
    </properties>
​
    <dependencies>
        <dependency>
            <groupId>pers.orange</groupId>
            <artifactId>processor</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <annotationProcessorPaths>
                        // 需要注意处理器的加载顺序,需要processor项目先
                        <path>
                            <groupId>pers.orange</groupId>
                            <artifactId>processor</artifactId>
                            <version>1.0-SNAPSHOT</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build></project>
    @Test
    public void test01(){
        Source source = new Source();
        source.setId( 1 );
        source.setName( "source" );
        Target target = new Target();
        BeanUtils.copyProperties( source, target );
        System.out.println(target);
        Target target1 = BeanUtils.copProperties( source, Target.class );
        System.out.println(target1);
    }
​
// 输出结果
Target{id=1, name='source'}
Target{id=1, name='source'}

可以看到对象映射成功~,同时在idea的target下也能很清楚的看到所生成的Java文件。

image-20220923143821299

image.png

总结

通过这篇水文,相信大家也能看出编译期注解处理器APT的魅力,通过它我们可以在编译期自定义生成各式各样的代码。在Java开发中总是存在着很多重复的代码,例如CRUD等,我们可以尝试使用APT来自动生成简化我们的开发~。如果想要加深对APT的使用,可以去看看MapStruct的源码。

参考资料