Java AOP & Spring AOP 原理和實現
1 定義介面,並建立介面實現類
public interface HelloWorld { void printHelloWorld(); void doPrint(); } public class HelloWorldImpl1 implements HelloWorld { public void printHelloWorld() { System.out.println("Enter HelloWorldImpl1.printHelloWorld()"); } public void doPrint() { System.out.println("Enter HelloWorldImpl1.doPrint()"); return ; } } public class HelloWorldImpl2 implements HelloWorld { public void printHelloWorld() { System.out.println("Enter HelloWorldImpl2.printHelloWorld()"); } public void doPrint() { System.out.println("Enter HelloWorldImpl2.doPrint()"); return ; } }
2 編輯AOP中需要使用到的通知類(橫切關注點,這裡是列印時間)
public class TimeHandler
{
public void printTime()
{
System.out.println("CurrentTime = " + System.currentTimeMillis());
}
}
在此可以定義前置和後置的列印。
3 配置容器初始化時需要的XML檔案
aop01.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: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://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"> <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" /> <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" /> <bean id="timeHandler" class="com.xrq.aop.TimeHandler" /> <aop:config> <aop:aspect id="time" ref="timeHandler"> <aop:pointcut id="addAllMethod" expression="execution(* com.xrq.aop.HelloWorld.*(..))" /> <aop:before method="printTime" pointcut-ref="addAllMethod" /> <aop:after method="printTime" pointcut-ref="addAllMethod" /> </aop:aspect> </aop:config> </beans>
4 測試程式碼Test.java如下:
package com.zhangguo.Spring052.aop01; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Test { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml"); HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1"); HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2"); hw1.printHelloWorld(); System.out.println(); hw1.doPrint(); System.out.println(); hw2.printHelloWorld(); System.out.println(); hw2.doPrint(); } }
執行結果為:
CurrentTime = 1446129611993
Enter HelloWorldImpl1.printHelloWorld()
CurrentTime = 1446129611993
CurrentTime = 1446129611994
Enter HelloWorldImpl1.doPrint()
CurrentTime = 1446129611994
CurrentTime = 1446129611994
Enter HelloWorldImpl2.printHelloWorld()
CurrentTime = 1446129611994
CurrentTime = 1446129611994
Enter HelloWorldImpl2.doPrint()
CurrentTime = 1446129611994
對於需要使用多個aspect,可以使用如下xml定義:
<aop:config>
<aop:aspect id="time" ref="timeHandler" order="1">
<aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
<aop:before method="printTime" pointcut-ref="addTime" />
<aop:after method="printTime" pointcut-ref="addTime" />
</aop:aspect>
<aop:aspect id="log" ref="logHandler" order="2">
<aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
<aop:before method="LogBefore" pointcut-ref="printLog" />
<aop:after method="LogAfter" pointcut-ref="printLog" />
</aop:aspect>
</aop:config>
要想讓logHandler在timeHandler前使用有兩個辦法:
(1)aspect裡面有一個order屬性,order屬性的數字就是橫切關注點的順序
(2)把logHandler定義在timeHandler前面,Spring預設以aspect的定義順序作為織入順序pointcut的expression用於匹配方法,增減其粒度可以達到不同的過濾效果。
<aop:config>
<aop:aspect id="time" ref="timeHandler" order="1">
<aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.print*(..))" />
<aop:before method="printTime" pointcut-ref="addTime" />
<aop:after method="printTime" pointcut-ref="addTime" />
</aop:aspect>
<aop:aspect id="log" ref="logHandler" order="2">
<aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.do*(..))" />
<aop:before method="LogBefore" pointcut-ref="printLog" />
<aop:after method="LogAfter" pointcut-ref="printLog" />
</aop:aspect>
</aop:config>
表示timeHandler只會織入HelloWorld介面print開頭的方法,logHandler只會織入HelloWorld介面do開頭的方法。
使用CGLIB生成代理
CGLIB針對代理物件為類的情況使用。在spring配置檔案中加入<aop:aspectj-autoproxy proxy-target-class="true"/>可強制使用CGLIB生成代理。
1、如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP
2、如果目標物件實現了介面,可以強制使用CGLIB實現AOP
3、如果目標物件沒有實現類介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換
JDK動態代理和CGLIB位元組碼生成的區別?
* JDK動態代理只能對實現了介面的類生成代理,而不能針對類
* CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法,因為是繼承,所以該類或方法最好不要宣告成final
CGLIB建立代理主要是建立Enhancer enhancer,並通過AdvisedSupport設定相應的屬性,比如目標類rootClass,如果由介面合併介面給代理類,最主要的是設定Callback集合和CallbackFilter,使用CallBackFilter可以根據方法的不同使用不同的Callback進行攔截和增強方法。其中最主要的使用於AOP的Callback是DynamicAdvisedInterceptor。
在Spring配置檔案中配置的區別:
<bean id="#" class="org.springframework.ProxyFactoryBean">
<property name="proxyTargetClass">
<value>true</value>
</property>
</bean>
<bean id="#" class="org.springframework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.gc.impl.TimeBookInterface</value>
</property>
</bean>
4.2 使用註解配置AOP
使用註解方式開發AOP比較靈活方便,其實現需要如下5個步驟。
1. 配置檔案中加入AOP的名稱空間
使用<aop:config/>標籤,需要給Spring配置檔案中引入基於xml schema的Spring AOP名稱空間。完成後的Spring配置檔案如下:
<?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-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd >
<!--Spring配置資訊-->
</beans>
2. 啟用自動代理功能
<?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">
<!-- 啟用元件掃描功能,在包com.spring.aop.imp及其子包下面自動掃描通過註解配置的元件 -->
<context:component-scan base-package="com.spring.aop.service"/>
<!-- 啟用自動代理功能 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 使用者服務物件 -->
<bean id="userService" class="cn.spring.aop.service.imp.PersonServiceBean" />
</beans>
3. 編寫實現類
package com.spring.aop.service;
public interface PersonServer {
public void save(String name);
public void update(String name, Integer id);
public String getPersonName(Integer id);
}
package com.spring.aop.service.imp;
import com.spring.aop.service.PersonServer;
public class PersonServiceBean implements PersonServer{
@Override
public void save(String name) {
System.out.println("我是save方法");
// throw new RuntimeException();
}
@Override
public void update(String name, Integer id) {
System.out.println("我是update()方法");
}
@Override
public String getPersonName(Integer id) {
System.out.println("我是getPersonName()方法");
return "xxx";
}
}
4. 編寫切面類,幷包含@Aspect註解
切面類用於實現切面功能,切面首先是一個IOC中的bean,即加入@Component註解,切面還需要加入@Aspect註解
package com.spring.aop.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
//指定切面的優先順序,當有多個切面時,數值越小優先順序越高
@Order(1)
//把這個類宣告為一個切面:需要把該類放入到IOC容器中。再宣告為一個切面.
@Aspect
@Component
public class LoggingAspect {
/**
* 宣告切入點表示式,一般在該方法中不再新增其他程式碼。
* 使用@Pointcut來宣告切入點表示式。
* 後面的通知直接使用方法名來引用當前的切入點表示式。
*/
@Pointcut("execution(public int com.spring.aop.impl.PersonServiceBean.*(..))")//此處定義了其切入點
public void declareJoinPointExpression() {}
/**
*前置通知,在目標方法開始之前執行。
*@Before("execution(public int com.spring.aop.impl.PersonServiceBean.add(int, int))")這樣寫可以指定特定的方法。
* @param joinpoint
*/
@Before("declareJoinPointExpression()")
//這裡使用切入點表示式即可。後面的可以都改成切入點表示式。如果這個切入點表示式在別的包中,在前面加上包名和類名即可。
public void beforeMethod(JoinPoint joinpoint) {
String methodName = joinpoint.getSignature().getName();
List<Object>args = Arrays.asList(joinpoint.getArgs());
System.out.println("前置通知:The method "+ methodName +" begins with " + args);
}
/**
*後置通知,在目標方法執行之後開始執行,無論目標方法是否丟擲異常。
*在後置通知中不能訪問目標方法執行的結果。
* @param joinpoint
*/
@After("execution(public int com.spring.aop.impl.PersonServiceBean.*(int, int))")
public void afterMethod(JoinPoint joinpoint) {
String methodName = joinpoint.getSignature().getName();
//List<Object>args = Arrays.asList(joinpoint.getArgs()); 後置通知方法中可以獲取到引數
System.out.println("後置通知:The method "+ methodName +" ends ");
}
/**
*返回通知,在方法正常結束之後執行。
*可以訪問到方法的返回值。
* @param joinpoint
* @param result 目標方法的返回值
*/
@AfterReturning(value="execution(public int com.spring.aop.impl.PersonServiceBean.*(..))", returning="result")
public void afterReturnning(JoinPoint joinpoint, Object result) {
String methodName = joinpoint.getSignature().getName();
System.out.println("返回通知:The method "+ methodName +" ends with " + result);
}
/**
*異常通知。目標方法出現異常的時候執行,可以訪問到異常物件,可以指定在出現特定異常時才執行。
*假如把引數寫成NullPointerException則只在出現空指標異常的時候執行。
* @param joinpoint
* @param e
*/
@AfterThrowing(value="execution(public int com.spring.aop.impl.PersonServiceBean.*(..))", throwing="e")
public void afterThrowing(JoinPoint joinpoint, Exception e) {
String methodName = joinpoint.getSignature().getName();
System.out.println("異常通知:The method "+ methodName +" occurs exception " + e);
}
/**
* 環繞通知類似於動態代理的全過程,ProceedingJoinPoint型別的引數可以決定是否執行目標方法。
* @param point 環繞通知需要攜帶ProceedingJoinPoint型別的引數。
* @return 目標方法的返回值。必須有返回值。
*/
/*不常用
@Around("execution(public int com.spring.aop.impl.PersonServiceBean.*(..))")
public Object aroundMethod(ProceedingJoinPoint point) {
Object result = null;
String methodName = point.getSignature().getName();
try {
//前置通知
System.out.println("The method "+ methodName +" begins with " + Arrays.asList(point.getArgs()));
//執行目標方法
result = point.proceed();
//翻譯通知
System.out.println("The method "+ methodName +" ends with " + result);
} catch (Throwable e) {
//異常通知
System.out.println("The method "+ methodName +" occurs exception " + e);
throw new RuntimeException(e);
}
//後置通知
System.out.println("The method "+ methodName +" ends");
return result;
}
*/
}
5. 測試類
package com.spring.aop.aspect;
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.spring.aop.service.imp.UserService;
import cn.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());
}
}
}
}
執行可知其達到了AOP的目的。
4.3 零配置實現Spring IoC與AOP
為了實現零配置在原有示例的基礎上我們新增一個類User,如下所示:
package com.zhangguo.Spring052.aop05;
public class User {
public void show(){
System.out.println("一個使用者物件");
}
}
該類並未註解,容器不會自動管理。因為沒有xml配置檔案,則使用一個作為配置資訊,ApplicationCfg.java檔案如下:
package com.zhangguo.Spring052.aop05;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration //用於表示當前類為容器的配置類,類似<beans/>
@ComponentScan(basePackages="com.zhangguo.Spring052.aop05") //掃描的範圍,相當於xml配置的結點<context:component-scan/>
@EnableAspectJAutoProxy(proxyTargetClass=true) //自動代理,相當於<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
public class ApplicationCfg {
//在配置中宣告一個bean,相當於<bean id=getUser class="com.zhangguo.Spring052.aop05.User"/>
@Bean
public User getUser(){
return new User();
}
}
該類的每一部分內容基本都與xml 配置有一對一的關係,請看註釋,這樣做要比寫xml方便,但不便釋出後修改。
Advice類程式碼如下:
package com.zhangguo.Spring052.aop04;
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;
/**
* 通知類,橫切邏輯
*/
@Component
@Aspect
public class Advices {
//切點
@Pointcut("execution(* com.zhangguo.Spring052.aop04.Math.a*(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut()")
public void before(JoinPoint jp){
System.out.println(jp.getSignature().getName());
System.out.println("----------前置通知----------");
}
//最終通知
@After("pointcut()")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
//環繞通知
@Around("execution(* com.zhangguo.Spring052.aop04.Math.s*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println(pjp.getSignature().getName());
System.out.println("----------環繞前置----------");
Object result=pjp.proceed();
System.out.println("----------環繞後置----------");
return result;
}
//返回結果通知
@AfterReturning(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.m*(..))",returning="result")
public void afterReturning(JoinPoint jp,Object result){
System.out.println(jp.getSignature().getName());
System.out.println("結果是:"+result);
System.out.println("----------返回結果----------");
}
//異常後通知
@AfterThrowing(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.d*(..))",throwing="exp")
public void afterThrowing(JoinPoint jp,Exception exp){
System.out.println(jp.getSignature().getName());
System.out.println("異常訊息:"+exp.getMessage());
System.out.println("----------異常通知----------");
}
}
測試程式碼如下:
package com.zhangguo.Spring052.aop05;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
// 通過類初始化容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(ApplicationCfg.class);
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 0;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
try {
math.div(n1, n2);
} catch (Exception e) {
}
User user=ctx.getBean("getUser",User.class);
user.show();
}
}
測試結果能夠滿足要求。
5. Spring 中的advice和aspect的區別
5.1 含義區別
— 方/切 面(Aspect):一個關注點的模組化,這個關注點實現可能另外橫切多個物件。事務管理是J2EE應用中一個很好的橫切關注點例子。方面用spring的Advisor或攔截器實現。
— 通知(Advice):在特定的連線點,AOP框架執行的動作。各種型別的通知包括“around”、“before”和“throws”通知。
— 切入點(Pointcut):指定一個通知將被引發的一系列連線點的集合。AOP框架必須允許開發者指定切入點,例如,使用正則表示式。
— 連線點/織入點(Joinpoint):程式執行過程中明確的點,如方法的呼叫或特定的異常被丟擲。
所以“<aop:aspect>”實際上是定義橫切邏輯,就是在連線點上做什麼,“<aop:advisor>”則定義了在哪些連線點應用什麼<aop:aspect>。Spring這樣做的好處就是可以讓多個橫切邏輯(即<aop:aspect>定義的)多次使用,提供可重用性。
1、Advisor是一種特殊的Aspect,Advisor代表spring中的Aspect
2、區別:advisor只持有一個Pointcut和一個advice,而aspect可以多個pointcut和多個advice
5.2 advisor和aspect的使用區別
在spring的配置中,會用到這兩個標籤.那麼他們的區別是什麼呢?
<bean id="testAdvice" class="com.myspring.app.aop.MyAdvice"/> //切面程式碼
分別使用aspect和advisor定義一個aspect如下:
<aop:config>
<aop:aspect ref="testAdvice" id="testAspect">
<aop:pointcut expression="(execution(* com.myspring.app.aop.TestPoint.*(..)))" id="testPointcut"/>
<aop:before method="doBefore" pointcut-ref="testPointcut"/>
</aop:aspect>
</aop:config>
<aop:config>
<aop:pointcut expression="(execution(* com.myspring.app.aop.TestPoint.*(..)))" id="mypoint"/>
<aop:advisor advice-ref="testAdvice" pointcut-ref="mypoint"/>
</aop:config>
使用程式碼如下:
package com.myspring.app.aop;
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.springframework.aop.MethodBeforeAdvice;
/**
* 方法前置通知
* @author Michael
*
*/
@Component("myAdvice")//如果是自動裝配,在定義切面的時候直接寫在ref屬性裡就可以了
public class MyAdvice implements MethodBeforeAdvice{
//如果使用aop:advisor配置,那麼切面邏輯必須要實現advice接口才行!否則會失敗!
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("前置通知");
}
//如果是<aop:aspect>配置,編寫一般的方法就可以了,然後在切面配置中指定具體的方法名稱!
public void doBefore(JoinPoint point) throws Throwable {
}
}
兩者的區別在於:
1. 如果使用aop:advisor配置,那麼切面邏輯必須要實現advice接口才行!否則會失敗。
2. 如果是aop:aspect配置,編寫一般的方法就可以了,然後在切面配置中指定具體的方法名稱。