跳到主要内容

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, 有兴趣的小伙伴们可以参阅下哈