查看原文
其他

技术分享 | APT结合JavaPoet生成模板化Java源代码文件

朱同学 明源云天际PaaS平台 2022-09-25



源宝导读:APT工具包作为一个代码打桩工具能消除我们项目中大量的样板化代码, 一定程度的减轻开发人员的工作量, 并且规范了代码结构, 能有效解决企业级项目代码复杂化的问题, 接下来我们看看如何从零开始构建一个代码打桩框架。



一、背景介绍  

我讨厌写一些业务代码, 不仅仅因为它们的原始意图并非我设计, 成功了是产品的功劳, 失败就要代码背锅。还有一个更重要的原因, 就是重复性的代码太多了, 一个复杂的业务逻辑要找到它的Bug, 也得下一番“苦力”。这里说的真的是苦力, 而不是脑力, 说明了大部分是低劣的重复劳动, 那有什么好的途径能减少这些重复性劳动呢?


二、大名鼎鼎的Lombok了解一下?

  Lombok是一款Java代码功能增强库, 在Github上已有9.8k+Star。它会自动集成到你的编辑器和构建工具中, 从而使你的Java代码更加生动有趣。通过Lombok的注解, 你可以不用再写getter、setter、equals等方法, 只需要加一个@Data注解, Lombok将会在代码编译时为你自动生成, 当然Lombok不仅仅只有一个Data注解, 还有一些其他强大的功能。

  • Builder: 基于建造者模式来创建对象,建造者模式加链式调用,能大大简化多属性对象创建方式;

  • NoArgsConstructor/AllArgsConstructor: 为Class生成无参构造器或者全参数构造器;

  • Cleanup: 当我们在Java中使用资源时,不可避免地需要在使用后关闭资源。使用@Cleanup注解可以自动生成显式关闭资源的代码块;

  • val: 能在Java这个强类型语言中也使用弱类型语言的变量定义方式, 其实本质是通过java类型推断来在编译期替换为实际类型;

  • NonNull: 在方法上使用@NonNull注解可以做非空判断,如果传入空值参数的话会直接抛出NullPointerException。

  • ... ...

还有很多很多其他的的功能我就不一一累述了, 其实我在这里拿Lombok来说只是因为它更加的耳熟能详, 市面上还有许许多多的同类框架, 比如对象赋值转换框架Map struct、事件总线EventBus、依赖注入Dagger2以及阿里的ARouter路由框架等等都是依赖于动态生成模板代码的功能特性。


三、要不咱也来个'仿制品'?

  既然有如此多的利器能解决代码重复冗余的问题, 我们何不效仿着也整一个出来试试? 刚好最近在使用数据持久化框架JPA时, 发现JPA仅默认支持了常用的基础类型对应数据库的Column类型的映射关联, 如Integer、Double、Long、BigDecimal、Float和String等基础类型。
  然而处理自定义的对象类型和集合类型时需要扩展AttributeConverter接口来做数据库类型和Java类型的转换映射。而且每个自定义对象类型都需要我们写一套Converter扩展实现, 为了解决这些重复性的繁琐劳动, 接下来我们要做的就是, 创造一个"自动代码生产器"来帮我们完成这些Converter转换类的编写。


四、先准备一下干事的家伙们

4.1 JDK 编译时注解处理器 Annotation Processing Tool

  编译时注解处理器Annotation Processing Tool 简称APT, 是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。简单讲, 在源代码编译阶段, 通过APT我们可以获取源文件内注解(Annotation)相关内容, 然后进行一些额外的扩展。
  由于注解处理器可以在程序编译阶段工作, 所以我们可以在编译期间通过注解处理器进行我们需要的操作。比较常用的用法就是在编译期间获取相关注解数据, 然后动态生成.java源文件, 通常是自动产生一些有规律性的重复代码, 解决了手工编写重复代码的问题, 大大提升编码效率。


4.2 JavaPoet 源代码生成器

  JavaPoet 是由Square推出的开源Java代码生成框架, 能够提供Java生成源代码文件文件的能力, 通过这种自动化生成代码的方式, 可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。

JavaPoet框架中核心类说明

Class文件

功能描述

JavaFile

用于构造输出包含一个顶级类的Java文件, 是对.java文件的抽象定义

TypeSpec

TypeSpec是类/接口/枚举的抽象类型

MethodSpec

MethodSpec是方法/构造函数的抽象定义

FieldSpec

FieldSpec是成员变量/字段的抽象定义

ParameterSpec

ParameterSpec用于创建方法参数

AnnotationSpec

AnnotationSpec用于创建标记注解


五、Jpa Conversion Tool 的诞生

step 1. 首先我们一起来看看AttributeConverter的源码结构
public interface AttributeConverter<X,Y> {
/** * Converts the value stored in the entity attribute into the * data representation to be stored in the database. * * @param attribute the entity attribute value to be converted * @return the converted data to be stored in the database * column */ public Y convertToDatabaseColumn (X attribute);
/** * Converts the data stored in the database column into the * value to be stored in the entity attribute. * Note that it is the responsibility of the converter writer to * specify the correct <code>dbData</code> type for the corresponding * column for use by the JDBC driver: i.e., persistence providers are * not expected to do such type conversion. * * @param dbData the data from the database column to be * converted * @return the converted value to be stored in the entity * attribute */ public X convertToEntityAttribute (Y dbData);}

  

AttributeConverter本身是一个抽象接口, 里面的泛型类型X为实体类的字段类型, 而Y则是数据库的Column类型, 在这个接口里面仅仅定义了两个方式需要子类去继承实现。JPA在执行数据库持久化时会根据Entity中的变量类型去匹配查找出对应的Converter, 然后执行convertToDatabaseColumn方法把成员变量转换为数据库支持类型再进行存储操作, 而在读取数据库数据时又会调用convertToEntityAttribute来把数据库中保存的数据映射转换为Entity中的变量类型并进行反射赋值处理。这样就建立起了Entity与Database之间的数据关联。

step 2. 构造通用的Converter基类来统一处理类型转换功能
public abstract class AbstractJsonConverter<T> implements AttributeConverter<T, String> {
protected static ObjectMapper objectMapper;
@Autowired public void setObjectMapper(ObjectMapper objectMapper) { AbstractJsonConverter.objectMapper = objectMapper; }
@Override public String convertToDatabaseColumn(T attribute) { objectMapper.setConfig(objectMapper.getSerializationConfig().without(SerializationFeature.FAIL_ON_EMPTY_BEANS)); ObjectWriter writer = objectMapper.writerFor(getJsonType()); try { return writer.writeValueAsString(attribute); } catch (JsonProcessingException e) { e.printStackTrace(); return null; } }
@Override public T convertToEntityAttribute(String dbData) { ObjectReader reader = objectMapper.readerFor(getJsonType()); try { return dbData == null ? null : reader.readValue(dbData); } catch (IOException e) { e.printStackTrace(); return null; } }
/** * 获取子类中 Json 转换的 TypeReference * * @return TypeReference<T> */ protected TypeReference<T> getJsonType() { Type[] actualTypeArguments = ((ParameterizedType) (getClass().getGenericSuperclass())).getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length > 0) { return new TypeReference<T>() { @Override public Type getType() { return actualTypeArguments[0]; }
}; }
return null; }}
我们这里的AbstractJsonConverter继承至AttributeConverter并指定了数据库类型为String, 而实体字段为任意泛型类型。这样我们就能随意指定实体类型了, 更加的灵活使用。当我们需要使用的时候, 我们仅仅需要指定泛型类型即可。

step 3. AnnotationProcessor编译时注解处理器扫描

前面的准备代码工作已经完成了, 现在就是我们的主角AnnotationProcessor登场了, 它是用来扫描出所有需要生成Converter的target类, 并使用JavaPoet来动态生成代码片段。这里有两个关键类需要注意, 一个是标记性自定义注解JsonAutoConverter, 只有在类上使用此注解的Class才会被AnnotationProcessor扫描到并进行处理, 还有一个就是我们Processor的实现类JsonConvertProcessor, 这个是用来处理扫描出的目标类, 并为之生成Converter源代码文件。

@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.SOURCE)public @interface JsonAutoConverter {
/** * 是否开启自动转换 * * @return */ boolean autoApply() default true;
}
@AutoService(Processor.class)@SupportedSourceVersion(SourceVersion.RELEASE_8)@SupportedAnnotationTypes("com.zhucan.jpa.conversion.annotation.JsonAutoConverter")public class JsonConvertProcessor extends AbstractProcessor {
private Filer filer; private Messager messager; private Elements elementUtils; public static final String doc = "\n This codes are generated automatically. Do not modify! \n -.- \n created by zhuCan \n";
@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnv.getFiler(); messager = processingEnvironment.getMessager(); elementUtils = processingEnvironment.getElementUtils(); }
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(Diagnostic.Kind.NOTE, "Processor : " + getClass().getSimpleName());
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(JsonAutoConverter.class);
elements.forEach(x -> { // 被扫描的类的包路径 PackageElement packageElement = elementUtils.getPackageOf(x); String packageName = packageElement.getQualifiedName().toString();
// 获取类上面的注解 JsonAutoConverter annotation = x.getAnnotation(JsonAutoConverter.class);
// 构建类 TypeSpec clazz = TypeSpec.classBuilder(x.getSimpleName() + "Converter") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addAnnotation(AnnotationSpec.builder(Converter.class).addMember("autoApply", CodeBlock.builder().add("$L", annotation.autoApply()).build()).build()) .addJavadoc(" generator for Json converter " + doc) .superclass(ParameterizedTypeName.get(ClassName.get(AbstractJsonConverter.class), ClassName.get((TypeElement) x))) .build(); try { // 创建java文件 JavaFile javaFile = JavaFile.builder(packageName, clazz) .build(); // 写入 javaFile.writeTo(filer); } catch (IOException e) { e.printStackTrace(); }
// 构建集合转换类 TypeSpec listClazz = TypeSpec.classBuilder(x.getSimpleName() + "ListConverter") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addAnnotation(AnnotationSpec.builder(Converter.class).addMember("autoApply", CodeBlock.builder().add("$L", annotation.autoApply()).build()).build()) .addJavadoc(" generator for Json converter " + doc) .superclass(ParameterizedTypeName.get(ClassName.get(AbstractJsonConverter.class), ParameterizedTypeName.get(ClassName.get(List.class), ClassName.get((TypeElement) x)))) .build(); try { // 创建java文件 JavaFile javaFile = JavaFile.builder(packageName, listClazz) .build(); // 写入 javaFile.writeTo(filer); } catch (IOException e) { e.printStackTrace(); }
}); return false; }}

  

下面展示的就是这几个核心处理类的关联关系图 

step 4. 成果检验

 在上面所有代码完成了后, 我们就需要来验证下它的效果如何了, 进入项目POM文件所在目录执行Maven代码编译命令 mvn compile -Dmaven.test.skip=true, 然后查看我们编译源文件包, 在/target/generated-sources/annotations/文件夹中能看到一个命名为OrderNoticeEventConverter的文件, 这个就是使用JavaPoet构建而成的Converter源码文件。

  我们可以看到这个源文件中, 我们定义了一个Java类, 继承至AbstractJsonConverter基类, 并且指定了需要转换的类型泛型为OrderEvent, Class上面标记的一个Converter注解来开启全局自动类型转换的功能。


六、后记

  其实在各类开发语言平台上面从来都不缺少这些"小技巧", 只是我们平时没有静下心来去发现而已, 只要我们耐心的去往深处挖掘, 发散自己的思维就能找到打开新世界大门的钥匙, 这时我们就能看到不一样的软件世界。

附: 源码地址 https://gitee.com/zhucan123/extension-spring-boot-starter/tree/master/jpa-conversion,有兴趣的小伙伴们可以参阅下哈。


------ END ------
作者简介
朱同学: 应用市场的开发工程师,目前负责天际平台相关开发工作。

也许您还想看:
技术分享|Java SDK动态数据源和上下文机制
技术分享|NodeJS分布式链路追踪实现

更多明源云·天际开放平台场景案例与开发小知识,可以关注明源云天际开发者社区公众号:
【建模】文档服务提供高保真打印模式
明源云·天际硬核技术认可:获华为鲲鹏技术认证书
天际·开发者社区“重装发布”!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存