Spring AOP 深刻剖析

原貼:http://www.cnblogs.com/digdeep/p/4528353.htmlhtml

AOP是Spring提供的關鍵特性之一。AOP即面向切面編程,是OOP編程的有效補充。使用AOP技術,能夠將一些系統性相關的編程工做,獨立提取出來,獨立實現,而後經過切面切入進系統。從而避免了在業務邏輯的代碼中混入不少的系統相關的邏輯——好比權限管理,事物管理,日誌記錄等等。這些系統性的編程工做均可以獨立編碼實現,而後經過AOP技術切入進系統便可。從而達到了將不一樣的關注點分離出來的效果。本文深刻剖析Spring的AOP的原理。java

1. AOP相關的概念spring

1)Aspect:切面,切入系統的一個切面。好比事務管理是一個切面,權限管理也是一個切面;express

2)Join point:鏈接點,也就是能夠進行橫向切入的位置;編程

3)Advice:通知,切面在某個鏈接點執行的操做(分爲:Before advice,After returning advice,After throwing advice,After (finally) advice,Around advice);數組

4)Pointcut:切點,符合切點表達式的鏈接點,也就是真正被切入的地方;spring-mvc

2. AOP 的實現原理mvc

AOP分爲靜態AOP和動態AOP。靜態AOP是指AspectJ實現的AOP,他是將切面代碼直接編譯到Java類文件中。動態AOP是指將切面代碼進行動態織入實現的AOP。Spring的AOP爲動態AOP,實現的技術爲:JDK提供的動態代理技術  CGLIB(動態字節碼加強技術)。儘管實現技術不同,但都是基於代理模式,都是生成一個代理對象。app

1) JDK動態代理ide

主要使用到 InvocationHandler 接口和 Proxy.newProxyInstance() 方法。JDK動態代理要求被代理實現一個接口,只有接口中的方法纔可以被代理。其方法是將被代理對象注入到一箇中間對象,而中間對象實現InvocationHandler接口,在實現該接口時,能夠在 被代理對象調用它的方法時,在調用的先後插入一些代碼。而 Proxy.newProxyInstance() 可以利用中間對象來生產代理對象。插入的代碼就是切面代碼。因此使用JDK動態代理能夠實現AOP。咱們看個例子:

被代理對象實現的接口,只有接口中的方法纔可以被代理:

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

被代理對象:

public class UserServiceImpl implements UserService {
    public void 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 {
    public static void 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 {
    public void 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);
    }
    
    public void addUser(String username){
        userMapper.addUser(username);
//        int i = 1/0;    // 測試事物的回滾
    }
    
    public void deleteUser(int id){
        userMapper.deleteByPrimaryKey(id);
//        int i = 1/0;    // 測試事物的回滾
    }
    
    @Transactional (rollbackFor = BaseBusinessException.class)
    public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
        userMapper.addUser(username);
        this.m1();
        userMapper.deleteByPrimaryKey(id);
    }
    
    private void m1() throws BaseBusinessException {
        throw new BaseBusinessException("xxx");
    }

    public int insertUser(User user) {
        return this.userMapper.insert(user);
    }
}

 

搞定。這種事務配置方式,不須要咱們書寫pointcut表達式,而是咱們在須要事務的類上進行註解。可是若是咱們本身來寫切面的代碼時,仍是要寫pointcut表達式。下面看一個例子(本身寫切面邏輯):

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

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

啓用@AspectJ風格的註解:

<aop:aspectj-autoproxy />

這裏有兩個屬性,<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>, proxy-target-class="true" 這個最好不要隨便使用,它是指定只能使用CGLIB代理,那麼對於final方法時會拋出錯誤,因此仍是讓spring本身選擇是使用JDK動態代理,仍是CGLIB. expose-proxy="true"的做用後面會講到。

切面代碼:

 

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    // for aop
@Component // for auto scan
@Order(0)  // execute before @Transactional
public class DataSourceInterceptor {
    @Pointcut("execution(public * net.aazj.service..*.get*(..))")
    public void dataSourceSlave(){};
    
    @Before("dataSourceSlave()")
    public void before(JoinPoint jp) {
        DataSourceTypeManager.set(DataSources.SLAVE);
    }
}

 

咱們使用到了 @Aspect 來定義一個切面;@Component是配合<context:component-scan/>,否則掃描不到;@Order定義了該切面切入的順序,由於在同一個切點,可能同時存在多個切面,那麼在這多個切面之間就存在一個執行順序的問題。該例子是一個切換數據源的切面,那麼他應該在 事務處理 切面以前執行,因此咱們使用 @Order(0) 來確保先切換數據源,而後加入事務處理。@Order的參數越小,優先級越高,默認的優先級最低:

 

/**
 * Annotation that defines ordering. The value is optional, and represents order value
 * as defined in the {@link Ordered} interface. Lower values have higher priority.
 * The default value is {@code Ordered.LOWEST_PRECEDENCE}, indicating
 * lowest priority (losing to any other specified order value).
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Order {
    /**
     * The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}.
     * @see Ordered#getOrder()
     */
    int value() default Ordered.LOWEST_PRECEDENCE;
}

 

關於數據源的切換能夠參加專門的博文:http://www.cnblogs.com/digdeep/p/4512368.html

3)切點表達式(pointcut)

上面咱們看到,不管是 <aop:config> 風格的配置,仍是 @Aspect 風格的配置,切點表達式都是重點。都是咱們必須掌握的。

 1> pointcut語法形式(execution):

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

帶有 ? 號的部分是可選的,因此能夠簡化成:ret-type-pattern name-pattern(param_pattern) 返回類型,方法名稱,參數三部分來匹配。

配置起來其實也很簡單: * 表示任意返回類型,任意方法名,任意一個參數類型; .. 連續兩個點表示0個或多個包路徑,還有0個或多個參數。就是這麼簡單。看下例子:

execution(* net.aazj.service..*.get*(..)) :表示net.aazj.service包或者子包下的以get開頭的方法,參數能夠是0個或者多個(參數不限);

execution(* net.aazj.service.AccountService.*(..)): 表示AccountService接口下的任何方法,參數不限;

注意這裏,將類名和包路徑是一塊兒來處理的,並無進行區分,由於類名也是包路徑的一部分。

參數param-pattern部分比較複雜: () 表示沒有參數,(..)參數不限,(*,String) 第一個參數不限類型,第二參數爲String.

2> within() 語法:

within()只能指定(限定)包路徑(類名也能夠看作是包路徑),表示某個包下或者子報下的全部方法:

within(net.aazj.service.*), within(net.aazj.service..*),within(net.aazj.service.UserServiceImpl.*)

3> this() 與 target():

this是指代理對象,target是指被代理對象(目標對象)。因此 this() 和 target() 分別限定 代理對象的類型和被代理對象的類型:

this(net.aazj.service.UserService): 實現了UserService的代理對象(中的全部方法);

target(net.aazj.service.UserService): 被代理對象實現了UserService(中的全部方法);

4> args():

限定方法的參數的類型:

args(net.aazj.pojo.User): 參數爲User類型的方法。

5> @target(), @within(), @annotation(), @args():

這些語法形式都是針對註解的,好比帶有某個註解的,帶有某個註解的方法參數的類型帶有某個註解:

@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)

二者都是指被代理對象上有 @Transactional 註解的(類的全部方法),(二者彷佛沒有區別???)

@annotation(org.springframework.transaction.annotation.Transactional): 方法 帶有@Transactional 註解的全部方法

@args(org.springframework.transaction.annotation.Transactional):參數的類型 帶有@Transactional 註解的全部方法

6> bean(): 指定某個bean的名稱

bean(userService): bean的id爲 "userService" 的全部方法;

bean(*Service): bean的id爲 "Service"字符串結尾的全部方法;

另外注意上面這些表達式是能夠利用 ||, &&, ! 進行自由組合的。好比:execution(public * net.aazj.service..*.getUser(..)) && args(Integer,..)

4. 向註解處理方法傳遞參數

有時咱們在寫註解處理方法時,須要訪問被攔截的方法的參數。此時咱們可使用 args() 來傳遞參數,下面看一個例子:

 

@Aspect
@Component // for auto scan
//@Order(2)
public class LogInterceptor {    
    @Pointcut("execution(public * net.aazj.service..*.getUser(..))")
    public void myMethod(){};
    
    @Before("myMethod()")
    public void before() {
        System.out.println("method start");
    } 
    
    @After("myMethod()")
    public void after() {
        System.out.println("method after");
    } 
    
    @AfterReturning("execution(public * net.aazj.mapper..*.*(..))")
    public void AfterReturning() {
        System.out.println("method AfterReturning");
    } 
    
    @AfterThrowing("execution(public * net.aazj.mapper..*.*(..))")
//  @Around("execution(public * net.aazj.mapper..*.*(..))")
    public void AfterThrowing() {
        System.out.println("method AfterThrowing");
    } 
    
    @Around("execution(public * net.aazj.mapper..*.*(..))")
    public Object Around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("method Around");
        SourceLocation sl = jp.getSourceLocation();
        Object ret = jp.proceed();
        System.out.println(jp.getTarget());
        return ret;
    } 
    
    @Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
    public void before3(int userId) {
        System.out.println("userId-----" + userId);
    }  
    
    @Before("myMethod()")
    public void before2(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println("userId11111: " + (Integer)args[0]);
        System.out.println(jp.getTarget());
        System.out.println(jp.getThis());
        System.out.println(jp.getSignature());
        System.out.println("method start");
    }    
}

 

方法:

@Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
    public void before3(int userId) {
        System.out.println("userId-----" + userId);
    }

它會攔截 net.aazj.service 包下或者子包下的getUser方法,而且該方法的第一個參數必須是int型的,那麼使用切點表達式args(userId,..)就可使咱們在切面中的處理方法before3中能夠訪問這個參數。

before2方法也讓咱們知道也能夠經過 JoinPoint 參數來得到被攔截方法的參數數組。JoinPoint 是每個切面處理方法都具備的參數,@Around類型的具備的參數類型爲ProceedingJoinPoint。經過JoinPoint或者ProceedingJoinPoint參數能夠訪問到被攔截對象的一些信息(參見上面的before2方法)。

5. Spring AOP的缺陷

由於Spring AOP是基於動態代理對象的,那麼若是target中的方法不是被代理對象調用的,那麼就不會織入切面代碼,看個例子:

 

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }
    
    public void addUser(String username){
        getUser(2);
        userMapper.addUser(username);
    }

 

看到上面的 addUser() 方法中,咱們調用了 getUser() 方法,而getUser() 方法是誰調用的呢?是UserServiceImpl的實例,不是代理對象,那麼getUser()方法就不會被織入切面代碼。

切面代碼以下:

 

@Aspect
@Component
public class AOPTest {
    @Before("execution(public * net.aazj.service..*.getUser(..))")
    public void m1(){
        System.out.println("in m1...");
    }    
    @Before("execution(public * net.aazj.service..*.addUser(..))")
    public void m2(){
        System.out.println("in m2...");
    }
}

 

執行以下代碼:

public class Test {
    public static void main(String[] args){        
        ApplicationContext context = new ClassPathXmlApplicationContext(
                        new String[]{"config/spring-mvc.xml","config/applicationContext2.xml"});

        UserService us = context.getBean("userService", UserService.class);
        if(us != null){
            us.addUser("aaa");

 

輸出結果以下:

in m2...

雖然 getUser()方法 被調用了,可是由於不是代理對象調用的,因此 AOPTest.m1() 方法並無執行。這就是Spring aop的缺陷。解決方法以下:

首先: 將 <aop:aspectj-autoproxy /> 改成:

<aop:aspectj-autoproxy expose-proxy="true"/>

而後,修改UserServiceImpl中的 addUser() 方法:

 

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }
    
    public void addUser(String username){
        ((UserService)AopContext.currentProxy()).getUser(2);
        userMapper.addUser(username);
    }

 

((UserService)AopContext.currentProxy()).getUser(2); 先得到當前的代理對象,而後在調用 getUser() 方法,就好了。

expose-proxy="true" 表示將當前代理對象暴露出去,否則 AopContext.currentProxy() 或得的是 null .

修改以後的運行結果:
in m2...
in m1...
相關文章
相關標籤/搜索