1. 程式人生 > >annotation與框架的那些祕密

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));
     }//for over
     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;就技術層面是這樣的,否則只有修改設計是最佳的方法。