本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營鏈接:http://item.jd.com/12299018.html

上節我們探討了反射,反射相關的類中都有方法獲取注解信息,我們在前面章節中也多次提到過注解,注解到底是什么呢?
在Java中,注解就是給程序添加一些信息,用字符@開頭,這些信息用於修飾它后面緊挨着的其他代碼元素,比如類、接口、字段、方法、方法中的參數、構造方法等,注解可以被編譯器、程序運行時、和其他工具使用,用於增強或修改程序行為等。這么說比較抽象,下面我們會具體來看,先來看Java的一些內置注解。
內置注解
Java內置了一些常用注解,比如:@Override、@Deprecated、@SuppressWarnings,我們簡要介紹下。
@Override
@Override修飾一個方法,表示該方法不是當前類首先聲明的,而是在某個父類或實現的接口中聲明的,當前類"重寫"了該方法,比如:
static class Base { public void action() {}; } static class Child extends Base { @Override public void action(){ System.out.println("child action"); } @Override public String toString() { return "child"; } }
Child的action()重寫了父類Base中的action(),toString()重寫了Object類中的toString()。這個注解不寫也不會改變這些方法是"重寫"的本質,那有什么用呢?它可以減少一些編程錯誤。如果方法有Override注解,但沒有任何父類或實現的接口聲明該方法,則編譯器會報錯,強制程序員修復該問題。比如,在上面的例子中,如果程序員修改了Base方法中的action方法定義,變為了:
static class Base { public void doAction() {}; }
但是,程序員忘記了修改Child方法,如果沒有Override注解,編譯器不會報告任何錯誤,它會認為action方法是Child新加的方法,doAction會調用父類的方法,這與程序員的期望是不符的,而有了Override注解,編譯器就會報告錯誤。所以,如果方法是在父類或接口中定義的,加上@Override吧,讓編譯器幫你減少錯誤。
@Deprecated
@Deprecated可以修飾的范圍很廣,包括類、方法、字段、參數等,它表示對應的代碼已經過時了,程序員不應該使用它,不過,它是一種警告,而不是強制性的,在IDE如Eclipse中,會給Deprecated元素加一條刪除線以示警告,比如,Date中很多方法就過時了:
@Deprecated public Date(int year, int month, int date) @Deprecated public int getYear()
調用這些方法,編譯器也會顯示刪除線並警告,比如:

在聲明元素為@Deprecated時,應該用Java文檔注釋的方式同時說明替代方案,就像Date中的API文檔那樣,在調用@Deprecated方法時,應該先考慮其建議的替代方案。
@SuppressWarnings
@SuppressWarnings表示壓制Java的編譯警告,它有一個必填參數,表示壓制哪種類型的警告,它也可以修飾大部分代碼元素,在更大范圍的修飾也會對內部元素起效,比如,在類上的注解會影響到方法,在方法上的注解會影響到代碼行。對於上面Date方法的調用,如果不希望顯示警告,可以這樣:
@SuppressWarnings({"deprecation","unused"})
public static void main(String[] args) {
Date date = new Date(2017, 4, 12);
int year = date.getYear();
}
除了這些內置注解,Java並沒有給我們提供更多的可以直接使用的注解,我們日常開發中使用的注解基本都是自定義的,不過,一般也不是我們定義的,而是由各種框架和庫定義的,我們主要還是根據它們的文檔直接使用。
框架和庫的注解
各種框架和庫定義了大量的注解,程序員使用這些注解配置框架和庫,與它們進行交互,我們看一些例子。
Jackson
在63節,我們介紹了通用的序列化庫Jackson,並介紹了如何利用注解對序列化進行定制,比如:
- 使用@JsonIgnore和@JsonIgnoreProperties配置忽略字段
- 使用@JsonManagedReference和@JsonBackReference配置互相引用關系
- 使用@JsonProperty和@JsonFormat配置字段的名稱和格式等
在Java提供注解功能之前,同樣的配置功能也是可以實現的,一般通過配置文件實現,但是配置項和要配置的程序元素不在一個地方,難以管理和維護,使用注解就簡單多了,代碼和配置放在一起,一目了然,易於理解和維護。
依賴注入容器
現代Java開發經常利用某種框架管理對象的生命周期及其依賴關系,這個框架一般稱為DI(Dependency Injection)容器,DI是指依賴注入,流行的框架有Spring、Guice等,在使用這些框架時,程序員一般不通過new創建對象,而是由容器管理對象的創建,對於依賴的服務,也不需要自己管理,而是使用注解表達依賴關系。這么做的好處有很多,代碼更為簡單,也更為靈活,比如容器可以根據配置返回一個動態代理,實現AOP,這部分我們后續章節再介紹。
看個簡單的例子,Guice定義了Inject注解,可以使用它表達依賴關系,比如像下面這樣:
public class OrderService { @Inject UserService userService; @Inject ProductService productService; //.... }
Servlet 3.0
Servlet是Java為Web應用提供的技術框架,早期的Servlet只能在web.xml中進行配置,而Servlet 3.0則開始支持注解,可以使用@WebServlet配置一個類為Servlet,比如:
@WebServlet(urlPatterns = "/async", asyncSupported = true) public class AsyncDemoServlet extends HttpServlet {...}
Web應用框架
在Web開發中,典型的架構都是MVC(Model-View-Controller),典型的需求是配置哪個方法處理哪個URL的什么HTTP方法,然后將HTTP請求參數映射為Java方法的參數,各種框架如Spring MVC, Jersey等都支持使用注解進行配置,比如,使用Jersey的一個配置示例為:
@Path("/hello")
public class HelloResource {
@GET
@Path("test")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> test(
@QueryParam("a") String a) {
Map<String, Object> map = new HashMap<>();
map.put("status", "ok");
return map;
}
}
類HelloResource將處理Jersey配置的根路徑下/hello下的所有請求,而test方法將處理/hello/test的GET請求,響應格式為JSON,自動映射HTTP請求參數a到方法參數String a。
神奇的注解
通過以上的例子,我們可以看出,注解似乎有某種神奇的力量,通過簡單的聲明,就可以達到某種效果。在某些方面,它類似於我們在62節介紹的序列化,序列化機制中通過簡單的Serializable接口,Java就能自動處理很多復雜的事情。它也類似於我們在並發部分中介紹的synchronized關鍵字,通過它可以自動實現同步訪問。
這些都是聲明式編程風格,在這種風格中,程序都由三個組件組成:
- 聲明的關鍵字和語法本身
- 系統/框架/庫,它們負責解釋、執行聲明式的語句
- 應用程序,使用聲明式風格寫程序
在編程的世界里,訪問數據庫的SQL語言,編寫網頁樣式的CSS,以及后續章節將要介紹的正則表達式、函數式編程都是這種風格,這種風格降低了編程的難度,為應用程序員提供了更為高級的語言,使得程序員可以在更高的抽象層次上思考和解決問題,而不是陷於底層的細節實現。
創建注解
框架和庫是怎么實現注解的呢?我們來看注解的創建。
@Override的定義
我們通過一些例子來說明,先看@Override的定義:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
定義注解與定義接口有點類似,都用了interface,不過注解的interface前多了@,另外,它還有兩個元注解@Target和@Retention,這兩個注解專門用於定義注解本身。
@Target
@Target表示注解的目標,@Override的目標是方法(ElementType.METHOD),ElementType是一個枚舉,其他可選值有:
- TYPE:表示類、接口(包括注解),或者枚舉聲明
- FIELD:字段,包括枚舉常量
- METHOD:方法
- PARAMETER:方法中的參數
- CONSTRUCTOR:構造方法
- LOCAL_VARIABLE:本地變量
- ANNOTATION_TYPE:注解類型
- PACKAGE:包
目標可以有多個,用{}表示,比如@SuppressWarnings的@Target就有多個,定義為:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }
如果沒有聲明@Target,默認為適用於所有類型。
@Retention
@Retention表示注解信息保留到什么時候,取值只能有一個,類型為RetentionPolicy,它是一個枚舉,有三個取值:
- SOURCE:只在源代碼中保留,編譯器將代碼編譯為字節碼文件后就會丟掉
- CLASS:保留到字節碼文件中,但Java虛擬機將class文件加載到內存時不一定會在內存中保留
- RUNTIME:一直保留到運行時
如果沒有聲明@Retention,默認為CLASS。
@Override和@SuppressWarnings都是給編譯器用的,所以@Retention都是RetentionPolicy.SOURCE。
定義參數
可以為注解定義一些參數,定義的方式是在注解內定義一些方法,比如@SuppressWarnings內定義的方法value,返回值類型表示參數的類型,這里是String[],使用@SuppressWarnings時必須給value提供值,比如:
@SuppressWarnings(value={"deprecation","unused"})
當只有一個參數,且名稱為value時,提供參數值時可以省略"value=",即上面的代碼可以簡寫為:
@SuppressWarnings({"deprecation","unused"})
注解內參數的類型不是什么都可以的,合法的類型有基本類型、String、Class、枚舉、注解、以及這些類型的數組。
參數定義時可以使用default指定一個默認值,比如,Guice中Inject注解的定義:
@Target({ METHOD, CONSTRUCTOR, FIELD }) @Retention(RUNTIME) @Documented public @interface Inject { boolean optional() default false; }
它有一個參數optional,默認值為false。如果類型為String,默認值可以為"",但不能為null。如果定義了參數且沒有提供默認值,在使用注解時必須提供具體的值,不能為null。
@Inject多了一個元注解@Documented,它表示注解信息包含到Javadoc中。
@Inherited
與接口和類不同,注解不能繼承。不過注解有一個與繼承有關的元注解@Inherited,它是什么意思呢?我們看個例子:
public class InheritDemo { @Inherited @Retention(RetentionPolicy.RUNTIME) static @interface Test { } @Test static class Base { } static class Child extends Base { } public static void main(String[] args) { System.out.println(Child.class.isAnnotationPresent(Test.class)); } }
Test是一個注解,類Base有該注解,Child繼承了Base但沒有聲明該注解,main方法檢查Child類是否有Test注解,輸出為true,這是因為Test有注解@Inherited,如果去掉,輸出就變成false了。
查看注解信息
創建了注解,就可以在程序中使用,注解指定的目標,提供需要的參數,但這還是不會影響到程序的運行。要影響程序,我們要先能查看這些信息。我們主要考慮@Retention為RetentionPolicy.RUNTIME的注解,利用反射機制在運行時進行查看和利用這些信息。
在上節中,我們提到了反射相關類中與注解有關的方法,這里匯總說明下,Class、Field、Method、Constructor中都有如下方法:
//獲取所有的注解 public Annotation[] getAnnotations() //獲取所有本元素上直接聲明的注解,忽略inherited來的 public Annotation[] getDeclaredAnnotations() //獲取指定類型的注解,沒有返回null public <A extends Annotation> A getAnnotation(Class<A> annotationClass) //判斷是否有指定類型的注解 public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
Annotation是一個接口,它表示注解,具體定義為:
public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); //返回真正的注解類型 Class<? extends Annotation> annotationType(); }
實際上,所有的注解類型,內部實現時,都是擴展的Annotation。
對於Method和Contructor,它們都有方法參數,而參數也可以有注解,所以它們都有如下方法:
public Annotation[][] getParameterAnnotations()
返回值是一個二維數組,每個參數對應一個一維數組,我們看個簡單的例子:
public class MethodAnnotations { @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) static @interface QueryParam { String value(); } @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) static @interface DefaultValue { String value() default ""; } public void hello(@QueryParam("action") String action, @QueryParam("sort") @DefaultValue("asc") String sort){ // ... } public static void main(String[] args) throws Exception { Class<?> cls = MethodAnnotations.class; Method method = cls.getMethod("hello", new Class[]{String.class, String.class}); Annotation[][] annts = method.getParameterAnnotations(); for(int i=0; i<annts.length; i++){ System.out.println("annotations for paramter " + (i+1)); Annotation[] anntArr = annts[i]; for(Annotation annt : anntArr){ if(annt instanceof QueryParam){ QueryParam qp = (QueryParam)annt; System.out.println(qp.annotationType().getSimpleName()+":"+ qp.value()); }else if(annt instanceof DefaultValue){ DefaultValue dv = (DefaultValue)annt; System.out.println(dv.annotationType().getSimpleName()+":"+ dv.value()); } } } } }
這里定義了兩個注解@QueryParam和@DefaultValue,都用於修飾方法參數,方法hello使用了這兩個注解,在main方法中,我們演示了如何獲取方法參數的注解信息,輸出為:
annotations for paramter 1 QueryParam:action annotations for paramter 2 QueryParam:sort DefaultValue:asc
代碼比較簡單,就不贅述了。
定義了注解,通過反射獲取到注解信息,但具體怎么利用這些信息呢?我們看兩個簡單的示例,一個是定制序列化,另一個是DI容器。
應用注解 - 定制序列化
定義注解
上節我們演示了一個簡單的通用序列化類SimpleMapper,在將對象轉換為字符串時,格式是固定的,本節演示如何對輸出格式進行定制化。我們實現一個簡單的類SimpleFormatter,它有一個方法:
public static String format(Object obj)
我們定義兩個注解,@Label和@Format,@Label用於定制輸出字段的名稱,@Format用於定義日期類型的輸出格式,它們的定義如下:
@Retention(RUNTIME) @Target(FIELD) public @interface Label { String value() default ""; } @Retention(RUNTIME) @Target(FIELD) public @interface Format { String pattern() default "yyyy-MM-dd HH:mm:ss"; String timezone() default "GMT+8"; }
使用注解
可以用這兩個注解來修飾要序列化的類字段,比如:
static class Student { @Label("姓名") String name; @Label("出生日期") @Format(pattern="yyyy/MM/dd") Date born; @Label("分數") double score; public Student() { } public Student(String name, Date born, Double score) { super(); this.name = name; this.born = born; this.score = score; } @Override public String toString() { return "Student [name=" + name + ", born=" + born + ", score=" + score + "]"; } }
我們可以這樣來使用SimpleFormatter:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Student zhangsan = new Student("張三", sdf.parse("1990-12-12"), 80.9d); System.out.println(SimpleFormatter.format(zhangsan));
輸出為:
姓名:張三 出生日期:1990/12/12 分數:80.9
利用注解信息
可以看出,輸出使用了自定義的字段名稱和日期格式,SimpleFormatter.format()是怎么利用這些注解的呢?我們看代碼:
public static String format(Object obj) { try { Class<?> cls = obj.getClass(); StringBuilder sb = new StringBuilder(); for (Field f : cls.getDeclaredFields()) { if (!f.isAccessible()) { f.setAccessible(true); } Label label = f.getAnnotation(Label.class); String name = label != null ? label.value() : f.getName(); Object value = f.get(obj); if (value != null && f.getType() == Date.class) { value = formatDate(f, value); } sb.append(name + ":" + value + "\n"); } return sb.toString(); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }
對於日期類型的字段,調用了formatDate,其代碼為:
private static Object formatDate(Field f, Object value) { Format format = f.getAnnotation(Format.class); if (format != null) { SimpleDateFormat sdf = new SimpleDateFormat(format.pattern()); sdf.setTimeZone(TimeZone.getTimeZone(format.timezone())); return sdf.format(value); } return value; }
這些代碼都比較簡單,我們就不解釋了。
應用注解 - DI容器
定義@SimpleInject
我們再來看一個簡單的DI容器的例子,我們引入一個注解@SimpleInject,修飾類中字段,表達依賴關系,定義為:
@Retention(RUNTIME) @Target(FIELD) public @interface SimpleInject { }
使用@SimpleInject
我們看兩個簡單的服務ServiceA和ServiceB,ServiceA依賴於ServiceB,它們的定義為:
public class ServiceA { @SimpleInject ServiceB b; public void callB(){ b.action(); } } public class ServiceB { public void action(){ System.out.println("I'm B"); } }
ServiceA使用@SimpleInject表達對ServiceB的依賴。
DI容器的類為SimpleContainer,提供一個方法:
public static <T> T getInstance(Class<T> cls)
應用程序使用該方法獲取對象實例,而不是自己new,使用方法如下所示:
ServiceA a = SimpleContainer.getInstance(ServiceA.class); a.callB();
利用@SimpleInject
SimpleContainer.getInstance會創建需要的對象,並配置依賴關系,其代碼為:
public static <T> T getInstance(Class<T> cls) { try { T obj = cls.newInstance(); Field[] fields = cls.getDeclaredFields(); for (Field f : fields) { if (f.isAnnotationPresent(SimpleInject.class)) { if (!f.isAccessible()) { f.setAccessible(true); } Class<?> fieldCls = f.getType(); f.set(obj, getInstance(fieldCls)); } } return obj; } catch (Exception e) { throw new RuntimeException(e); } }
代碼假定每個類型都有一個public默認構造方法,使用它創建對象,然后查看每個字段,如果有SimpleInject注解,就根據字段類型獲取該類型的實例,並設置字段的值。
定義@SimpleSingleton
在上面的代碼中,每次獲取一個類型的對象,都會新創建一個對象,實際開發中,這可能不是期望的結果,期望的模式可能是單例,即每個類型只創建一個對象,該對象被所有訪問的代碼共享,怎么滿足這種需求呢?我們增加一個注解@SimpleSingleton,用於修飾類,表示類型是單例,定義如下:
@Retention(RUNTIME) @Target(TYPE) public @interface SimpleSingleton { }
使用@SimpleSingleton
我們可以這樣修飾ServiceB:
@SimpleSingleton public class ServiceB { public void action(){ System.out.println("I'm B"); } }
利用@SimpleSingleton
SimpleContainer也需要做修改,首先增加一個靜態變量,緩存創建過的單例對象:
private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();
getInstance也需要做修改,如下所示:
public static <T> T getInstance(Class<T> cls) { try { boolean singleton = cls.isAnnotationPresent(SimpleSingleton.class); if (!singleton) { return createInstance(cls); } Object obj = instances.get(cls); if (obj != null) { return (T) obj; } synchronized (cls) { obj = instances.get(cls); if (obj == null) { obj = createInstance(cls); instances.put(cls, obj); } } return (T) obj; } catch (Exception e) { throw new RuntimeException(e); } }
首先檢查類型是否是單例,如果不是,就直接調用createInstance創建對象。否則,檢查緩存,如果有,直接返回,沒有的話,調用createInstance創建對象,並放入緩存中。
createInstance與第一版的getInstance類似,代碼為:
private static <T> T createInstance(Class<T> cls) throws Exception { T obj = cls.newInstance(); Field[] fields = cls.getDeclaredFields(); for (Field f : fields) { if (f.isAnnotationPresent(SimpleInject.class)) { if (!f.isAccessible()) { f.setAccessible(true); } Class<?> fieldCls = f.getType(); f.set(obj, getInstance(fieldCls)); } } return obj; }
小結
本節介紹了Java中的注解,包括注解的使用、自定義注解和應用示例。
注解提升了Java語言的表達能力,有效地實現了應用功能和底層功能的分離,框架/庫的程序員可以專注於底層實現,借助反射實現通用功能,提供注解給應用程序員使用,應用程序員可以專注於應用功能,通過簡單的聲明式注解與框架/庫進行協作。
下一節,我們來探討Java中一種更為動態靈活的機制 - 動態代理。
(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic,位於包shuo.laoma.dynamic.c85下)
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心原創,保留所有版權。

