七、Spring AOP(動態代理)

一、概述

  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. 事務的控制類獨立於業務代碼,降低了耦合性

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

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