理解动态代理,理解Spring AOP -- notes

video-course:https://www.bilibili.com/video/BV1Sb411s7vP

(1/2)内容索引:

--------------------------------------------------------------------------------
1.Demo--问题引入;
2.动态代理-JDK;
3.动态代理-CGLIB;
4.问题解决:(对业务层代码进行动态代理的增强--添加事务操作);
5.AOP概念 && 术语;
6.Spring AOP的XML 和 Annotation配置;
-------------------------------------------------------------------------------

(2/2)详解:


--------------------------------------------------------------------------------

1.Demo--问题引入:

(1)项目结构:

 

 

(2)需求: 

 实现转账操作。


(3)业务逻辑:
 1 @Override
 2 public void transfer(String sourceName, String targetName) {
 3 //1.根据名称查询转出账户
 4 Account source = accountDao.findAccountByName(sourceName);
 5 //2.根据名称查询转入账户
 6 Account target = accountDao.findAccountByName(targetName);
 7 //3.转出账户减钱
 8 source.setMoney(source.getMoney() - 100);
 9 //4.转入账户加钱
10 target.setMoney(target.getMoney() + 100);
11 //5.更新转出账户
12 accountDao.updateAccount(source);
13 //6.更新转入账户
14 accountDao.updateAccount(target);
15 }
(4)数据访问层需要:
 1 @Override
 2 public Account findAccountByName(String accountName) {
 3 try{
 4 List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where name=?",new BeanListHandler<>(Account.class),accountName);
 5 if(accounts == null || accounts.size() == 0){
 6 return null;
 7 }
 8 if(accounts.size() > 1){
 9 throw new RuntimeException("结果不唯一");
10 }
11 return accounts.get(0);
12 } catch (SQLException e) {
13 throw new RuntimeException(e);
14 }
15 }
16 //此方法依赖:
17 1.ConnectionUtils()
18 (1)获取数据源;
19 (2)获取连接;
20 1)连接需要绑定线程,即获取绑定线程的连接--实现同步;
21 private ThreadLocal<Connection> t1 = new ThreadLocal<Connection>();
22 (3)移除连接;(解绑线程与连接)
23 2.commons-dbutils.jar(QueryRunner对象、结果集操作对象)

..依赖注入--Spring配置;
..测试;

(5)问题:   

1)程序发生异常,数据会不同步--需要事务支持;

一改:添加事务支持!
--------------------------------------------------------------------------------

(1)事务管理工具类编写:
 1 public class TransactionManager {
 2 
 3 private ConnectionUtils connectionUtils;
 4 
 5 public void setConnectionUtils(ConnectionUtils connectionUtils) {
 6   this.connectionUtils = connectionUtils;
 7 }
 8 
 9 //开启事务
10 public void beginTransaction(){
11   try{
12     connectionUtils.getThreadConnection().setAutoCommit(false);
13   } catch (SQLException e) {
14     e.printStackTrace();
15   }
16 }
17 //提交事务
18 public void commit() {
19   try{
20     connectionUtils.getThreadConnection().commit();
21   } catch (SQLException e) {
22     e.printStackTrace();
23   }
24 }
25 //回滚事务
26 public void rollback(){
27   try{
28     connectionUtils.getThreadConnection().rollback();
29   } catch (SQLException e) {
30     e.printStackTrace();
31   }
32 }
33 //释放连接
34 public void release(){
35   try{
36     connectionUtils.getThreadConnection().close();//还回线程池
37     connectionUtils.removeConnection();//线程和连接解绑
38   } catch (SQLException e) {
39     e.printStackTrace();
40   }
41 }
42 }
(2)为业务添加事务管理:
 1 @Override
 2 public void transfer(String sourceName, String targetName) {
 3   try{
 4     //开启事务
 5     txManager.beginTransaction();
 6     //执行操作 1-6
 7     //1.根据名称查询转出账户
 8     Account source = accountDao.findAccountByName(sourceName);
 9     //2.根据名称查询转入账户
10     Account target = accountDao.findAccountByName(targetName);
11     //3.转出账户减钱
12     source.setMoney(source.getMoney() - 100);
13     //4.转入账户加钱
14     target.setMoney(target.getMoney() + 100);
15     //5.更新转出账户
16     accountDao.updateAccount(source);
17     //6.更新转入账户
18     accountDao.updateAccount(target);
19     //提交事务
20     txManager.commit();
21   }catch (Exception e){
22     //事务回滚
23     txManager.rollback();
24     throw new RuntimeException(e);
25   }finally {
26     //释放连接
27     txManager.release();
28   }
29 }

对修改后代码中存在的问题分析:

(1)事务与业务代码纠缠不清,不便于维护;
(2)对于业务操作(需要事务支持的),每次都要写事务操作代码(繁琐,效率低);

--------------------------------------------------------------------------------

二改分析:(根据JDK/CGLIB动态代理技术,对业务层代码(AccountServiceImpl)进行增强!)

2.动态代理-JDK(默认)

(1)接口:
1 public interface Iproducer {
2   //销售
3   public void saleProduct(float money);
4   //售后
5   public void afterService(float money);
6 }
(2)实现类(被代理类):
 1 public class Producer implements Iproducer{
 2   //销售
 3   public void saleProduct(float money){
 4     System.out.println("==================");
 5     System.out.println("销售产品,回钱! "+money);
 6   }
 7   //售后
 8   public void afterService(float moeny){
 9     System.out.println("==================");
10     System.out.println("服务,收取费用! "+moeny);
11   }
12 }
(3)JDK动态代理测试(通过反射技术,根据接口,对被代理类进行代码增强):
 1 public class Client {
 2   public static void main(String[] args){
 3     Producer producer = new Producer();
 4     //不使用动态代理
 5     // producer.saleProduct(10000f);
 6     // producer.afterService(10000f);
 7     //使用动态代理:
 8     /*特点:字节码随用随创建,随加载
 9     * 作用:不修改源码的基础上对方法进行增强
10     * 分类:
11     * 基于接口(JDK); 涉及类:Proxy; 创建对象:newProxyInstance; 需要:代理类至少实现一个接口
12     * newProxyInstance()方法的参数:
13     * ClassLoader:类加载器,用于加载代理对象字节码,和被代理对象使用相同类加载器:固定写法。
14     * Class[]:用于让代理对象 和 被代理对象具有相同的方法:固定写法。
15     * InvocationHandler:用于提供增强的代码:让写如何代理!
16     * 一般写接口的实现类,一般写内部类,不必须
17     * 基于子类(CGLIB);
18     * 可以没有接口,但必须不是最终类(即可以被继承)
19     * */
20     Iproducer proxyInstance = (Iproducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
21                     producer.getClass().getInterfaces(),
22                     new InvocationHandler() {
23                     //方法作用:执行被代理对象的任何接口都会经过该方法
24                     /*方法参数的含义:
25                     * proxy:代理对象的引用
26                     * method:当前执行的方法-被代理对象的方法
27                     * args:当前执行方法所需的参数 和 被代理对象有相同的返回值
28                     * */
29                       @Override
30                       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
31                         Object result = null;
32                         //提供增强的代码
33                         //获取方法参数:
34                         Float money = (Float) args[0];
35                         //判断当前方法是不是销售(所需要增强的方法?)
36                         if("saleProduct".equals(method.getName())){
37                           result = method.invoke(producer,money*0.8f);
38                         }
39                         return result;
40                       }
41                     });
42     proxyInstance.saleProduct(10000f);
43   }
44 }

3.动态代理-CGLIB;

(1)被代理类(同上):
 1 public class Producer implements Iproducer{
 2   //销售
 3   public void saleProduct(float money){
 4     System.out.println("==================");
 5     System.out.println("销售产品,回钱! "+money);
 6   }
 7   //售后
 8   public void afterService(float moeny){
 9     System.out.println("==================");
10     System.out.println("服务,收取费用! "+moeny);
11   }
12 }
(2)CGLIB动态代理测试(通过反射技术,根据接口,对被代理类进行代码增强):
 1 public class Client {
 2     public static void main(String[] args) {
 3         Producer producer = new Producer();
 4         //不使用动态代理
 5         // producer.saleProduct(10000f);
 6         // producer.afterService(10000f);
 7         //使用动态代理:
 8         /*特点:字节码随用随创建,随加载
 9         * 作用:不修改源码的基础上对方法进行增强
10         * 分类:
11         * 基于子类(第三方cglib库); 涉及类:Enhancer; 创建对象:create方法; 需要:被代理类不能是最终类
12         * create()方法的参数:
13         * Class:字节码,被代理对象的字节码,固定写法。
14         * Callback:用于提供增强的代码:让写如何代理!一般写子接口(MethodInterceptor)的实现类
15         * 基于子类(CGLIB);
16         * 可以没有接口,但必须不是最终类(即可以被继承)
17         * */
18         Producer cglibProxy = (Producer) Enhancer.create(producer.getClass(),
19                                                         new MethodInterceptor() {
20                                                           /*参数列表:
21                                                           * proxy
22                                                           * method
23                                                           * args
24                                                           * 以上三个参数,和基于接口的方法invoke参数一样
25                                                           * methodProxy:当前执行方法的代理
26                                                           * */
27                                                           @Override
28                                                           public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
29                                                             Object result = null;
30                                                             //提供增强的代码
31                                                             //获取方法参数:
32                                                             Float money = (Float) args[0];
33                                                             //判断当前方法是不是销售(所需要增强的方法?)
34                                                             if("saleProduct".equals(method.getName())){
35                                                               result = method.invoke(producer,money*0.8f);
36                                                             }
37                                                             return result;
38                                                           }
39                                                         });
40         cglibProxy.saleProduct(10000f);
41     }
42 }

4.问题解决:(对业务层代码进行动态代理的增强--添加事务操作)

(1)通过对动态代理技术的分析,可以很容易联想到本案例(Demo)中把事务操作“织入”业务层代码的情形,并且考虑易于后期维护和兼顾开发效率等因素二改:对Demo中的业务层代码,使用动态代理技术,对其进行增强(即添加事务支持)
(2)代码:对于Demo中新建包:factory,用于存放对业务层添加事务管理抽取出的工具类:
 1 //Beanfactory.java--用于创建Service对象的代理对象的工厂:
 2 public class BeanFactory { 
 3     //事务管理对象
 4     private TransactionManager txManager;
 5     //使用Spring注入所需要setter方法
 6     public final void setTxManager(TransactionManager txManager) {
 7         this.txManager = txManager;
 8     }
 9 
10     //使用JDK动态代理,需要的接口
11     private IAccountService accountService;
12     public void setAccountService(IAccountService accountService) {
13         this.accountService = accountService;
14     }
15 
16     //获取Service代理对象(核心--创建该工厂的目的; 方式:动态代理)
17     public IAccountService getAccountService(){
18         Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
19         accountService.getClass().getInterfaces(),
20         new InvocationHandler() {
21             //添加事务支持(增强)
22             @Override
23             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
24                 //过滤掉test方法
25                 if ("test".equals(method.getName())) {
26                     return method.invoke(accountService, args);
27                 }
28                 Object result = null;
29                 try {
30                     //开启事务
31                     txManager.beginTransaction();
32                     //执行方法
33                     result = method.invoke(accountService, args);
34                     //提交事务
35                     txManager.commit();
36                     return result;
37                 } catch (Exception e) {
38                     //事务回滚
39                     txManager.rollback();
40                     throw new RuntimeException(e);
41                 } finally {
42                 //释放连接
43                     txManager.release();
44                 }
45             }
46     });
47     return accountService;
48     }
49 }
(3)IoC容器中声明该工厂,就可以使用了;

由于声明的各个对象之间相互引用,故贴出所有配置项:

<?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">

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

<!--beanFactory-->
<bean id="beanFactory" class="com.spring_02_aop._01_demo.factory.BeanFactory">
  <property name="accountService" ref="accountService"/>
  <property name="txManager" ref="txManager"/>
</bean>

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

<!--事务管理-->
<bean id="txManager" class="com.spring_02_aop._01_demo.utils.TransactionManager">
  <property name="connectionUtils" ref="connectionUtils"></property>
</bean>

<!--配置Dao对象-->
<bean id="accountDao" class="com.spring_02_aop._01_demo.dao.impl.AccountDaoImpl">
  <!-- 注入QueryRunner -->
  <property name="runner" ref="runner"></property>
  <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.cj.jdbc.Driver"></property>
  <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy?serverTimezone=UTC"></property>
  <property name="user" value="root"></property>
  <property name="password" value="921112"></property>
</bean>

<bean id="connectionUtils" class="com.spring_02_aop._01_demo.utils.ConnectionUtils">
  <property name="dataSource" ref="dataSource"/>
</bean>
</beans>

 

二改问题总结:

(1)解决了需求 && 解决了以上--一改后代码中存在的问题;
(2)业务层代码 和 事务代码相互分离,一目了然,“又好吃又好听,都好都好~”;

(4)备注(我的开发环境):

  IDE:IntelliJ IDEA
  Spring.version:Spring 5.2.1
  java.version:1.8.0_221
  DBMS:MySQL 8.0.19
  数据库eesy(取自网上教程资料),建表语句:

create table `account` (
  `id` int (11),
  `name` varchar (120),
  `money` float 
);

  OS:Windows NT6.1
  Demo中涉及的第三方Jar:
    cglib-3.2.5.jar(依赖包:asm-5.0.4.jar);
    commons-dbutils-1.6.jar
    spring核心包
    数据库相关:
    驱动:
      mysql-connector-java-8.0.19.jar
    连接池:
      mchange-commons-java-0.2.10.jar
      c3p0-0.9.5.0.jar
--------------------------------------------------------------------------------

5.AOP(基于动态代理实现)概念 && 术语;

(1)Joinpoint--切入点--业务中的方法(不论是不是要被增强,如上述transfer()及其他所有业务方法)
(2)Pointcut--切入点--业务中可被增强的方法(如上述transfer())
(3)Advice--通知--拦截到Joinpoint或者Pointcut之后要执行的操作(要增强写增强代码,不要增强,不写或者啥也不做)
(4)Aspect--切面--切入点和通知的结合(大概如此),包含多种类型:

    1)before
    2)after-returning
    3)after-throwing
    4)after
    5)around(若用环绕通知可以实现1-4四种类型能完成的需求,推荐用环绕通知

(5)其他术语,请移步官网自行查询;

--------------------------------------------------------------------------------

6.Spring AOP的XML 和 Annotation的简单配置;

(0)我用的jar(读者可根据需要版本自行下载,注意不同版本之间可能的异常):

  1)aopalliance-1.0.jar
  2)aspectjweaver-1.8.7.jar
  3)spring-aop-5.2.1.RELEASE.jar
  4)spring-aspects-5.2.1.RELEASE.jar

(1)XML配置:

  0)需求:业务层方法中添加日志!(..)
  1)项目结构:
  

  2)AccountServiceImpl.java

 1 //业务层代码
 2 public class AccountServiceImpl implements IAccountService {
 3 
 4   @Override
 5   public void saveAccount() {
 6     System.out.println("执行了保存");
 7   }
 8 
 9   @Override
10   public void updateAccount(int i) {
11   System.out.println("执行了更新 "+i);
12 
13 }
14 
15   @Override
16   public int deleteAccount() {
17     System.out.println("执行了删除");
18     return 0;
19   }
20 }

3)Logger.java

1 //待织入日志代码
2 public class Logger {
3   public void printLog(){
4     System.out.println("Logger记录日志");
5   }
6 }

4)AOP配置--application-config.xml(作用同bean.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"
    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="com.spring_02_aop._03_spring_aop.service.impl.AccountServiceImpl"></bean>
<!--配置Logger-->
<bean id="logger" class="com.spring_02_aop._03_spring_aop.utils.Logger"></bean>

<!--配置AOP-->
<aop:config>
  <aop:pointcut id="pt1" expression="execution(* com.spring_02_aop._03_spring_aop.service.impl.*.*(..))"/>
  <!--配置切面-->
  <aop:aspect id="logAdvice" ref="logger">
     <!--配置通知的类型-->
     <!-- <aop:before method="printLog" pointcut="execution(* com.spring_02_aop._03_spring_aop.service.impl.*.*(..))"></aop:before>-->
      <!-- <aop:after-returning method="printLog" pointcut="execution(* com.spring_02_aop._03_spring_aop.service.impl.*.*(..))"></aop:after-returning>-->
     <!-- <aop:after-throwing method="printLog" pointcut="execution(* com.spring_02_aop._03_spring_aop.service.impl.*.*(..))"></aop:after-throwing>-->
     <!-- <aop:after method="printLog" pointcut="execution(* com.spring_02_aop._03_spring_aop.service.impl.*.*(..))"></aop:after>-->
     <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
  </aop:aspect>
</aop:config>
</beans>
<!--spring中基于XML的AOP配置步骤
  1、把通知Bean也交给spring来管理
  2、使用aop:config标签表明开始AOP的配置
  3、使用aop:aspect标签表明配置切面
    id属性:是给切面提供一个唯一标识
    ref属性:是指定通知类bean的Id。
  4、在aop:aspect标签的内部使用对应标签来配置通知的类型
    我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
      aop:before:表示配置前置通知
        method属性:用于指定Logger类中哪个方法是前置通知
        pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

  切入点表达式的写法:
    关键字:execution(表达式)
    表达式:
      访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
-->

 

5)自行测试。
--------------------------------------------------------------------------------

(2)Annotation方式:

  1)业务层代码加@Service注解
  2)日志类代码加@Component && @Aspect注解
  3)日志方法代码可根据AOP配置及通知类型(除环绕通知)加不同注解
  4)application-config.xml的配置:

<!--头文件同上-->
<!--Spring创建容器要扫描的包-->
<context:component-scan base-package="com.spring_02_aop._04_annotationAOP"></context:component-scan>
<!--开启注解aop的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

  5)测试:...

--------------------------------------------------------------------------------
**环绕通知:(如事务、日志、安全等通用且与具体应用无关的代码应用环绕通知注意点)

(1)通用代码内方法,需要添加ProceedingJoinPoint类型参数
(2)基本算法:
//如方法添加ProceedingJoinPoint pjp类型参数
try{
  1.pjp对象获取所有参数(代理方法所需要参数--即业务方法所需参数)
  //前置通知
  2.执行代理方法,获取结果
  result = pjp.proceed(args);
  //返回通知
}catch(){
  //异常通知
}finally{
  //最终通知
}
(3)Debug截图(底层代码实现复杂,我能力有限,大概流程):

  1)通过代理获取被代理的方法,及参数类型

 

   2)通过JDK动态代理,执行代理方法invoke

 

 

 

  3)......底层层层的执行、判断、过滤,获取到参数对象列表(这里只有一个参数)

 

 

 4)用获取的参数执行方法,获取结果:

 

 

 

 

::以上仅供学习参考,如有错误,非常希望指出!

 

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