1. 程式人生 > >基於註解方式的AOP的配置與應用

基於註解方式的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的代理物件。

  • AOP代理(AOP Proxy) :在Spring AOP中有兩種代理方式,JDK動態代理和CGLIB代理。預設情況下,TargetObject實現了介面時,則採用JDK動態代理,例如,AServiceImpl;反之,採用CGLIB代理,例如,BServiceImpl。強制使用CGLIB代理需要將 <aop:config>的 proxy-target-class屬性設為true。

通知(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:aspect>配置如下:

<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() (打印出正在被通知的方法的有用資訊)。