天天看點

【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注解處理器