理解動態代理,理解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)用獲取的參數執行方法,獲取結果:

 

 

 

 

::以上僅供學習參考,如有錯誤,非常希望指出!

 

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