最先接觸註解的時候,仍是在繼承那裏講到覆蓋父類的方法的時候,子類覆蓋了父類的方法的時候:html
@Override
public int hashCode() {
return Objects.hash(XXX);
}
複製代碼
這個@Override註解用以標識這個方法覆蓋了父類的方法,其實把註解去掉也沒事。那麼問題就來了,註解在代碼中究竟是怎麼樣一種存在,既然無關緊要,爲何還用途這麼廣呢?
引用《Java編程思想》中的話來定義註解的話,就是註解(也被稱爲元數據)爲咱們在代碼中添加信息提供了一種形式化的方法,使咱們在稍後某個時刻很是方便地使用這些數據。其實說到底,註解就是類中的一小段信息,能夠攜帶參數,能夠在運行時被閱讀。可是要知道,這小段信息編譯器是不會去識別而且解析的,只是爲咱們去實現本身的邏輯所服務的。git
前面提到的@Override是Java SE5內置註解中的其中一種,在這先詳細介紹這三種內置註解:
程序員
那麼如何新建一個符合咱們需求的註解呢?與新建類同樣: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);
}
複製代碼
但有時候咱們必須限制註解可以標記的位置,或者想保留註解到運行期間,這時候就須要元註解(標記註解的註解)了,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 | 容許註解重複 |
public class MyStringHashMapDecorator {
String name;
@SpringBootApplication
public MyStringHashMapDecorator(String name) {
this.name = name;
}
複製代碼
@SpringBootApplication
public static final int number=1;
@SpringBootApplication
String name;
@SpringBootApplication
HashMap hashMap = new HashMap();
enum spell{
@SpringBootApplication
a,
b,
c,
d
}
複製代碼
public void put(String key, String value) {
@SpringBootApplication
int i=0;
}
複製代碼
@SpringBootApplication
public void put(String key, String value) {
}
複製代碼
@SpringBootApplication
package com.github.hcsp.test;
複製代碼
public void put(@SpringBootApplication String key, String value) {
}
複製代碼
@SpringBootApplication
enum spell{
a,
b,
c,
d
}
複製代碼
@SpringBootApplication
public @interface SpringBootApplications {
}
複製代碼
public class MyStringHashMapDecorator<@SpringBootApplication T>
複製代碼
通常狀況下,重複聲明註解都是會報錯的: 緩存
public @interface SpringBootApplications {
SpringBootApplication[] value();
}
@Repeatable(SpringBootApplications.class)
public @interface SpringBootApplication {
}
複製代碼
在註解中一樣能夠聲明成員變量。可是對成員變量的類型有要求:bash
注意:不能使用Integer等裝箱類型,也不能使用Object。
同時,還能夠給註解元素設置默認值:
框架
public @interface SpringBootApplication {
public int id() default 0;
public String klass() default " ";
}
複製代碼
可是注意:默認值不能爲null,換句話說就是不能有不肯定的值。dom
若是不人爲對註解進行處理,註解就相似於註釋了。因此,下面用兩個例子來介紹註解在平常中的用處:
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();
}
}
複製代碼
查詢數據庫和往數據庫中插入數值的操做,如今咱們想每次操做開始前,都顯示一下當時的時間,好比這樣:
14:51:58.210
queryDatabase...
14:51:58.210
14:51:58.211
selectDatabase...
14:51:58.211
複製代碼
這並非什麼難事,在先後加上LocalTime.now便可,可是問題來了,若是我有成百上千個方法都要這樣作呢,興許某一天我不想要時間了,而是加一些別的話呢,沒有人會去一個一個改吧?因此這時候就能夠用上註解了。
實現思路在於,新建一個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...");
}
}
複製代碼
接下來使用動態字節碼加強技術實現功能,這一步可選的方法有不少:
在這裏,使用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
複製代碼
能夠看到,我沒有對源代碼作任何修改,就實現了對源代碼功能的加強。從這裏能夠看出,註解就是爲咱們本身的業務邏輯所服務的,再次印證了那句話「若是對註解不作處理,那麼註解也不會比註釋有用」。
意思就是模擬數據庫的查找操做,第一次查找的時候將獲取的數值加入到緩存中,若是第二次查找的仍是這個數則直接返回這個數。這樣就省去了重複查找同一個數的時間。
實現思路:
前部分與上例中相似,顯示截取類中帶有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;
}
}
}
複製代碼
其實照這樣看來,註解自己起不了多大的做用,關鍵是看程序員或者框架自己對註解的理解與使用。因此,一方面要熟記基本語法,另外一方面想深究的話能夠去看下框架對註解具體是怎麼處理的。
1.《Java編程思想》[第四版] (美)Bruce Eckel 著;陳浩鵬譯.——北京:機械工業出版社.2007.6(2018.9重印)
2.掘金.《Java註解詳解》點擊此處查看源文章
3.博客園.《@SuppressWarning 抑制警告註解》點擊此處查看源文章