一、序言
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參數有: :構造器的聲明 :域聲明(包括enum執行個體) :局部變量聲明 :方法聲明 :包聲明 :參數聲明 :類、接口(包括注解類型)或 聲明 |
@Retention | 表示需要在什麼級别儲存該注解資訊。可選的RetentionPolicy參數包括: :注解将被編譯器丢棄 :注解在class檔案中可用,但會被VM丢棄 :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 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程式注冊這個處理器:
新增一個資源檔案夾,添加META-INF/services路徑,然後在下面建立一個名字很長的文本檔案:
javax.annotation.processing.Processor
然後在其中寫入你要注冊的處理器的完整名稱,因為javac支援多個注解處理器,其實你可以在一個jar包中,打包許多處理器,然後在這個檔案中,一行一個,将他們的完整名字寫下來。
我們這個檔案裡就一行:
com.example.MyProcessor
好的,将我們的處理器和依賴庫一同打包,這樣,一個可用的處理器就寫好了,使用時,隻需引入這個jar包即可。
注意,如果你和我一樣用Intellij的話,還需要做一下項目的配置:
看,我們要的接口已經自動生成出來了: