Spring源碼剖析6:Spring AOP概述

原文出處: 五月的倉頡html

咱們爲何要使用 AOP

前言
一年半前寫了一篇文章Spring3:AOP,是當時學習如何使用Spring AOP的時候寫的,比較基礎。這篇文章最後的推薦以及回覆認爲我寫的對你們有幫助的評論有不少,可是如今從我我的的角度來看,這篇文章寫得並很差,甚至能夠說是沒有太多實質性的內容,所以這些推薦和評論讓我以爲受之有愧。程序員

基於以上緣由,更新一篇文章,從最基礎的原始代碼–>使用設計模式(裝飾器模式與代理)–>使用AOP三個層次來說解一下爲何咱們要使用AOP,但願這篇文章能夠對網友朋友們有益。面試

原始代碼的寫法
既然要經過代碼來演示,那必需要有例子,這裏個人例子爲:spring

1
有一個接口Dao有insert、delete、update三個方法,在insert與update被調用的先後,打印調用前的毫秒數與調用後的毫秒數
首先定義一個Dao接口:sql

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public interface Dao {
 
    public void insert();
     
    public void delete();
     
    public void update();
     
}

而後定義一個實現類DaoImpl:數據庫

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class DaoImpl implements Dao {
 
    @Override
    public void insert() {
        System.out.println("DaoImpl.insert()");
    }
 
    @Override
    public void delete() {
        System.out.println("DaoImpl.delete()");
    }
 
    @Override
    public void update() {
        System.out.println("DaoImpl.update()");
    }
     
}

最原始的寫法,我要在調用insert()與update()方法先後分別打印時間,就只能定義一個新的類包一層,在調用insert()方法與update()方法先後分別處理一下,新的類我命名爲ServiceImpl,其實現爲:express

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class ServiceImpl {
 
    private Dao dao = new DaoImpl();
     
    public void insert() {
        System.out.println("insert()方法開始時間:" + System.currentTimeMillis());
        dao.insert();
        System.out.println("insert()方法結束時間:" + System.currentTimeMillis());
    }
     
    public void delete() {
        dao.delete();
    }
     
    public void update() {
        System.out.println("update()方法開始時間:" + System.currentTimeMillis());
        dao.update();
        System.out.println("update()方法結束時間:" + System.currentTimeMillis());
    }
     
}

這是最原始的寫法,這種寫法的缺點也是一目瞭然:編程

方法調用先後輸出時間的邏輯沒法複用,若是有別的地方要增長這段邏輯就得再寫一遍
若是Dao有其它實現類,那麼必須新增一個類去包裝該實現類,這將致使類數量不斷膨脹
使用裝飾器模式
接着咱們使用上設計模式,先用裝飾器模式,看看能解決多少問題。裝飾器模式的核心就是實現Dao接口並持有Dao接口的引用,我將新增的類命名爲LogDao,其實現爲:後端

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class LogDao implements Dao {
 
    private Dao dao;
     
    public LogDao(Dao dao) {
        this.dao = dao;
    }
 
    @Override
    public void insert() {
        System.out.println("insert()方法開始時間:" + System.currentTimeMillis());
        dao.insert();
        System.out.println("insert()方法結束時間:" + System.currentTimeMillis());
    }
 
    @Override
    public void delete() {
        dao.delete();
    }
 
    @Override
    public void update() {
        System.out.println("update()方法開始時間:" + System.currentTimeMillis());
        dao.update();
        System.out.println("update()方法結束時間:" + System.currentTimeMillis());
    }
 
}

在使用的時候,可使用」Dao dao = new LogDao(new DaoImpl())」的方式,這種方式的優勢爲:設計模式

透明,對調用方來講,它只知道Dao,而不知道加上了日誌功能
類不會無限膨脹,若是Dao的其它實現類須要輸出日誌,只須要向LogDao的構造函數中傳入不一樣的Dao實現類便可
不過這種方式一樣有明顯的缺點,缺點爲:

輸出日誌的邏輯仍是沒法複用
輸出日誌的邏輯與代碼有耦合,若是我要對delete()方法先後一樣輸出時間,須要修改LogDao
可是,這種作法相比最原始的代碼寫法,已經有了很大的改進。

使用代理模式
接着咱們使用代理模式嘗試去實現最原始的功能,使用代理模式,那麼咱們就要定義一個InvocationHandler,我將它命名爲LogInvocationHandler,其實現爲:

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class LogInvocationHandler implements InvocationHandler {
 
    private Object obj;
     
    public LogInvocationHandler(Object obj) {
        this.obj = obj;
    }
     
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if ("insert".equals(methodName) || "update".equals(methodName)) {
            System.out.println(methodName + "()方法開始時間:" + System.currentTimeMillis());
            Object result = method.invoke(obj, args);
            System.out.println(methodName + "()方法結束時間:" + System.currentTimeMillis());
             
            return result;
        }
         
        return method.invoke(obj, args);
    }
     
}

其調用方式很簡單,我寫一個main函數:

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public static void main(String[] args) {
    Dao dao = new DaoImpl();
         
    Dao proxyDao = (Dao)Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class<?>[]{Dao.class}, new LogInvocationHandler(dao));
         
    proxyDao.insert();
    System.out.println("----------分割線----------");
    proxyDao.delete();
    System.out.println("----------分割線----------");
    proxyDao.update();
}

結果就不演示了,這種方式的優勢爲:

輸出日誌的邏輯被複用起來,若是要針對其餘接口用上輸出日誌的邏輯,只要在newProxyInstance的時候的第二個參數增長Class<?>數組中的內容便可
這種方式的缺點爲:

JDK提供的動態代理只能針對接口作代理,不能針對類作代理
代碼依然有耦合,若是要對delete方法調用先後打印時間,得在LogInvocationHandler中增長delete方法的判斷
使用CGLIB
接着看一下使用CGLIB的方式,使用CGLIB只須要實現MethodInterceptor接口便可:

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class DaoProxy implements MethodInterceptor {
 
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
        String methodName = method.getName();
         
        if ("insert".equals(methodName) || "update".equals(methodName)) {
            System.out.println(methodName + "()方法開始時間:" + System.currentTimeMillis());
            proxy.invokeSuper(object, objects);
            System.out.println(methodName + "()方法結束時間:" + System.currentTimeMillis());
             
            return object;
        }
         
        proxy.invokeSuper(object, objects);
        return object;
    }
 
}

代碼調用方式爲:

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public static void main(String[] args) {
    DaoProxy daoProxy = new DaoProxy();
     
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(DaoImpl.class);
    enhancer.setCallback(daoProxy);
         
    Dao dao = (DaoImpl)enhancer.create();
    dao.insert();
    System.out.println("----------分割線----------");
    dao.delete();
    System.out.println("----------分割線----------");
    dao.update();
}

使用CGLIB解決了JDK的Proxy沒法針對類作代理的問題,可是這裏要專門說明一個問題:使用裝飾器模式能夠說是對使用原生代碼的一種改進,使用Java代理能夠說是對於使用裝飾器模式的一種改進,可是使用CGLIB並非對於使用Java代理的一種改進。

前面的能夠說改進是由於使用裝飾器模式比使用原生代碼更好,使用Java代理又比使用裝飾器模式更好,可是Java代理與CGLIb的對比並不能說改進,由於使用CGLIB並不必定比使用Java代理更好,這兩種各有優缺點,像Spring框架就同時支持Java Proxy與CGLIB兩種方式。

從目前看來代碼又更好了一些,可是我認爲還有兩個缺點:

不管使用Java代理仍是使用CGLIB,編寫這部分代碼都稍顯麻煩
代碼之間的耦合仍是沒有解決,像要針對delete()方法加上這部分邏輯就必須修改代碼

使用AOP

最後來看一下使用AOP的方式,首先定義一個時間處理類,我將它命名爲TimeHandler:

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class TimeHandler {
     
    public void printTime(ProceedingJoinPoint pjp) {
        Signature signature = pjp.getSignature();
        if (signature instanceof MethodSignature) {
            MethodSignature methodSignature = (MethodSignature)signature;
            Method method = methodSignature.getMethod();
            System.out.println(method.getName() + "()方法開始時間:" + System.currentTimeMillis());
             
            try {
                pjp.proceed();
                System.out.println(method.getName() + "()方法結束時間:" + System.currentTimeMillis());
            } catch (Throwable e) {
                 
            }
        }
    }
     
}

到第8行的代碼與第12行的代碼分別打印方法開始執行時間與方法結束執行時間。我這裏寫得稍微複雜點,使用了<aop:around>的寫法,其實也能夠拆分爲<aop:before>與<aop:after>兩種,這個看我的喜愛。

這裏多說一句,切面方法printTime自己能夠不用定義任何的參數,可是有些場景下須要獲取調用方法的類、方法簽名等信息,此時能夠在printTime方法中定義JointPoint,Spring會自動將參數注入,能夠經過JoinPoint獲取調用方法的類、方法簽名等信息。因爲這裏我用的<aop:around>,要保證方法的調用,這樣才能在方法調用先後輸出時間,所以不能直接使用JoinPoint,由於JoinPoint無法保證方法調用。此時可使用ProceedingJoinPoint,ProceedingPointPoint的proceed()方法能夠保證方法調用,可是要注意一點,ProceedingJoinPoint只能和<aop:around>搭配,換句話說,若是aop.xml中配置的是<aop:before>,而後printTime的方法參數又是ProceedingJoinPoint的話,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: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-3.0.xsd
 
 
http://www.springframework.org/schema/aop
 
 
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
 
    <bean id="daoImpl" class="org.xrq.spring.action.aop.DaoImpl" />
    <bean id="timeHandler" class="org.xrq.spring.action.aop.TimeHandler" />
 
    <aop:config>
        <aop:pointcut id="addAllMethod" expression="execution(* org.xrq.spring.action.aop.Dao.*(..))" />
        <aop:aspect id="time" ref="timeHandler">
            <aop:before method="printTime" pointcut-ref="addAllMethod" />
            <aop:after method="printTime" pointcut-ref="addAllMethod" />
        </aop:aspect>
    </aop:config>
     
</beans>

我不大會寫expression,也懶得去百度了,所以這裏就攔截Dao下的全部方法了。測試代碼很簡單:

=
/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class AopTest {
 
    @Test
    @SuppressWarnings("resource")
    public void testAop() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring/aop.xml");
         
        Dao dao = (Dao)ac.getBean("daoImpl");
        dao.insert();
        System.out.println("----------分割線----------");
        dao.delete();
        System.out.println("----------分割線----------");
        dao.update();
    }
     
}

AOP總結

結果就不演示了。到此我總結一下使用AOP的幾個優勢:

切面的內容能夠複用,好比TimeHandler的printTime方法,任何地方須要打印方法執行前的時間與方法執行後的時間,均可以使用TimeHandler的printTime方法
避免使用Proxy、CGLIB生成代理,這方面的工做所有框架去實現,開發者能夠專一於切面內容自己
代碼與代碼之間沒有耦合,若是攔截的方法有變化修改配置文件便可
下面用一張圖來表示一下AOP的做用:

咱們傳統的編程方式是垂直化的編程,即A–>B–>C–>D這麼下去,一個邏輯完畢以後執行另一段邏輯。可是AOP提供了另一種思路,它的做用是在業務邏輯不知情(即業務邏輯不須要作任何的改動)的狀況下對業務代碼的功能進行加強,這種編程思想的使用場景有不少,例如事務提交、方法執行以前的權限檢測、日誌打印、方法調用事件等等。

AOP使用場景舉例
上面的例子純粹爲了演示使用,爲了讓你們更加理解AOP的做用,這裏以實際場景做爲例子。

第一個例子,咱們知道MyBatis的事務默認是不會自動提交的,所以在編程的時候咱們必須在增刪改完畢以後調用SqlSession的commit()方法進行事務提交,這很是麻煩,下面利用AOP簡單寫一段代碼幫助咱們自動提交事務(這段代碼我我的測試過可用):

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class TransactionHandler {
 
    public void commit(JoinPoint jp) {
        Object obj = jp.getTarget();
        if (obj instanceof MailDao) {
            Signature signature = jp.getSignature();
            if (signature instanceof MethodSignature) {
                SqlSession sqlSession = SqlSessionThrealLocalUtil.getSqlSession();               
                 
                MethodSignature methodSignature = (MethodSignature)signature;
                Method method = methodSignature.getMethod();
                  
                String methodName = method.getName();
                if (methodName.startsWith("insert") || methodName.startsWith("update") || methodName.startsWith("delete")) {
                    sqlSession.commit();
                }
                 
                sqlSession.close();
            }
        }
    }
     
}

這種場景下咱們要使用的aop標籤爲<aop:after>,即切在方法調用以後。

這裏我作了一個SqlSessionThreadLocalUtil,每次打開會話的時候,都經過SqlSessionThreadLocalUtil把當前會話SqlSession放到ThreadLocal中,看到經過TransactionHandler,能夠實現兩個功能:

insert、update、delete操做事務自動提交
對SqlSession進行close(),這樣就不須要在業務代碼裏面關閉會話了,由於有些時候咱們寫業務代碼的時候會忘記關閉SqlSession,這樣可能會形成內存句柄的膨脹,所以這部分切面也一併作了
整個過程,業務代碼是不知道的,而TransactionHandler的內容能夠充分再多處場景下進行復用。

第二個例子是權限控制的例子,不論是從安全角度考慮仍是從業務角度考慮,咱們在開發一個Web系統的時候不可能全部請求都對全部用戶開放,所以這裏就須要作一層權限控制了,你們看AOP做用的時候想必也確定會看到AOP能夠作權限控制,這裏我就演示一下如何使用AOP作權限控制。咱們知道原生的Spring MVC,Java類是實現Controller接口的,基於此,利用AOP作權限控制的大體代碼以下(這段代碼純粹就是一段示例,我構建的Maven工程是一個普通的Java工程,所以沒有驗證過):

/**
 * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class PermissionHandler {
 
    public void hasPermission(JoinPoint jp) throws Exception {
        Object obj = jp.getTarget();
         
        if (obj instanceof Controller) {
            Signature signature = jp.getSignature();
            MethodSignature methodSignature = (MethodSignature)signature;
             
            // 獲取方法簽名
            Method method = methodSignature.getMethod();
            // 獲取方法參數
            Object[] args = jp.getArgs();
             
            // Controller中惟一一個方法的方法簽名ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
            // 這裏對這個方法作一層判斷
            if ("handleRequest".equals(method.getName()) && args.length == 2) {
                Object firstArg = args[0];
                if (obj instanceof HttpServletRequest) {
                    HttpServletRequest request = (HttpServletRequest)firstArg;
                    // 獲取用戶id
                    long userId = Long.parseLong(request.getParameter("userId"));
                    // 獲取當前請求路徑
                    String requestUri = request.getRequestURI();
                     
                    if(!PermissionUtil.hasPermission(userId, requestUri)) {
                        throw new Exception("沒有權限");
                    }
                }
            }
        }
         
    }
     
}

毫無疑問這種場景下咱們要使用的aop標籤爲<aop:before>。這裏我寫得很簡單,獲取當前用戶id與請求路徑,根據這二者,判斷該用戶是否有權限訪問該請求,你們明白意思便可。

後記
文章演示了從原生代碼到使用AOP的過程,一點一點地介紹了每次演化的優缺點,最後以實際例子分析了AOP能夠作什麼事情。

微信公衆號【黃小斜】做者是螞蟻金服 JAVA 工程師,專一於 JAVA 後端技術棧:SpringBoot、SSM全家桶、MySQL、分佈式、中間件、微服務,同時也懂點投資理財,堅持學習和寫做,相信終身學習的力量!關注公衆號後回覆」架構師「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源
相關文章
相關標籤/搜索