使用AnnotationProcessor自动生成代码

在上一篇实现类spring框架的文章中,android基于注解实现类似spring服务端框架使用了注解实现了与web服务本身的解耦,但实现的方式是使用反射得到method对象然后在请求到来时实例化对象然后用反射调用对应的函数,这种方法有一定的性能的损耗,使用上不够极致,这里再用注解处理器的方法实现对注解自动生成代码,实现一样的功能.
注解处理器的基础就不多介绍了,这里直接说说如何实现.

第一步:将注解放在一个library工程

因为注解公是定义,所以只要是一般的java library工程即可,其中一个定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
    String name() default "/";
    boolean needPermissonControl() default true;
}

第二步:实现注解处理工程

这个工程就是用来自动生成代码的,这个工程必须为java library工程,因为注解处理器相关类并不在android的类库里,对应的包javax.annotation.processing只有一般java工程才能引用,不过一定要用android library也不是不可以,只是要把相应的类引进来才能正常编译,关于如何引入javax.annotation.processing比较麻烦,github有相关的代码,这里不多做介绍.
建好后引入必要的依赖,看注解工程的build.gradle

// java工程
apply plugin: 'java'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly fileTree(dir: '../app/libs', includes: ['*.jar'])
    // google的注解生成器的封装,也可以不用,但自己实现起来比较麻烦
    compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
    // 生成java代码需要用到
    compileOnly 'com.squareup:javapoet:1.9.0'
    compile project(path: ':Annotations')
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

接着定义注解处理类

// 指定这个类是注解处理类,用了这个注解编译时就知道要用这个类来处理注解
@AutoService(Processor.class)
public class ControllerProcessor extends AbstractProcessor {
    // 输出java文件用
    private Filer mFiler;
    // 打印LOG用
    private Messager mMessager;
    // 获取类成员等用
    private Elements mElementUtils;

    // 初始化拿到注解处理相关实现用类,打Log类,输出java代码类
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }
    // 支持的注解类
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Controller.class.getCanonicalName());
//        annotations.add(RequestMapping.class.getCanonicalName());
        return annotations;
    }

    // 支持的java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
       
    // 在编译时会调到这里来,这个函数会被调用多次,注意不要出现重复生成的错误,调用多次的原因应该是会对生成后的代码再扫描一遍
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
    
    }
}

注解处理类的关键的方法就是上面的几个,其中process方法会在编译扫描源代码的时候会执行进来,处理注解相关的逻辑就在这里实现.这里我定义了一些操作类用来方便生成代码,process函数实现:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
    try {
        // 循环遍历有Controller注解的类
        for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Controller.class)) {
            // Check if a class has been annotated with @Factory
            if (annotatedElement.getKind() != ElementKind.CLASS) {
                throw new ProcessingException(annotatedElement, "Only classes can be annotated with @%s",
                        Controller.class.getSimpleName());
            }

            // We can cast it, because we know that it of ElementKind.CLASS
            TypeElement typeElement = (TypeElement) annotatedElement;
            ControllerAnnotatedClass annotatedClass = new ControllerAnnotatedClass(typeElement, mMessager);
            note("process " + annotatedClass.getSimpleName());
            // 扫描到Controller类,加入list
            classes.add(annotatedClass);
        }

        // 将扫描到有Controller注解的类生成代码
        classes.generateCode(roundEnv, mElementUtils, mFiler, mMessager);
        classes.clear();
        return true;
    } catch (ProcessingException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return false;
}

这里介绍讲解一下Element这个类,编译找描代码的过程中,每个类或者函数或者变量都是Element,如下所示:

package com.example;    // PackageElement
public class Foo {        // TypeElement
    private int a;      // VariableElement
    private Foo other;  // VariableElement
    public Foo () {}    // ExecuteableElement
    public void setA (  // ExecuteableElement
                    int newA   // VariableElement
                    ) {}
}

生成代码的逻辑主要看具体的需求,这里的需求是有Controller注解的类被当作http请求响应类,其中在这种类下面有RequestMapping注解的方法来处理具体的请求.

public void generateCode(RoundEnvironment roundEnv, Elements elementUtils, Filer filer, Messager messager) throws IOException {
        // 如果为空,不处理,防止重复生成
        if (list.size() <= 0) {
            return;
        }

       // ......
        //generate each handler class
        for (ControllerAnnotatedClass annotatedClass : list) {
            TypeElement typeElement = annotatedClass.getTypeElement();
            DesktopApp desktopApp = typeElement.getAnnotation(DesktopApp.class);
            if (desktopApp != null) {
                sb.append(annotatedClass.getQualifiedName()).append(".class").append(',');
            }
            annotatedClass.generateCode(roundEnv, elementUtils, filer, messager);
            annotatedClass.generateInjectCode(method, elementUtils, messager);
        }
    }

这里是对每个类进行生成代码

/**
     * 生成相关处理类
     * @param roundEnv
     * @param elementUtils
     * @param filer
     * @param messager
     * @throws IOException
     */
    public void generateCode(RoundEnvironment roundEnv, Elements elementUtils, Filer filer, Messager messager) throws IOException {
        // 方法列表
        ArrayList<MethodSpec> methodSpecs = new ArrayList<>();
        // 构造函数
        MethodSpec constructionMethod = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .build();
        methodSpecs.add(constructionMethod);
        // get方法
        MethodSpec.Builder getMethod = MethodSpec.methodBuilder("get")
                .addParameter(IHTTPSession.class, "session")
                .addModifiers(Modifier.PUBLIC)
                .addException(IllegalAccessError.class)
                .addException(IllegalAccessException.class)
                .addException(InstantiationError.class)
                .addException(InstantiationException.class)
                .returns(TypeName.get(Response.class));

        Controller controller = typeElement.getAnnotation(Controller.class);
        if (controller.needPermissonControl()) {
            getMethod.beginControlFlow("if (!org.cmdmac.enlarge.server.AppNanolets.PermissionEntries.isRemoteAllow(session.getRemoteIpAddress())) ")
                    .addStatement("return org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse(\"not allow\")")
                    .endControlFlow();
        }

        // 生成子方法
        for (Element ee : elementUtils.getAllMembers(typeElement)) {
            ExecutableElement eElement = (ExecutableElement) ee;
            Annotation annotation = eElement.getAnnotation(RequestMapping.class);
            if (annotation != null) {
                RequestMapping requestMapping = (RequestMapping) annotation;
                String invokeAndReturnStatement = String.format("return invoke_%s(params)",eElement.getSimpleName());
                // ......
                MethodSpec m = generateMethodCode(className, eElement.getSimpleName().toString(),
                        eElement.getParameters(), messager);
                methodSpecs.add(m);
            }
        }

        getMethod.addStatement("return null");
        methodSpecs.add(getMethod.build());

        //创建类,增加方法
        ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(pkgName, className)));
        FieldSpec fieldSpec = FieldSpec.builder(typeName, "cls").initializer(getQualifiedName() + ".class").build();
        TypeSpec.Builder classSpec = TypeSpec.classBuilder(className + "_Proxy")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(TypeName.get(BaseController.class))
                .addField(fieldSpec);
        for (MethodSpec methodSpec : methodSpecs) {
            classSpec.addMethod(methodSpec);
        }
        JavaFile.builder(pkgName, classSpec.build()).build().writeTo(filer);
    }

生成代码的逻辑其实并不复杂,其实就是按照正常写代码的过程来实现,比如这里先生成一个构造方法,再生成一个get方法,get方法里的逻辑根据RequestMapping再生成子方法,最后调用JavaFile写入filer即可.
这里遍历的关键在于: elementUtils.getAllMembers这是获取类下面的所有函数的TypeElement,另外可能遇到的是如何生成泛型定义:如Map<String, List>,这要借助TypeName来实现:

ParameterizedTypeName parameterSpec = ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class), ParameterizedTypeName.get(List.class, String.class));

这里是使用ParameterizedTypeName来生成,第一个参数是泛型类的TypeName,通过ClassName.get(Map.class)得到,第二个参数是第一个泛型参数ClassName.get(String.class),第三个参数是第二个泛型参数,由于本身又是一个泛型所以再一次用ParameterizedTypeName生成.具体就不多做介绍了,源码在Enlarge-Android

第三步:使用生成的代码

注解处理器有一个缺点是,不能修改已经生成的代码,就是本身的项目中存在代码是不能修改的,只能新生成代码,所以生成代码后,需要有个调用的地方:

 public static class UriRouter extends BaseRouter {
    RouterMatcher mStaticMatcher = new RouterMatcher("", StaticPageHandler.class);
    public UriRouter() {
        super();
        //使用生成的代码,一键注册所有Controller类,后面只需增加和修改有Controller注解的类
        ControllerInject.inject(this);
//        addRoute(StaticPageHandler.class);
        mappings.add(mStaticMatcher);
    }

    @Override
    public void addRoute(Class<?> handler) {
        mappings.add(new ControllerMatcher("/", handler));
    }

    public void addRoute(String url, Class<?> handler) {
        mappings.add(new ControllerMatcher(url, handler));
    }

    @Override
    public Response process(IHTTPSession session) {
        Response response = super.process(session);
        if (response == null) {
            return mStaticMatcher.process(null, session);
        } else {
            return response;
        }
    }
}

总结:

annotation processor是一个强大的工具,用来可以很容易实现代码的解耦而不损失性能,目前有很多开源的项目如Butternife,Dagger,EventBus都使用了这个工具.实现这个工具一开始可能会不太适应,主要是没有定义好要生成代码的样子,所以要快速上手,可以先把想要生成的代码范例自己手写一遍,再根据这个代码来填充成通用的.全部代码放在Enlarge-Android

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值