Java拾遺:015 - Java註解與自定義註解

Java註解

註解(Annontation)是Java5開始引入的新特徵,是那些插入在源碼中的程序可讀的註釋信息。註解信息不會改變程序的編譯方式和運行方式(反射纔會),實際上若是不使用反射解釋(能夠理解爲解析、提取等,找不到合適的詞來描述這一動做)註解信息,註解就不會對程序有任何影響。java

註解做用

註解自己是無害(對程序無影響)的,但咱們會經過反射來解釋註解,並影響程序行爲。 註解通常用於IDE靜態檢查代碼是否符合某些規範,有些註解能夠用於生成文檔,應用中用得更多的是能夠動態改變程序行爲的註解,如:Spring中的@Cacheable等,也有一些僅僅只是一個標記(沒有任何屬性)。 實際上註解種類繁多且應用普遍,並且對代碼的侵入性相對較小(不解釋就不生效)。git

實現原理

註解本質是一個繼承了Annotation的特殊接口,咱們能夠用反射從類、方法、字段、參數等對象中取得它們的信息,若是須要實現註解申明的功能,就須要使用反射API解釋註解信息,根據註解提供的標記(註解自己)、屬性等來動態改變程序行爲,如:Spring的@Cacheable註解,後面咱們會模擬這一過程。實際應用中不太可能在業務代碼中寫一堆反射代碼,因此咱們一般會用動態代理的方式,爲目標類(接口)生成應用了註解的動態代理,來簡化應用過程。程序員

元註解

咱們在編寫註解時並不是無章可循,須要藉助一些基礎的註解來實現,這些註解被稱做元註解,java.lang.annotation提供了四種元註解:github

  • @Documented 這是一個標記註解(沒有任何屬性),用於描述其它類型的annotation應該被做爲被標註的程序成員的公共API,所以能夠被像javadoc這樣的工具文檔化
  • @Inherited 這也是一個標記註解,被其標註的類註解是能夠被繼承的,例如:在父類中使用了被@Inherited註解標記的註解,那麼其子類將自動繼承該註解
  • @Retention 描述註解的生命週期,其值由RetentionPolicy枚舉類決定,包含:SOURCE(表示只在源碼階段有效,編譯階段丟棄)、CLASS(在類加載的時候丟棄)、RUNTIME(始終保留,運行期也存在,因此咱們能夠在運行期使用反射來解釋這類註解,通常自定義註解時會使用這種方式)
  • @Target 表示註解應用範圍,其值由ElementType枚舉類決定,包含:TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE(見名知意,這裏就再也不解釋了)。

JDK經常使用註解

  • @Override 用於IDE檢查子類是否正確重寫了父類方法,建議在編寫子類時使用,避免手誤等問題。
  • @Deprecated 用於標記類、接口、方法等不推薦使用,表示後續再也不支持(可能會移除),通常遇到這種註解,那麼應儘可能避免使用被其標記的類、接口、方法,避免後續升級版本過程當中形成代碼不兼容。
  • @SuppressWarnings 用於去除一些警告,好比:@SuppressWarnings("unchecked")

JDK中提供的註解多用於標記(提供給IDE檢查用),通常推薦使用。緩存

經常使用框架註解

註解由於使用方法,因此在框架和庫中被廣爲使用,典型的像Spring、Mybatis等。併發

Spring
  • @Component
  • @Controller
  • @Service
  • @Repository
  • @Autowired
  • @RequestMapping
  • ... ... Spring生態體系(Spring Framework、Spring MVC、Spring Boot、Spring Cloud等)中的框架大量使用的註解,這裏再也不一一列舉
Mybatis
  • @Insert
  • @Select
  • @Update
  • @Delete
  • @Param
  • @Results
  • @Result
  • ... ... Mybatis裏一樣使用了大量的註解,但我的不太推薦使用相似@Insert這樣的註解(該註解用於編寫插入SQL語句)來實現業務邏輯,SQL與Java代碼耦合在一塊兒,這跟不使用註解直接把SQL寫在Java代碼中也沒什麼分別了,相比之下寫在XML方便統一管理會更爲合適(以上爲我的愚見,不喜勿噴)。

自定義註解

除了幾個元註解,JDK及開源框架中的註解也都是相應的程序員來實現的,有些註解確實很實用,那麼在咱們平常開發中,有些功能和特性不妨用自定義註解來實現,代碼會更優雅,複用程度也會更高(我的以爲相似Cloneable、Serializable這樣的標記接口,換成註解來實現會不會更優雅)。 下面以緩存註解爲例演示自定義註解的定義及解釋(應用)過程。app

定義緩存及驅逐緩存註解

定義兩個註解@Cacheable@CacheEvict分別描述緩存和刪除緩存邏輯(只做演示用,完整功能請參考Spring的實現)框架

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {

    /**
     * 緩存前綴
     *
     * @return
     */
    String prefix() default "";

    /**
     * 緩存前綴,至關於prefix的別名,value表示是一個默認屬性(當只有這一個屬性時,能夠省略屬性名)
     *
     * @return
     */
    String value() default "";

    /**
     * 緩存版本
     *
     * @return
     */
    int version() default 0;

}

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheEvict {

    /**
     * 緩存前綴
     * @return
     */
    String prefix() default "";

    /**
     * 緩存前綴,至關於prefix的別名,value表示是一個默認屬性(當只有這一個屬性時,能夠省略屬性名)
     * @return
     */
    String value() default "";

    /**
     * 緩存版本
     * @return
     */
    int version() default 0;

}

咱們有一個業務接口及實現dom

public interface UserService {

    @Cacheable(prefix = "user", version = 16)
    String get(long userId);

    @CacheEvict(prefix = "user", version = 16)
    void update(long userId, String name);

}

public class UserServiceImpl implements UserService {

    @Override
    public String get(long userId) {
        System.out.println("執行查詢邏輯!");
        // 隨機休眠[0, 256)毫秒模擬程序實際執行過程
        try {
            Thread.sleep(new Random().nextLong() & 255L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return String.format("user-%d", userId);
    }

    @Override
    public void update(long userId, String name) {
        System.out.println("執行更新邏輯!");
    }
}

測試一下這個業務實現ide

private UserService service;

    @Before
    public void init() {
        service = new UserServiceImpl();
    }

    @Test
    public void get() throws Exception {
        long time = System.currentTimeMillis();
        String name = service.get(10086L);
        System.out.println(String.format("程序執行耗時:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);
    }

從測試結果來看,方法會執行實現類中的邏輯,性能相對較差(使用休眠模擬,在[0, 256)毫秒範圍內)。

使用反射與動態代理解釋並應用自定義註解

實際我在接口上添加了緩存註解,因此須要使用反射解釋該註解,應用緩存來優化業務接口。

使用反射解釋註解

private UserService service;

    @Before
    public void init() {
        service = new UserServiceImpl();
    }

    @Test
    public void cache() throws NoSuchMethodException {
        // 假設HashMap是咱們的緩存
        HashMap<String, Object> cache = new HashMap<>();

        // 假設咱們調用是像下面這樣的
        // String name = service.get(10086L);
        String name = null;

        // 使用反射獲取方法上的註解
        Method method = service.getClass().getDeclaredMethod("get", long.class);
        Cacheable cacheable = method.getAnnotation(Cacheable.class);
        if (cacheable != null) {
            // 解釋該註解裏的配置項
            String prefix = cacheable.prefix();
            if (prefix.length() == 0) {
                prefix = cacheable.value();
            }
            // 當設置了緩存鍵
            if (prefix.length() > 0) {
                // 1. 繼續取出version等信息,這裏簡化處理,忽略這兩項
                int version = cacheable.version();
                // 2. 設置了緩存鍵,因此將方法執行結果緩存(若是緩存中未命中)
                String key = String.format("%s:%d:%d", prefix, 10086, version);
                if (cache.containsKey(key)) {
                    name = (String) cache.get(key);
                } else {
                    name = service.get(10086L);
                    cache.put(key, name);
                }
            }

        } else {
            name = service.get(10086L);
        }

        assertEquals("user-10086", name);

    }

代碼描述的使用反射解釋註解和應用註解的過程,但在實際開發中,不會在業務代碼中夾雜這麼多的反射代碼,因此咱們把它封裝成動態代理工廠類,簡化業務端代碼。

使用動態代理封裝註解解釋過程

定義一個動態代理工廠類,封裝動態代理生成過程,動態代理代碼裏解釋了緩存註解,應實現了其聲明的功能。

public class CacheProxyFactory {

    /**
     * 僅做測試,這裏不考慮併發狀況
     */
    private static final HashMap<String, Object> CACHE_STORAGE = new HashMap<>();

    public static final <T> T createProxyInstance(T target) {

        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new CacheInvocationHandler(target));
    }

    private static class CacheInvocationHandler<T> implements InvocationHandler {

        private final T target;

        public CacheInvocationHandler(T target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object r = null;
            // 從target和method中提取註解信息
            Cacheable cacheable = method.getAnnotation(Cacheable.class);
            CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class);
            if (cacheable != null) {
                r = cache(cacheable, method, args);
            } else if (cacheEvict != null) {
                r = remove(cacheEvict, method, args);
            } else {
                r = method.invoke(target, args);
            }

            return r;
        }

        /**
         * 處理@Cacheable註解
         *
         * @param cacheable
         * @param method
         * @param args
         * @return
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        private Object cache(Cacheable cacheable, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            Object r = null;
            // 解釋該註解裏的配置項
            String prefix = cacheable.prefix();
            if (prefix.length() == 0) {
                prefix = cacheable.value();
            }
            // 當設置了緩存鍵
            if (prefix.length() > 0) {
                // 1. 繼續取出version等信息,這裏簡化處理,忽略這兩項
                int version = cacheable.version();
                // 2. 設置了緩存鍵,因此將方法執行結果緩存(若是緩存中未命中)
                String key = String.format("%s:%d:%d", prefix, 10086, version);
                if (CACHE_STORAGE.containsKey(key)) {
                    r = CACHE_STORAGE.get(key);
                } else {
                    r = method.invoke(target, args);
                    CACHE_STORAGE.put(key, r);
                }
            } else {
                // 應該拋出異常(使用該註解,必須配置value或prefix屬性)
            }
            return r;
        }

        /**
         * 處理@CacheEvict註解
         *
         * @param cacheEvict
         * @param method
         * @param args
         * @return
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        private Object remove(CacheEvict cacheEvict, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            // 解釋該註解裏的配置項
            String prefix = cacheEvict.prefix();
            if (prefix.length() == 0) {
                prefix = cacheEvict.value();
            }
            if (prefix.length() > 0) {
                int version = cacheEvict.version();
                CACHE_STORAGE.remove(String.format("%s:%d:%d", prefix, 10086, version));
            } else {
                // 應該拋出異常(使用該註解,必須配置value或prefix屬性)
            }
            return method.invoke(target, args);
        }

    }
}

測試應用了緩存註解的代理對原業務接口性能的提高

private UserService service;

    @Before
    public void init() {
        service = new UserServiceImpl();
    }

    @Test
    public void proxy() {

        // 生成代理
        service = CacheProxyFactory.createProxyInstance(service);

        System.out.println("-- 1 --");
        long time = System.currentTimeMillis();
        String name = service.get(10086L);
        // 程序執行耗時:112毫秒
        System.out.println(String.format("程序執行耗時:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);

        System.out.println("-- 2 --");
        time = System.currentTimeMillis();
        name = service.get(10086L);
        // 程序執行耗時:0毫秒
        System.out.println(String.format("程序執行耗時:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);

        // 執行更新方法移除緩存(請忽略實際更新邏輯)
        service.update(10086L, "Peter");

        System.out.println("-- 3 --");
        time = System.currentTimeMillis();
        name = service.get(10086L);
        // 程序執行耗時:243毫秒
        System.out.println(String.format("程序執行耗時:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);
    }

經過測試結果能夠看出,第二次調用get方法時,直接走了緩存,因此性能有了大幅提高(實際提高效果視緩存的實現方案而定)。而且在執行update方法後,緩存被清空,再次調用get方法時,又從新初始化了緩存,從而實現了完整的@Cacheable和@CacheEvict註解功能。 這套緩存註解僅僅是從概念是模擬了Spring的緩存註解,相比之下,Spring提供了更完整的功能和程序健壯性,因此應用開發中推薦使用。

結語

自定義註解與反射和動態代理是緊密相連的,因此要掌握自定義註解,反射和動態代理技術是前置條件,不瞭解的請閱讀筆者前面的文章

源碼倉庫:

相關文章
相關標籤/搜索