1. 程式人生 > >什麼是面向切面程式設計(AOP)

什麼是面向切面程式設計(AOP)

這種在執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計。

AOP是Spring提供的關鍵特性之一。AOP即面向切面程式設計,是OOP程式設計的有效補充。使用AOP技術,可以將一些系統性相關的程式設計工作,獨立提取出來,獨立實現,然後通過切面切入進系統。從而避免了在業務邏輯的程式碼中混入很多的系統相關的邏輯——比如許可權管理,事物管理,日誌記錄等等。這些系統性的程式設計工作都可以獨立編碼實現,然後通過AOP技術切入進系統即可。從而達到了 將不同的關注點分離出來的效果。本文深入剖析Spring的AOP的原理。

1. AOP相關的概念

1) Aspect

 :切面,切入系統的一個切面。比如事務管理是一個切面,許可權管理也是一個切面;

2) Join point :連線點,也就是可以進行橫向切入的位置;

3) Advice :通知,切面在某個連線點執行的操作(分為: Before advice , After returning advice , After throwing advice , After (finally) advice , Around advice );

4) Pointcut :切點,符合切點表示式的連線點,也就是真正被切入的地方;

2. AOP 的實現原理

AOP分為靜態AOP和動態AOP。靜態AOP是指AspectJ實現的AOP,他是將切面程式碼直接編譯到Java類檔案中。動態AOP是指將切面程式碼進行動態織入實現的AOP。Spring的AOP為動態AOP,實現的技術為: JDK提供的動態代理技術

 和 CGLIB(動態位元組碼增強技術) 。儘管實現技術不一樣,但 都是基於代理模式 , 都是生成一個代理物件 。

1) JDK動態代理

主要使用到 InvocationHandler 介面和 Proxy.newProxyInstance() 方法。 JDK動態代理要求被代理實現一個介面,只有介面中的方法才能夠被代理 。其方法是將被代理物件注入到一箇中間物件,而中間物件實現InvocationHandler介面,在實現該介面時,可以在 被代理物件呼叫它的方法時,在呼叫的前後插入一些程式碼。而 Proxy.newProxyInstance() 能夠利用中間物件來生產代理物件。插入的程式碼就是切面程式碼。所以使用JDK動態代理可以實現AOP。我們看個例子:

被代理物件實現的介面,只有介面中的方法才能夠被代理:

public interface UserService {
    publicvoid addUser(User user);
    public User getUser(int id);
}

被代理物件:

public class UserServiceImpl implements UserService {
	publicvoid addUser(User user) {
		System.out.println("add user into database.");
	}
	public User getUser(int id) {
		User user = new User();
		user.setId(id);
		System.out.println("getUser from database.");
		return user;
	}
}

代理中間類:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyUtil implements InvocationHandler {
	private Object target;	// 被代理的物件
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("do sth before....");
		Object result =  method.invoke(target, args);
		System.out.println("do sth after....");
		return result;
	}
	ProxyUtil(Object target){
		this.target = target;
	}
	public Object getTarget() {
		return target;
	}
	public void setTarget(Object target) {
		this.target = target;
	}
}

測試:

import java.lang.reflect.Proxy;
import net.aazj.pojo.User;
public class ProxyTest {
	public static void main(String[] args){
		Object proxyedObject = new UserServiceImpl();	// 被代理的物件
		ProxyUtil proxyUtils = new ProxyUtil(proxyedObject);
		// 生成代理物件,對被代理物件的這些介面進行代理:UserServiceImpl.class.getInterfaces()
		UserService proxyObject = (UserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 
					UserServiceImpl.class.getInterfaces(), proxyUtils);
		proxyObject.getUser(1);
		proxyObject.addUser(new User());
	}
}

執行結果:

do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....

我們看到在 UserService介面中的方法 addUser 和 getUser方法的前面插入了我們自己的程式碼。這就是JDK動態代理實現AOP的原理。

我們看到該方式有一個要求, 被代理的物件必須實現介面,而且只有介面中的方法才能被代理 。

2)CGLIB (code generate libary)

位元組碼生成技術實現AOP,其實就是繼承被代理物件,然後Override需要被代理的方法,在覆蓋該方法時,自然是可以插入我們自己的程式碼的。因為需要Override被代理物件的方法,所以自然CGLIB技術實現AOP時,就 必須要求需要被代理的方法不能是final方法,因為final方法不能被子類覆蓋 。我們使用CGLIB實現上面的例子:

package net.aazj.aop;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CGProxy implements MethodInterceptor{
	private Object target;	// 被代理物件
	public CGProxy(Object target){
		this.target = target;
	}
	public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy proxy) throws Throwable {
		System.out.println("do sth before....");
		Object result = proxy.invokeSuper(arg0, arg2);
		System.out.println("do sth after....");
		return result;
	}
	public Object getProxyObject() {
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(this.target.getClass());	// 設定父類
		// 設定回撥
		enhancer.setCallback(this);	// 在呼叫父類方法時,回撥 this.intercept()
		// 建立代理物件
		return enhancer.create();
	}
}
public class CGProxyTest {
	publicstaticvoid main(String[] args){
		Object proxyedObject = new UserServiceImpl();	// 被代理的物件
		CGProxy cgProxy = new CGProxy(proxyedObject);
		UserService proxyObject = (UserService) cgProxy.getProxyObject();
		proxyObject.getUser(1);
		proxyObject.addUser(new User());
	}
}

輸出結果:

do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....

我們看到達到了同樣的效果。它的原理是生成一個父類 enhancer.setSuperclass( this.target.getClass()) 的子類 enhancer.create() ,然後對父類的方法進行攔截enhancer.setCallback( this) . 對父類的方法進行覆蓋,所以父類方法不能是final的。

3) 接下來我們看下spring實現AOP的相關原始碼:

@SuppressWarnings("serial")
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface()) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

從上面的原始碼我們可以看到:

if (targetClass.isInterface()) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);

如果被代理物件實現了介面,那麼就使用JDK的動態代理技術,反之則使用CGLIB來實現AOP,所以 Spring預設是使用JDK的動態代理技術實現AOP的 。

JdkDynamicAopProxy的實現其實很簡單:

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {	
@Override
public Object getProxy(ClassLoader classLoader) {
	if (logger.isDebugEnabled()) {
		logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
	}
	Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
	findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
	return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

3. Spring AOP的配置

Spring中AOP的配置一般有兩種方法,一種是使用 <aop:config> 標籤在xml中進行配置,一種是使用註解以及@Aspect風格的配置。

1) 基於<aop:config>的AOP配置

下面是一個典型的事務AOP的配置:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager"?>
	<tx:attributes >
		<tx:method name="add*" propagation="REQUIRED" />
		<tx:method name="append*" propagation="REQUIRED" />
		<tx:method name="insert*" propagation="REQUIRED" />
		<tx:method name="save*" propagation="REQUIRED" />
		<tx:method name="update*" propagation="REQUIRED" />
		<tx:method name="get*" propagation="SUPPORTS" />
		<tx:method name="find*" propagation="SUPPORTS" />
		<tx:method name="load*" propagation="SUPPORTS" />
		<tx:method name="search*" propagation="SUPPORTS" />
		<tx:method name="*" propagation="SUPPORTS" />
	</tx:attributes>
</tx:advice>
<aop:config>
	<aop:pointcut id="transactionPointcut" expression="execution(* net.aazj.service..*Impl.*(..))" />
	<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
</aop:config>

再看一個例子:

<bean id="aspectBean" class="net.aazj.aop.DataSourceInterceptor"/>
<aop:config>
	<aop:aspect id="dataSourceAspect" ref="aspectBean">
		<aop:pointcut id="dataSourcePoint" expression="execution(public * net.aazj.service..*.getUser(..))" />
		<aop:pointcut expression="" id=""/>
		<aop:before method="before" pointcut-ref="dataSourcePoint"/>
		<aop:after method=""/>
		<aop:around method=""/>
	</aop:aspect>
	<aop:aspect></aop:aspect>
</aop:config>

<aop:aspect> 配置一個切面;<aop:pointcut>配置一個切點,基於切點表示式;<aop:before>,<aop:after>,<aop:around>是定義不同型別的advise. aspectBean 是切面的處理bean:

public class DataSourceInterceptor {
    publicvoid before(JoinPoint jp) {
        DataSourceTypeManager.set(DataSources.SLAVE);
    }
}

2) 基於註解和@Aspect風格的AOP配置

我們以事務配置為例:首先我們啟用基於註解的事務配置

<!-- 使用annotation定義事務 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

然後掃描Service包:

<context:component-scan base-package="net.aazj.service,net.aazj.aop" />

最後在service上進行註解:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
	@Autowired
	private UserMapper userMapper;
	@Transactional (readOnly=true)
	public User getUser(int userId) {
		System.out.println("in UserServiceImpl getUser");
		System.out.println(DataSourceTypeManager.get());
		return userMapper.getUser(userId);
	}
	publicvoid addUser(String username){
		userMapper.addUser(username);
//		int i = 1/0;	// 測試事物的回滾
	}
	publicvoid deleteUser(int id){
		userMapper.deleteByPrimaryKey(id);
//		int i = 1/0;	// 測試事物的回滾
	}
	@Transactional (rollbackFor = BaseBusinessException.class)
	publicvoid addAndDeleteUser(String username, int id) throws BaseBusinessException{
		userMapper.addUser(username);
		this.m1();
		userMapper.deleteByPrimaryKey(id);
	}
	privatevoid m1() throws BaseBusinessException {
		throw new BaseBusinessException("xxx");
	}
	publicint insertUser(User user) {
		return this.userMapper.insert(user);
	}
}

搞定。這種事務配置方式,不需要我們書寫pointcut表示式,而是我們在需要事務的類上進行註解。但是如果我們自己來寫切面的程式碼時,還是要寫pointcut表示式。下面看一個例子(自己寫切面邏輯):

首先去掃描 @Aspect 註解定義的 切面:

<context:component-scan base-package="net.aazj.aop" />

切面程式碼:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect