七、Spring AOP(XML配置實現)

一、概述

  1. 問題
  1. 完善user案例
  2. 分析案例中的問題
  3. 動態代理技術
  4. 動態代理的另一種實現方式
  5. 解決案例中的問題
  6. AOP概念
  7. spring中AOP的相關術語
  8. spring中基於XML和註解的AOP配置

一、問題

1.1 業務場景

在轉賬的業務中,需要保證轉出方扣錢成功,同時要保證轉入方收到相同金額的錢,這就要求整個轉賬的過程在同一個事務中

  1. 業務層代碼
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    private TransactionManager txManager;

    // 使用set注入dao
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

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

    @Override
    public int addOne(User user) {
        try {
            // 1. 開啓事務
            txManager.start();
            // 2. 執行操作
            int count = userDao.addOne(user);
            // 3. 提交事務
            txManager.commit();
            // 4. 返回數據
            return count;
        } catch (Exception e) {
            // 5. 異常回滾
            txManager.rollBack();
        } finally {
            // 6. 結束事務
            txManager.release();
        }
        return 0;
    }

    @Override
    public int deleteOne(int id) {
        try {
            // 1. 開啓事務
            txManager.start();
            // 2. 執行操作
            int count = userDao.deleteOne(id);
            // 3. 提交事務
            txManager.commit();
            // 4. 返回數據
            return count;
        } catch (Exception e) {
            // 5. 異常回滾
            txManager.rollBack();
        } finally {
            // 6. 結束事務
            txManager.release();
        }
        return 0;
    }

    @Override
    public int updateOne(User user) {
        try {
            // 1. 開啓事務
            txManager.start();
            // 2. 執行操作
            int count = userDao.updateOne(user);
            // 3. 提交事務
            txManager.commit();
            // 4. 返回數據
            return count;
        } catch (Exception e) {
            // 5. 異常回滾
            txManager.rollBack();
        } finally {
            // 6. 結束事務
            txManager.release();
        }
        return 0;
    }

    @Override
    public User findOne(int id) {
        try {
            // 1. 開啓事務
            txManager.start();
            // 2. 執行操作
            User user = userDao.findOne(id);
            // 3. 提交事務
            txManager.commit();
            // 4. 返回結果
            return user;
        } catch (Exception e) {
            // 5. 異常回滾
            txManager.rollBack();
        } finally {
            // 6. 結束事務
            txManager.release();
        }
        return null;
    }

    @Override
    public List<User> findAll() {
        try {
            // 1. 開啓事務
            txManager.start();
            // 2. 執行操作
            List<User> list = userDao.findAll();
            // 3. 提交事務
            txManager.commit();
            // 4. 返回結果
            return list;
        } catch (Exception e) {
            // 5. 異常回滾
            txManager.rollBack();
        } finally {
            // 6. 結束事務
            txManager.release();
        }
        return null;
    }

    /**
     * 轉賬操作
     * @author: [email protected]
     * @date: 2020/3/20 11:36 下午
     * @param originAccount
     * @param targetAccount
     * @param money
     * @return int
     */
    @Override
    public int transferMoney(Integer originAccount, Integer targetAccount, Double money) {
        try {
            // 1. 開啓事務
            txManager.start();
            // 2. 執行操作
            // 2.1 查出轉出賬戶
            User originUser = userDao.findOne(originAccount);
            // 2.2 查出站如賬戶
            User targetUser = userDao.findOne(targetAccount);
            // 2.3 轉出賬戶減少金額
            double originMoney = originUser.getMoney() - money;
            originUser.setMoney(originMoney);
            // 2.4 轉入賬戶增加金額
            double targetMoney = targetUser.getMoney() + money;
            targetUser.setMoney(targetMoney);
            // 2.5 更新轉出賬戶
            userDao.updateOne(originUser);
            int num = 1/0;
            // 2.6 更新轉入賬戶
            userDao.updateOne(targetUser);
            // 3. 提交事務
            txManager.commit();
            // 4. 返回結果
            return 1;
        } catch (Exception e) {
            // 5. 異常回滾
            txManager.rollBack();
            e.printStackTrace();
        } finally {
            // 6. 結束事務
            txManager.release();
        }
        return 0;
    }
}

  • 問:事務控制爲什麼需要加到業務層?
  • 答:通常一個業務需要連續多次操作數據庫,如果中間某一次操作數據庫的時候發生了異常,整個業務沒有回滾就提交了部分操作,就會發生異常;比如最常見的轉賬業務,如果轉出方轉出成功,而轉入的時候發生了異常,就會造成轉入方沒有收到轉賬,從而造成金額損失
  1. beans.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置service -->
    <bean id="userService" class="com.lizza.service.impl.UserServiceImpl">
        <!-- 注入Dao -->
        <property name="userDao" ref="userDao"></property>
        <property name="txManager" ref="txManager"></property>
    </bean>

    <!-- 配置事務管理器 -->
    <bean name="txManager" class="com.lizza.util.TransactionManager">
        <property name="jdbcUtil" ref="jdbcUtil"></property>
    </bean>

    <!-- 配置dao -->
    <bean id="userDao" class="com.lizza.dao.impl.UserDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="queryRunner" ref="queryRunner"></property>
        <property name="jdbcUtil" ref="jdbcUtil"></property>
    </bean>

    <!-- 配置JdbcUtil -->
    <bean id="jdbcUtil" class="com.lizza.util.JdbcUtil">
        <!-- 注入數據源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置QueryRunner;QueryRunner需要配置成多例模式,避免發生線程安全問題 -->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

    <!-- 配置數據源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3360/spring"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>

1.2 存在問題

  1. 代碼大量重複:每一個涉及到事務的控制都需要增加事務控制的代碼
  2. 代碼重複導致耦合度增大,開發維護成本升高:新的人接手代碼時,需要更多的時間去了解與業務無關的事務控制的代碼

1.3 解決方案

靜態代理,動態代理(本案例選擇動態代理)

二、動態代理

2.1 概述

動態的在運行期創建某個類或者接口的代理類

2.2 分類

  1. JDK動態代理
  2. cglib動態代理
類型 特點 使用方式
JDK動態代理 基於接口的動態代理 使用Proxy的newProxyInstance()方法創建代理對象
cglib動態代理 基於類的動態代理 使用Enhancer類中的create()方法創建代理對象

2.3 JDK動態代理示例

public class Client {

    /**
     * JDK動態代理
     *  定義:可以在運行期動態創建某個interface的實例
     *  作用:在不修改源碼的基礎上實現對方法的增強
     *  特點:需要接口,並且實現類的實例
     *  分類:
     *      1. 基於接口的動態代理
     *      2. 基於子類的動態代理
     * 基於接口的動態代理
     *  涉及的類:Proxy
     *  提供者:JDK官方
     * 創建動態代理的要求
     *  被代理的類最少實現一個接口,如果沒有則不能使用
     * newProxyInstance方法的參數
     *  ClassLoader:類加載器
     *      用於加載代理對象的字節碼;與被代理對象使用相同的類加載器;固定寫法
     *  Class<?>[]:字節碼數組
     *
     *  InvocationHandler:用於提供增強的代碼
     *      用於實現增強的代碼;通常情況下是匿名內部類;非必需
     */
    public static void main(String[] args){
        Producer producer = new Producer();
        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:執行被代理對象的任何接口方法時都會執行該方法
                     * @author: [email protected]
                     * @date: 2020/3/23 9:19 下午
                     * @param proxy     代理對象的引用
                     * @param method    當前執行的方法
                     * @param args      當前執行方法所需的參數
                     * @return          和被代理對象具有相同的返回值
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 1. 獲取傳入的參數
                        if("sale".equals(method.getName())) {
                            return method.invoke(producer, (float) args[0] * 0.8f);
                        }
                        return method.invoke(producer, args);
                    }
                }
        );
        proxyProducer.sale(10000);
    }
}

2.4 cglib動態代理示例

public class Client {

    /**
     * cglib動態代理
     *  定義:可以在運行期動態創建某個class的實例
     *  作用:在不修改源碼的基礎上實現對方法的增強
     *  特點:對指定類進行代理
     *  分類:
     *      1. 基於接口的動態代理
     *      2. 基於子類的動態代理
     * 基於接口的動態代理
     *  涉及的類:Proxy
     *  提供者:第三方cglib庫
     * 創建動態代理的要求
     *  被代理的類不能用final修飾
     * 如何創建代理對象
     *  使用Enhancer類中的create方法
     * create方法的參數
     *  ClassLoader:類加載器
     *      用於加載代理對象的字節碼;與被代理對象使用相同的類加載器;固定寫法
     *  Class<?>[]:字節碼數組
     *
     *  Callback:用於提供增強的代碼
     *      用於實現增強的代碼;通常情況下是匿名內部類;非必需
     *      一般創建MethodInterceptor
     */
    public static void main(String[] args){
        Producer producer = new Producer();
        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), producer.getClass().getInterfaces(), new MethodInterceptor() {
            /**
             * 執行被代理對象的任何方法都會經過該方法
             * @author: [email protected]
             * @date: 2020/3/25 8:21 下午
             * @param proxy
             * @param method
             * @param args
             * @param methodProxy
             * @return java.lang.Object
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                // 1. 獲取傳入的參數
                if("sale".equals(method.getName())) {
                    return method.invoke(producer, (float) args[0] * 0.8f);
                }
                return method.invoke(producer, args);
            }
        });
        cglibProducer.sale(12000);
    }
}

三、用動態代理解決問題

3.1 代碼示例

  1. 創建動態代理對象並增加事務控制
public class BeanFactory {

    private UserService userService;

    private TransactionManager txManager;

    public UserService getUserService() {
        return (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result = null;
                        try {
                            txManager.start();
                            result = method.invoke(userService, args);
                            txManager.commit();
                        } catch (Exception e) {
                            txManager.rollBack();
                            e.printStackTrace();
                        } finally {
                            txManager.release();
                        }
                        return result;
                    }
                });
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }
}
  1. 配置beans.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置帶事務控制的service -->
    <bean id="beanFactory" class="com.lizza.factory.BeanFactory">
        <property name="txManager" ref="txManager"></property>
        <property name="userService" ref="userService"></property>
    </bean>
    <bean id="proxyUserService" factory-bean="beanFactory" factory-method="getUserService"></bean>

    <!-- 配置service -->
    <bean id="userService" class="com.lizza.service.impl.UserServiceImpl">
        <!-- 注入Dao -->
        <property name="userDao" ref="userDao"></property>
    </bean>

    <!-- 配置事務管理器 -->
    <bean name="txManager" class="com.lizza.util.TransactionManager">
        <property name="jdbcUtil" ref="jdbcUtil"></property>
    </bean>

    <!-- 配置dao -->
    <bean id="userDao" class="com.lizza.dao.impl.UserDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="queryRunner" ref="queryRunner"></property>
        <property name="jdbcUtil" ref="jdbcUtil"></property>
    </bean>

    <!-- 配置JdbcUtil -->
    <bean id="jdbcUtil" class="com.lizza.util.JdbcUtil">
        <!-- 注入數據源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置QueryRunner;QueryRunner需要配置成多例模式,避免發生線程安全問題 -->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

    <!-- 配置數據源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>
  1. 進行測試
public class UserServiceTest {

    @Test
    public void addOne() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = context.getBean("userService", UserService.class);

        User user = new User();
        user.setId(4);
        user.setName("test");
        user.setAge(19);

        Assert.assertEquals(1, userService.addOne(user));
    }

    @Test
    public void deleteOne() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = context.getBean("userService", UserService.class);

        Assert.assertEquals(1, userService.deleteOne(4));

    }

    @Test
    public void updateOne() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = context.getBean("userService", UserService.class);
        User user = new User();
        user.setId(4);
        user.setName("test");
        user.setAge(0);
        Assert.assertEquals(0, userService.updateOne(user));
    }

    @Test
    public void findOne() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = context.getBean("userService", UserService.class);
        User user = userService.findOne(1);
        Assert.assertEquals(1, user.getId());
    }

    @Test
    public void findAll() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = context.getBean("userService", UserService.class);
        List<User> list = userService.findAll();
        System.out.println(list);
        Assert.assertNotNull(list);
    }

    @Test
    public void transferMoney() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        // 獲取代理對象
        UserService userService = context.getBean("proxyUserService", UserService.class);
        userService.transferMoney(1, 2, 100.0);
        List<User> list = userService.findAll();
        System.out.println(list);
    }
}

3.2 總結

  1. 可以看到動態代理在不改變源碼的基礎上實現了事務的控制
  2. 事務的控制類獨立於業務代碼,降低了耦合性

四、Spring AOP

4.1 AOP概述

  1. 定義

AOP(Aspect Oriented Programming)面向切面編程,通過預編譯和運行期動態代理的方式,實現了程序各層級業務邏輯的隔離,降低了程序的耦合性,提高了程序開發的效率

  1. 作用

在程序運行期間,通過動態代理的方式不改變源碼實現對方法的增強

  1. 優勢
  1. 降低了代碼的耦合性
  2. 提高了開發效率
  3. 方便維護

五、Spring AOP

5.1 相關術語

  1. 連接點 Joinpoint

被攔截到的點;spring中指方法,因爲spring只支持方法的攔截

  1. 切入點 Pointcut

進行了增強的連接點

  1. 通知 Advice

在切入點進行的所有增強的操作
通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dJTZeKFz-1585275979604)(http://note.youdao.com/yws/res/18102/2CD79EB1657E4A67A4947CA699B0DE2D)]

  1. 切面 Aspect

切入點和通知的結合

  1. 目標對象 Target

被代理的對象

  1. 代理對象 Proxy

增強後的對象

  1. 織入 Weaving

把切面應用到目標對象產生增強後的代理對象的過程
注意:spring採用動態代理織入,而AspectJ採用編譯器織入和類裝載期織入

  1. 引入

5.2 xml配置實現spring aop

  1. xml配置步驟
  1. 使用aop:config標籤開始spring aop的配置
  2. 使用aop:aspect標籤開始配置切面
  • id: 切面ID
  • ref: 切面處理Bean
  1. 使用aop:aspect等子標籤配置通知類型
  • method: 通知處理的具體方法
  • pointcut: 切入點表達式,用於描述哪些連接點可以作爲切入點
  1. 切入點表達式
  • 關鍵字:execution
  • 表達式:訪問修飾符 返回值 類限定名.方法名(參數列表)
  • 標準寫法:public void com.lizza.service.UserService.update(int)
  • 訪問修飾符可以省略:void com.lizza.service.UserService.update(int)
  • 返回值可以使用通配符:* com.lizza.service.UserService.update(int)
  • 包名可以使用通配符,有幾級包寫幾個*:* ..*.UserService.update(int)
  • 包名使用…表示當前包及其子包:* com.lizza…update(int)
  • 類名和方法名都可以使用通配符:* com.lizza…*()
  • 參數列表:* com.lizza…*(…)
  • 基本類型:直接寫名稱,如int,char,double
  • 應用類型:全限定名.類名,如:java.lang.String
  • *:表示必須有參數
  • …:表示有無參數均可
  • 全通配形式:* .*(…)
  • beans.xml
<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userService" class="com.lizza.service.UserService"></bean>

    <bean id="log" class="com.lizza.util.Log"></bean>

    <!--
        Spring中配置AOP的步驟
        1. 使用<aop:config>標籤開始spring aop的配置
        2. 使用<aop:aspect>標籤開始配置切面
            id: 切面ID
            ref: 切面處理Bean
        3. 使用<aop:aspect>等子標籤配置通知類型
            method: 通知處理的具體方法
            pointcut: 切入點表達式,用於描述哪些連接點可以作爲切入點
        4. 切入點表達式
            關鍵字:execution
            表達式:訪問修飾符 返回值 類限定名.方法名(參數列表)
            標準寫法:public void com.lizza.service.UserService.update(int)
            訪問修飾符可以省略:void com.lizza.service.UserService.update(int)
            返回值可以使用通配符:* com.lizza.service.UserService.update(int)
            包名可以使用通配符,有幾級包寫幾個*:* *.*.*.UserService.update(int)
            包名使用..表示當前包及其子包:* com.lizza..update(int)
            類名和方法名都可以使用通配符:* com.lizza..*()
            參數列表:* com.lizza..*(..)
                基本類型:直接寫名稱,如int,char,double
                應用類型:全限定名.類名,如:java.lang.String
                *:表示必須有參數
                ..:表示有無參數均可
            全通配形式:* *..*.*(..)
    -->
    
    <aop:config>
        <aop:aspect id="logAdvice" ref="log">
            <aop:before method="log" pointcut="execution(* com.lizza..*())"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

源碼地址:https://github.com/KJGManGlory/spring-framework

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