如何实现自定义Java编译时注解功能--定制自己的AndroidAnnotations

上一篇,我们基本了解了整个编译时注解的实现流程,现在我们就要开始来真正写一个有意义的功能了

本文最终demo已经上传到github上,欢迎大家star,follow

再次声明本文参考了鸿洋的demo

项目结构

同之前一样你要创建一个Java Library,但是这里你需要将android.jar拷贝进libs里面,因为我们这里需要使用到android的相关类

Library的编写

首先你需要将android.jar拷贝到java Library里面来,因为我们需要一些android的类

1 注解类的实现 ViewInject.java

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface ViewInject {    
    int value();
}

我们将通过注解的id进行相应的处理

2 具体实现的功能 Finder.java

public enum Finder {    
    Activity {        
        @Override        
        public void setContentView(Object activity, int id) {            
            ((Activity) activity).setContentView(id);        
       }    
    };    
    public abstract void setContentView(Object activity, int id);
}

本次功能主要是针对Activity去实现setContentView的功能。在枚举类里面定义了抽象方法,这样枚举对象就一定要实现才行。这样写的好处是方便扩展,后续扩展其他功能以及支持Fragment、View的话比较方便

3 动态生成类功能接口 InjectInterface.java

public interface InjectInterface {    
    void inject(Finder finder, T target);
}

每一个动态生成的类需要实现这个接口,这样activity就可以直接通过这个接口使用Finder来实现真正的setContentView功能

4 编译时执行类的实现 ViewInjectProcessor.java

提前介绍ProxyInfo.java,这个类存储着Activity的信息,我们声明一个map对象去用键值对存储不同Activity的信息,做到不混乱

//存储需要循环生成的注解类信息
HashMap proxyInfoHashMap;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {    
    super.init(processingEnv);    
    proxyInfoHashMap=new HashMap<>(); 
}

在初始化方法里面初始化一下

下面开始重写process方法

首先定义三个对象用来存储完整类路径、类名、包名

String fqClassName, className, packageName;

fqClassName是用来当键用的,因为包名或者类名都有可能出现重复的情况
包名跟类名是后续ProxyInfo需要使用的

for (Element element : roundEnv.getElementsAnnotatedWith(ViewInject.class)) {
    if (element.getKind()== ElementKind.CLASS) {
        TypeElement classElement= (TypeElement) element;
        PackageElement packageElement= (PackageElement) classElement.getEnclosingElement();
        fqClassName=classElement.getQualifiedName().toString();
        className=classElement.getSimpleName().toString();
        packageName=packageElement.getQualifiedName().toString();
        int layoutId=classElement.getAnnotation(ViewInject.class).value();
    }
}

遍历获得ViewInject下的所有被注解class的信息,同时获取到注解内容的值,也就是view的id。这边通过之前的学习,应该没有什么难度看不懂了

if (proxyInfoHashMap.containsKey(fqClassName)) {    
    proxyInfoHashMap.get(fqClassName).setLayoutId(layoutId);
} else {    
    ProxyInfo proxyInfo=new ProxyInfo(packageName, className);    
    proxyInfo.setClassElement(classElement);    
    proxyInfo.setLayoutId(layoutId);    
    proxyInfoHashMap.put(fqClassName, proxyInfo);
}

把每次取到的classElement信息放到那个map里面去,等全部加完之后,我们要开始手动去创建class文件了,这些文件就是提供给activity里面去操作的方法

5 存储Activity信息的类 ProxyInfo.java

public class ProxyInfo {    
    //原始包名    
    String packageName;    
    //注解上的class名称    
    String className;    
    //生成注解类名称    
    String proxyClassName;    
    //类元素    
    TypeElement classElement;    
    //布局Id    
    int layoutId;    
    public static final String PROXY = "PROXY";    
    public ProxyInfo(String packageName, String className) {        
        this.packageName = packageName;        
        this.className = className;        
        //设置新生成的注解类名        
        this.proxyClassName=className+"$$"+PROXY;    
    }    
    public TypeElement getClassElement() {        
        return classElement;    
    }    
    public void setClassElement(TypeElement classElement) {        
        this.classElement = classElement;    
    }    
    public int getLayoutId() {        
        return layoutId;    
    }    
    public void setLayoutId(int layoutId) {        
        this.layoutId = layoutId;    
    }    
    public String getProxyClassName() {        
        return proxyClassName;    
    }    

    /**     
    * 创建模板代码     
    * @return     
    */    
    public String generateJavaCode() {        
        StringBuilder builder = new StringBuilder();        
        builder.append("// Generated code from ViewInjectProcessor. Do not modify!\n"); 
        builder.append("package ").append(packageName).append(";\n\n");        
        builder.append("import com.example.annotation.Finder;\n");        
        builder.append("import com.example.annotation.InjectInterface;\n");        
        builder.append('\n');        
        builder.append("public class ").append(proxyClassName);        
        builder.append("");        
        builder.append(" implements InjectInterface");        
        builder.append(" {\n\n");        
        generateInjectMethod(builder);        
        builder.append("\n}\n");        
        return builder.toString();    
    }    

    /**     
    * 具体方法实现代码     
    */    
    private void generateInjectMethod(StringBuilder builder) {        
        builder.append("  @Override\n");        
        builder.append("  public void inject(Finder finder, T target) {");        
        if (layoutId > 0) {            
            builder.append("    finder.setContentView(target, "+layoutId+");\n");        
        }        
        builder.append("\n  }\n");    
    }
}

其实虽然代码看起来好像不少,但是实际上很简单,就是在class包下面创建好相应的类class
生成的代码是这样的

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.rg.annotationdemo;

import com.example.annotation.Finder;
import com.example.annotation.InjectInterface;
import com.rg.annotationdemo.MainActivity;

public class MainActivity$$PROXY implements InjectInterface {    
    public MainActivity$$PROXY() {    
    }    
    public void inject(Finder finder, T target) {        
        finder.setContentView(target, 2130968601);    
    }
}

只要能调用到这个方法,那么setContentView的功能就能实现


如何实现自定义Java编译时注解功能--定制自己的AndroidAnnotations_第1张图片
动态class生成

6 提供给Activity传递参数的类 ButterKnife.java

public class ButterKnife {    
    static final Map, InjectInterface> INJECTORS = new LinkedHashMap<>();    
    public void bind(Activity activity) {        
        InjectInterface injectInterface=findInjector(activity);     
        injectInterface.inject(Finder.Activity, activity);    
    }    

    private InjectInterface findInjector(Object object) {        
        Class class_=object.getClass();        
        InjectInterface injectInterface=INJECTORS.get(class_);        
        if (injectInterface==null) {            
            try {               
                Class injectClass=Class.forName(class_.getName()+"$$"+ProxyInfo.PROXY);
                injectInterface= (InjectInterface) injectClass.newInstance();    
                INJECTORS.put(class_, injectInterface);            
            } catch (ClassNotFoundException e) {                
                e.printStackTrace();            
            } catch (InstantiationException e) {                
                e.printStackTrace();            
            } catch (IllegalAccessException e) {                
                e.printStackTrace();            
            }        
        }        
        return injectInterface;    
    }
}
 
 

这个类其实也是很简单就能理解的,通过反射将之前动态生成的类找到,然后传递参数Finder跟Activity,再调用inject方法,完成setContentView的真正实现

Activity的使用

通过上述步骤,我们就完成了编译时注解,同上篇文章一样,打个包丢进去

@ViewInject(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {    
    TextView textview;    
    @Override    
    protected void onCreate(Bundle savedInstanceState) {      
        super.onCreate(savedInstanceState);        
        ButterKnife butterKnife=new ButterKnife();        
        butterKnife.bind(this);    
    }    

    public void click() {        
        Toast.makeText(this, "HHH", Toast.LENGTH_SHORT).show();    
    }
}

实际效果不截图了,也没什么意思,无非就是一个页面而已,大家可以根据我的demo自行尝试

注意点

没事多gradle clean/gradle build,有些不能理解的异常,比如说动态生成的类不符合JUnit文件规范之列的,都可以通过clean build解决

你可能感兴趣的:(如何实现自定义Java编译时注解功能--定制自己的AndroidAnnotations)