Annotation生成代码

前言

Android客户端框架Butterknife很好的解决了Activity中大量的findViewById等模版代码,用户只要在需要注入的地方写上Annotation,框架会自动帮开发者完成各种注入代码,减轻了开发者负担。Butterknife内部的实现就使用了Annotation处理器,通过用户配置信息在编译器生成注入代码,最后在运行期动态生成注入类完成绑定工作。这里就来简单的实现一个注解绑定回调事件的Demo,通过这个简单示例更好地理解Butterknife的实现原理。

基础代码

Demo中使用J2SE来生成一个简单的桌面窗口应用,主窗口中有两个按钮,第一个按钮点击之后会再生成一个新的二级窗口,这个二级窗口里也包含两个按钮,这两个按钮都是打印一句话;主窗口的第二个按钮点击之后会打印一句话。

public class Main extends JFrame {
    @BindListener(ButtonActionListener.class)
    JButton btn;
    @BindListener(ToggleActionListener.class)
    JToggleButton toggle;

    public Main() {
        super("测试窗口");
        setSize(250, 250); 
        setLocationRelativeTo(null); 
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();    

        btn = new JButton("测试按钮");
        panel.add(btn);

        toggle = new JToggleButton();
        toggle.setText("测试文本");
        panel.add(toggle);
        setContentPane(panel);
        setVisible(true);

        BindRuntime.bind(this);

   }

    public static void main(String[] args) {
        new Main();
    }
}

public class ButtonActionListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        new SecondFrame();
    }
}

public class ToggleActionListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("点击了Toggle");
    }

}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindListener {
    Class<? extends ActionListener> value();
}

但是这些按钮的事件都是单独的类来实现,为了避免写大量的设置点击事件的模版代码,需要使用一个BindListener的注解通过编译器生成代码实现事件绑定操作。二级窗口SecondFrame的代码与主窗口基本一致,这里不再多做描述。最后是BindListener注解的实现,目标是字段类型,value则是ActionListener类型的类,只保留到源文件中。

Annotation解析

现在已经知道Main主窗口中包含@BindListener的注解,如何获取这些被注解了的字段其实在Java的APT(Annotation Processor Tool)解析框架已经为开发者定义好了开发接口。用户需要继承自AbstractProcessor,同时实现几个特定的方法。

public class BindProcessor extends AbstractProcessor {
    // 需要被查看的注解名
    private Set<String> mAnnotationSet = new HashSet<>();

    // 打印
    private Messager mMessager;
    // 用于生成java源文件的辅助对象
    private Filer mFiler;
    // 用于解析Element对象的辅助类
    private Elements mElementUtils;
    // 已经被解析包含注解的缓存
    private Map<String, AnnotatedClass> mContainer = new HashMap<>();

    // 该方法返回的注解才会被处理器查找
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return mAnnotationSet;
    }

    // 固定写就行了
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    // 处理器初始化的时候执行,获取各种工具对象
    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        mMessager = env.getMessager();
        mFiler = env.getFiler();
        mElementUtils = env.getElementUtils();

        // 将BindListener加入到要处理的注解容器里
        mAnnotationSet.add(BindListener.class.getName());
    }

    // 注解处理过程
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 从源代码解析对象中获取被BindListener注释的元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindListener.class);
        // 如果没有被注解的元素,不用继续处理
        if (elements == null || elements.isEmpty()) {
            return false;
        }

        for (Element e : elements) {
            // 由于被注解的都是字段,可以判定是变量元素
            VariableElement v = (VariableElement) e;
            // 获取被注解元素包裹元素,也就是它们所在的类元素
            TypeElement type = (TypeElement) e.getEnclosingElement();
            String className = type.getSimpleName().toString();

            AnnotatedClass annotatedClass = mContainer.get(className);
            if (annotatedClass == null) {
                annotatedClass = new AnnotatedClass(type, mElementUtils, mFiler);
                mContainer.put(className, annotatedClass);
            }

            // 将在同一个类中的注解字段都放到同一个annotatedClass
            annotatedClass.addAnnotatedField(new AnnotatedField(v, mElementUtils));
        }

        // 遍历解析出来的AnnotatedClass,并且为它生成Java文件
        for (Map.Entry<String, AnnotatedClass> entry : mContainer.entrySet()) {
            entry.getValue().writeToFiler();
        }

        return true;
    }

}

上面的AnnotatedClass就代表每个Class和它内部所有被注解字段元素的集合,如果有多个Class含有注解,那么就会有多个AnnotatedClass。比如本例中有Main和SecondFrame两个类中都包含了BindListener注解,那么就会生成两个AnnotationClass对象。AnnotationField则包含了每一个被注解的字段,比如本里中Main有两个字段被注解了,那么Main对应的AnnotatedClass里就有两个AnnotationField。

public class AnnotatedField {
    private String name;
    private Elements mElementUtils;
    private VariableElement vElement;

    public AnnotatedField(VariableElement vElement, Elements elementUtils) {
        super();
        this.vElement = vElement;
        this.mElementUtils = elementUtils;
        this.name = vElement.getSimpleName().toString();
    }

    // 生成解绑时候的代码
    public String getUnbindFieldJava() {
        return "." + name + " = null;\n";
    }

    // 生成绑定时候的Java代码
    public String getBindFieldJava() {
        BindListener listener = vElement.getAnnotation(BindListener.class);
        String clazzName;
        try {
            clazzName = listener.value().getName();
        } catch (MirroredTypeException mte) {
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            clazzName = classTypeElement.getQualifiedName().toString();
        }

        // 生成结果:.adActionListener(new XXXListener());
        return "." + name + ".addActionListener(new " + clazzName + "()); \n";
    }

}

生成代码

绑定事件时究竟需要生成什么样的代码,参考Butterknife的实现只是生成一个类,这个类有两个方法bind和unbind分别用来绑定和解绑,两个方法都需要传入被绑定的对象。

public interface IBind<T> {
    public void bind(T t);
    public void unbind(T t);
}

为了方便理解需要把最后生成的绑定类源码放到代码生成实现之前,在它的bind方法里为当前类注解的字段都添加了点击回调事件,需要注意这里只是代码设置还没有任何地方调用这些代码,这就需要用户在自己的代码里调用BindRuntime.bind(this)来实现绑定。

class Main$$BindListener implements IBind<Main> {
    // 在绑定操作是为被注解的变量添加处理事件
    public void bind(Main object) { 
        object.btn.addActionListener(new ButtonActionListener()); 
        object.toggle.addActionListener(new ToggleActionListener()); 
    } 

    public void unbind(Main object) { 
        object.btn = null;
        object.toggle = null;
    }
}

生成代码之后需要使用BindRuntime类来实现运行期的事件绑定操作,这里会根据传入对象的类名获取生成的绑定类,它会加载绑定类对象之后生成具体的绑定对象,最后调用这个绑定对象的bind方法,也就是上面的object.btn.addActionListener(new ButtonActionListener());设置事件回调。

public class BindRuntime {
    private static Map<String, IBind> sCache = new HashMap<>();
    public static final String CLASS_PREFIX = "$$BindListener";

    public static void bind(Object object) {
        String key = object.getClass().getName();
        IBind bind = sCache.get(key);
        if (bind == null) {
            try {
                Class<? extends IBind> clazz = (Class<? extends IBind>) Class.forName(key + CLASS_PREFIX);
                bind = clazz.newInstance();
                sCache.put(key, (IBind<?>) bind);
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
        }

        bind.bind(object);
    }

    public static void unbind(Object object) {
        String key = object.getClass().getName();
        IBind bind = sCache.get(key);
        if (bind != null) {
            bind.unbind(object);
        }
        sCache.remove(key);
    }
}

了解了生成代码如何工作,那么在AnnotatedClass中可以实现如下的Java代码生成逻辑,这里主要就是做字符串的拼接操作,需要非常细心不小心就会导致编译错误。

public class AnnotatedClass {
    // 当前类中所有包含的
    private List<AnnotatedField> mAnnotateFields = new ArrayList<>();
    private TypeElement mType;
    private Elements mElementUtils;
    private Filer mFiler;

    public AnnotatedClass(TypeElement type, Elements elementUtils, Filer filer) {
        this.mType = type;
        this.mElementUtils = elementUtils;
        this.mFiler = filer;
    }

    public void addAnnotatedField(AnnotatedField annotatedField) {
        mAnnotateFields.add(annotatedField);
    }

    public void writeToFiler() {
        try {
            JavaFileObject file = mFiler.createSourceFile(mType.getSimpleName().toString() + BindRuntime.CLASS_PREFIX, (Element[]) null);
            Writer writer = file.openWriter();
            writer.append("class ");
            writer.append(mType.getSimpleName().toString() + BindRuntime.CLASS_PREFIX);
            writer.append(" implements IBind<" + mType.getSimpleName().toString() + "> {\n");

            // 生成bind方法
            writer.append(" public void bind(" + mType.getSimpleName() + " object) { \n");

            // 遍历被注解的字段,并且生成添加事件处理代码
            for (AnnotatedField field : mAnnotateFields) {
                writer.append("object" + field.getBindFieldJava());
            }
            writer.append("}/n");

            // 生成unbind方法
            writer.append(" public void unbind(" + mType.getSimpleName() + " object) { \n");
            // 遍历被注解的字段,并且生成添加解绑操作
            for (AnnotatedField field : mAnnotateFields) {
                writer.append("object" + field.getUnbindFieldJava());
            }
            writer.append("}");

            writer.append("}");
            writer.flush();
            writer.close();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
}

运行

最后在命令行调用javac为注解处理生成class文件,需要调用这个processor对象要在编译Main.java时加上processor选项。

javac BindProcessor.java
javac -processor BindProcessor Main.java SecondFrame.jva
java Main

最后运行这个Demo程序,并且点击主窗口和次级窗口的按钮,成功打印出了点击事件的句子。整个Demo实现的逻辑还是有些绕,需要查看所有代码请求点击查看源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值