Spring-AOP詳解和案例實現

相關概念

  • AOP:面向切面編程,實際上就是八個我們重複的代碼抽取出來,使用動態代理結束,在不修改源碼的基礎上,對方法增強。減少重複重複代碼、方便維護而且提高效率。
  • 舉一個例子:我們要一次操作要執行多條sql語句時,就會要進行開啓事務、提交事務、回滾事務、釋放資源,那麼如果我們有多個這樣的操作,我們就可以抽取出開啓事務、提交事務、回滾事務、釋放資源這些代碼。

動態代理

  • 特點:字節碼隨用隨創建,隨用隨加載

  • 作用:不修改源碼的基礎上對方法增強

  • 分類:基於接口的動態代理(本案例選用)、 基於子類的動態代理

  • 基於接口的動態代理:

    涉及的類:Proxy
    提供者:JDK官方
    如何創建代理對象:使用Proxy類中的newProxyInstance方法
    創建代理對象的要求:被代理類最少實現一個接口,如果沒有則不能使用

  • newProxyInstance方法的參數:

    ClassLoader:類加載器,它是用於加載代理對象字節碼的。和被代理對象使用相同的類加載器。固定寫法。
    Class[]:字節碼數組它是用於讓代理對象和被代理對象有相同方法。固定寫法。
    InvocationHandler:用於提供增強的代碼,它是讓我們寫如何代理。我們一般都是些一個該接口的實現類,通常情況下都是匿名內部類,但不是必須的。此接口的實現類都是誰用誰寫。

這是一個連接的工具類,它用於從數據源中獲取一個連接,並且實現和線程的綁定。

public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    private DataSource dataSource;
    
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 獲取當前線程上的連接
     * @return
     */
    public Connection getThreadConnection() {
        try{
            //1.先從ThreadLocal上獲取
            Connection conn = tl.get();
            //2.判斷當前線程上是否有連接
            if (conn == null) {
                //3.從數據源中獲取一個連接,並且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回當前線程上的連接
            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 把連接和線程解綁
     */
    public void removeConnection(){
        tl.remove();
    }
}

這是一個和事務管理相關的工具類,它包含了,開啓事務,提交事務,回滾事務和釋放連接,這也是動態代理對象進行增強方法。

public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 開啓事務
     */
    public  void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 提交事務
     */
    public  void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 回滾事務
     */
    public  void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 釋放連接
     */
    public  void release(){
        try {
            connectionUtils.getThreadConnection().close();//還回連接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

這是用於創建service的代理對象的工廠,使用了動態代理。這樣只要是使用BeanFactory創建的AccountService對象調用的每一個方法,都已經被代理對象進行了增強(開啓事務,提交事務,回滾事務和釋放連接)。

public class BeanFactory {
    private IAccountService accountService;
    private TransactionManager txManager;

    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                /**
                 * 增強事務功能
                 */
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object returnValue = null;
                        try {
                            //1.開啓事務
                            txManager.beginTransaction();
                            //2.執行操作
                            returnValue = method.invoke(accountService, args);
                            //3.提交事務
                            txManager.commit();
                            //4.返回結果
                            return returnValue;
                        } catch (Exception e) {
                            //5.回滾操作
                            txManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //6.釋放連接
                            txManager.release();
                        }
                    }
                });
    }
}

在spring的主配置文件xml中進行配置,用於創建對象,並放入spring容器中。

  • < bean id=“proxyAccountService” factory-bean=“beanFactory” factory-method=“getAccountService” >< /bean>:這個bean的創建用了工廠方法創建對象的方式。這就是那個動態代理對象。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置代理的service-->
    <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

    <!--配置BeanFactory:增強了事務功能的的service對象-->
    <bean id="beanFactory" class="cn.itcast.factory.BeanFactory">
        <!--注入service-->
        <property name="accountService" ref="accountService"></property>
        <!--注入事務管理器-->
        <property name="txManager" ref="txManger"></property>
    </bean>

    <!-- 配置Service -->
    <bean id="accountService" class="cn.itcast.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao對象-->
    <bean id="accountDao" class="cn.itcast.dao.impl.AccountDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入數據源-->
        <!--<constructor-arg name="ds" ref="dataSource"></constructor-arg>-->
    </bean>

    <!-- 配置數據源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--連接數據庫的必備信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_study_db"></property>
        <property name="user" value="root"></property>
        <property name="password" value="admin"></property>
    </bean>

    <!--配置Connection的工具類, ConnectionUtils-->
    <bean id="connectionUtils" class="cn.itcast.utils.ConnectionUtils">
        <!--注入數據源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事務管理器-->
    <bean id="txManger" class="cn.itcast.utils.TransactionManager">
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

我們學習 spring 的 aop,其實就是通過配置的方式,實現這個動態代理的功能。

相關術語

重點理解一下切面和通知

  • Joinpoint(連接點):(接口中所有方法)所謂連接點是指那些被攔截到的點。在 spring 中,這些點指的是方法,因爲 spring 只支持方法類型的 連接點。
  • Pointcut(切入點):(接口中要增強的方法) 所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義。
  • Advice(通知/增強):(invoke方法)所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知。通知的類型:

前置通知:TxManger.beginTransaction();
後置通知:TxManger.commit();
異常通知:TxManger.rollback();
最終通知:TxManger.release();
環繞通知:整個invoke方法執行的就是環繞通知,在環繞通知中有明確的切入點方法調用。method.invoke(object,args);

  • Introduction(引介):(瞭解) 引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運行期爲類動態地添加一些方 法或 Field。
  • Target(目標對象):(被代理對象) 代理的目標對象。
  • Weaving(織入):(創建代理對象的過程,Proxy.newProxyInstance()執行的過程)是指把增強應用到目標對象來創建新的代理對象的過程。spring 採用動態代理織入,而 AspectJ 採用編譯期織入和類裝載期織入。
  • Proxy(代理):(代理對象,也就是Proxy.newProxyInstance()的返回值)一個類被 AOP 織入增強後,就產生一個結果代理類。
  • Aspect(切面):(建立切入點方法和通知方法在執行調用關係,一般就是xml中進行配置) 是切入點和通知(引介)的結合。

spring中基於XML的AOP

配置步驟

  • 使用aop:config標籤表明開始AOP的配置
  • 使用aop:aspect標籤表明配置切面

id屬性:是給切面提供一個唯一標識
ref屬性:是指定通知類bean和id

  • aop:aspect內部使用對應標籤來配置通知的類型

aop:before:前置通知:在切入點方法執行之前執行
aop:after-returning:後置通知:在切入點方法正常執行之後值。它和異常通知永遠只能執行一個
aop:after-throwing :異常通知:在切入點方法執行產生異常之後執行。它和後置通知永遠只能執行一個
aop:after:最終通知:無論切入點方法是否正常執行它都會在其後面執行
aop:around:環繞通知:……
method屬性:用於指定Logger類中那個方法是前置通知
pointcut屬性:用於hiding切入點表達式,該表達式的含義是對業務層中那些方法增強

環繞通知

  • 環繞通知:它是spring框架爲我們提供的一種可以在代碼中手動控制增強方法何時執行的方式。
  • Spring框架爲我們提供了一個接口:ProceedingJoinPoint。該接口有一個方法proceed(),此方法就相當於明確調用切入點方法。該接口可以作爲環繞通知的方法參數,在程序執行時,spring框架會爲我們提供該接口的實現類供我們使用。

改寫增加TransactionManager 類中的環繞通知方法。

    public Object aroundPringLog(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法執行所需參數
            beginTransaction();
            rtValue = pjp.proceed(args);//明確調用業務層方法(切入點方法)
            commit();
            return rtValue;
        } catch (Throwable t) {
            rollback();
            throw new RuntimeException(t);
        } finally {
            release();
        }
    }
}

切入點表達式

org.aspectjweaver 負責解析切入點表達式

  • 關鍵字: execution(表達式)
  • 表達式:訪問修飾符 返回值 包名…包名.類名.方法名(參數列表)
  • 標準的表達式寫法:

public void cn.itcast.service.impl.AccountServiceImpl.saveAccount()
execution(public void cn.itcast.service.impl.AccountServiceImpl.saveAccount())

  • 訪問修飾符public可以省略
  • 返回值可以使用通配符,表示任意返回值,void可改爲 *
  • 包名可以使用通配符,表示任意包。但是有幾級包,就需要寫幾個*
  • 包名可以使用…表示當前包及其子包
  • 類名和方法名都可以使用*來實現通配
  • 參數列表

可以直接寫數據類型:
基本類型直接寫名稱 int
引用類型寫包名.類名的方式 java.lang.String
可以使用通配符表示任意類型,但是必須有參數
可以使用…表示有無參數均可,有參數可以是任意類型

  • 全通配寫法:* * . . * . * ( . . )

  • 實際開發中切入點表達式的通常寫法:切到業務層實現類下的所有方法
    * cn.itcast.service.impl..(…)

改寫主配置文件

使用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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置Service -->
    <bean id="accountService" class="cn.itcast.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao對象-->
    <bean id="accountDao" class="cn.itcast.dao.impl.AccountDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入數據源-->
        <!--<constructor-arg name="ds" ref="dataSource"></constructor-arg>-->
    </bean>

    <!-- 配置數據源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--連接數據庫的必備信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_study_db"></property>
        <property name="user" value="root"></property>
        <property name="password" value="admin"></property>
    </bean>

    <!--配置Connection的工具類, ConnectionUtils-->
    <bean id="connectionUtils" class="cn.itcast.utils.ConnectionUtils">
        <!--注入數據源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事務管理器-->
    <bean id="txManger" class="cn.itcast.utils.TransactionManager">
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置transactionManager-->
    <bean id="txManager" class="cn.itcast.utils.TransactionManager"></bean>

    <!--配置AOP-->
    <aop:config>
        <aop:pointcut id="txManager1" expression="execution(* cn.itcast.service.impl..*.*(..))"/>
        <aop:aspect id="txAdvice" ref="txManger">
            <!-- 配置前置通知-->
            <aop:before method="beginTransaction" pointcut-ref="txManager1" ></aop:before>

            <!-- 配置後置通知-->
            <aop:after-returning method="commit" pointcut-ref="txManager1"></aop:after-returning>

            <!-- 配置異常通知-->
            <aop:after-throwing method="rollback" pointcut-ref="txManager1"></aop:after-throwing>

            <!-- 配置最終通知-->
            <aop:after method="release" pointcut-ref="txManager1"></aop:after>

            <!-- 配置環繞通知 -->
            <aop:around method="aroundPringLog" pointcut-ref="txManager1"></aop:around> 
        </aop:aspect>
    </aop:config>
</beans>

重點關注 < aop:config >:這個標籤的配置就相當於動態代理的實現,就相當於那個BeanFactory類對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章