1. 程式人生 > >java自定義註解解析及相關場景實現

java自定義註解解析及相關場景實現

註解(Annotation)是java1.5之後提供的一種語法。其主要作用是編譯檢查(比如@override)和程式碼分析(通過程式碼中添加註解,利用註解解析器對添加了註解的程式碼進行分析,獲取想要的結果,一般自定義的註解都是這一種功能)。

1.1 JDK提供的註解

JDK提供的註解最常用的是3個,@Override,@Deprecated和@SuppressWarnings.

1.1.1 @Override

@Override表示子類重寫了父類的方法,或者實現了介面的方法。幫助開發者確認子類是否正確的覆蓋了父類的方法,若父類中沒有此方法,編譯器即報錯。但是,子類與父類有同樣的方法,但子類的方法上沒有@Override註解,是不會報錯。
以基類Object的方法toString ()為例:

//正確的寫法
public class ChildClass  {


    //@Override是幫助開發者確認子類是否正確的覆蓋了父類的方法

    public void read(){
        System.out.println("this is a parent method!");
    }

    @Override
    public String toString(){

        return "ChildClass";
    }

}

但是如果toString()不加Override,也沒問題,只是簡單的子類重寫父類(Object)的方法。

public class ChildClass  {


    //@Override是幫助開發者確認子類是否正確的覆蓋了父類的方法

    public void read(){
        System.out.println("this is a parent method!");
    }

    public String toString(){

        return "ChildClass";
    }

}

但是,如果把toString()方法改成toString1()方法就會報錯。

public class ChildClass  {


    //@Override是幫助開發者確認子類是否正確的覆蓋了父類的方法
public void read(){ System.out.println("this is a parent method!"); } @Override public String toString1(){ return "ChildClass"; } }

提示錯誤:The method toString1() of type ChildClass must override or implement a supertype method
翻譯過來就是toString1()方法必須是重寫重寫父類的方法或者實現相關介面。即提示父類中沒有toString1() 方法。這樣就通過提示來確保開發者能正確重寫toString()方法。

1.1.2 @Deprecated

@Deprecated用於提示開發者,標註此註解的方法已經被棄用了。請使用另外推薦的方法

    @Deprecated()
    public String toString1(){

        return "ChildClass";
    }

    public static void main(String[] args){

        ChildClass child=new ChildClass();
        child.toString1();
    }

使用toString1()方法,編譯器會提示,將toString1()畫一條橫線,表示此方法已經過期。

1.1.3 @SuppressWarnings

@SuppressWarnings是抑制警告的意思。比如我們新建一個變數,但是沒有用,編譯器會提示此變數未使用的警告。如果在方法中,添加了@SuppressWarnings的相關注解,這個警告就不會再提示了。

@SuppressWarnings({"unused"})
    public static void main(String[] args){

        List<Integer>list=new ArrayList<>();
    }

2. 自定義註解

除了使用java自帶的註解,我們也可以自定義註解,用於幫助為相關程式碼打上標籤,然後我們在解析註解的邏輯中就可以通過這些標籤來完成相關的工作,比如,許可權控制,日記記錄等等。

2.1 自定義註解語法

定義一個自定義註解,與定義一個介面類似,只不過在interface前加是哪個@。其內部可以新增屬性值,其屬性值的定義為
修飾符 返回值型別 屬性名() [default value]
其中,修飾符只能用public 和abstract。 返回值為基本型別、字串、列舉、註解以及以上型別的一維陣列。
定義自定義註解,還需要用到元註解,用於修飾自定義註解,一般我們會用到兩個。@Retention和@Target。
@Retention
用於確定註解的生命週期。其有三個列舉變數可選

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * SOURCE級別表示程式碼級別可見,經過編譯器編譯生成位元組碼物件時,此註解就沒了。
     * 比如@override就是程式碼級別可見
     */
    SOURCE,  

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * CLASS表示位元組碼物件級別可見,但是位元組碼物件被虛擬機器載入時,
     * 這個註解會被拋棄,這是預設的可見級別
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     * RUNTIME表示執行時也可見,當虛擬機器載入位元組碼物件時,此註解仍然可見。
     * 因此可以通過反射獲取註解資訊,然後完成相應的註解解析工作,一般自定義的註解都是執行時可見。
     */
    RUNTIME
}

@Target
用於修飾此註解可以用於什麼型別上。比如註解可以用在類級別、方法、成員欄位或者建構函式上。

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration 可以修飾類*/
    TYPE,  

    /** Field declaration (includes enum constants) 可以修飾字段*/
    FIELD,

    /** Method declaration 可以修飾方法*/
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration 構造方法*/
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

下面是一個簡單的自定義註解:

/**
 * @author sks
 * 這個註解用於日誌管理  ,從修飾註解的元註解可知,這個註解可以修飾方法和類。其可見範圍到執行時都可見。
 *
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Myanno {

    /** 下面時候註解的屬性 **/  
    /** 要執行的操作型別比如:add操作 **/  
    public String operationType() default "";  

    /** 要執行的具體操作比如:新增使用者 **/  
    public String operationName() default "";
}

2.2 自定義註解解析

上述定義的自定義註解,只是一個空的定義,沒有任何的意義。因此需要我們自己定義相關的自定義註解的解析。上面提到,自定義的註解需要定義註解的可見範圍。一般我們都定義為執行時可見。因此,通過反射,我們可以拿到註解的內容。通過反射拿到程式碼的註解內容,進行相關的邏輯處理工作,以達到註解的目的。
通過反射獲得註解內容的常用方法有

T getAnnotation(Class) : 獲得當前物件的指定的註解。
Annotation[] getAnnotations() X: 獲得當前物件的所有註解
boolean isAnnotationPresent(annotationClass): 當前物件是否有註解。

2.3自定義註解舉例

1、 定義一個註解,用於獲取水果的名字。

   @Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FruitName {

    public String value() default "fruit";

}

上面這個註解定義說明此註解可以用在類,方法和成員地段上。是執行時可見的,其內有一個屬性,預設值是“fruit”。
2、然後我們定義一種叫蘋果的水果。其內部有個成員變數appleName。我們在這個成員欄位上加上@FruitName註解。

public class Apple {

    @FruitName("Apple")
    private String appleName;

}

3、註解的解析

public class FruitInfoUtil {


    public static void main(String[] args){

        FruitInfoUtil util=new FruitInfoUtil();
        util.getInfomation(Apple.class);
    }

    /*
    *這個方法用於註解的解析,其輸入是Class型別的物件。通過反射這個Class物件獲取相關的註解,進行註解的解析
    */
    public void getInfomation(Class<?>clazz){

        //因為註解是在成員欄位上,因此需要獲得類的所有欄位資訊
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            //判斷這個欄位上是否有相應的註解資訊(FruitName.class)
            if (field.isAnnotationPresent(FruitName.class)) {

                FruitName fruitName = field.getAnnotation(FruitName.class);
                System.out.println("水果名字是"+fruitName.value());
            }

        }



    }

}

其結果是:

水果名字是Apple

這個例子簡單的說明了自定義註解如何使用。但是在實際應用中,也許會有很多類都應用了自定義註解,即我們不知道是具體哪個類或者哪個方法使用了自定義註解。一般可以通過SpringMVC的攔截器或者SpringAOP獲取添加了註解的方法,在攔截器或者AOP的通知裡對註解進行處理。具體可看下面的章節。

3.自定義註解應用

一般web開發都用到spring框架。結合spring的SpringMVC的攔截器或者SpringAOP,註解可以完成許可權控制,日誌記錄等功能。

3.1 自定義註解+SpringMVC攔截器實現許可權控制功能

我們想實現這麼一個註解。方法上添加了此註解的方法,不需要登入許可權即可執行,否則就要檢視其http請求的session中是否包含相關登入資訊,以確定是否執行方法裡的內容。

/**
 * @author sks
 * 這個註解用於許可權控制
 *
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogin {


}

實現HandlerInterceptor 介面,完成自定義SpringMVC攔截器,在攔截器內部實現註解的解析功能。

public class MyInceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        if (!(handler instanceof HandlerMethod)) {
              System.out.println("當前操作handler不為HandlerMethod=" + handler.getClass().getName() + ",req="
                        + request.getQueryString());
                return false;
        }
        //獲得經過攔截器的方法
        HandlerMethod handlerMethod=(HandlerMethod) handler;

        String methodName  = handlerMethod.getMethod().getName();
        //通過反射的getAnnotation方法獲得其方法上的指定的NoLogin型別的註解。
        NoLogin  myanno= handlerMethod.getMethod().getAnnotation(NoLogin.class);
        if (myanno!=null) {  //如果獲得的註解不為空的話,說明此方法不需要許可權就可執行。
            System.out.println("當前操作不需要登入");
            return true;
        }
        //否則就要看其session 的屬性裡是否有關於LOGIN屬性的資訊,若沒有,則攔截此方法,不執行方法的操作
        if (request.getSession().getAttribute("LOGIN")==null) {
            System.out.println("當前操作" + methodName + "使用者未登入,ip=" + request.getRemoteAddr());
            return false;
        }

        System.out.println("當前操作" + methodName + "使用者登入:" + request.getSession().getAttribute("LOGIN"));
          return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        // TODO Auto-generated method stub

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        // TODO Auto-generated method stub

    }

}

然後,在springMVC的配置檔案裡配置攔截器的相關資訊。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">

    <context:component-scan base-package="com.test.controller" />
    <context:component-scan base-package="com.test.spring*" />
    <context:component-scan base-package="com.test.aop*" />
    <mvc:annotation-driven />
    <!-- 配置註解驅動 -->
    <!--  <mvc:annotation-driven conversion-service="conversionService"/> -->

    <mvc:resources location="/img/" mapping="/img/**"/> 
    <mvc:resources location="/css/" mapping="/css/**"/> 
    <mvc:resources location="/js/" mapping="/js/**"/>
    <mvc:resources location="/views/" mapping="/views/**"/>
    <mvc:resources location="/ui/" mapping="/ui/**"/>
    <mvc:resources location="/fonts/" mapping="/fonts/**"/>

    <mvc:resources location="/bower_components/" mapping="/bower_components/**"/>
    <mvc:resources location="/dist/" mapping="/dist/**"/>
    <mvc:resources location="/documentation/" mapping="/documentation/**"/>

    <!-- mvc:interceptors攔截器 ,注意其寫法,要先寫攔截的路徑,然後再排除相關的不攔截的路徑-->
     <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>       <!-- 攔截器攔截所有的方法 -->
            <!-- 一般login申請和退出登入都不應該攔截。比如登入頁面最終的http請求是/user/login
            那麼就要寫成path="/*/login",而寫成path="/login"則是不行的-->
             <mvc:exclude-mapping path="/*/login" /> 
            <mvc:exclude-mapping  path="/img/**"/>      <!--靜態資源也不應該攔截 -->
            <mvc:exclude-mapping path="/css/**"/> 
            <mvc:exclude-mapping path="/js/**"/>
            <mvc:exclude-mapping path="/views/**"/> 
            <mvc:exclude-mapping path="/ui/**"/>
            <mvc:exclude-mapping  path="/fonts/**"/>

            <mvc:exclude-mapping  path="/bower_components/**"/>
            <mvc:exclude-mapping  path="/dist/**"/>
            <mvc:exclude-mapping  path="/documentation/**"/>
            <bean class="com.test.intercept.MyInceptor"/>


        </mvc:interceptor>
    </mvc:interceptors> 


    <!-- 載入配置檔案 -->
    <context:property-placeholder location="classpath:conf/resource.properties" />


    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>

</beans>

最後,看看Controller的內容,

@Controller
public class LoginController {

    @RequestMapping("/user/login")
    @ResponseBody
    private String loginCheck(HttpServletRequest request){

        //EcgUser ecgUser=userService.queryUserById(itemId);
          String username=request.getParameter("username");
          String password=request.getParameter("password");

          HttpSession session = request.getSession();

          if (username.equals("andy")&&password.equals("123")) {

              session.setAttribute("LOGIN", username);
              return "success";
        }

        return "false";
    }


    @NoLogin
    @RequestMapping("/login/nologin")
    @ResponseBody
    private String test(Model model) throws JsonProcessingException{        


        System.out.println("有申請");
        List<Product> list = new ArrayList<Product>();

        //這裡把“類別名稱”和“銷量”作為兩個屬性封裝在一個Product類裡,每個Product類的物件都可以看作是一個類別(X軸座標值)與銷量(Y軸座標值)的集合
        list.add(new Product("襯衣", 10));
        list.add(new Product("短袖", 20));
        list.add(new Product("大衣", 30));
        list.add(new Product("dayin", 30));

        ObjectMapper mapper=new ObjectMapper();


        String writeValueAsString = mapper.writeValueAsString(list);

         return writeValueAsString;
    }

    @RequestMapping("/login/{itemId}")
    @ResponseBody
    private String queryUserById(@PathVariable String itemId,Model model){      


         return "未登入";
    }

}

上面的類中,第一個方法登入方法,此方法不經過攔截器,(如果此方法攔截了,這時候確實還處於未登入狀態,那麼loginCheck方法就不會被執行了。因此不能攔截)。
類中的第二個方法和第三個方法都會經過攔截器。由於第二個方法加上了@NoLogin註解,表示這個方法未經過登入也可以執行。第三個方法沒有@NoLogin註解,如果使用者未登入,那麼是不會被執行的。如果使用者已登入,http請求中的session包含了LOGIN屬性,那麼就會執行。具體的結果就不貼了。
這樣,通過自定義註解和springmvc的攔截器,可以實現一個簡單的許可權控制功能。

3.2 自定義註解+SpringAOP實現日誌記錄功能

springAOP:面向切面程式設計,是spring的兩大核心模組之一,用於將系統中通用的模組或者功能抽取出來。其基本原理是AOP代理(分為動態代理和cglib代理)。利用aop裡的通知,實現自定義註解的解析,可以完成相關的工作。
這裡我們設計一個註解,即在需要進行日誌記錄的地方加上此註解即可實現日誌自動記錄的功能。
首先,仍是定義相關注解:


/**
 * @author sks
 * 這個註解用於日誌管理
 *
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Myanno {


    /** 要執行的操作型別比如:add操作 **/  
    public String operationType() default "";  

    /** 要執行的具體操作比如:新增使用者 **/  
    public String operationName() default "";
}

其次,定義相關的aop代理通知。這裡,把註解的解析工作放在事後通知上,即下面的after方法。這裡用簡單的system.out來模擬日誌記錄功能。

/**
 * @author sks
 *
 */
public class MyAdvice4Anno {

    public void before(JoinPoint joinPoint){

        System.out.println("獲取引數--》前置通知");

    for (int i = 0; i < joinPoint.getArgs().length; i++) {

        System.out.println("獲取引數--》"+joinPoint.getArgs()[i].toString());
    }   


    }

    /*
    *自定義的註解放在事後通知中
    */
    public void after(JoinPoint joinPoint){
        System.out.println("後置通知");
        //通過連線點JoinPoint 獲得代理的方法,進而獲取方法上的註解資訊
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        for (Method method : methods) {
            Myanno annotation = method.getAnnotation(Myanno.class);
            if (annotation!=null) {
                String operationName = annotation.operationName();
                String operationType = annotation.operationType();

                  //*========控制檯輸出=========*//  
                 System.out.println("=====controller後置通知開始=====");  
                 System.out.println("請求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()")+"."+operationType);  
                 System.out.println("方法描述:" + operationName);  

                break;
            }


        }
    }

    }
    //異常通知
    public void afterException(){
        System.out.println("出事啦!出現異常了!!");
    }

}

然後在springMvc的配置檔案中,做好aop的配置工作。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">

    <context:component-scan base-package="com.test.controller" />
    <context:component-scan base-package="com.test.spring*" />
    <context:component-scan base-package="com.test.aop*" />
    <mvc:annotation-driven />
    <!-- 配置註解驅動 -->
    <!--  <mvc:annotation-driven conversion-service="conversionService"/> -->

    <mvc:resources location="/img/" mapping="/img/**"/> 
    <mvc:resources location="/css/" mapping="/css/**"/> 
    <mvc:resources location="/js/" mapping="/js/**"/>
    <mvc:resources location="/views/" mapping="/views/**"/>
    <mvc:resources location="/ui/" mapping="/ui/**"/>
    <mvc:resources location="/fonts/" mapping="/fonts/**"/>

    <mvc:resources location="/bower_components/" mapping="/bower_components/**"/>
    <mvc:resources location="/dist/" mapping="/dist/**"/>
    <mvc:resources location="/documentation/" mapping="/documentation/**"/>

    <!-- 載入配置檔案 -->
    <context:property-placeholder location="classpath:conf/resource.properties" />

    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>



     <bean id="myAdvice4Anno" class="com.test.zhujie.myanno.MyAdvice4Anno"></bean>

    <aop:config>
        配置切入點
        <aop:pointcut expression="execution(* com.test.controller.AnnoController.*(..))" id="pointcut2"/>

        <aop:aspect  ref="myAdvice4Anno">
            <aop:before method="before" pointcut-ref="pointcut2"/>
            <aop:after method="after" pointcut-ref="pointcut2"/> 
            <aop:after-throwing method="afterException" pointcut-ref="pointcut2"/>
        </aop:aspect>

    </aop:config> 

</beans>

最後,看相關的控制層程式碼。

**
 * @author sks
 *
 */
@Controller
public class AnnoController {



    private static Logger logger = LoggerFactory.getLogger("log");

    @RequestMapping("/anno/queryData")
    @ResponseBody
    private E3Result queryData(Model model,String username,String password) throws JsonProcessingException{     
        logger.warn("查詢使用者id");

    //  userService.

        List<Product> list = new ArrayList<Product>();

        //這裡把“類別名稱”和“銷量”作為兩個屬性封裝在一個Product類裡,每個Product類的物件都可以看作是一個類別(X軸座標值)與銷量(Y軸座標值)的集合
        list.add(new Product("襯衣", 10));
        list.add(new Product("短袖", 20));
        list.add(new Product("大衣", 30));

        E3Result.ok(list);

        ObjectMapper mapper=new ObjectMapper();


        String writeValueAsString = mapper.writeValueAsString(list);

         return  E3Result.ok(list);
    }
//  
    @Myanno(operationType="add操作",operationName="新增使用者")
    @RequestMapping("/anno/test")
    @ResponseBody
    public String test() throws JsonProcessingException{        
        logger.warn("查詢使用者id");

        System.out.println("有申請");
        List<Product> list = new ArrayList<Product>();

        //這裡把“類別名稱”和“銷量”作為兩個屬性封裝在一個Product類裡,每個Product類的物件都可以看作是一個類別(X軸座標值)與銷量(Y軸座標值)的集合
        list.add(new Product("襯衣", 10));
        list.add(new Product("短袖", 20));
        list.add(new Product("大衣", 30));
        list.add(new Product("dayin", 30));

        ObjectMapper mapper=new ObjectMapper();


        String writeValueAsString = mapper.writeValueAsString(list);

         return writeValueAsString;
    }




}

上述的test方法假設需要進行日誌記錄操作,就在test()方法上加上@Myanno(operationType=”add操作”,operationName=”新增使用者”)。
此時,訪問這個方法,編譯器輸出:

=====controller後置通知開始=====
請求方法:com.test.controller.AnnoController.test().add操作
方法描述:新增使用者

成功。
注意事項:切點所代表的方法需要是public方法,aop代理才能成功。之前,test的方法用的是private修飾,一直aop代理不成功。後查才發現是此問題。原因是aop代理要麼是動態代理,要麼是cglib代理,一個是介面實現,一個是繼承父類,都需要其方法是public方法才能代理成功。