文章目錄
- AOP
- AOP簡介
- AOP簡介和作用
- AOP的應用場景
- 為什么要學習AOP
- AOP入門案例
- 思路分析
- 代碼實現
- AOP中的核心概念
- AOP工作流程
- AOP工作流程
- AOP核心概念
- 在測試類中驗證代理對象
- AOP切入點表達式
- 語法格式
- 通配符
- 書寫技巧
- AOP通知類型
- AOP通知分類
- AOP通知詳解
- AOP案例
- 案例-測量業務層接口萬次執行效率
- 需求和分析
- 代碼實現
- AOP切入點數據獲取
- 獲取參數
- 獲取返回值
- 獲取異常
- 案例-百度網盤密碼數據兼容處理
- 需求和分析
- 代碼實現
- Spring事務管理
- Spring事務簡介
- Spring事務作用
- 需求和分析
- 代碼實現
- Spring事務角色
- Spring事務相關配置
- 事務配置
- 案例-轉賬業務追加日志
- 代碼實現
- 事務傳播行為
AOP
AOP簡介
AOP簡介和作用
- AOP(Aspect Oriented Programming)面向切面編程,一種編程范式,指導開發者如何組織程序結構
- 作用:在不改變方法源代碼的基礎上對方法進行功能增強
- Spring理念:無入侵式
AOP的應用場景
- 在工程運行慢的過程中,對目標方法進行運行耗時統計
- 對目標方法添加事務管理
- 對目標方法添加權限訪問控制
- …
為什么要學習AOP
1、簡化開發
- AOP減少了手動創建動態代理的步驟
- AOP減少了手動創建動態代理的代碼量
2、靈活性強
- 手動創建動態代理一次只能代理一個對象
- AOP可以批量代理多個對象
3、spring事務使用AOP實現
- Spring的事務管理使用AOP實現
AOP入門案例
思路分析
- 導入坐標(pom.xml)
- 制作連接點方法(原始操作,dao接口與實現類)
- 制作共性功能(通知類與通知)
- 定義切入點
- 綁定切入點與通知關系(切面)
代碼實現
- 導入aop相關坐標
<dependencies><!--spring核心依賴,會將spring-aop傳遞進來--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.10.RELEASE</version></dependency><!--切入點表達式依賴,目的是找到切入點方法,也就是找到要增強的方法--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version></dependency>
</dependencies>
- 定義dao接口和實現類
public interface BookDao {public void save();public void update();
}@Repository
public class BookDaoImpl implements BookDao {public void save() {System.out.println(System.currentTimeMillis());System.out.println("book dao save ...");}public void update(){System.out.println("book dao update ...");}
}
- 定義通知類,制作通知方法
//通知類必須配置成Spring管理的bean
@Component
public class MyAdvice {public void method(){System.out.println(System.currentTimeMillis());}
}
- 定義切點表達式,配置切面(綁定切入點與通知關系)
//通知類必須配置成Spring管理的bean
@Component
//設置當前類為切面類類
@Aspect
public class MyAdvice {//設置切入點,@Pointcut注解要求配置在方法上方@Pointcut("execution(void com.itheima.dao.BookDao.update())")private void pt(){}//設置在切入點pt()的前面運行當前操作(前置通知)@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}
}
- 在配置類中進行Spring注解包掃描和開啟AOP功能
@Configuration
@ComponentScan("com.itheima")
//開啟注解開發AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 編寫測試類和運行結果
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao = ctx.getBean(BookDao.class);bookDao.update();}
}
AOP中的核心概念
- 連接點(JoinPoint):正在執行的方法,例如:update()
- 切入點(PointCut):匹配連接點的式子
- 在SpringAOP中,一個切入點可以只描述一個具體方法,也可以匹配多個方法
- 一個具體方法:com.itheima.dao包下的BookDao接口中的無形參無返回值的save方法
- 匹配多個方法:所有的save方法,所有的get開頭的方法,所有以Dao結尾的接口中的任意方法,所有帶有一個參數的方法
- 在SpringAOP中,一個切入點可以只描述一個具體方法,也可以匹配多個方法
- 通知(Advice):在切入點前后執行的操作,也就是增強的共性功能
- 在SpringAop中,功能最終以方法的形式呈現
- 通知類:通知方法所在的類叫做通知類
- 切面(Aspect):描述通知與切入點的對應關系,也就是哪些通知方法對應哪些切入點方法
AOP工作流程
AOP工作流程
- Spring容器啟動
- 讀取所有切面配置中的切入點
- 初始化bean,判定bean對應的類中的方法是否匹配到任意切入點
- 匹配失敗,創建原始對象
- 匹配成功,創建原始對象(目標對象)的代理對象
- 獲取bean執行方法
- 獲取的bean是原始對象時,調用方法并執行,完成操作
- 獲取的bean是代理對象時,根據代理對象的運行模式運行原始方法與增強的內容,完成操作
AOP核心概念
- 目標對象(Target):被代理的對象,也叫原始對象,該對象中的方法沒有任何功能增強。
- 代理對象(Proxy):代理后生成的對象,由Spring幫我們創建代理對象。
在測試類中驗證代理對象
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao = ctx.getBean(BookDao.class);bookDao.update();//打印對象的類名System.out.println(bookDao.getClass());}
}
AOP切入點表達式
語法格式
- 切入點:要進行增強的方法
- 切入點表達式:要進行增強的方法的描述方式
- 描述方式一:執行com.itheima.dao包下的BookDao接口中的無參數update方法
execution(void com.itheima.dao.BookDao.update())
- 描述方式二:執行com.itheima.dao.impl包下的BookDaoImpl類中的無參數update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
- 切入點表達式標準格式:動作關鍵字(訪問修飾符 返回值 包名.類/接口名.方法名(參數) 異常名)
execution(public User com.itheima.service.UserService.findById(int))
- 動作關鍵字:描述切入點的行為動作,例如execution表示執行到指定切入點
- 訪問修飾符:public,private等,可以省略
- 返回值:寫返回值類型
- 包名:多級包使用點連接
- 類/接口名:
- 方法名:
- 參數:直接寫參數的類型,多個類型用逗號隔開
- 異常名:方法定義中拋出指定異常,可以省略
通配符
目的:可以使用通配符描述切入點,快速描述
- * :單個獨立的任意符號,可以獨立出現,也可以作為前綴或者后綴的匹配符出現
匹配com.itheima包下的任意包中的UserService類或接口中所有find開頭的帶有一個參數的方法
execution(public * com.itheima.*.UserService.find*(*))
- . . :多個連續的任意符號,可以獨立出現,常用于簡化包名與參數的書寫
匹配com包下的任意包中的UserService類或接口中所有名稱為findById的方法
execution(public User com..UserService.findById(..))
- + :專用于匹配子類類型
execution(* *..*Service+.*(..))
書寫技巧
- 所有代碼按照標準規范開發,否則以下技巧全部失效
- 描述切入點通常描述接口,而不描述實現類
- 訪問控制修飾符針對接口開發均采用public描述(可省略訪問控制修飾符描述)
- 返回值類型對于增刪改類使用精準類型加速匹配,對于查詢類使用*通配快速描述
- 包名書寫盡量不使用. .匹配,效率過低,常用*做單個包描述匹配,或精準匹配
- 接口名/類名書寫名稱與模塊相關的采用*匹配,例如UserService書寫成*Service,綁定業務層接口名
- 方法名書寫以動詞進行精準匹配,名詞采用_匹配,例如getById書寫成getBy_,selectAll書寫成selectAll
- 參數規則較為復雜,根據業務方法靈活調整
- 通常不使用異常作為匹配規則
AOP通知類型
AOP通知分類
- AOP通知描述了抽取的共性功能,根據共性功能抽取的位置不同,最終運行代碼時要將其加入到合理的位置
- AOP通知共分為5種類型
- 前置通知:在切入點方法執行之前執行
- 后置通知:在切入點方法執行之后執行,無論切入點方法內部是否出現異常,后置通知都會執行。
- **環繞通知(重點):**手動調用切入點方法并對其進行增強的通知方式。
- 返回后通知(了解):在切入點方法執行之后執行,如果切入點方法內部出現異常將不會執行。
- 拋出異常后通知(了解):在切入點方法執行之后執行,只有當切入點方法內部出現異常之后才執行。
AOP通知詳解
- 前置通知:@Before,當前通知方法在原始切入點方法前運行
- 后置通知:@After,當前通知方法在原始切入點方法后運行
- 返回后通知:@AfterReturning,當前通知方法在原始切入點方法正常執行完畢后運行
- 拋出異常后通知:@AfterThrowing,當前通知方法在原始切入點方法運行拋出異常后執行
- 環繞通知:@Around,當前通知方法在原始切入點方法前后運行
環繞通知代碼:
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("around before advice ...");Object ret = pjp.proceed();System.out.println("around after advice ...");return ret;
}
環繞通知注意事項:
- 環繞通知方法形參必須是ProceedingJoinPoint,表示正在執行的連接點,使用該對象的proceed()方法表示對原始對象方法進行調用,返回值為原始對象方法的返回值。
- 環繞通知方法的返回值建議寫成Object類型,用于將原始對象方法的返回值進行返回,哪里使用代理對象就返回到哪里。
AOP案例
案例-測量業務層接口萬次執行效率
需求和分析
需求:任意業務層接口執行均可顯示其執行效率(執行時長)
分析:
- 業務功能:業務層接口執行前后分別記錄時間,求差值得到執行效率
- 通知類型選擇前后均可以增強的類型–環繞通知
代碼實現
在上面代碼的基礎上
- 編寫通知類
@Component
@Aspect
public class Advice {@Pointcut("execution(public * org.example.dao.BookDao.save())")public void pc(){}@Around("pc()")public Object around(ProceedingJoinPoint pjp) throws Throwable {//獲取執行的簽名對象Signature signature = pjp.getSignature();//獲取類名/類全限定名String className = signature.getDeclaringTypeName();//獲取方法名String methodName = signature.getName();long startTime = System.currentTimeMillis();Object proceed = null;for (int i = 0; i < 10000; i++) {proceed = pjp.proceed();}long endTime = System.currentTimeMillis();System.out.println("萬次執行:"+ className+"."+methodName+"---->" +(endTime-startTime) + "ms");return proceed;}
}
- 在Spring配置類中開啟AOP注解功能
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy //開啟AOP注解功能
public class SpringConfig {
}
- 運行測試類,查看結果
public class App {public static void main( String[] args ) {//創建ioc容器ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);//獲取beanBookDao bookDao = ctx.getBean(BookDao.class);//執行方法Integer result = bookDao.save();System.out.println(result);
//
// System.out.println(bookDao.getClass());}
}
AOP切入點數據獲取
獲取參數
- JoinPoint對象描述了連接點方法的運行狀態,可以獲取到原始方法的調用參數(除了環繞的其他通知)
@Before("pt()")
public void before(JoinPoint jp) {Object[] args = jp.getArgs(); //獲取連接點方法的參數們System.out.println(Arrays.toString(args));
}
- ProccedingJointPoint是JoinPoint的子類
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {Object[] args = pjp.getArgs(); //獲取連接點方法的參數們System.out.println(Arrays.toString(args));Object ret = pjp.proceed();return ret;
}
獲取返回值
- 返回后通知可以獲取切入點方法返回值信息,使用形參可以接收對應的返回值
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(String ret) { //變量名要和returning="ret"的屬性值一致System.out.println("afterReturning advice ..."+ret);
}
- 環繞通知中可以手工書寫對原始方法的調用,得到的結果即為原始方法的返回值
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {// 手動調用連接點方法,返回值就是連接點方法的返回值Object ret = pjp.proceed();return ret;
}
獲取異常
- 拋出異常后通知可以獲取切入點方法中出現的異常信息,使用形參可以接收對應的異常對象
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {//變量名要和throwing = "t"的屬性值一致System.out.println("afterThrowing advice ..."+ t);
}
- 拋出異常后通知可以獲取切入點方法運行的異常信息,使用形參可以接收運行時拋出的異常對象
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {Object ret = null;//此處需要try...catch處理,catch中捕獲到的異常就是連接點方法中拋出的異常try {ret = pjp.proceed();} catch (Throwable t) {t.printStackTrace();}return ret;
}
案例-百度網盤密碼數據兼容處理
需求和分析
需求:對百度網盤分享鏈接輸入密碼時尾部多輸入的空格做兼容處理
分析:
- 在業務方法執行之前對所有的輸入參數進行格式處理——trim()
- 使用處理后的參數調用原始方法——環繞通知中存在對原始方法的調用
代碼實現
- 編寫service類和dao類
//-------------service層代碼-----------------------
public interface ResourcesService {public boolean openURL(String url ,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {@Autowiredprivate ResourcesDao resourcesDao;public boolean openURL(String url, String password) {return resourcesDao.readResources(url,password);}
}
//-------------dao層代碼-----------------------
public interface ResourcesDao {boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {public boolean readResources(String url, String password) {System.out.println(password.length());//模擬校驗return password.equals("root");}
}
- 編寫通知類
@Component
@Aspect
public class DataAdvice {@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")private void servicePt(){}@Around("DataAdvice.servicePt()")public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {Object[] args = pjp.getArgs();for (int i = 0; i < args.length; i++) {//判斷參數是不是字符串if(args[i].getClass().equals(String.class)){args[i] = args[i].toString().trim();}}Object ret = pjp.proceed(args);return ret;}
}
- 在Spring配置類上開啟AOP注解功能
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 運行測試類,查看結果
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);ResourcesService resourcesService = ctx.getBean(ResourcesService.class);boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root ");System.out.println(flag);}
}
Spring事務管理
Spring事務簡介
Spring事務作用
- 事務作用:在數據層保障一系列的數據庫操作同成功同失敗
- Spring事務作用:在數據層或業務層保障一系列的數據庫操作同成功同失敗
需求和分析
- 需求:實現任意兩個賬戶間轉賬操作
- 需求微縮:A賬戶減錢,B賬戶加錢
- 分析:
①:數據層提供基礎操作,指定賬戶減錢(outMoney),指定賬戶加錢(inMoney)
②:業務層提供轉賬操作(transfer),調用減錢與加錢的操作
③:提供2個賬號和操作金額執行轉賬操作
④:基于Spring整合MyBatis環境搭建上述操作 - 結果分析:
①:程序正常執行時,賬戶金額A減B加,沒有問題
②:程序出現異常后,轉賬失敗,但是異常之前操作成功,異常之后操作失敗,整體業務失敗
代碼實現
環境準備:
Spring整合Mybatis相關代碼(依賴、JdbcConfig、MybatisConfig、SpringConfig)省略。
public interface AccountDao {@Update("update tbl_account set money = money + #{money} where name = #{name}")void inMoney(@Param("name") String name, @Param("money") Double money);@Update("update tbl_account set money = money - #{money} where name = #{name}")void outMoney(@Param("name") String name, @Param("money") Double money);
}public interface AccountService {/*** 轉賬操作* @param out 傳出方* @param in 轉入方* @param money 金額*/public void transfer(String out,String in ,Double money) ;
}@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;public void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i = 1/0;accountDao.inMoney(in,money);}
}
- 在業務層接口上添加Spring事務管理
public interface AccountService {//配置當前接口方法具有事務@Transactionalpublic void transfer(String out,String in ,Double money) ;
}
注意事項
- Spring注解式事務通常添加在業務層接口中而不會添加到業務層實現類中,降低耦合
- 注解式事務可以添加到業務方法上表示當前方法開啟事務,也可以添加到接口上表示當前接口所有方法開啟事務
- 設置事務管理器(將事務管理器添加到IOC容器中)
可以在JdbcConfig中配置事務管理器
//配置事務管理器,mybatis使用的是jdbc事務
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();transactionManager.setDataSource(dataSource);return transactionManager;
}
注意事項
- 事務管理器要根據實現技術進行選擇
- MyBatis框架使用的是JDBC事務
- 開啟注解式事務驅動
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//開啟注解式事務驅動
@EnableTransactionManagement
public class SpringConfig {
}
- 運行測試類,查看結果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {@Autowiredprivate AccountService accountService;@Testpublic void testTransfer() throws IOException {accountService.transfer("Tom","Jerry",100D);}
}
Spring事務角色
- 事務管理員:發起事務方,在Spring中通常指代業務層開啟事務的方法
- 事務協調員:加入事務方,在Spring中通常指代數據層方法,也可以是業務層方法
Spring事務相關配置
事務配置
說明:對于RuntimeException類型異常或者Error錯誤,Spring事務能夠進行回滾操作。但是對于編譯器異常,Spring事務是不進行回滾的,所以需要使用rollbackFor來設置回滾的異常。
案例-轉賬業務追加日志
需求和分析
- 需求:實現任意兩個賬戶間轉賬操作,并對每次轉賬操作在數據庫進行留痕
- 需求微縮:A賬戶減錢,B賬戶加錢,數據庫記錄日志
- 分析:
①:基于轉賬操作案例添加日志模塊,實現數據庫中記錄日志
②:業務層轉賬操作(transfer),調用減錢、加錢與記錄日志功能 - 實現效果預期:
無論轉賬操作是否成功,均進行轉賬操作的日志留痕 - 存在的問題:
日志的記錄與轉賬操作隸屬同一個事務,同成功同失敗 - 實現效果預期改進:
無論轉賬操作是否成功,日志必須保留 - 事務傳播行為:事務協調員對事務管理員所攜帶事務的處理態度
代碼實現
準備工作
USE spring_db;
CREATE TABLE tbl_log(id INT PRIMARY KEY AUTO_INCREMENT,info VARCHAR(255),createDate DATE
);
public interface LogService {//propagation設置事務屬性:傳播行為設置為當前操作需要新事務@Transactionalvoid log(String out, String in, Double money);
}@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;public void log(String out,String in,Double money ) {logDao.log("轉賬操作由"+out+"到"+in+",金額:"+money);}
}public interface LogDao {@Insert("insert into tbl_log (info,createDate) values(#{info},now())")void log(String info);
}
- 在AccountServiceImpl中調用logService中添加日志的方法
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Autowiredprivate LogService logService;public void transfer(String out,String in ,Double money) {try{accountDao.outMoney(out,money);int i = 1/0;accountDao.inMoney(in,money);}finally {logService.log(out,in,money);}}
}
- 在LogService的log()方法上設置事務的傳播行為
public interface LogService {//propagation設置事務屬性:傳播行為設置為當前操作需要新事務@Transactional(propagation = Propagation.REQUIRES_NEW)void log(String out, String in, Double money);
}
- 運行測試類,查看結果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {@Autowiredprivate AccountService accountService;@Testpublic void testTransfer() throws IOException {accountService.transfer("Tom","Jerry",50D);}
}