一、為什么需要自定義注解
有的時候我們需要程序在編譯或者運行時可以檢測到某些標記而進行一些特殊處理,可以通過自定義注解來實現。注解可以看作時一種特殊的標記,可以用在類,屬性,方法和包上,是一種能被添加到java源代碼中的元數據。
二、注解的原理
注解的本質是繼承了Annotation接口的特殊接口,⑦具體實現類是Java 運行時生成的動態代理類。而我們通過反射獲取注解時,返回的是Java 運行時生成的動態代理對象$Proxy1。通過代理對象調用自定義注解(接口)的方法,會最終調用AnnotationInvocationHandler 的invoke 方法。該方法會從memberValues 這個Map 中索引出對應的值。而memberValues 的來源是Java 常量池。
三、了解自定義注解
1.我們先看一段自定義注解的代碼,認識一下:
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnnotation {
// 姓名
String name() default "匿名";
// 地址
String adress() default "";
}
2.如何聲明自定義注解
修飾符: 必須為public,不寫默認為pubic
關鍵字: @interface,使用@interface自定義注解時,自動繼承了java.lang.annotation.Annotation接口
自定義注解名稱:自己命名
使用元注解修飾自定義注解:@Target,@Retention,@Document,@Inherited用來修飾注解
聲明配置參數:每一個方法實際上是聲明了一個配置參數。
3.@Target
表面該注解可以應用在何種java類型上(比如:類,屬性,方法,包等類型上),可以組合使用
Target類型 | 描述 |
---|---|
ElementType.TYPE | 應用於類、接口(包括注解類型)、枚舉 |
ElementType.FIELD | 應用於屬性(包括枚舉中的常量) |
ElementType.METHOD | 應用於方法 |
ElementType.PARAMETER | 應用於方法的形參 |
ElementType.CONSTRUCTOR | 應用於構造函數 |
ElementType.LOCAL_VARIABLE | 應用於局部變量 |
ElementType.ANNOTATION_TYPE | 應用於注解類型 |
ElementType.PACKAGE | 應用於包 |
ElementType.TYPE_PARAMETER | 1.8版本新增,應用於類型變量) |
ElementType.TYPE_USE | 1.8版本新增,應用於任何使用類型的語句中(例如聲明語句、泛型和強制轉換語句中的類型) |
4.@Retention
表面該注解的生命周期
生命周期類型 | 描述 |
---|---|
RetentionPolicy.RUNTIME | 由JVM 加載,包含在類文件中,在運行時可以被獲取到 |
RetentionPolicy.SOURCE | 編譯時被丟棄,不包含在類文件中 |
RetentionPolicy.CLASS | JVM加載時被丟棄,包含在類文件中,默認值 |
5.@Document
表明該注解標記的元素可以被Javadoc 或類似的工具文檔化
6.@Inherited
表明使用了@Inherited注解的注解,所標記的類的子類也會擁有這個注解
7.配置參數
- 其中的每一個方法就是一個配置參數
- 方法的名稱就是參數的名稱
- 返回值就是參數的類型(返回值只能是基本類型,Class,String,enum)
- 可以通過default來聲明參數的默認值
- 如果返回一個參數成員,一般參數名為value
- 配置參數必須要有值,經常我們用空字符串和0作為默認值
8.jdk1.8又提供了以下兩個元注解:
- @Native:指定字段是一個常量,其值引用native code。
- @Repeatable:注解上可以使用重復注解,即可以在一個地方可以重復使用同一個注解,像spring中的包掃描注解就使用了這個。
9.自定義注解的應用
四、條件注解
1.@Conditional注解
Conditional 是由 SpringFramework 提供的一個注解,位於 org.springframework.context.annotation 包內,定義如下。
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
Conditional 注解類里只有一個 value 屬性,需傳入一個 Condition 類型的數組,我們先來看看這個 Condition 接口長什么樣。
package org.springframework.context.annotation;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
其中,matches() 方法傳入的參數 ConditionContext 是專門為 Condition 而設計的一個接口類,可以從中獲取到Spring容器的以下對象信息:
當一個 Bean 被 Conditional 注解修飾時,Spring容器會對數組中所有 Condition 接口的 matches() 方法進行判斷,只有當其中所有 Condition 接口的 matches()方法都為 ture 時,才會創建 Bean 。
2.自定義Conditional
接下來,我們將以一個國際化 I18n Bean 動態創建為例(根據配置中的 i18n.lang 屬性值來動態地創建國際化 I18n Bean),對如何使用 Conditional 注解進行簡單舉例:
- 當 i18n.lang=zh_CN 就創建中文 I18nChs Bean
- 當 i18n.lang=en_US 就創建英文 I18nEng Bean
創建好的兩個 Condition 實現類 I18nChsCondition 和 I18nEngCondition 代碼如下
package com.lys.myannotation;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class I18nChsCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String lang = context.getEnvironment().getProperty("i18n.lang");
ConditionOutcome outCome = new ConditionOutcome("zh_CN".equals(lang), "i18n.lang=" + lang);
return outCome;
}
}
package com.lys.myannotation;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class I18nEngCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String lang = context.getEnvironment().getProperty("i18n.lang");
ConditionOutcome outCome = new ConditionOutcome("en_US".equals(lang), "i18n.lang=" + lang);
return outCome;
}
}
I18n 接口定義:
package com.lys.myannotation;
public interface I18n {
// 獲取 name 屬性的值
String i18n(String name);
}
I18n 接口的兩個實現類 I18nChs 和 I18nEng 定義如下。
package com.lys.myannotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Component
@Conditional(I18nChsCondition.class)
public class I18nChsImpl implements I18n {
Map<String, String> map = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("lang", "中文");
}
};
@Override
public String i18n(String name) {
return map.get(name);
}
}
package com.lys.myannotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Component
@Conditional(I18nEngCondition.class)
public class I18nEngImpl implements I18n {
Map<String, String> map = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("lang", "English");
}
};
@Override
public String i18n(String name) {
return map.get(name);
}
}
配置 application.properties 內容如下:
language : zh_CN/Chinese,en_US/America
i18n.lang=zh_CN
測試代碼如下:
package com.lys;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.lys.bean.Person;
import com.lys.myannotation.I18n;
@RunWith(SpringRunner.class)
@SpringBootTest
public class LysStartTests {
@Autowired
I18n i18n;
@Test
public void testConditional() {
System.out.println(i18n.getClass().getName());
System.out.println(i18n.i18n("lang"));
}
}
運行testConditional()測試方法,打印結果:
配置 application.properties 內容如下:
language : zh_CN/Chinese,en_US/America
i18n.lang=en_US
再次運行程序,打印結果:
為了書寫和調用方便,我們還可以把上面的條件定義成注解,以 I18nChsCondition 為例,定義代碼如下。
package com.lys.myannotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(I18nChsCondition.class)
public @interface I18nChs {
}
將 I18nChs 注解添加到 I18nChsImpl 上。
package com.lys.myannotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
@I18nChs
public class I18nChsImpl implements I18n {
Map<String, String> map = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("lang", "中文");
}
};
@Override
public String i18n(String name) {
return map.get(name);
}
}
3.SpringBoot 擴展注解
從上面的示例不難看出,如果要使用我們自定義條件類實現起來還是有點小麻煩的,不過比較慶幸的是, SpringBoot 在 Conditional 注解的基礎上已經提前為我們定義好了一系列功能豐富的注解,我們可以直接使用。
接下來我們使用 ConditionalOnProperty 注解來實現上面的國際化示例。
僅需修改 I18nChsImpl 和 I18nEngImpl 兩個實現組件類,其他代碼不變,程序執行結果與之前相同。
@Component
@ConditionalOnProperty(name = "i18n.lang", havingValue = "zh_CN", matchIfMissing = true)
public class I18nChsImpl implements I18n {//內容同上,此處省略}
@Component
@ConditionalOnProperty(name = "i18n.lang", havingValue = "en_US", matchIfMissing = false)
public class I18nEngImpl implements I18n {//內容同上,此處省略}