技术分享 | APT结合JavaPoet生成模板化Java源代码文件
源宝导读: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。
... ...
三、要不咱也来个'仿制品'?
既然有如此多的利器能解决代码重复冗余的问题, 我们何不效仿着也整一个出来试试? 刚好最近在使用数据持久化框架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);
}
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;
}
}
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,有兴趣的小伙伴们可以参阅下哈。
朱同学: 应用市场的开发工程师,目前负责天际平台相关开发工作。