AOP (面向切面編程) 系列之一(spring xml 配置方式)

AOP (面向切面編程) 系列之一

上接 java 注解(annotation) ,注解是 元數(shù)據(jù) ,本身不具備任何業(yè)務(wù)處理能力。AOP 編程就是賦予注解實際處理能力的一種常見方式。

AOP 是什么

AOP 是 Aspect Oriented Programming 的簡稱,一般翻譯為面向切面編程 。
提到 AOP 可能還有一些陌生,先說一個人盡皆知的 OOP (Object-oriented programming,面向?qū)ο蟪绦蛟O(shè)計)。把現(xiàn)實事物抽象為程序?qū)ο蟮木幊趟枷氪蟠筇岣哕浖闹赜眯?、靈活性和擴展性,被廣泛地應(yīng)用于軟件開發(fā)領(lǐng)域。
AOP 是指可以通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)在不修改源代碼的情況下給程序動態(tài)統(tǒng)一添加功能的一種技術(shù),目的是調(diào)用者和被調(diào)用者之間的解耦,提高代碼的靈活性和可擴展性。
因此, AOP 取代 OOP 的說法根本就是無稽之談。二者適用的場景和設(shè)計的目標并不相同。
通俗的說 AOP 是在程序編譯過程或者運行過程(動態(tài)代理)中根據(jù)提前設(shè)置的切面統(tǒng)一添加特定的功能,而對業(yè)務(wù)代碼無侵入或低侵入。

常見的 AOP 使用場景

目前常見的應(yīng)用場景主要是日志記錄,性能統(tǒng)計,安全控制,事務(wù)處理,異常處理等等。尤其在編程框架中最為常見。

spring XML 配置方式 AOP

Talk is cheap, just show you the code
本文使用純 spring XML 配置方式使用 AOP ,不使用 AspectJ 注解(注解方式是挖的另一個坑,下一篇填)。

用作切面的注解

借用 java 注解(annotation) 的自定義注解 AnnotationDemo (僅僅只是偷懶罷了)。
注解在上文中已經(jīng)解釋地很詳細了。

package com.hxx.annotation;

import com.hxx.enums.UserType;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AnnotationDemo {
    long time() default 0;

    int count() default 0;

    String name() default "XiaoMing";

    UserType userType() default UserType.SYSTEM_ADMIN;

}

切面方法定義

這里是指切面觸發(fā)的特定邏輯(aspect definition)。
實在是不擅長文字描述,直接看代碼吧。

package com.hxx.aop;

import com.hxx.annotation.AnnotationDemo;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

/**
 * <ul>
 * <li>功能說明:使用 spring 的 XML 方式配置 AOP</li>
 * <li>作者:tal on 2018/10/17 0017 17:20 </li>
 * <li>郵箱:houxiangxiang@cibfintech.com</li>
 * </ul>
 */
public class SpringAspect {
    /**
     * 切點執(zhí)行前執(zhí)行的方法
     *
     * @param point
     */
    public void doBefore(JoinPoint point) {
        System.out.println("---------------> before ");
    }

    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        System.out.println("---------------> around start");
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        AnnotationDemo annotation = methodSignature.getMethod().getAnnotation(AnnotationDemo.class);
        System.out.println("annotation ---------------> " + annotation);
        Object proceed = null;
        try {
            proceed = point.proceed();
        } catch (Throwable throwable) {
            throw throwable;
        }
        System.out.println("---------------> around end");
        return proceed;
    }

    /**
     * 切點執(zhí)行后執(zhí)行的方法
     *
     * @param point
     */
    public void doAfter(JoinPoint point) {
        System.out.println("---------------> after");
    }

    /**
     * 后置異常通知:在切點拋出異常之后執(zhí)行,可以訪問到異常信息,且可以指定出現(xiàn)特定異常信息時執(zhí)行代碼
     *
     * @param point
     */
    public void afterThrowing(JoinPoint point, Throwable throwable) {
        System.out.println("---------------> afterThrowing");
        System.out.println(throwable.getMessage());
    }

    /**
     *
     * @param point
     * @param returnValue
     */
    public void afterReturning(JoinPoint point, int returnValue) {
        System.out.println("---------------> afterReturning +++ " + returnValue);
    }
}

說不用 AspectJ 注解就一個都不用。
雖然沒有用 AspectJ 注解,但也用了 aspectj 的幾個類,所以是要引入 aspectj 依賴的。本文只舉例 maven 的 pom 文件依賴方式,jar 包版本號一般選擇最新版即可。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${aspectj.version}</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
</dependency>

除此之外這就是一個普普通通的類了。類里的方法名和參數(shù)名會在 spring xml 配置里用到,叫什么不要緊,一致即可(不一致編譯就會報錯)。

spring XML 配置

使用 spring xml 配置先引入 spring 依賴,照舊只上 maven 依賴,版本自己選。

<!-- spring context 支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${spring.version}</version>
</dependency>
<!-- spring aop 支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.version}</version>
</dependency>

spring context 入口文件(applicationContext.xml):

<?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:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.hxx" />
    <!-- 支持注解 -->
    <context:annotation-config />
    <!-- 支持 aop -->
    <aop:aspectj-autoproxy />
    <import resource="applicationContext-aop.xml" />
</beans>

重頭戲是 aop 的 xml 配置(applicationContext-aop.xml),其內(nèi)容是這樣的:

<?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:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="springAspect" class="com.hxx.aop.SpringAspect" />
    <aop:config proxy-target-class="true">
        <aop:aspect ref="springAspect">
            <aop:pointcut id="springPointcut"
                expression="@annotation(com.hxx.annotation.AnnotationDemo)" />
            <aop:around pointcut-ref="springPointcut" method="doAround" />
            <aop:before pointcut-ref="springPointcut" method="doBefore" />
            <aop:after pointcut-ref="springPointcut" method="doAfter" />
            <aop:after-throwing pointcut-ref="springPointcut" method="afterThrowing"
                throwing="throwable" />
            <aop:after-returning pointcut-ref="springPointcut" method="afterReturning"
                returning="returnValue" />
        </aop:aspect>
    </aop:config>
</beans>

先來讀一下這個配置文件
<aop:config></aop:config> 是一條 aop 規(guī)則,當然實際使用中可以根據(jù)需求定義多條規(guī)則。
proxy-target-class 指定是否創(chuàng)建基于類的(CGLIB)代理,默認創(chuàng)建 jdk 代理(Java interface-based)。一般選 true 即可(這二者代理的性能差別也先挖個坑,以后再填)。
aop:aspect 標簽定義切面,ref 指定切面的實例;id 設(shè)置切面的標識; order 可以指定切面的順序,用于同一個切入點(連接點)多個切面時指定執(zhí)行順序。
aop:pointcut 標簽定義一個切入點,即切面執(zhí)行的條件。id 設(shè)置標識,指定切入點對應(yīng)要執(zhí)行的方法時會使用。expression 指切點表達式,符合條件則觸發(fā)切入點。@annotation(com.hxx.annotation.AnnotationDemo)的意思是被 AnnotationDemo 注解標注的地方是切入點(expression 的詳細解釋、種類及復(fù)合用法也先挖個坑)。
aop:around 標簽定義切點圍繞的方法,這里可以根據(jù)條件拒絕執(zhí)行切點方法,也可以處理過程中產(chǎn)生的異常(吞掉或者包裝)。pointcut-ref 指定屬于哪個切入點,method 指定要執(zhí)行切面方法名,本例中是 SpringAspect 的 doAround 方法。此時 doAround 方法只能有一個 JoinPoint 類型的參數(shù),否則運行過程中會因參數(shù)不匹配而報錯。
在 around 指定的方法里,調(diào)用 JoinPoint 的 proceed 方法就會執(zhí)行切入點。around 也因圍繞著這個過程而得名。
aop:before 標簽定義切入點執(zhí)行前要執(zhí)行的方法。method 指定要執(zhí)行切面的方法名,即 JoinPoint 的 proceed 方法執(zhí)行前執(zhí)行的方法(如果同時也有 around 配置的話)。
aop:after 標簽定義切入點執(zhí)行后要執(zhí)行的方法。
aop:after-throwing 標簽定義發(fā)生異常時執(zhí)行的方法,此時可以拿到發(fā)生的異常,但無法阻止異常拋出。throwing 指定 在 method 指定的方法里異常的參數(shù)名。出現(xiàn) throwing 配置也要在方法里有同名的 Throwable 類型參數(shù)接收發(fā)生的異常,否則同樣在運行過程中會因參數(shù)不匹配而報錯。
aop:after-returning 標簽定義返回結(jié)果后執(zhí)行的方法,與 aop:after-throwing 類似,returning 指代切面方法中接收返回值的形參。配置和方法必須同時出現(xiàn)。
看完這些就可以看出 around 最為強大,ProceedingJoinPoint 參數(shù)可以控制切入點的執(zhí)行,因此其使用場景也最多。
從 JoinPoint 中可以拿到切入點的方法以及其注解信息,還原出切點處的注解配置。

AOP 使用

定義并配置好了切面、切入點,來看看怎么用。
首先模擬業(yè)務(wù)場景定義一個接口:

package com.hxx.api;

/**
 * <ul>
 * <li>功能說明:模擬業(yè)務(wù)接口</li>
 * <li>作者:tal on 2018/10/18 0018 14:34 </li>
 * <li>郵箱:houxiangxiang@cibfintech.com</li>
 * </ul>
 */

public interface ApiDemo {
    /**
     * 模擬業(yè)務(wù)方法定義
     *
     * @param input 輸入
     * @return 輸出
     */
    int work(int input);
}

簡單實現(xiàn)一下這個接口,并使用一下配置好的 AOP :

package com.hxx.api.impl;

import com.hxx.annotation.AnnotationDemo;
import com.hxx.api.ApiDemo;
import com.hxx.enums.UserType;

import org.springframework.stereotype.Service;

@Service("apiDemo")
public class ApiDemoImpl implements ApiDemo {

    @AnnotationDemo(time = 1, count = 2, name = "admin", userType = UserType.SYSTEM_ADMIN)
    public int work(int input) {
        System.out.println("------->> ApiDemo work");
        return 3 / input;
    }

}

使用非常簡單,在方法上添加注解即可,對業(yè)務(wù)完全無侵入

結(jié)果測試

借助 junit 測試用例進行測試:

package com.hxx.api;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class) //使用junit4進行測試
@ContextConfiguration(locations = {"classpath*:applicationContext.xml"}) //加載配置文件
public class ApiDemoTest {

    @Resource(name = "apiDemo")
    private ApiDemo apiDemo;

    @Test
    public void testWork() throws Exception {
        apiDemo.work(1);
    }

    @Test
    public void testWorkException() throws Exception {
        apiDemo.work(0);
    }
} 

運行 testWork() 可以得到結(jié)果:

---------------> around start
annotation ---------------> @com.hxx.annotation.AnnotationDemo(name=admin, count=2, time=1, userType=SYSTEM_ADMIN - 系統(tǒng)管理員)
---------------> before 
------->> ApiDemo work
---------------> around end
---------------> after
---------------> afterReturning +++ 3

執(zhí)行結(jié)果完全符合預(yù)期,但沒測試到出現(xiàn)異常的情況,我們再運行一下 testWorkException() :

---------------> around start
annotation ---------------> @com.hxx.annotation.AnnotationDemo(name=admin, count=2, time=1, userType=SYSTEM_ADMIN - 系統(tǒng)管理員)
---------------> before 
------->> ApiDemo work
---------------> after
---------------> afterThrowing
/ by zero

java.lang.ArithmeticException: / by zero
    at com.hxx.api.impl.ApiDemoImpl.work(ApiDemoImpl.java:15)
    ……

當出現(xiàn)異常且在 around 中沒有吞掉時,afterThrowing 執(zhí)行了,after 也執(zhí)行了,但 afterReturning 無法再執(zhí)行,around 中拋出異常后的語句也無法再執(zhí)行。
如果在 around 中吞掉異常,修改 SpringAspect 的 doAround 方法為(僅僅注釋了 throw 語句):

 public Object doAround(ProceedingJoinPoint point) throws Throwable {
        System.out.println("---------------> around start");
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        AnnotationDemo annotation = methodSignature.getMethod().getAnnotation(AnnotationDemo.class);
        System.out.println("annotation ---------------> " + annotation);
        Object proceed = 0;
        try {
            proceed = point.proceed();
        } catch (Throwable throwable) {
           // throw throwable;
        }
        System.out.println("---------------> around end");
        return proceed;
    }

重新運行 testWorkException 后得到結(jié)果:

---------------> around start
annotation ---------------> @com.hxx.annotation.AnnotationDemo(name=admin, count=2, time=1, userType=SYSTEM_ADMIN - 系統(tǒng)管理員)
---------------> before 
------->> ApiDemo work
---------------> around end
---------------> after
---------------> afterReturning +++ 0

由此看到可以在 around 中吞掉異常,給回默認返回值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • AOP實現(xiàn)可分為兩類(按AOP框架修改源代碼的時機): 靜態(tài)AOP實現(xiàn):AOP框架在編譯階段對程序進行修改,即實現(xiàn)...
    數(shù)獨題閱讀 2,401評論 0 22
  • 本章內(nèi)容: 面向切面編程的基本原理 通過POJO創(chuàng)建切面 使用@AspectJ注解 為AspectJ切面注入依賴 ...
    謝隨安閱讀 3,425評論 0 9
  • 一、AOP 簡介 AOP(Aspect-Oriented Programming, 面向切面編程): 是一種新的方...
    leeqico閱讀 911評論 0 1
  • AOP,也就是面向方面編程或者說面向面編程,是一種很重要的思想。在企業(yè)級系統(tǒng)中經(jīng)常需要打印日志、事務(wù)管理這樣針對某...
    樂百川閱讀 963評論 0 8
  • 中華武術(shù)博大精深,武俠小說更是源遠流長、流傳甚廣,其中最讓人神往的就是各類武林秘籍啦,葵花寶典、易筋經(jīng)、九陰真經(jīng)、...
    振曰閱讀 2,068評論 0 5

友情鏈接更多精彩內(nèi)容