之前的文章中介绍过Java注解相关的内容(见Java Annotations),本文将介绍Lombok及其背后的编译时注解处理技术。
首先定义一个注解 @Data
:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Data { } 复制代码
实现@Data的注解处理器
public class DataProcessor extends AbstractProcessor { private Messager messager; private JavacTrees trees; // 创建AST节点工具 private TreeMaker treeMaker; private Names names; /** 获取处理器框架组件 */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); messager = processingEnv.getMessager(); trees = JavacTrees.instance(processingEnv); Context context = ((JavacProcessingEnvironment) processingEnv).getContext(); treeMaker = TreeMaker.instance(context); names = Names.instance(context); } /** 注解处理逻辑,在这里修改AST */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { messager.printMessage(Kind.NOTE, "starting process"); /** 获取Data注解的元素 */ Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Data.class); for (Element element : elements) { JCTree jcTree = trees.getTree(element); // 遍历类定义的元素 jcTree.accept(new TreeTranslator() { @Override public void visitClassDef(JCClassDecl jcClassDecl) { super.visitClassDef(jcClassDecl); try { java.util.List<JCVariableDecl> jcVariableDeclList = new ArrayList<>(); for (JCTree tree : jcClassDecl.defs) { // 收集所有field if (tree.getKind() == Tree.Kind.VARIABLE) { jcVariableDeclList.add((JCVariableDecl) tree); } } for (JCVariableDecl jcVariableDecl : jcVariableDeclList) { // 生成相应的geeter,setter generateElement(jcClassDecl, jcVariableDecl); } } catch (Throwable e) { try { e.printStackTrace(new PrintStream(new FileOutputStream("error.txt"))); } catch (FileNotFoundException e1) { messager.printMessage(Kind.ERROR, "Error create log file"); } } } }); } return true; } /** 处理的注解类型 */ @Override public Set<String> getSupportedAnnotationTypes() { LinkedHashSet<String> supportedAnnotations = new LinkedHashSet<>(); supportedAnnotations.add("*"); return supportedAnnotations; } /** 支持的JDK版本 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } } 复制代码
具体方法生成逻辑:
private void generateElement(JCClassDecl jcClassDecl, JCVariableDecl jcVariableDecl) throws Exception { // 将生成的getter方法插入类语法树中 JCMethodDecl getter = generateGetterMethodElement(jcVariableDecl); jcClassDecl.defs = jcClassDecl.defs.append(getter); // 将生成的setter方法插入类语法树中 JCMethodDecl setter = generateSetterMethodElement(jcVariableDecl); jcClassDecl.defs = jcClassDecl.defs.append(setter); } /** 生成getter方法 */ private JCMethodDecl generateGetterMethodElement(JCVariableDecl jcVariableDecl) { String field = jcVariableDecl.getName().toString(); Name name = names.fromString("get" + field.substring(0, 1).toUpperCase() + field.substring(1, field.length())); ListBuffer<JCStatement> statements = new ListBuffer<>(); statements.append( treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()))); JCTree.JCBlock body = treeMaker.Block(0, statements.toList()); return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), name, jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null); } /** 生成setter方法 */ private JCMethodDecl generateSetterMethodElement(JCVariableDecl jcVariableDecl) throws Exception { String field = jcVariableDecl.getName().toString(); Name name = names.fromString("set" + field.substring(0, 1).toUpperCase() + field.substring(1, field.length())); JCTree.JCExpression returnMethodType = treeMaker .Type((Type) (Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance())); ListBuffer<JCStatement> statements = new ListBuffer<>(); statements.append(treeMaker.Exec(treeMaker .Assign(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName())))); JCTree.JCBlock body = treeMaker.Block(0, statements.toList()); JCTree.JCVariableDecl param = treeMaker .VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariableDecl.name, jcVariableDecl.vartype, jcVariableDecl.nameexpr); List<JCTree.JCVariableDecl> parameters = List.of(param); return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), name, returnMethodType, List.nil(), parameters, List.nil(), body, null); } 复制代码
用@Data注解去修饰一个类:
@Data public class Member { private Long id; private String name; private Date joinDate; } 复制代码
搞定!验证一下:
javac DataProcessor.java && javac -g -processor DataProcessor Member.java && javap -p Member 复制代码
maven构建配置:
<build> <resources> <resource> <directory>src/main/resources</directory> <excludes> <!-- 构建时排除Service声明,此时Processor还不可用 --> <exclude>META-INF/**/*</exclude> </excludes> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> <executions> <execution> <!-- prepare-package阶段再把services拷贝到生成类目标目录 --> <id>process-META</id> <phase>prepare-package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>target/classes</outputDirectory> <resources> <resource> <directory>${basedir}/src/main/resources/</directory> <includes> <include>**/*</include> </includes> </resource> </resources> </configuration> </execution> </executions> </plugin> </plugins> </build> 复制代码
新建项目,引入依赖:
<dependency> <groupId>org.yannis</groupId> <artifactId>srctool</artifactId> <version>1.0-SNAPSHOT</version> </dependency> 复制代码
复制Member.java至新项目:
mvn clean package
,观察target生成类Member.class反编译结果:
至此,已经实现了一个简单的lombok功能!
Lombok是我在个人项目中比较喜欢用的工具,它能使我们仅通过简单的注解就可以在编译时为类生成一系列样板式的代码,源码则可以保持非常简洁。下面简单介绍几个常用注解,更多用法可以去官网查看。
Dependency:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> <scope>provided</scope> </dependency> 复制代码
注:在idea,eclipse等ide中安装Lombok插件,就可以直接看到生成的方法了
为实例field生成getter/setter方法,默认为public,可以通过AccessLevel改变访问控制级别。通过@Accessors(prefix = "m")可以标识属性前缀。
生成对象的toString()方法,通过 exclude 设置需要排除的字段。
生成对象的hashCode()和equals()方法,通过 exclude 设置需要排除的字段
生成无参数,带参数(final或@NotNull修饰的属性)的构造器,全参数构造器。
相当于@ToString、@EqualsAndHashCode、@Getter / @Setter和@RequiredArgsConstructor的集合,效果如下: