閱讀154 返回首頁    go 阿裏雲 go 技術社區[雲棲]


java之annotation與框架的那些秘密

在大家使用spring MVC或Hibernate 3.0以上的版本時,可能會注意到annotation帶來的方便性,不過這往往讓人覺得annotation真的很強大,而這算是一種接近錯誤的理解吧,annotation其實本身是屬於一種文檔注解的方式,幫助我們在編譯時、運行時、文檔生成時使用,部分annotation其實基本和注釋差不多,這裏其實是要說下annotation的原理,以及各種功能在它上麵如何實現的,以及在繼承的時候,他會發生什麼?為什麼會這樣?


首先,就我個人使用的理解,annotation是一種在類、類型、屬性、參數、局部變量、方法、構造方法、包、annotation本身等上麵的一個附屬品(ElementType這個枚舉中有闡述),他依賴於這些元素而存在,他本身並沒有任何作用,annotation的作用是根據其附屬在這些對象上,根據外部程序解析引起了他的作用,例如編譯時的,其實編譯階段就在運行:java Compiler,他就會檢查這些元素,例如:@SuppressWarnings、@Override、@Deprecated等等;


生成文檔運行javadoc也是單獨的一個進程去解析的,其實他是識別這些內容的,而spring MVC和Hibernate的注解,框架程序在運行時去解析這些annotation,至於運行的初始化還是什麼時候要和具體的框架結合起來看,那麼今天我們就要說下所謂的annotation是如何實現功能的(再次強調:它本身沒有功能,功能又程序決定,他隻是上麵描述的幾大元素的附屬品而已,如果認為他本身有功能,就永遠不知道annotation是什麼);


我們首先自己來寫個annotation,寫annotation就像寫類一樣,創建一個java文件,和annotation的名稱保持一致,他也會生成class文件,說明他也是java,隻是以前要麼是interface、abstract class、class開頭,現在多了一個@interface,可見它是屬於jvm可以識別的一種新的對象,就像序列化接口一樣的標記,那麼我們簡單寫一個:

下麵的代碼可能你看了覺得沒啥意思,接著向下可能你會找到有意思的地方:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR , ElementType.FIELD , ElementType.TYPE})
public @interface NewAnnotation {
  String value() default "";
}


那麼上麵的annotation代表:

在Runtime的時候將會使用(RetentionPolicy裏麵有闡述其他的SOURCE、CLASS級別),可以注解到方法、構造方法、屬性、類型和類上麵;名稱為:NewAnnotation、裏麵有一個屬性為value,為String類型,默認值為空字符串,也就是可以不傳遞參數。


程序中使用例如:

public class A {
   @NewAnnotation
   private String b;

   @NewAnnotation(value = "abc")
   public void setB() {...}
}

那麼很多人看到這裏都會問,這樣寫了有什麼用途呢?貌似是沒啥用途,我第一次看到這裏也沒太看懂,而且看到spring MVC做得如此多功能,這到底是怎麼回事?


再一些項目的框架製作中,我逐步發現一些功能,如果有一種代碼的附屬品,將會將框架製作得更加漂亮和簡潔,於是又聯想到了spring的東西,spring的AOP是基於字節碼增強技術完成,攔截器的實現不再是神話,那麼反過來如如果annotation是可以被解析的,基於annotation的注入就是十分簡單明了的事情了,Hibernate也是如此,當然我這不討論一些解析的緩存問題,因為不會讓每個對象都這樣去解析一次,都會盡量記憶下來使得性能更高,這裏隻說他的原理而已。


這裏拿一個簡單的request對象轉換為javaBean對象的,假如我們用DO為後綴,而部分請求的參數名和實際的屬性並不一樣(一般規範要求是一樣的),其次在網絡傳輸中某個項目前台的日期提交到後台都是以毫秒之方式提交到後台,但是需要轉換為對應的字符串格式來處理,提交中包含:String、int、Integer、Long、long、String[]這幾種數據類型,日期的我們大家可以擴展,帶著這些小需求我們來簡單寫一個:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ReuqestAnnotation {
 String name() default "";//傳入的參數名

 boolean dateString() default false;//是否為dateString類型
}

這裏的annotation一個是name一個是dateString,兩個都有默認值,name我們認為是request中參數名,否則直接以屬性名為主,dateString屬性是否是日期字符串(前麵描述傳遞的日期都會變成毫秒值,所以需要自動轉換下)。

那麼我們寫一個DO:

import xxx.xxx.ReuqestAnnotation;//import部分請自己根據項目引用

public class RequestTemplateDO {

  private String name;

  @ReuqestAnnotation(name = "myemail")
  private String email;

  private String desc;

  @ReuqestAnnotation(dateString = true)
  private String inputDate;

  private Integer int1;

  private int int2;


  public int getInt1() {
    return int1;
  }


 public int getInt2() {
   return int2;
 }


  public String getInputDate() {
    return inputDate;
  }


  public String getName() {
    return name;
  }


  public String getEmail() {
    return email;
  }


  public String getDesc() {
    return desc;
  }
}

注意這裏沒有寫set方法,我們為了說明問題,而不是怎麼去調用set方法,所以我們直接用屬性去設置值,屬性是private一樣可以設置,下麵就是一個轉換方法:

假如有一個HttpUtils,我們寫一個靜態方法:

/**
     * 通過request獲取對應的對象
     * @param <T>
     * @param request
     * @param clazz
     * @return
     * @throws IllegalAccessException 
     * @throws InstantiationException 
     */
    @SuppressWarnings("unchecked")
public static <T extends Object> T convertRequestToDO(HttpServletRequest request , Class<T> clazz) 
     throws InstantiationException, IllegalAccessException {
     Object object = clazz.newInstance();
     Field []fields = clazz.getDeclaredFields();
     for(Field field : fields) {
     field.setAccessible(true);
     String requestName = field.getName();
     ReuqestAnnotation requestAnnotation = field.getAnnotation(ReuqestAnnotation.class);
     boolean isDateString = false;
     if(requestAnnotation != null) {
     if(StringUtils.isNotEmpty(requestAnnotation.name())) requestName = requestAnnotation.name();
     isDateString = requestAnnotation.dateString();
     }
     Class <?>clazzf = field.getType();
     if(clazzf == String.class) {
     if(isDateString) {
     String dateStr = request.getParameter(requestName);
     if(dateStr != null) {
     field.set(object, DateTimeUtil.getDateTime(new Date(Long.valueOf(dateStr)) , DateTimeUtil.DEFAULT_DATE_FORMAT));
     }
     }else {
         field.set(object, request.getParameter(requestName));
       }
     }
     else if(clazzf == Integer.class) field.set(object, getInteger(request.getParameter(requestName)));
     else if(clazzf == int.class) field.set(object, getInt(request.getParameter(requestName)));
     else if(clazzf == Long.class) field.set(object, getLongWapper(request.getParameter(requestName)));
     else if(clazzf == long.class) field.setLong(object, getLong(request.getParameter(requestName)));
     else if(clazzf == String[].class) field.set(object, request.getParameterValues(requestName));
     }
     return (T)object;
}


這裏麵就會負責將request相應的值填充到數據中,返回對應的DO,而代碼中使用的是:

RequestTemplateDO requestTemplateDO = HttpUtils.convertRequestToDO(request , RequestTemplateDO.class);

注意:這部分spring幫我們寫了,隻是我在說大概原理,而且spring本身實現和這部分也有區別,也更加完整,這裏僅僅是為了說明局部問題。spring在攔截器中攔截後就可以組裝好這個DO,所以在spring MVC中可以將其直接作為擴展參數傳遞進入我們的業務方法中,首先知道業務方法的annotation,根據URL決定方法後,獲取參數列表,根據參數類型,如果是業務DO,那麼填充業務DO即可,Hibernate也可以同樣的方式去推理。


OK,貌似很簡單,如果你真的覺得簡單了,那麼這塊你就真的懂了,那麼我們說點特殊的,就是繼承,貌似annotation很少去繼承,但是在我遇到一些朋友的項目中,由於部分設計需要或本身設計缺陷但是又不想修改的時候,就會遇到,多個DO大部分屬性是一樣的,如果不抽象父親類出來,如果修改屬性要同時修改非常多的DO,而且操作的時候絕大部分情況是操作這些共享的屬性,所以還想用上溯造型來完成代碼的通用性並保持多態,當時一問我還真蒙了,因為是基於類似annotation的一些框架,例如hibernate,後來帶著問題做了很多測試並且和資料對應上,是如果annotation在class級別、構造方法級別,是不會被子類所擁有的,也就是當子類通過XXX.class.getAnnotation(XXXAnnotation.class)的時候是獲取不到,不過public類型的方法、public的屬性是可以的,其次,如果子類重寫了父類的某個屬性或某個方法,不管子類是否寫過annotation,這個子類中父類的屬性或方法的所有的annotation全部失效,也就是如父親類有一個屬性A,有兩個annotation,若A是public的,子類可以繼承這個屬性和annotation,若子類也有一個A屬性,不管A是否有annotation,父類中這些annotation在子類中都將失效掉。

這就是為什麼我說annotation是屬性、方法、包。。。的附屬品,他是被綁定在這些元素上的,而並非擁有實際功能,當重寫的時候,將會被覆蓋,屬性、方法當被覆蓋的時候,其annotation也隨之被覆蓋,而不會按照annotation再單獨有一個覆蓋;所以當時要解決那個問題,我就告訴他,要用的方法隻有public,否則沒辦法,即使用反射也不行,因為hibernate根本找不到這個field,還沒有機會提供一個setAccessible的能力,因為這個時候根本看不到這些field,但是通過父類本身可以看到這些field;就技術層麵是這樣的,否則隻有修改設計是最佳的方法。


最後更新:2017-04-02 15:28:26

  上一篇:go android input及簡單am命令
  下一篇:go 升級到Win 8 ,你必須知道的