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)用獲取的參數執行方法,獲取結果:
::以上僅供學習參考,如有錯誤,非常希望指出!