程序員:想自己寫框架?不瞭解Java註解機制可不行

無論是在JDK還是框架中,註解都是很重要的一部分,我們使用過很多註解,但是你有真正去了解過他的實現原理麼?你有去自己寫過註解麼?


程序員:想自己寫框架?不瞭解Java註解機制可不行


概念

註解(Annotation),也叫元數據。一種代碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等的前面,用來對這些元素進行說明,註釋。

在JDK中定義了許多註解,其作用大致可以分為以下幾類:

  • 編寫文檔:通過代碼裡標識的元數據生成文檔【生成文檔doc文檔】
  • 代碼分析:通過代碼裡標識的元數據對代碼進行分析【使用反射】
  • 編譯檢查:通過代碼裡標識的元數據讓編譯器能夠實現基本的編譯檢查【Override】

註解功能的實現

我們以spring中比較常見的Autowired來舉例分析

創建註解

<code>@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

\t/**
\t * Declares whether the annotated dependency is required.
\t *

Defaults to {@code true}.
\t */
\tboolean required() default true;

}

/<code>

註解的創建看起來很像接口,需要用@interface來修飾,然後我們看到在Autowired註釋之上還有三個註釋來進行修飾。

他們三個都叫做“元註釋”,Jdk5所定義的源註釋還有@Retention、@Documented、@Inherited,這些類型和它們所支持的類在java.lang.annotation包中可以找到。

@Target

用於描述註解的使用範圍,也就是在什麼時候生效,一般情況下,我們的註釋可能在packages、types(類、接口、枚舉、Annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)等任何一個地方生效。

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type

* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}/<code>

注:比較有意思的是,@Target也被自己修飾,這可能有更深層的原理,在此不再深入

通過源碼我們看出@Target下有一個類型為ElementType[] 的值value(),進入ElementType[],這是一個枚舉類型,他提供了我們對於這個值的選項,可供選擇的值有:

<code>public enum ElementType {
/** 描述類、接口(包括註解類型) 或enum聲明 */
TYPE,

/** 描述域 */
FIELD,

/** 描述方法 */
METHOD,

/** 描述參數 */
PARAMETER,

/** 描述構造器*/
CONSTRUCTOR,

/** 描述本地值 */
LOCAL_VARIABLE,

/** 描述註解類型 */
ANNOTATION_TYPE,

/** 描述包 */
PACKAGE,

/**

* 描述類型參數
*
* @since 1.8
*/
TYPE_PARAMETER,

/**
* 一個類型的用戶
*
* @since 1.8
*/
TYPE_USE
}/<code>

在Autowired中我們給值是{ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE},也就是說,我們的這個註解可以用於構造函數、方法、參數、域,註解等等。

@Retention

用於描述註解的被保留的時間段,我們定義註解時有時候會希望它一直保存,而不是在編譯時就被拋棄,就可以用到這個註解。

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}/<code>

@Retention下有一個類型為Retention的值,可供選擇的值有:

<code>public enum RetentionPolicy {
/**
* 在源文件中有效(即源文件保留)

*/
SOURCE,

/**
* 在class文件中有效(即class保留)
*/
CLASS,

/**
* 在運行時有效(即運行時保留)
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}/<code>

我們再回頭看@Autowired,對應Retention中的取值是RetentionPolicy.RUNTIME,也就是說我們的Autowired註解是應行時有效的,這也正是我們預期的其在spring框架工作時的狀態。

@Documented

@Documented用於描述其它類型的annotation應該被作為被標註的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化。Documented是一個標記註解,沒有成員。

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}/<code>

@Inherited

此註解是一個標記註解,如果我們的註解被@Inherited註解,那麼我們的註解被用於一個類時,就說明我們的註解應當是這個類的子類

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}/<code>

註解中的值設定

註解中的值設定是可選擇的,並不是每一個註解都需要去設定值,比如我們的@Inherited註解僅作為一個標記註解,其中是沒有值的。同時,如果我們的註解中有值,那我們也可以去通過“default”關鍵字設置一個默認值,比如我們上面的@Autowired註解中required這個值,就有一個默認的值:true,我們在使用這個註解的時候,就可以不聲明這個值,採用他的默認值

小總結

看到這裡,我們應當明白幾件事

  • 註解應當被@Target標註,用以確定我們的註解的作用範圍
  • 註解應當被@Retention標註,用以確定我們的註解可以工作到什麼時候
  • 註解可選擇地去設置自己的值,也可以設置一個默認值,用以後來的工作

實現註解功能

我們想要用註解去配合被我們註解的類或其他來實現某種功能,那麼首先我們得明白如何將註解與被我們註解的類聯繫起來,我們都知道Autowired可以對類成員變量、方法及構造函數進行標註,讓 spring 完成 bean 自動裝配的工作,那麼Spring內部是如何進行對被Autowired註解的變量進行操作的呢?

首先我要你明白Spring的加載機制,你需要知道,所有被@Service標註的類都在初始化時被實例化。

我們來看一下Spring中實現@Autowired的邏輯代碼,該代碼在org.springframework.beans.factory.annotation的org.springframework.beans.factory.annotation包下,有興趣的可以自己去看一下

<code>\tprivate InjectionMetadata buildAutowiringMetadata(final Class> clazz) {
\t\tLinkedList<injectionmetadata.injectedelement> elements = new LinkedList<injectionmetadata.injectedelement>();
\t\tClass> targetClass = clazz;//需要處理的目標類

\t\tdo {
\t\t\tfinal LinkedList<injectionmetadata.injectedelement> currElements =
\t\t\t\t\tnew LinkedList<injectionmetadata.injectedelement>();
//通過反射獲取目標類的所有字段,遍歷所有字段,如果有字段用@Autowired註解,那就返回Autowired的相關屬性
\t\t\tReflectionUtils.doWithLocalFields(targetClass, new ReflectionUtils.FieldCallback() {
\t\t\t\t@Override
\t\t\t\tpublic void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
\t\t\t\t\tAnnotationAttributes ann = findAutowiredAnnotation(field);
\t\t\t\t\tif (ann != null) {//判斷Autowired註解是否是在static方法上
\t\t\t\t\t\tif (Modifier.isStatic(field.getModifiers())) {
\t\t\t\t\t\t\tif (logger.isWarnEnabled()) {
\t\t\t\t\t\t\t\tlogger.warn("Autowired annotation is not supported on static fields: " + field);

\t\t\t\t\t\t\t}
\t\t\t\t\t\t\treturn;
\t\t\t\t\t\t}
\t\t\t\t\t\tboolean required = determineRequiredStatus(ann);//判斷required
\t\t\t\t\t\tcurrElements.add(new AutowiredFieldElement(field, required));
\t\t\t\t\t}
\t\t\t\t}
\t\t\t});
//和上面一樣的邏輯,但是是通過反射處理類的method
\t\t\tReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() {
\t\t\t\t@Override
\t\t\t\tpublic void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
\t\t\t\t\tMethod bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
\t\t\t\t\tif (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
\t\t\t\t\t\treturn;
\t\t\t\t\t}
\t\t\t\t\tAnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
\t\t\t\t\tif (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
\t\t\t\t\t\tif (Modifier.isStatic(method.getModifiers())) {
\t\t\t\t\t\t\tif (logger.isWarnEnabled()) {
\t\t\t\t\t\t\t\tlogger.warn("Autowired annotation is not supported on static methods: " + method);
\t\t\t\t\t\t\t}
\t\t\t\t\t\t\treturn;
\t\t\t\t\t\t}
\t\t\t\t\t\tif (method.getParameterTypes().length == 0) {
\t\t\t\t\t\t\tif (logger.isWarnEnabled()) {
\t\t\t\t\t\t\t\tlogger.warn("Autowired annotation should only be used on methods with parameters: " +
\t\t\t\t\t\t\t\t\t\tmethod);
\t\t\t\t\t\t\t}
\t\t\t\t\t\t}
\t\t\t\t\t\tboolean required = determineRequiredStatus(ann);
\t\t\t\t\t\tPropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
\t\t\t\t\t\tcurrElements.add(new AutowiredMethodElement(method, required, pd));
\t\t\t\t\t}
\t\t\t\t}
\t\t\t});
//用@Autowired修飾的註解可能不止一個,因此都加在currElements這個容器裡面,一起處理
\t\t\telements.addAll(0, currElements);
\t\t\ttargetClass = targetClass.getSuperclass();
\t\t}
\t\twhile (targetClass != null && targetClass != Object.class);

\t\treturn new InjectionMetadata(clazz, elements);
\t}/<injectionmetadata.injectedelement>/<injectionmetadata.injectedelement>/<injectionmetadata.injectedelement>/<injectionmetadata.injectedelement>/<code>

加了註釋是不是很容易懂了?什麼?還不懂?別急,我這裡有一份簡化版的spring代碼

<code>/**
*@描述
*@方法名 populateBean
*@參數 [beanName, beanDefinition, beanWrapper]
*@返回值 void
*@創建人 Baldwin
*@創建時間 2020/3/9
*@修改人和其它信息
*/
private void populateBean(String beanName, BeanDefinition beanDefinition, BeanWrapper beanWrapper) {

Class> clazz = beanWrapper.getWrappedClass();

//獲得所有的成員變量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//如果沒有被Autowired註解的成員變量則直接跳過
if (!field.isAnnotationPresent(YzAutowired.class)) {
continue;
}

YzAutowired autowired = field.getAnnotation(YzAutowired.class);
//拿到需要注入的類名
String autowiredBeanName = autowired.value().trim();
if ("".equals(autowiredBeanName)) {
autowiredBeanName = field.getType().getName();
}

//強制訪問該成員變量
field.setAccessible(true);

try {
if (this.factoryBeanInstanceCache.get(autowiredBeanName) == null) {
continue;
}
//將容器中的實例注入到成員變量中
field.set(beanWrapper.getWrapperInstance(), this.factoryBeanInstanceCache.get(autowiredBeanName).getWrapperInstance());

} catch (IllegalAccessException e) {
e.printStackTrace();
}
}/<code>


上面這個是我手寫的簡版spring框架,雖然沒有完成,但是在這裡用來解釋@Autowired的實現機制已經是足夠的。

我們來看上面的代碼,其中YzAutowired對應著spring中的Autowired,我們來看一下他的邏輯步驟

  • 獲取目標類:通過反射獲取所有的目標類,具體實現過程,有需要的話我再詳解
  • 獲取目標類的所有成員變量進行遍歷:這一步是為了得到那些被YzAutowired註解的變量
  • 判斷:如果當前變量沒有被YzAutowired註解,那麼下一個,如果有被註解,那麼開始我們實現需要的功能
  • 實現功能:首先我們註解的時候是有參數的,我們可以通過註解參數名的方式來獲取這個註解的參數值,然後去使用它,然後就是我們的功能邏輯代碼了

到這裡,我們的註解與被修飾這之間已經聯繫上,而且也實現了我們預期的功能了

小總結

在功能實現這一部分我們最終應該要做的事主要有兩件

  • 獲取被註解者
  • 獲取值並且完成功能實現


程序員:想自己寫框架?不瞭解Java註解機制可不行


註解實戰:創建一個自己的註解

看完上面的內容,我相信你或許對註解已經有了一定的瞭解,現在可以跟著作者一起來創建一個註解並且實現一個功能。功能要求比較簡單,就是為通過註解為某個變量注入一個值。

創建註解:InjectInt

<code>import java.lang.annotation.*;

@Target({ElementType.FIELD})//作用於域
@Retention(RetentionPolicy.RUNTIME)//存在於運行時
@Documented
public @interface InjectInt {
int value() default 0;//默認值為0
}/<code>

實現註解功能

<code>import java.lang.reflect.Field;

/**
* 類描述
*
* @author: 12405
* @date: 2020/3/27-23:48
*/
public class DoInject {
public static void inject(){
//反射獲取對象類的class

Class clazz = AnnotationDemo.class;
//獲得所有的成員變量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields){

//如果沒有被InjectInt註解的成員變量則直接跳過
if (!field.isAnnotationPresent(InjectInt.class)) {
continue;
}

//拿到當前變量的註解
InjectInt injectInt = field.getAnnotation(InjectInt.class);

//拿到註解值
int value = injectInt.value();

//強制訪問該成員變量
field.setAccessible(true);

//將值注入
try {
field.setInt(Integer.class,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
應用註解
/**
* 類描述
*
* @author: 12405
* @date: 2020/3/27-23:37
*/
public class AnnotationDemo {

//註解並設置值
@InjectInt(value = 12) static int m;

public static void main(String[] args) {
//開啟註解功能
DoInject.inject();


System.out.println(m);
}
}/<code>

輸出

<code>"C:\\Program Files\\Java\\jdk1.8.0_171\\bin\\java.exe" "-javaagent:E:\\tools\\IntelliJ IDEA 2019.3.3\\lib\\idea_rt.jar=61875:E:\\tools\\IntelliJ IDEA 2019.3.3\\bin" -Dfile.encoding=UTF-8 -classpath "C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\charsets.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\deploy.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\access-bridge-64.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\cldrdata.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\dnsns.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\jaccess.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\jfxrt.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\localedata.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\nashorn.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\sunec.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\sunjce_provider.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\sunmscapi.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\sunpkcs11.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\ext\\zipfs.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\javaws.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\jce.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\jfr.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\jfxswt.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\jsse.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\management-agent.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\plugin.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\resources.jar;C:\\Program Files\\Java\\jdk1.8.0_171\\jre\\lib\\rt.jar;E:\\Workspaces\\IdeaProjects\\DemoTest\\out\\production\\DemoTest" cn.yzstu.annotation.AnnotationDemo

Process finished with exit code 0/<code>

這樣我們就完成了一個簡單的註解的實現,再次創建int值時可以使用@InjectInt來實現賦值,當然這個功能並不是實用性功能,只是拋磚引玉來給大家展示註解的實現。

總結

最後總結一下註解實現的三部曲:

  • 創建註解,選擇合適的作用域和生存時機
  • 實現註解邏輯,這一步需要我們找到註解的位置
  • 開啟註解,讓註解功能實現

實際上,我們在正常工作時需要自己創建註解的時候並不多,大多數時候只需要我們理解註解的用法即可,但是註解的應用在造輪子時是非常重要的,所以如果我們希望自己能夠有朝一日向大佬們一樣去自己造輪子的話,還是要多瞭解一些註解的知識,同時,瞭解註解機制的實現還可以讓我們更好的瞭解現有框架的實現。


程序員:想自己寫框架?不瞭解Java註解機制可不行


分享到:


相關文章: