java注解之编译时注解RetentionPolicy.CLASS 基本用法

1 前言

我们知道,在日常开发中我们常用的两种注解是运行时注解和编译时注解,运行时注解是通过反射来实现注解处理器的,对性能稍微有一点损耗,而编译时注解是在程序编译期间生成相应的代理类,替我们完成某些功能。今天我们来讲解一下编译时注解以及写一个小例子,以便加深对编译时注解的理解。

2 编译时注解

编译时注解(RetentionPolicy.CLASS),指@Retention(RetentionPolicy.CLASS)作用域class字节码上,生命周期只有在编译器间有效。编译时注解注解处理器的实现主要依赖于AbstractProcessor来实现,这个类是在javax.annotation.processing包中,同时为了我们自己生成java源文件方便,我们还需要引入一些第三方库,主要包括
javapoet 用于生成java源文件,可参考https://github.com/square/javapoet
auto-service 主要用于生成一些辅助信息,例如META-INF/services 一些信息等

编译时注解的核心就是实现AbstractProcessor的process()方法,一般来说主要有以下两个步骤
1 搜集信息,包括被注解的类的类信息,方法,字段等信息,还有注解的值
2 生成对应的java源代码,主要根据上一步的信息,生成响应的代码

下面我们来实际的写一个编译时注解的例子

3 编译时注解例子

假设我们写这样一个类注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {

    int value();

}

这个Bind注解只能作用与类上面,一般我们作用于某个类上面了,我们做什么工作呢,我们新建一个类,这个类的内容大体如下:

public class XXX$$value {
  public int sayHello(int n) {
    return value;
  }
}

其中,XXX是被注解的类名,value是注解的值。
看起来很简单,对吧?下面我们就开始吧。

1 新建两个java model,注意不是Android library model
这里写图片描述
model的gradle配置如下

apply plugin: 'java'

为什么要新建两个module呢,
(1) 是因为后面再引用的时候,两者有些不一样,可以看一下

    api project(':ioc-annotation')
    annotationProcessor project(':ioc-compiler')

(2) 因为两者的引用方式不同,导致最好把注解处理器和注解分开,这样我们也能更好的解耦

注意,ioc-compiler需要引用以下两个开源库

apply plugin: 'java'

//解决编译中文乱码问题
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.google.auto.service:auto-service:1.0-rc4'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':ioc-annotation')
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

2 新建Bind注解,指定注解类型为class

/**
 * @author Created by qiyei2015 on 2018/4/14.
 * @version: 1.0
 * @email: 1273482124@qq.com
 * @description: Bind注解 指明注解作用域为类
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {

    int value();

}

这一步挺好理解的。

3 新建BindProcessor继承与AbstractProcessor并实现其process方法

/**
 * @author Created by qiyei2015 on 2018/4/15.
 * @version: 1.0
 * @email: 1273482124@qq.com
 * @description: Bind注解处理器
 */
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor{

    /**
     * java源文件操作相关类,主要用于生成java源文件
     */
    private Filer mFiler;
    /**
     * 注解类型工具类,主要用于后续生成java源文件使用
     * 类为TypeElement,变量为VariableElement,方法为ExecuteableElement
     */
    private Elements mElementsUtils;
    /**
     * 日志打印,类似于log,可用于输出错误信息
     */
    private Messager mMessager;

    private static final ClassName sClassName = ClassName.get("com.qiyei.ioc.api", "Test");

    /**
     * 初始化,主要用于初始化各个变量
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFiler = processingEnv.getFiler();
        mElementsUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * 支持的注解类型
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {

        Set<String> typeSet = new LinkedHashSet<>();

        typeSet.add(Bind.class.getCanonicalName());

        return typeSet;
    }

    /**
     * 支持的版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     *
     * 1.搜集信息
     * 2.生成java源文件
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        if (!annotations.isEmpty()){
            //获取Bind注解类型的元素,这里是类类型TypeElement
            Set<? extends Element> bindElement = roundEnv.getElementsAnnotatedWith(Bind.class);

            try {
                generateCode(bindElement);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }

        return false;
    }

    /**
     *
     * @param elements
     */
    private void generateCode(Set<? extends Element> elements) throws IOException{

        for (Element element : elements){

            //由于是在类上注解,那么获取TypeElement
            TypeElement typeElement = (TypeElement) element;

            //获取全限定类名
            String className = typeElement.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.WARNING,"className:" + className);

            //获取包路径
            PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
            String packageName = packageElement.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.WARNING,"packageName:" + packageName);

            //获取用于生成的类名
            className = getClassName(typeElement,packageName);

            //获取注解值
            Bind bindAnnotation = typeElement.getAnnotation(Bind.class);
            int value = bindAnnotation.value();
            System.out.println("value:" + value);

            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("sayHello")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeName.INT,"n")
                    .returns(TypeName.INT);

            //$L表示字面量 $T表示类型
            methodBuilder.addStatement("return $L",value);

            //生成的类
            TypeSpec type = TypeSpec
                    .classBuilder(className + "$$" + value)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            //创建javaFile文件对象
            JavaFile javaFile = JavaFile.builder(packageName,type).build();
            //写入源文件
            javaFile.writeTo(mFiler);

        }
    }

    /**
     * 根据type和package获取类名
     * @param type
     * @param packageName
     * @return
     */
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen)
                .replace('.', '$');
    }
}

其他方法暂时不做讲解,逻辑都很简单,主要讲解generateCode(Set

            //获取全限定类名
            String className = typeElement.getQualifiedName().toString();
            //获取包路径
            PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
            String packageName = packageElement.getQualifiedName().toString();

(2) 构造如上所示的java源文件

            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("sayHello")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeName.INT,"n")
                    .returns(TypeName.INT);

            //$L表示字面量 $T表示类型
            methodBuilder.addStatement("return $L",value);

            //生成的类
            TypeSpec type = TypeSpec
                    .classBuilder(className + "$$" + value)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

这里主要是用到javapoet,关于为什么用这个开源库,首先是用法比较简单,其次很多开源框架都在用,包括ButterKnife等。关于生成java代码请一定参考https://github.com/square/javapoet 里面有很详细的说明。

(3) 将java源码写入文件

            //创建javaFile文件对象
            JavaFile javaFile = JavaFile.builder(packageName,type).build();
            //写入源文件
            javaFile.writeTo(mFiler);

(4) 使用Bind注解,然后编译生成代码
我们在我们的一个Activity上使用Bind注解,如下:

/**
 * @author Created by qiyei2015 on 2017/8/28.
 * @version: 1.0
 * @email: 1273482124@qq.com
 * @description:
 */
@Bind(10)
public class MainActivity extends BaseSkinActivity {

    private RecyclerView mRecyclerView;

    private static final int MY_PERMISSIONS_REQUEST_WRITE_STORE = 1;

    /**
     * ViewModel
     */
    private MainMenuViewModel mMenuViewModel;

    private MainMenuAdapter mMenuAdapter;

    /**
     * 标题栏
     */
    private CommonTitleBar mTitleBar = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        PermissionManager.requestAllDangerousPermission(this);
        initData();
        initView();
        LogManager.i(TAG,"onCreate");
    }
    .......

}

然后编译,生成的代码如下:
代码路径:\build\generated\source\apt\baidu\debug\com\qiyei\appdemo\ui\activity

package com.qiyei.appdemo.ui.activity;

public class MainActivity$$10 {
  public int sayHello(int n) {
    return 10;
  }
}

可以看到,符合我们的预期,这样我们一个简单的编译时注解就已经完成了

注:虽然我们的代码如约生成了,但是我们并没有用生成的代码,所以我们还应该写一个api来使用生成的java源代码文件,不过这都是后话了,下次再介绍。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qiyei2009/article/details/79947352
个人分类: java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭