Java的註解

Java的註解(annotation)

\quad最先接觸註解的時候,仍是在繼承那裏講到覆蓋父類的方法的時候,子類覆蓋了父類的方法的時候:html

@Override
    public int hashCode() {
        return Objects.hash(XXX);
    }
複製代碼

\quad這個@Override註解用以標識這個方法覆蓋了父類的方法,其實把註解去掉也沒事。那麼問題就來了,註解在代碼中究竟是怎麼樣一種存在,既然無關緊要,爲何還用途這麼廣呢?
\quad 引用《Java編程思想》中的話來定義註解的話,就是註解(也被稱爲元數據)爲咱們在代碼中添加信息提供了一種形式化的方法,使咱們在稍後某個時刻很是方便地使用這些數據。其實說到底,註解就是類中的一小段信息,能夠攜帶參數,能夠在運行時被閱讀。可是要知道,這小段信息編譯器是不會去識別而且解析的,只是爲咱們去實現本身的邏輯所服務的。git

1.註解的基本語法

\quad 前面提到的@Override是Java SE5內置註解中的其中一種,在這先詳細介紹這三種內置註解:
程序員

  • @Override,表示當前的方法定義爲將覆蓋超類的方法。若是不當心拼寫錯誤,或者方法簽名對不上被覆蓋的方法,編譯器就會發出警告信息。
  • @Deprecated,表示被註解的內容已經廢棄,若是程序員使用了註解爲它的元素,那麼編譯器會發出警告信息。
  • @SuppressWarnings,關閉不當的編譯器警告信息。

\quad 那麼如何新建一個符合咱們需求的註解呢?與新建類同樣:github

語法以下:

public @interface SpringBootApplication {
}
複製代碼

注意:註解都隱含的繼承了Annotation類。
點擊運行的話,能夠看到在target目錄下,爲註解也生成了一個臨時文件 數據庫

這時候就能夠把這個註解加到任何地方了:

@SpringBootApplication
public class MyStringHashMapDecorator {
    @SpringBootApplication
    HashMap hashMap = new HashMap();

    @SpringBootApplication
    public void put(String key, String value) {
        hashMap.put(key, value);
    }
複製代碼

2.元註解

\quad但有時候咱們必須限制註解可以標記的位置,或者想保留註解到運行期間,這時候就須要元註解(標記註解的註解)了,Java目前只內置了五種元註解:編程

註解
含義
@Target
(默認狀況下是能夠標記在任何元素上)
表示該註解能夠用於什麼地方。可選範圍包括:
CONSTRUCTOR:構造器的聲明
FIELD:域聲明(包括enum)
LOCAL_VARIABLE:局部變量聲明
METHOD:方法聲明
PACKAGE:包聲明
PARAMETER:參數聲明
TYPE:類、接口(包括註解類型)或者enum枚舉聲明
ANNOTATION_TYPE:標記註解的註解
在JDK1.8以後,新添了兩個:
TYPE_USE:類型使用聲明
TYPE_PARAMETER:類型參數聲明
@Retention
(默認爲CLASS級別)
表示須要在什麼級別保存該註解信息。
可選的RetentionPolicy參數包括:
SOURCE:註解將會被編譯器丟棄
CLASS:註解在class文件中可用,但會被VM丟棄
RUNTIME:VM將在運行期也保留註解,所以能夠經過反射機制讀取註解的信息
@Document 將此註解包含在Javadoc中
@Inherited 容許子類繼承父類的註解
@Repeatable 容許註解重複

下面就逐一用例子演示一下上面的聲明:
  • 1.ElementType.Constructor-適用於構造器
public class MyStringHashMapDecorator {
    
    String name;
    
    @SpringBootApplication
    public MyStringHashMapDecorator(String name) {
        this.name = name;
    }

複製代碼
  • 2.ElementType.Field(這個Field最好仍是作成員變量講)
@SpringBootApplication
    public static final int number=1;
    @SpringBootApplication
    String name;
    @SpringBootApplication
    HashMap hashMap = new HashMap();

    enum spell{
        @SpringBootApplication
        a,
        b,
        c,
        d
    }
複製代碼
  • 3.ElementType.LOCAL_VARIABLE-適用於局部變量
public void put(String key, String value) {
        @SpringBootApplication
        int i=0;
    }
複製代碼
  • 4.ElementType.METHOD-適用於方法
@SpringBootApplication
    public void put(String key, String value) {
        
    }
複製代碼
  • 5.ElementType.Package-適用於包聲明
    其實在每一個包裏面均可以放一個package-info

能夠在其中添加註解:

@SpringBootApplication
package com.github.hcsp.test;
複製代碼
  • 6.ElementType.PARAMETER-適用於參數聲明
public void put(@SpringBootApplication String key, String value) {

    }
複製代碼
  • 7.ElementType.TYPE-適用於類,接口或enum聲明
    \quad類、接口沒什麼好說的,就是enum聲明,前面在FIELD中不是提到過嘛?其實否則,這裏的eunm是對整個枚舉類型而言的,而FIELD那裏只是對單個實例而言的。
@SpringBootApplication
    enum spell{

        a,
        b,
        c,
        d
    }
複製代碼
  • 8.ANNOTATION_TYPE-標記註解
@SpringBootApplication
public @interface SpringBootApplications {
    
}
複製代碼
  • 9.TYPE_USE-類型使用聲明
    能夠標註任何類型名稱。(不能做用於包聲明及方法聲明)
  • 10.TYPE_PARAMETER-對泛型的聲明
public class MyStringHashMapDecorator<@SpringBootApplication T>
複製代碼
  • @Repeatable用法

通常狀況下,重複聲明註解都是會報錯的: 緩存

全部這時候就要體現@Repeatable的價值了:
代碼以下

public @interface SpringBootApplications {
    SpringBootApplication[] value();
}

@Repeatable(SpringBootApplications.class)
public @interface SpringBootApplication {
    
}
複製代碼

這時候就不報錯了。

3.註解元素

在註解中一樣能夠聲明成員變量。可是對成員變量的類型有要求:bash

  • 全部的基本類型
  • String類型
  • Class
  • enum
  • Annotation

注意:不能使用Integer等裝箱類型,也不能使用Object。
同時,還能夠給註解元素設置默認值:
框架

public @interface SpringBootApplication {
    public int id() default 0;
    public String klass() default " ";
}
複製代碼

可是注意:默認值不能爲null,換句話說就是不能有不肯定的值。dom

4.註解實用

若是不人爲對註解進行處理,註解就相似於註釋了。因此,下面用兩個例子來介紹註解在平常中的用處:

  • 1.對標記爲@Log的方法實現動態字節碼加強
    假設咱們如今有這樣兩個方法:
public class AnnotationPractise {
    private static void queryDatabase(){
        System.out.println("queryDatabase...");
    }
    private static void insertDatabase(){
        System.out.println("insertDatabase...");
    }

    public static void main(String[] args) {
        queryDatabase();
        selectDatabase();
    }
}
複製代碼

\quad查詢數據庫和往數據庫中插入數值的操做,如今咱們想每次操做開始前,都顯示一下當時的時間,好比這樣:

14:51:58.210
queryDatabase...
14:51:58.210
14:51:58.211
selectDatabase...
14:51:58.211
複製代碼

\quad這並非什麼難事,在先後加上LocalTime.now便可,可是問題來了,若是我有成百上千個方法都要這樣作呢,興許某一天我不想要時間了,而是加一些別的話呢,沒有人會去一個一個改吧?因此這時候就能夠用上註解了。
\quad實現思路在於,新建一個Log註解,爲每一個須要添加額外信息的方法添加Log註解,接着在主函數中利用反射機制,拿到類中的全部方法,過濾出其中帶有Log標記的方法,使用動態字節碼加強技術,實現功能的擴展。

  • 新建一個Log註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)//保留到運行期間,不然不能經過反射拿到方法
public @interface Log {
}
複製代碼
  • 爲須要加強的方法添加註解
public class AnnotationPractise {
    @Log
    public void queryDatabase() {
        System.out.println("queryDatabase...");
    }

    @Log
    public void selectDatabase() {
        System.out.println("selectDatabase...");
    }


    public void foo() {
        System.out.println("noLog...");
    }
}

複製代碼

\quad接下來使用動態字節碼加強技術實現功能,這一步可選的方法有不少:
在這裏,使用byte-buddy實現,固然也有中文版的:Byte Buddy 教程,先引入ByteBuddy maven倉庫。接着根據教程中的代碼,修改爲符合本身需求的代碼:

/**
     * 實現動態加強字節碼
     * @return 返回新的AnnotationPractise實例
     * @throws NoSuchMethodException 方法沒有找到
     * @throws IllegalAccessException 訪問了private方法
     * @throws InvocationTargetException 被訪問的方法丟出了異常,可是沒有被接收
     * @throws InstantiationException 不能建立這樣一個實例,好比說是抽象類和接口
     */
    private static AnnotationPractise enhanceAnnotation() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        return new ByteBuddy()
                //動態的生成AnnotationPractise的子類
                .subclass(AnnotationPractise.class)
                //匹配帶Log註解的方法
                .method(ElementMatchers.isAnnotatedWith(Log.class))
                //將方法攔截並委託給LoggerInterceptor
                .intercept(MethodDelegation.to(LoggerInterceptor.class))
                //建立一個新的AnnotationPractise實例
                .make()
                .load(Main.class.getClassLoader())
                .getLoaded()
                .getConstructor()
                .newInstance();
    }
    
複製代碼
/**
     * 實現截取父類中帶Log註解的方法,而後添加輸出語句
     */
    public static class LoggerInterceptor {
        public static void log(@SuperCall Callable<Void> zuper)
                throws Exception {
            System.out.println(LocalTime.now());
            try {
                zuper.call();
            } finally {
                System.out.println(LocalTime.now());
            }
        }
    }
}
複製代碼

效果以下:

08:51:56.916
selectDatabase...
08:51:56.917
08:51:56.917
queryDatabase...
08:51:56.917

Process finished with exit code 0
複製代碼

\quad能夠看到,我沒有對源代碼作任何修改,就實現了對源代碼功能的加強。從這裏能夠看出,註解就是爲咱們本身的業務邏輯所服務的,再次印證了那句話「若是對註解不作處理,那麼註解也不會比註釋有用」。

  • 2.實現一個基於@Cache註解的裝飾器,可以將傳入的服務類的Class進行裝飾,使之具備緩存功能

\quad 意思就是模擬數據庫的查找操做,第一次查找的時候將獲取的數值加入到緩存中,若是第二次查找的仍是這個數則直接返回這個數。這樣就省去了重複查找同一個數的時間。
實現思路:
\quad 前部分與上例中相似,顯示截取類中帶有Cache註解的方法,對其實現動態字節碼加強。
新建註解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    // 標記緩存的時長(秒),默認60s
    int cacheSeconds() default 60;
}
複製代碼

新建兩個測試方法,一個帶註解一個不帶

public class DataService {
    /**
     * 根據數據ID查詢一列數據,有緩存。
     *
     * @param id 數據ID
     * @return 查詢到的數據列表
     */
    @Cache
    public List<Object> queryData(int id) {
        // 模擬一個查詢操做
        Random random = new Random();
        int size = random.nextInt(10) + 10;
        return IntStream.range(0, size)
                .mapToObj(i -> random.nextInt(10))
                .collect(Collectors.toList());
    }

    /**
     * 根據數據ID查詢一列數據,無緩存。
     *
     * @param id 數據ID
     * @return 查詢到的數據列表
     */
    public List<Object> queryDataWithoutCache(int id) {
        // 模擬一個查詢操做
        Random random = new Random();
        int size = random.nextInt(10) + 1;
        return IntStream.range(0, size)
                .mapToObj(i -> random.nextBoolean())
                .collect(Collectors.toList());
    }
}
複製代碼

使用ByteBuddy加強原方法:

@SuppressWarnings("unchecked")
    public static <T> Class<T> decorate(Class<T> klass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        return (Class<T>) new ByteBuddy()
                //子類化,爲原先的類添加新的功能
                .subclass(klass)
                .method(ElementMatchers.isAnnotatedWith(Cache.class))
                .intercept(MethodDelegation.to(CacheInterceptor.class))
                .make()
                .load(klass.getClassLoader())
                .getLoaded();
    }
複製代碼

攔截器中的具體內容:

public class CacheInterceptor {
    private static ConcurrentHashMap<CacheKey, CacheValue> hashMap = new ConcurrentHashMap<>();

    @RuntimeType //這個註解的做用在於當攔截方法發生時,ByteBuddy可以找到對應的方法去調用它
    public static Object cache(
            //聲明調用父類的方法
            @SuperCall Callable<Object> superCall,
            //當前正在被調用的方法
            @Origin Method method,
            //實體方法的對象
            @This Object object,
            //得到全部的參數
            @AllArguments Object[] arguments) throws Exception {
        CacheKey cacheKey = new CacheKey(method.getName(), object, arguments);
        CacheValue value = hashMap.get(cacheKey);
        if (value != null) {
            //緩存中存在對應的值直接返回
            if (isNotOvertime(method, value)) {
                return value.result;
            }
            return getObject(superCall,cacheKey);
        } else {
            //不然調用父類中的方法,拿到值
            //方法一:使用method.invoke(object,arguments)調用方法,這樣很慢
            //方法二:使用ByteBuddy提供的@SuperCall調用父類方法
            return getObject(superCall, cacheKey);
        }
    }

    private static boolean isNotOvertime(@Origin Method method, CacheValue value) {
        return (System.currentTimeMillis()-value.time)<method.getAnnotation(Cache.class).cacheSeconds()*1000;
    }

    private static Object getObject(@SuperCall Callable<Object> superCall, CacheKey cacheKey) throws Exception {
        Object realResult = superCall.call();
        long time = System.currentTimeMillis();
        CacheValue cacheValue = new CacheValue(realResult,time);
        hashMap.put(cacheKey, cacheValue);
        return realResult;
    }

    static class CacheValue{
        private Object result;
        private long time;

        public CacheValue(Object result, long time) {
            this.result = result;
            this.time = time;
        }
    }

    static class CacheKey {
        private String methodName;
        private Object thisObject;
        private Object[] arguments;

        public CacheKey(String methodName, Object thisObject, Object[] arguments) {
            this.methodName = methodName;
            this.thisObject = thisObject;
            this.arguments = arguments;
        }


        //Map的key遵循的是equals和hashcode約定,CacheKey必須重寫equals和hashCode方法
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            CacheKey cacheKey = (CacheKey) o;
            return Objects.equals(methodName, cacheKey.methodName) &&
                    Objects.equals(thisObject, cacheKey.thisObject) &&
                    Arrays.equals(arguments, cacheKey.arguments);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(methodName, thisObject);
            result = 31 * result + Arrays.hashCode(arguments);
            return result;
        }
    }
}

複製代碼

5.小結

\quad 其實照這樣看來,註解自己起不了多大的做用,關鍵是看程序員或者框架自己對註解的理解與使用。因此,一方面要熟記基本語法,另外一方面想深究的話能夠去看下框架對註解具體是怎麼處理的。

6.參考資料

1.《Java編程思想》[第四版] (美)Bruce Eckel 著;陳浩鵬譯.——北京:機械工業出版社.2007.6(2018.9重印)
2.掘金.《Java註解詳解》點擊此處查看源文章
3.博客園.《@SuppressWarning 抑制警告註解》點擊此處查看源文章

相關文章
相關標籤/搜索