基於註解的Spring AOP的配置和使用

AOP是OOP的延續,是Aspect Oriented Programming的縮寫,意思是面向切面編程。可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是調用者和被調用者之間的解耦,AOP可以說也是這種目標的一種實現。

我們現在做的一些非業務,如:日誌、事務、安全等都會寫在業務代碼中(也即是說,這些非業務類橫切於業務類),但這些代碼往往是重複,複製——粘貼式的代碼會給程序的維護帶來不便,AOP就實現了把這些業務需求與系統需求分開來做。這種解決的方式也稱代理機制。

先來了解一下AOP的相關概念,《Spring參考手冊》中定義了以下幾個AOP的重要概念,結合以上代碼分析如下:

*切面(Aspect):官方的抽象定義爲“一個關注點的模塊化,這個關注點可能會橫切多個對象”,在本例中,“切面”就是類TestAspect所關注的具體行爲,例如,AServiceImpl.barA()的調用就是切面TestAspect所關注的行爲之一。“切面”在ApplicationContext中<aop:aspect>來配置。

*連接點(Joinpoint) :程序執行過程中的某一行爲,例如,UserService.get的調用或者UserService.delete拋出異常等行爲。

*通知(Advice) :“切面”對於某個“連接點”所產生的動作,例如,TestAspect中對com.spring.service包下所有類的方法進行日誌記錄的動作就是一個Advice。其中,一個“切面”可以包含多個“Advice”,例如ServiceAspect。

*切入點(Pointcut) :匹配連接點的斷言,在AOP中通知和一個切入點表達式關聯。例如,TestAspect中的所有通知所關注的連接點,都由切入點表達式execution(* com.spring.service.*.*(..))來決定。

*目標對象(Target Object) :被一個或者多個切面所通知的對象。例如,AServcieImpl和BServiceImpl,當然在實際運行時,Spring AOP採用代理實現,實際AOP操作的是TargetObject的代理對象。

通知(Advice)類型:

*前置通知(Before advice):在某連接點(JoinPoint)之前執行的通知,但這個通知不能阻止連接點前的執行。ApplicationContext中在<aop:aspect>裏面使用<aop:before>元素進行聲明。例如,TestAspect中的doBefore方法。

*後置通知(After advice):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。ApplicationContext中在<aop:aspect>裏面使用<aop:after>元素進行聲明。例如,ServiceAspect中的returnAfter方法,所以Teser中調用UserService.delete拋出異常時,returnAfter方法仍然執行。

*返回後通知(After return advice):在某連接點正常完成後執行的通知,不包括拋出異常的情況。ApplicationContext中在<aop:aspect>裏面使用<after-returning>元素進行聲明。

*環繞通知(Around advice):包圍一個連接點的通知,類似Web中Servlet規範中的Filter的doFilter方法。可以在方法的調用前後完成自定義的行爲,也可以選擇不執行。ApplicationContext中在<aop:aspect>裏面使用<aop:around>元素進行聲明。例如,ServiceAspect中的around方法。

*拋出異常後通知(After throwing advice):在方法拋出異常退出時執行的通知。ApplicationContext中在<aop:aspect>裏面使用<aop:after-throwing>元素進行聲明。例如,ServiceAspect中的returnThrow方法。

注:可以將多個通知應用到一個目標對象上,即可以將多個切面織入到同一目標對象。
使用Spring AOP可以基於兩種方式,一種是比較方便和強大的註解方式,另一種則是中規中矩的xml配置方式。

先說註解,使用註解配置Spring AOP總體分爲兩步,第一步是在xml文件中聲明激活自動掃描組件功能,同時激活自動代理功能(同時在xml中添加一個UserService的普通服務層組件,來測試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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
    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-3.0.xsd">

<!-- 激活組件掃描功能,在包cn.ysh.studio.spring.aop及其子包下面自動掃描通過註解配置的組件 -->
<context:component-scan base-package="cn.ysh.studio.spring.aop"/>
<!-- 激活自動代理功能 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

<!-- 用戶服務對象 -->
<bean id="userService" class="cn.ysh.studio.spring.aop.service.UserService" />
</beans>

第二步是爲Aspect切面類添加註解:

package cn.ysh.studio.spring.aop.aspect;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
 * 系統服務組件Aspect切面Bean
 * @author Shenghany
 * @date 2013-5-28
 */
//聲明這是一個組件
@Component
//聲明這是一個切面Bean
@Aspect
public class ServiceAspect {
private final static Log log = LogFactory.getLog(ServiceAspect.class);

//配置切入點,該方法無方法體,主要爲方便同類中其他方法使用此處配置的切入點
@Pointcut("execution(* cn.ysh.studio.spring.aop.service..*(..))")
public void aspect(){   }

/*
 * 配置前置通知,使用在方法aspect()上註冊的切入點
 * 同時接受JoinPoint切入點對象,可以沒有該參數
 */
@Before("aspect()")
public void before(JoinPoint joinPoint){
    if(log.isInfoEnabled()){
        log.info("before " + joinPoint);
    }
}

//配置後置通知,使用在方法aspect()上註冊的切入點
@After("aspect()")
    public void after(JoinPoint joinPoint){
        if(log.isInfoEnabled()){
            log.info("after " + joinPoint);
        }
    }

    //配置環繞通知,使用在方法aspect()上註冊的切入點
    @Around("aspect()")
    public void around(JoinPoint joinPoint){
        long start = System.currentTimeMillis();
        try {
            ((ProceedingJoinPoint) joinPoint).proceed();
            long end = System.currentTimeMillis();
            if(log.isInfoEnabled()){
                log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
            }
        } catch (Throwable e) {
            long end = System.currentTimeMillis();
            if(log.isInfoEnabled()){
                log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
            }
        }
    }

    //配置後置返回通知,使用在方法aspect()上註冊的切入點
    @AfterReturning("aspect()")
    public void afterReturn(JoinPoint joinPoint){
        if(log.isInfoEnabled()){
            log.info("afterReturn " + joinPoint);
        }
    }

    //配置拋出異常後通知,使用在方法aspect()上註冊的切入點
    @AfterThrowing(pointcut="aspect()", throwing="ex")
    public void afterThrow(JoinPoint joinPoint, Exception ex){
        if(log.isInfoEnabled()){
            log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
        }
    }   
}

測試代碼:

package cn.ysh.studio.spring.aop;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.ysh.studio.spring.aop.service.UserService;
import cn.ysh.studio.spring.mvc.bean.User;

/**
 * Spring AOP測試
 * @author Shenghany
 * @date 2013-5-28
 */
public class Tester {

    private final static Log log = LogFactory.getLog(Tester.class);

    public static void main(String[] args) {
        //啓動Spring容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //獲取service組件
        UserService service = (UserService) context.getBean("userService");
        //以普通的方式調用UserService對象的三個方法
        User user = service.get(1L);
        service.save(user);
        try {
            service.delete(1L);
        } catch (Exception e) {
            if(log.isWarnEnabled()){
                log.warn("Delete user : " + e.getMessage());
            }
        }
    }
}

控制檯輸出如下:

 INFO [spring.aop.aspect.ServiceAspect:40] before execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))
 INFO [spring.aop.service.UserService:19] getUser method . . .
 INFO [spring.aop.aspect.ServiceAspect:60] around execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))    Use time : 42 ms!
 INFO [spring.aop.aspect.ServiceAspect:48] after execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))
 INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))
 INFO [spring.aop.aspect.ServiceAspect:40] before execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))
 INFO [spring.aop.service.UserService:26] saveUser method . . .
 INFO [spring.aop.aspect.ServiceAspect:60] around execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))   Use time : 2 ms!
 INFO [spring.aop.aspect.ServiceAspect:48] after execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))
 INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))
 INFO [spring.aop.aspect.ServiceAspect:40] before execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))
 INFO [spring.aop.service.UserService:32] delete method . . .
 INFO [spring.aop.aspect.ServiceAspect:65] around execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))  Use time : 5 ms with exception : spring aop ThrowAdvice演示
 INFO [spring.aop.aspect.ServiceAspect:48] after execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))
 INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))
 WARN [studio.spring.aop.Tester:32] Delete user : Null return value from advice does not match primitive return type for: public boolean cn.ysh.studio.spring.aop.service.UserService.delete(long) throws java.lang.Exception

可以看到,正如我們預期的那樣,雖然我們並沒有對UserSerivce類包括其調用方式做任何改變,但是Spring仍然攔截到了其中方法的調用,或許這正是AOP的魔力所在。

再簡單說一下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/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
        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-3.0.xsd">


    <!-- 系統服務組件的切面Bean -->
    <bean id="serviceAspect" class="cn.ysh.studio.spring.aop.aspect.ServiceAspect"/>
    <!-- AOP配置 -->
    <aop:config>
        <!-- 聲明一個切面,並注入切面Bean,相當於@Aspect -->
        <aop:aspect id="simpleAspect" ref="serviceAspect">
            <!-- 配置一個切入點,相當於@Pointcut -->
            <aop:pointcut expression="execution(* cn.ysh.studio.spring.aop.service..*(..))" id="simplePointcut"/>
            <!-- 配置通知,相當於@Before、@After、@AfterReturn、@Around、@AfterThrowing -->
            <aop:before pointcut-ref="simplePointcut" method="before"/>
            <aop:after pointcut-ref="simplePointcut" method="after"/>
            <aop:after-returning pointcut-ref="simplePointcut" method="afterReturn"/>
            <aop:after-throwing pointcut-ref="simplePointcut" method="afterThrow" throwing="ex"/>
        </aop:aspect>
    </aop:config>
</beans>

個人覺得不如註解靈活和強大,你可以不同意這個觀點,但是不知道如下的代碼會不會讓你的想法有所改善:

//配置前置通知,攔截返回值爲cn.ysh.studio.spring.mvc.bean.User的方法
@Before("execution(cn.ysh.studio.spring.mvc.bean.User cn.ysh.studio.spring.aop.service..*(..))")
public void beforeReturnUser(JoinPoint joinPoint){
    if(log.isInfoEnabled()){
        log.info("beforeReturnUser " + joinPoint);
    }
}

//配置前置通知,攔截參數爲cn.ysh.studio.spring.mvc.bean.User的方法
@Before("execution(* cn.ysh.studio.spring.aop.service..*(cn.ysh.studio.spring.mvc.bean.User))")
public void beforeArgUser(JoinPoint joinPoint){
    if(log.isInfoEnabled()){
        log.info("beforeArgUser " + joinPoint);
    }
}

//配置前置通知,攔截含有long類型參數的方法,並將參數值注入到當前方法的形參id中
@Before("aspect()&&args(id)")
public void beforeArgId(JoinPoint joinPoint, long id){
    if(log.isInfoEnabled()){
        log.info("beforeArgId " + joinPoint + "\tID:" + id);
    }
}


附上UserService的代碼(其實很簡單):



package cn.ysh.studio.spring.aop.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import cn.ysh.studio.spring.mvc.bean.User;

/**
 * 用戶服務模型
 * @author Shenghany
 * @date 2013-5-28
 */
public class UserService {

    private final static Log log = LogFactory.getLog(UserService.class);

    public User get(long id){
        if(log.isInfoEnabled()){
            log.info("getUser method . . .");
        }
        return new User();
    }

    public void save(User user){
        if(log.isInfoEnabled()){
            log.info("saveUser method . . .");
        }
    }

    public boolean delete(long id) throws Exception{
        if(log.isInfoEnabled()){
            log.info("delete method . . .");
            throw new Exception("spring aop ThrowAdvice演示");
        }
        return false;
    }
}

應該說學習Spring AOP有兩個難點,第一點在於理解AOP的理念和相關概念,第二點在於靈活掌握和使用切入點表達式。概念的理解通常不在一朝一夕,慢慢浸泡的時間長了,自然就明白了,下面我們簡單地介紹一下切入點表達式的配置規則吧。

通常情況下,表達式中使用”execution“就可以滿足大部分的要求。表達式格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

modifiers-pattern:方法的操作權限
ret-type-pattern:返回值
declaring-type-pattern:方法所在的包
name-pattern:方法名
parm-pattern:參數名
throws-pattern:異常

其中,除ret-type-pattern和name-pattern之外,其他都是可選的。上例中,execution(* com.spring.service..(..))表示com.spring.service包下,返回值爲任意類型;方法名任意;參數不作限制的所有方法。

最後說一下通知參數

可以通過args來綁定參數,這樣就可以在通知(Advice)中訪問具體參數了。例如,配置如下:

<aop:config>
  <aop:aspect id="TestAspect" ref="aspectBean">
   <aop:pointcut id="businessService"
    expression="execution(* com.spring.service.*.*(String,..)) and args(msg,..)" />
    <aop:after pointcut-ref="businessService" method="doAfter"/>
  </aop:aspect>
</aop:config>

上面的代碼args(msg,..)是指將切入點方法上的第一個String類型參數添加到參數名爲msg的通知的入參上,這樣就可以直接使用該參數啦。

訪問當前的連接點

在上面的Aspect切面Bean中已經看到了,每個通知方法第一個參數都是JoinPoint。其實,在Spring中,任何通知(Advice)方法都可以將第一個參數定義爲 org.aspectj.lang.JoinPoint類型用以接受當前連接點對象。JoinPoint接口提供了一系列有用的方法, 比如 getArgs() (返回方法參數)、getThis() (返回代理對象)、getTarget() (返回目標)、getSignature() (返回正在被通知的方法相關信息)和 toString() (打印出正在被通知的方法的有用信息)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章