前言
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实现的逻辑还是有些绕,需要查看所有代码请求点击查看源码。