之前写过一篇通过SPI方式解耦三方组件的文章,本文目的一样也是为了解耦,不过是在编译源码时,通过把自定义注解替换成合适的功能注解实现的。Java ServiceLoader、Spring SpringFactoriesLoader、SPI方式解耦第三方组件_殷长庆的博客-CSDN博客_serviceloader spring
框架中有很多事通过注解实现的功能,像定时任务、事务、日志等等,这些功能大都可以通过引入第三方组件进行实现,如果直接引用第三方组件提供的注解,项目架构需要更换组件时可能会修改各处的源码。
为减少项目架构变化造成的源码改动,我们可以自定义一些注解,项目引用自定义注解,在编译打包过程中自动替换成合适的注解,实现相同的功能。
本文自定义了一个定时器注解,在编译时替换成XxlJob或者是spring默认的Scheduled。
新建一个common项目用来放我们的自定义注解及注解处理类。
自定义一个定时器注解,我把spring的Scheduled注解作为自定义注解的结构体,方便我们编译时替换。
@Retention(RetentionPolicy.SOURCE)// 编译期注解
package com.luck.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Hello {
String cron() default "";
String zone() default "";
long fixedDelay() default -1;
String fixedDelayString() default "";
long fixedRate() default -1;
String fixedRateString() default "";
long initialDelay() default -1;
String initialDelayString() default "";
}
通过在编译期处理注解,改变为合适的定时任务注解
package com.luck.annotation;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes(HelloProcessor.CLZ)
public class HelloProcessor extends AbstractProcessor {
public final static String CLZ = "com.luck.annotation.Hello";
private static Map tis = new ConcurrentHashMap<>();
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
boolean thirdPresent = isPresent("com.xxl.job.core.handler.annotation.XxlJob");
// 判断方法应该添加什么注解
String pkg, clz;
if (thirdPresent) {
pkg = "com.xxl.job.core.handler.annotation";
clz = "XxlJob";
} else {
pkg = "org.springframework.scheduling.annotation";
clz = "Scheduled";
}
for (Element element : roundEnvironment.getElementsAnnotatedWith(Hello.class)) {
javax.lang.model.element.Name methodName = element.getSimpleName();
TypeElement typeElem = (TypeElement) element.getEnclosingElement();
String typeName = typeElem.getQualifiedName().toString();
String list = tis.get(typeName);
if (null == list) {
JCTree.JCIdent jcIdent = treeMaker.Ident(names.fromString(pkg));
Name className = names.fromString(clz);
JCTree.JCFieldAccess jcFieldAccess = treeMaker.Select(jcIdent, className);
JCTree.JCImport anImport = treeMaker.Import(jcFieldAccess, false);
// 导入注解类
TreePath path = trees.getPath(typeElem);
JCTree.JCCompilationUnit jccu = (JCCompilationUnit) path.getCompilationUnit();
jccu.defs = jccu.defs.prepend(anImport);
tis.put(typeName, "Scheduled");
}
JCTree jcTree = trees.getTree(typeElem);
jcTree.accept(new TreeTranslator() {
@Override
public void visitMethodDef(JCMethodDecl methodDecl) {
if (methodName.toString().equals(methodDecl.getName().toString())) {
if (methodDecl.mods.annotations.toString().contains("Hello")) {
List annotations = methodDecl.mods.annotations;
List nil = List.nil();
for (int i = 0; i < annotations.size(); i++) {
JCAnnotation anno = annotations.get(i);
if (CLZ.equals(anno.type.toString())) {
JCAnnotation e;
if (thirdPresent) {
e = treeMaker.Annotation(treeMaker.Ident(names.fromString(clz)), // 注解名称
List.of(treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(names.fromString("value")), // 注解属性
treeMaker.Literal(methodName.toString()))).expr));// 注解属性值
} else {
e = treeMaker.Annotation(treeMaker.Ident(names.fromString(clz)), anno.args);
}
nil = nil.append(e);
} else {
nil = nil.append(anno);
}
}
// 修改方法注解
methodDecl.mods.annotations = nil;
}
}
super.visitMethodDef(methodDecl);
}
});
}
return false;
}
// 类是否存在
private static boolean isPresent(String name) {
try {
HelloProcessor.class.getClassLoader().loadClass(name);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
common项目新建/META-INF/services/javax.annotation.processing.Processor文件
内容为com.luck.annotation.HelloProcessor
common项目pom文件修改plugin的
org.apache.maven.plugins
maven-compiler-plugin
3.1
1.8
UTF8
-proc:none
${java.home}\jre\lib\rt.jar;${java.home}\jre\lib\jce.jar
因为AST需要用到tools.jar,这个jar在jdk中,把他加到项目的依赖里面
其他项目的pom文件加入common项目依赖
在需要加定时器的方法上添加Hello注解
@Hello(fixedRate = 5000) // 隔五秒执行一次
eclipse中通过project clean是不会执行编译processor的,但是通过maven打包出来的class中会给替换成正常的注解,(可以查下eclipse怎么执行process Java Compiler -> Annotation Processing)
idea可以正常执行编译
AST参考文章:
Java-JSR-269-插入式注解处理器 | Liuye Notebook
java AST JCTree简要分析 | 易学教程
Java 中的屠龙之术:如何修改语法树? - OomSpot