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

原创 2015年03月16日 10:39:46

这里写图片描述

一、序言

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(4)]);
            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(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

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

最后显示效果:
这里写图片描述

三、编译时注解的妙用

一般情况下,注解了解到这样也许就够用了,但我很负责的跟你说,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 = 0;

    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中,你也可以使用注解来代替getSupportedAnnotationTypesgetSupportedSourceVersion,像这样:

@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 = 1;
            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程序注册这个处理器:
这里写图片描述

新增一个资源文件夹,添加META-INF/services路径,然后在下面建立一个名字很长的文本文件:
javax.annotation.processing.Processor
然后在其中写入你要注册的处理器的完整名称,因为javac支持多个注解处理器,其实你可以在一个jar包中,打包许多处理器,然后在这个文件中,一行一个,将他们的完整名字写下来。
我们这个文件里就一行:
com.example.MyProcessor

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

注意,如果你和我一样用Intellij的话,还需要做一下项目的配置:
这里写图片描述
这里写图片描述

看,我们要的接口已经自动生成出来了:
这里写图片描述

版权声明:本文为 西风逍遥游 原创文章,转载请注明出处 西风世界 http://blog.csdn.net/xfxyy_sxfancy

相关文章推荐

Java语言使用注解处理器生成代码——第二部分:注解处理器

本文是我的“关于Java语言使用注解处理器生成代码”系列第二部分。在第一部分中(请阅读这里),我们介绍了什么是Java语言的注解,以及它们的几种常用方式。 现在,在这第二部分中,我们将介绍注解处理器...

Java注解与自定义注解处理器

所以本文就注解与自定义的注解处理器来学习注解。

Java注解处理器使用详解

在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor)。在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么;然后,我将一步一步实...

电商网站秒杀与抢购的系统架构

一、大规模并发带来的挑战 在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻而易举地陷入到异常状态。我...

Java注解处理器使用详解

在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor)。在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么;然后,我将一步一步实...

Java中实现自定义的注解处理器(Annotation Processor)

在之前的《简单实现ButterKnife的注解功能》中,使用了运行时的注解实现了通过编写注解绑定View与xml。由于运行时注解需要在Activity初始化中进行绑定操作,调用了大量反射相关代码,在界...
  • ucxiii
  • ucxiii
  • 2016-07-25 19:42
  • 5336

Java注解处理器使用详解

在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor)。在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么;然后,我将一步一步实...

Java注解处理器 - 五分钟快速入门

基本概念Java 注解(Annotation)分为两类:编译时(Compile time)处理的注解和在运行时(Runtime)通过反射机制运行处理的注解。本文将重点介绍在编译时(Compile ti...

深入理解Java注解处理器

1、代码示例 package com.annotation.annotation; import java.lang.annotation.Documented; import java.lang...

深入理解Java:注解(Annotation)--注解处理器

深入理解Java:注解(Annotation)--注解处理器   如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)