Java Annotation AbstractProcessor,通过AST修改class文件,修改方法定时任务注解,编译期解耦第三方组件

背景

之前写过一篇通过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<String, String> 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<? extends TypeElement> 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<JCAnnotation> annotations = methodDecl.mods.annotations;
							List<JCAnnotation> 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;
		}
	}
}

META-INF

common项目新建/META-INF/services/javax.annotation.processing.Processor文件

内容为com.luck.annotation.HelloProcessor

Maven

common项目pom文件修改plugin的

<compilerArgument>-proc:none</compilerArgument>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <encoding>UTF8</encoding>
        <compilerArgument>-proc:none</compilerArgument>
        <compilerArguments>
            <bootclasspath>${java.home}\jre\lib\rt.jar;${java.home}\jre\lib\jce.jar</bootclasspath>
        </compilerArguments>
    </configuration>
</plugin>

引入tools.jar

因为AST需要用到tools.jar,这个jar在jdk中,把他加到项目的依赖里面 

其他项目引用common项目

其他项目的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 

猜你喜欢

转载自blog.csdn.net/anshichuxuezhe/article/details/127025797