天天看点

【Java二十周年】Java注解处理器

【Java二十周年】Java注解处理器

一、序言

Java的世界中,也许你会有个疑问,为什么

@Override

能够让编译器验证这个函数是否被有效重载,为什么Hibernate的注解能够使的数据库操作如此简便,今天,我们就来揭开注解的神秘面纱,了解一下Java编译器不为人知的一面。

注解的语法比较简单,除了@符号的使用之外,它基本与Java固有语法一致。Java SE5内置了三种标准注解:

  1. @Override,表示当前的方法定义将覆盖超类中的方法。
  2. @Deprecated,使用了注解为它的元素编译器将发出警告,因为注解@Deprecated是不赞成使用的代码,被弃用的代码。
  3. @SuppressWarnings,关闭编译器警告信息。

二、运行时注解的使用

在刚刚认识注解时,我想了很久,不知道这东西能做什么,但今天,我要给大家演示一个神奇的功能,自动函数调用。

我们通过注解,来描述一个函数应该被如何调用,这种自动的函数调用,也许能够较为方便的被例如聊天机器人等程序使用。

首先我们来创建一个注解,注解的语法稍有特殊,使用

@interface

来声明。

package com.abs.autocontext;

import java.lang.annotation.*;

/**
 * 这个注解是用来自动调用函数使用
 * Created by sxf on 15-3-15.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoCall {
    String name();
    String tip() default "";
}
           

这里我们会发现,注解的声明也用到了注解- -!

其实,为了表明注解的功能及使用条件,注解在定义时,可以附加如下注解标签:

注解 功能介绍
@Target 表示该注解可以用于什么地方,可能的ElementType参数有:

CONSTRUCTOR

:构造器的声明

FIELD

:域声明(包括enum实例)

LOCAL_VARIABLE

:局部变量声明

METHOD

:方法声明

PACKAGE

:包声明

PARAMETER

:参数声明

TYPE

:类、接口(包括注解类型)或

enum

声明
@Retention 表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:

SOURCE

:注解将被编译器丢弃

CLASS

:注解在class文件中可用,但会被VM丢弃

RUNTIME

:VM将在运行期间保留注解,因此可以通过反射机制读取注解的信息。
@Document 将注解包含在Javadoc中
@Inherited 允许子类继承父类中的注解

注解的使用

然后我们来创建一个带有一定功能的实现类,使用一下我们刚刚创建的注解:

package com.abs.autocontext;

/**
 * 功能测试类
 * Created by sxf on 15-3-15.
 */
public class TestA{

    public TestA(String name) {
        this.name = name;
    }

    String name;

    @AutoCall(name="打印")
    public void printName() {
        System.out.println("Hello "+name);
    }

    @AutoCall(name="打电话", tip="给谁打电话呢?")
    public void call(String... str) {
        System.out.println("正在打电话给"+str);
    }

    @AutoCall(name="发短信", tip="给谁发短信呢?")
    public void send(String... str) {
        StringBuilder sb = new StringBuilder();
        boolean flag = true;
        for (String s : str) {
            if (flag) flag = false; else sb.append("、");
            sb.append(s);
        }
        System.out.println("正在发短信给"+sb.toString());
    }
}
           

AutoCall

注解中的name属性,是表示该方法的调用名字,我们希望,通过这个name,来找到这个方法。

tip属性,则是在调用过程中,显示用的提示语,例如,我通过,发短信,找到这个函数,那么我将它的tip属性取出并显示,这个函数就会问我,给谁发短线呢?

光有这个类还不能解决问题,这个类并不能自己找到带有注解的函数,运行时注解,需要我们自己添加代码,在合适的时候,通过反射,查询我们要用到的函数。

我们于是考虑实现一个基类BaseClass,然后让刚刚我们编写的TestA从这个基类继承,然后在基类中添加部分功能,使得这个类能通过注解的名字找到对应的函数。

package com.abs.autocontext;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 自动调用框架的基类
 * Created by sxf on 15-3-15.
 */
public class BaseClass {

    Map<String, Method> methodCache = new HashMap<>();

    public BaseClass() {
        Class<? extends BaseClass> myclass = this.getClass();
        System.out.println(myclass.getName());
        Method methods[] = myclass.getMethods();
        for (Method m: methods) {
            AutoCall autocall = m.getDeclaredAnnotation(AutoCall.class);
            if (autocall != null) {
                methodCache.put(autocall.name(), m);
            }
        }
    }

    public Method findMethod(String name) {
        if (name == null) return null;
        return methodCache.get(name);
    }

    void CallFunc(String name, Object... objects) {
        Method m = findMethod(name);
        try {
            m.invoke(this, objects);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    void CallFunc(Method m, Object... objects) {
        try {
            m.invoke(this, objects);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
           

这段基类的代码,完整的实现了注解代码的寻找工作,通过在构造函数中先缓存所有的函数对象,然后将他们放到一个hash表中,然后在我们每次调用的时候,就插表找到对应的函数,反射调用。

最后,我们编写一个主类,来实现一个微型机器人的对话模式:

package com.abs.autocontext;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Random;

/**
 * 主函数类
 * Created by sxf on 15-3-15.
 */
public class Main {

    static String dj[] = new String[] {
        "你好,有什么我能帮你的吗?",
        "您有什么要我做的吗?",
        "有什么指示吗?",
        "需要我为您做点什么吗?"
    };

    public static void main(String[] args) {
        TestA a = new TestA("Sxf");
        Random r = new Random();
        String input = null;
        while (true) {
            System.err.println(dj[r.nextInt()]);
            BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
            try {
                input = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Method m = a.findMethod(input);
            AutoCall autocall = m.getDeclaredAnnotation(AutoCall.class);
            if (!"".equals(autocall.tip())) {
                System.err.println(autocall.tip());
                try {
                    input = br.readLine();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                String[] ss = input.split(" ");
                // 注意此处,可变参数列表的调用,必须将数组转成一个Object传进去,否则,自动被认为传了一堆参数
                a.CallFunc(m,(Object)ss);
            } else {
                a.CallFunc(m);
            }
            try {
                Thread.currentThread().sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
           

注意了,这里可变参数列表的调用,可能有些问题,必须把一整个String[]的数组,转成一个对象,这样才能正确的进行反射调用。

最后显示效果:

【Java二十周年】Java注解处理器

三、编译时注解的妙用

一般情况下,注解了解到这样也许就够用了,但我很负责的跟你说,Java的注解远比你想象中的强大。自从Java SE5开始,Java就引入了apt工具,可以对注解进行预处理,Java SE6,更是支持扩展注解处理器,并在编译时多趟处理,我们可以使用自定义注解处理器,在Java编译时,根据规则,生成新的Java代码。

自定义注解处理器也不是什么非常神秘的东西,它也是一段Java代码,它以Java源代码或编译好的代码为输入,新的Java程序为输出,实现Java代码的自动生成工作。但注意,已经写好的Java类是不能被修改的,想实现Java代码的动态修改还不是那么容易,真正的动态修改,应该是在编译时通过ASM这类代码生成库,对已有的代码进行修改,重新加载,才能做到真正的动态。

接口生成器实例

我们下面编写一个Java接口生成器来体验一下Java代码的自动生成功能。

我们这个项目可能并不实用,但也有一定的说明功能意义,我们的开发背景就是由于程序员很懒,连一个简单的接口都不愿意动手写,他想先写好一个类的实现,然后在某一个方法上面打一个注解,就有一个自动处理器将这个接口类生成出来。

import com.example.MyAnnotation;

/**
 * 接口生成实例
 * Created by sxf on 15-3-14.
 */
public class SomeOne {
    int k = ;

    public SomeOne(int k) {
        this.k = k;
    }

    @MyAnnotation
    public int getK() {
        Main main = context.b;
        return k;
    }

    @MyAnnotation
    public void printK() {
        printK();
    }

    @MyAnnotation
    public int HaveTest(int a) {
        return a + k;
    }


}
           

在打上

@MyAnnotation

注解的函数上,那么就会生成这样的接口:

public interface ISomeOne {
  int getK();

  void printK();

  int HaveTest(int param1);
}
           

这样我们需要先实现一个新的工程,这个工程就是为了开发一个代码生成器,然后我们把它打包成一个jar包,然后将这个jar包在我们需要用的位置引用。

我们首先创建一个简单的注解,用来标明功能:

package com.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
 * 将要被创建接口的方法
 * Created by sxf on 15-3-15.
 */
@Target(ElementType.METHOD)
public @interface MyAnnotation {
}
           

然后我们将创建注解处理器的核心类:

package com.example;

public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){
        super.init(env);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {
        return false;
     }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> strings = new TreeSet<String>();
        strings.add("com.example.MyAnnotation");
        return strings;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}
           

AbstractProcessor

这个类被称为抽象处理器类,每一个处理器都是从这个类继承。

init

这个方法在整个处理器被初始化的时候被调用。

process

在每一趟处理的时候被调用,由于我们处理器是一个递归处理的过程,新产生的代码,也可能保护能够被当前处理器处理的注解,所以采取的是多趟处理的方案。

getSupportedAnnotationTypes

返回能处理的注解的全名

getSupportedSourceVersion

返回能支持的代码版本

在Java 7中,你也可以使用注解来代替

getSupportedAnnotationTypes

getSupportedSourceVersion

,像这样:

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法注解全名的集合
 })
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
           

但从兼容性角度看,android 平台不建议使用这种注解的模式。

下面,我们为该类添加几个工具类:

private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment env){
        System.err.println("MyProcessor Run");
        super.init(env);
        elementUtils = env.getElementUtils();
        filer = env.getFiler();
        typeUtils = env.getTypeUtils();
        messager = env.getMessager();
    }
           

Elements:一个用来处理Element的工具类

Types:一个用来处理TypeMirror的工具类

Filer:这个工具可以支持向当前工程输出新的Java代码

Messager:可以让Javac编译器输出错误提示

然后我们编写process方法:

/**
 * Created by sxf on 15-3-15.
 */
package com.example;

import com.google.auto.service.AutoService;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.*;

public class MyProcessor extends AbstractProcessor {

    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment env){
        System.err.println("MyProcessor Run");
        super.init(env);
        elementUtils = env.getElementUtils();
        filer = env.getFiler();
        typeUtils = env.getTypeUtils();
        messager = env.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {
        System.err.println("MyProcessor Process");
        Map<String, MyAnnotatedClass> classmap = new HashMap<String, MyAnnotatedClass>();

        Set<? extends Element> elementSet = env.getElementsAnnotatedWith(MyAnnotation.class);
        // 获取可执行节点(函数)的方法,遍历所有标记了注解的语法元素
        for (Element e : elementSet) {
            if (e.getKind()!= ElementKind.METHOD) {
                error(e,"错误的注解类型,只有函数能够被该 @%s 注解处理", MyAnnotation.class.getSimpleName());
                return true;
            }

            ExecutableElement element = (ExecutableElement) e;
            // 将解析后的语法元素放置到自定义的数据结构中
            MyAnnotatedMethod mymethod = new MyAnnotatedMethod(element);
            String classname = mymethod.getSimpleClassName();

            // 将解析出的Class进行分类,同一类下的函数都生成一个接口
            MyAnnotatedClass myclass = classmap.get(classname);
            if (myclass == null) {
                PackageElement pkg = elementUtils.getPackageOf(element);
                myclass = new MyAnnotatedClass(pkg.getQualifiedName().toString(), classname);
                myclass.addMethod(mymethod);
                classmap.put(classname,myclass);
            } else
                myclass.addMethod(mymethod);

        }
        // 代码生成
        for (MyAnnotatedClass myclass : classmap.values()) {
            myclass.generateCode(elementUtils, filer);
        }
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> strings = new TreeSet<String>();
        strings.add("com.example.MyAnnotation");
        return strings;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    private void error(Element e, String msg, Object... args) {
        messager.printMessage(
                Diagnostic.Kind.ERROR,
                String.format(msg, args),
                e);
    }
}

           

这里我们添加了代码进行处理代码,这里的代码处理是遵循一定结构规范的,我们的Javac的编译器会首先将Java代码解析为抽象语法树(AST),而这个结构在处理器内部,就被表示为:

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   // TypeElement
    ){}
}
           

我们在处理代码时,就是在对这个抽象语法树进行遍历的操作,而每分析出一个合适的函数,就将这个函数的结构,例如函数的名字,所在的类,等等信息放置到一个类结构中:

package com.example;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;

/**
 * 被标记的注解方法
 * Created by sxf on 15-3-15.
 */
public class MyAnnotatedMethod {
    private ExecutableElement annotatedMethodElement;
    private String simpleMethodName;
    private String simpleClassName;

    private Class returnsType;
    private Class[] paramsType;

    public MyAnnotatedMethod(ExecutableElement annotatedMethodElement) {
        this.annotatedMethodElement = annotatedMethodElement;
        simpleMethodName = annotatedMethodElement.getSimpleName().toString();
        TypeElement parent = (TypeElement) annotatedMethodElement.getEnclosingElement();
        simpleClassName = parent.getQualifiedName().toString();

    }

    public ExecutableElement getAnnotatedMethodElement() {
        return annotatedMethodElement;
    }

    public String getSimpleMethodName() {
        return simpleMethodName;
    }

    public String getSimpleClassName() {
        return simpleClassName;
    }
}
           

但由于我们要生成接口,必须要获取函数所属类的信息,由于之前我们已经做个类的分类工作,这里我们就用这样一个类来描述我们的类结构,然后批量的进行代码生成工作:

package com.example;

import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

/**
 * 包含那些注解方法的类
 * Created by sxf on 15-3-15.
 */
public class MyAnnotatedClass {
    private String className;
    private String packageName;
    private List<MyAnnotatedMethod> methods = new LinkedList<MyAnnotatedMethod>();

    public MyAnnotatedClass(String packageName, String className) {
        this.className = className;
        this.packageName = packageName;
    }

    public void generateCode(Elements elementUtils, Filer filer) {
        TypeSpec.Builder myinterface = TypeSpec.interfaceBuilder("I" + className)
                .addModifiers(Modifier.PUBLIC);
        for (MyAnnotatedMethod m : methods) {
            MethodSpec.Builder mymethod =
                     MethodSpec.methodBuilder(m.getSimpleMethodName())
                    .addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
                    .returns(TypeName.get(m.getAnnotatedMethodElement().getReturnType()));

            int i = ;
            for (VariableElement e : m.getAnnotatedMethodElement().getParameters()) {
                mymethod.addParameter(TypeName.get(e.asType()),"param"+String.valueOf(i));
                ++i;
            }
            myinterface.addMethod(mymethod.build());
        }
        JavaFile javaFile = JavaFile.builder(packageName, myinterface.build()).build();
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void addMethod(MyAnnotatedMethod mymethod) {
        methods.add(mymethod);
    }
}
           

另外介绍一下,这里我们使用了一个javapoet的库进行代码生成工作,这个库是由原来著名的JavaWriter发展而来的,我们可以进行源代码的生成操作,而其使用方法也十分简单,而且和我们的注解处理器有很好的兼容性,提供了很多便利的接口,但这里,我们就不具体介绍javapoet的功能了,希望大家去其官网上了解这个库的使用方法,大部分都很简洁明了。

javapoet 的GitHub地址:https://github.com/square/javapoet

那么至此,我们已经开发完成了一个注解处理器,进行打包之后,就能被我们的javac识别为一个编译器组件了

但在这之前,我们还需要在打包之前,为其META-INF中添加一下注册信息,向我们的javac程序注册这个处理器:

【Java二十周年】Java注解处理器

新增一个资源文件夹,添加META-INF/services路径,然后在下面建立一个名字很长的文本文件:

javax.annotation.processing.Processor

然后在其中写入你要注册的处理器的完整名称,因为javac支持多个注解处理器,其实你可以在一个jar包中,打包许多处理器,然后在这个文件中,一行一个,将他们的完整名字写下来。

我们这个文件里就一行:

com.example.MyProcessor

好的,将我们的处理器和依赖库一同打包,这样,一个可用的处理器就写好了,使用时,只需引入这个jar包即可。

注意,如果你和我一样用Intellij的话,还需要做一下项目的配置:

【Java二十周年】Java注解处理器
【Java二十周年】Java注解处理器

看,我们要的接口已经自动生成出来了:

【Java二十周年】Java注解处理器