Java的世界中,也许你会有个疑问,为什么@Override
能够让编译器验证这个函数是否被有效重载,为什么Hibernate的注解能够使的数据库操作如此简便,今天,我们就来揭开注解的神秘面纱,了解一下Java编译器不为人知的一面。
注解的语法比较简单,除了@符号的使用之外,它基本与Java固有语法一致。Java SE5内置了三种标准注解:
@Override,表示当前的方法定义将覆盖超类中的方法。
@Deprecated,使用了注解为它的元素编译器将发出警告,因为注解@Deprecated是不赞成使用的代码,被弃用的代码。
@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中,你也可以使用注解来代替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 = 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/article/details/44275549