計算機程序的思惟邏輯 (85) - 註解

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

上節咱們探討了反射,反射相關的類中都有方法獲取註解信息,咱們在前面章節中也屢次提到過註解,註解究竟是什麼呢?java

在Java中,註解就是給程序添加一些信息,用字符@開頭,這些信息用於修飾它後面緊挨着的其餘代碼元素,好比類、接口、字段、方法、方法中的參數、構造方法等,註解能夠被編譯器、程序運行時、和其餘工具使用,用於加強或修改程序行爲等。這麼說比較抽象,下面咱們會具體來看,先來看Java的一些內置註解。git

內置註解

Java內置了一些經常使用註解,好比:@Override、@Deprecated、@SuppressWarnings,咱們簡要介紹下。程序員

@Override

@Override修飾一個方法,表示該方法不是當前類首先聲明的,而是在某個父類或實現的接口中聲明的,當前類"重寫"了該方法,好比:github

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方法定義,變爲了:web

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方法的調用,若是不但願顯示警告,能夠這樣:swift

@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關鍵字,經過它能夠自動實現同步訪問。

這些都是聲明式編程風格,在這種風格中,程序都由三個組件組成:

  1. 聲明的關鍵字和語法自己
  2. 系統/框架/庫,它們負責解釋、執行聲明式的語句
  3. 應用程序,使用聲明式風格寫程序

在編程的世界裏,訪問數據庫的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中一種更爲動態靈活的機制 - 動態代理。

(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…,位於包shuo.laoma.dynamic.c85下)


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索