[toc]
代碼冗余與裝飾器模式
代碼冗余現(xiàn)象
我們的Service層實(shí)現(xiàn)類中的每個(gè)方法都要加上事務(wù)控制,這樣使得每個(gè)方法的前后都要加上重復(fù)的事務(wù)控制的代碼,如下:
@Override
public void saveAccount(Account account) {
try {
TransactionManager.beginTransaction();
accountDao.save(account); // 唯一的一行業(yè)務(wù)代碼
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
@Override
public void updateAccount(Account account) {
try {
TransactionManager.beginTransaction();
accountDao.update(account); // 唯一的一行業(yè)務(wù)代碼
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
@Override
public void deleteAccount(Integer accountId) {
try {
TransactionManager.beginTransaction();
accountDao.delete(accountId); // 唯一的一行業(yè)務(wù)代碼
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
我們發(fā)現(xiàn)出現(xiàn)了兩個(gè)問題:
- 業(yè)務(wù)層方法變得臃腫了,里面充斥著很多重復(fù)代碼.
- 業(yè)務(wù)層方法和事務(wù)控制方法耦合了. 若提交,回滾,釋放資源中任何一個(gè)方法名變更,都需要修改業(yè)務(wù)層的代碼.
因此我們引入了裝飾模式解決代碼冗余和耦合現(xiàn)象.
解決代碼冗余的思路: 裝飾模式和動(dòng)態(tài)代理
動(dòng)態(tài)代理的寫法
常用的動(dòng)態(tài)代理分為兩種
- 基于接口的動(dòng)態(tài)代理,使用JDK 官方的 Proxy 類,要求被代理者至少實(shí)現(xiàn)一個(gè)接口
- 基于子類的動(dòng)態(tài)代理,使用第三方的 CGLib庫,要求被代理類不能是final類.
在這里我們使用基于接口的動(dòng)態(tài)代理,寫法如下:
接口名 新對(duì)象名 = (接口名)Proxy.newProxyInstance(
被代理的對(duì)象.getClass().getClassLoader(), // 被代理對(duì)象的類加載器,固定寫法
被代理的對(duì)象.getClass().getInterfaces(), // 被代理對(duì)象實(shí)現(xiàn)的所有接口,固定寫法
new InvocationHandler() { // 匿名內(nèi)部類,通過攔截被代理對(duì)象的方法來增強(qiáng)被代理對(duì)象
/* 被代理對(duì)象的任何方法執(zhí)行時(shí),都會(huì)被此方法攔截到
其參數(shù)如下:
proxy: 代理對(duì)象的引用,不一定每次都用得到
method: 被攔截到的方法對(duì)象
args: 被攔截到的方法對(duì)象的參數(shù)
返回值:
被增強(qiáng)后的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("方法名".equals(method.getName())) {
// 增強(qiáng)方法的操作
rtValue = method.invoke(被代理的對(duì)象, args);
// 增強(qiáng)方法的操作
return rtValue;
}
}
});
使用動(dòng)態(tài)代理解決代碼冗余現(xiàn)象
我們使用動(dòng)態(tài)代理對(duì)上述Service進(jìn)行改造,創(chuàng)建BeanFactory類作為service層對(duì)象工廠,通過其getAccountService方法得到業(yè)務(wù)層對(duì)象.
// 用于創(chuàng)建Service的代理對(duì)象的工廠
public class BeanFactory {
private IAccountService accountService; // 被增強(qiáng)的service對(duì)象
private TransactionManager txManager; // 事務(wù)控制工具類
// 成員變量的set方法,以便Spring容器注入
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
// 獲取增強(qiáng)后的Service對(duì)象
public IAccountService getAccountService() {
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
// 增強(qiáng)方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.開啟事務(wù)
txManager.beginTransaction();
//2.執(zhí)行操作
rtValue = method.invoke(accountService, args);
//3.提交事務(wù)
txManager.commit();
//4.返回結(jié)果
return rtValue;
} catch (Exception e) {
//5.回滾操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6.釋放連接
txManager.release();
}
}
});
}
}
在bean.xml中,添加如下配置
<!--配置beanfactory-->
<bean id="beanFactory" class="cn.maoritian.factory.BeanFactory">
<!-- 注入service -->
<property name="accountService" ref="accountService"></property>
<!-- 注入事務(wù)控制工具 -->
<property name="txManager" ref="txManager"></property>
</bean>
這樣,我們就可以通過Spring的IOC獲取增強(qiáng)后的Service對(duì)象.
使用SpringAOP解決代碼冗余
AOP相關(guān)術(shù)語
Joinpoint(連接點(diǎn)): 被攔截到的方法.Pointcut(切入點(diǎn)): 我們對(duì)其進(jìn)行增強(qiáng)的方法.-
Advice(通知/增強(qiáng)): 對(duì)切入點(diǎn)進(jìn)行的增強(qiáng)操作包括
前置通知,后置通知,異常通知,最終通知,環(huán)繞通知 Weaving(織入): 是指把增強(qiáng)應(yīng)用到目標(biāo)對(duì)象來創(chuàng)建新的代理對(duì)象的過程。Aspect(切面): 是切入點(diǎn)和通知的結(jié)合
使用XML配置AOP
使用XML配置AOP的步驟
在bean.xml中配置AOP要經(jīng)過以下幾步:
-
在
bean.xml中引入約束并將通知類注入Spring容器中<?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"> <!--通知類--> <bean id="logger" class="cn.maoritian.utils.Logger"></bean> </beans> -
使用
<aop:config>標(biāo)簽聲明AOP配置,所有關(guān)于AOP配置的代碼都寫在<aop:config>標(biāo)簽內(nèi)<aop:config> <!-- AOP配置的代碼都寫在此處 --> </aop:config> -
使用
<aop:aspect>標(biāo)簽配置切面,其屬性如下id: 指定切面的id-
ref: 引用通知類的id<aop:config> <aop:aspect id="logAdvice" ref="logger"> <!--配置通知的類型要寫在此處--> </aop:aspect> </aop:config>
-
使用
<aop:pointcut>標(biāo)簽配置切入點(diǎn)表達(dá)式,指定對(duì)哪些方法進(jìn)行增強(qiáng),其屬性如下id: 指定切入點(diǎn)表達(dá)式的id-
expression: 指定切入點(diǎn)表達(dá)式<aop:config> <aop:aspect id="logAdvice" ref="logger"> <aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut> </aop:aspect> </aop:config>
-
使用
<aop:xxx>標(biāo)簽配置對(duì)應(yīng)類型的通知方法-
其屬性如下:
-
method: 指定通知類中的增強(qiáng)方法名. -
ponitcut-ref: 指定切入點(diǎn)的表達(dá)式的id -
poinitcut: 指定切入點(diǎn)表達(dá)式
其中
pointcut-ref和pointref屬性只能有其中一個(gè) -
-
具體的通知類型:
-
<aop:before>: 配置前置通知,指定的增強(qiáng)方法在切入點(diǎn)方法之前執(zhí)行. -
<aop:after-returning>: 配置后置通知,指定的增強(qiáng)方法在切入點(diǎn)方法正常執(zhí)行之后執(zhí)行. -
<aop:after-throwing>: 配置異常通知,指定的增強(qiáng)方法在切入點(diǎn)方法產(chǎn)生異常后執(zhí)行. -
<aop:after>: 配置最終通知,無論切入點(diǎn)方法執(zhí)行時(shí)是否發(fā)生異常,指定的增強(qiáng)方法都會(huì)最后執(zhí)行. -
<aop:around>: 配置環(huán)繞通知,可以在代碼中手動(dòng)控制增強(qiáng)代碼的執(zhí)行時(shí)機(jī).
<aop:config> <aop:aspect id="logAdvice" ref="logger"> <!--指定切入點(diǎn)表達(dá)式--> <aop:pointcut expression="execution(* cn,maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut> <!--配置各種類型的通知--> <aop:before method="printLogBefore" pointcut-ref="pt1"></aop:before> <aop:after-returning method="printLogAfterReturning" pointcut-ref="pt1"></aop:after-returning> <aop:after-throwing method="printLogAfterThrowing" pointcut-ref="pt1"></aop:after-throwing> <aop:after method="printLogAfter" pointcut-ref="pt1"></aop:after> <!--環(huán)繞通知一般單獨(dú)使用--> <!-- <aop:around method="printLogAround" pointcut-ref="pt1"></aop:around> --> </aop:aspect> </aop:config> -
-
切入點(diǎn)表達(dá)式
切入點(diǎn)表達(dá)式的寫法:
execution([修飾符] 返回值類型 包路徑.類名.方法名(參數(shù)))-
切入點(diǎn)表達(dá)式的省略寫法:
-
全匹配方式:
<aop:pointcut expression="execution(public void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut> -
其中訪問修飾符可以省略:
<aop:pointcut expression="execution(void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut> -
返回值可使用
*,表示任意返回值:<aop:pointcut expression="execution(* cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut> -
包路徑可以使用
*,表示任意包. 但是*.的個(gè)數(shù)要和包的層級(jí)數(shù)相匹配<aop:pointcut expression="execution(* *.*.*.*.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut> -
包路徑可以使用
*..,表示當(dāng)前包,及其子包(因?yàn)楸纠又袑?code>bean.xml放在根路徑下,因此..可以匹配項(xiàng)目?jī)?nèi)所有包路徑)<aop:pointcut expression="execution(* *..AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut> -
類名可以使用
*,表示任意類<aop:pointcut expression="execution(* *..*.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut> -
方法名可以使用
*,表示任意方法<aop:pointcut expression="execution(* *..*.*(com.itheima.domain.Account))" id="pt1"></aop:pointcut> -
參數(shù)列表可以使用
*,表示參數(shù)可以是任意數(shù)據(jù)類型,但是必須存在參數(shù)<aop:pointcut expression="execution(* *..*.*(*))" id="pt1"></aop:pointcut> -
參數(shù)列表可以使用
..表示有無參數(shù)均可,有參數(shù)可以是任意類型<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut> -
全通配方式,可以匹配匹配任意方法
<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
-
-
切入點(diǎn)表達(dá)式的一般寫法
一般我們都是對(duì)業(yè)務(wù)層所有實(shí)現(xiàn)類的所有方法進(jìn)行增強(qiáng),因此切入點(diǎn)表達(dá)式寫法通常為
<aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
環(huán)繞通知
-
前置通知,后置通知,異常通知,最終通知的執(zhí)行順序Spring是基于動(dòng)態(tài)代理對(duì)方法進(jìn)行增強(qiáng)的,
前置通知,后置通知,異常通知,最終通知在增強(qiáng)方法中的執(zhí)行時(shí)機(jī)如下:// 增強(qiáng)方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ Object rtValue = null; try { // 執(zhí)行前置通知 // 執(zhí)行原方法 rtValue = method.invoke(accountService, args); // 執(zhí)行后置通知 return rtValue; } catch (Exception e) { // 執(zhí)行異常通知 } finally { // 執(zhí)行最終通知 } } -
環(huán)繞通知允許我們更自由地控制增強(qiáng)代碼執(zhí)行的時(shí)機(jī)Spring框架為我們提供一個(gè)接口
ProceedingJoinPoint,它的實(shí)例對(duì)象可以作為環(huán)繞通知方法的參數(shù),通過參數(shù)控制被增強(qiáng)方法的執(zhí)行時(shí)機(jī)。-
ProceedingJoinPoint對(duì)象的getArgs()方法返回被攔截的參數(shù) -
ProceedingJoinPoint對(duì)象的proceed()方法執(zhí)行被攔截的方法
// 環(huán)繞通知方法,返回Object類型 public Object printLogAround(ProceedingJoinPoint pjp) { Object rtValue = null; try { Object[] args = pjp.getArgs(); printLogBefore(); // 執(zhí)行前置通知 rtValue = pjp.proceed(args);// 執(zhí)行被攔截方法 printLogAfterReturn(); // 執(zhí)行后置通知 }catch(Throwable e) { printLogAfterThrowing(); // 執(zhí)行異常通知 }finally { printLogAfter(); // 執(zhí)行最終通知 } return rtValue; } -
使用注解配置AOP
半注解配置AOP
Spring注解配置AOP的步驟
半注解配置AOP,需要在bean,xml中加入下面語句開啟對(duì)注解AOP的支持
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
Spring用于AOP的注解
用于聲明切面的注解
@Aspect: 聲明當(dāng)前類為通知類,該類定義了一個(gè)切面.相當(dāng)于xml配置中的<aop:aspect>標(biāo)簽
@Component("logger")
@Aspect
public class Logger {
// ...
}
用于聲明通知的注解
-
@Before: 聲明該方法為前置通知.相當(dāng)于xml配置中的<aop:before>標(biāo)簽 -
@AfterReturning: 聲明該方法為后置通知.相當(dāng)于xml配置中的<aop:after-returning>標(biāo)簽 -
@AfterThrowing: 聲明該方法為異常通知.相當(dāng)于xml配置中的<aop:after-throwing>標(biāo)簽 -
@After: 聲明該方法為最終通知.相當(dāng)于xml配置中的<aop:after>標(biāo)簽 -
@Around: 聲明該方法為環(huán)繞通知.相當(dāng)于xml配置中的<aop:around>標(biāo)簽
屬性:
-
value: 用于指定切入點(diǎn)表達(dá)式或切入點(diǎn)表達(dá)式的引用
@Component("logger")
@Aspect //表示當(dāng)前類是一個(gè)通知類
public class Logger {
// 配置前置通知
@Before("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogBefore(){
System.out.println("前置通知Logger類中的printLogBefore方法開始記錄日志了。。。");
}
// 配置后置通知
@AfterReturning("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogAfterReturning(){
System.out.println("后置通知Logger類中的printLogAfterReturning方法開始記錄日志了。。。");
}
// 配置異常通知
@AfterThrowing("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogAfterThrowing(){
System.out.println("異常通知Logger類中的printLogAfterThrowing方法開始記錄日志了。。。");
}
// 配置最終通知
@After("execution(* cn.maoritian.service.impl.*.*(..))")
public void printLogAfter(){
System.out.println("最終通知Logger類中的printLogAfter方法開始記錄日志了。。。");
}
// 配置環(huán)繞通知
@Around("execution(* cn.maoritian.service.impl.*.*(..))")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();
printLogBefore(); // 執(zhí)行前置通知
rtValue = pjp.proceed(args); // 執(zhí)行切入點(diǎn)方法
printLogAfterReturning(); // 執(zhí)行后置通知
return rtValue;
}catch (Throwable t){
printLogAfterThrowing(); // 執(zhí)行異常通知
throw new RuntimeException(t);
}finally {
printLogAfter(); // 執(zhí)行最終通知
}
}
}
用于指定切入點(diǎn)表達(dá)式的注解
@Pointcut: 指定切入點(diǎn)表達(dá)式,其屬性如下:
-
value: 指定表達(dá)式的內(nèi)容
@Pointcut注解沒有id屬性,通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式.
@Component("logger")
@Aspect //表示當(dāng)前類是一個(gè)通知類
public class Logger {
// 配置切入點(diǎn)表達(dá)式
@Pointcut("execution(* cn.maoritian.service.impl.*.*(..))")
private void pt1(){}
// 通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式
@Before("pt1()")
public void printLogBefore(){
System.out.println("前置通知Logger類中的printLogBefore方法開始記錄日志了。。。");
}
// 通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式
@AfterReturning("pt1()")
public void printLogAfterReturning(){
System.out.println("后置通知Logger類中的printLogAfterReturning方法開始記錄日志了。。。");
}
// 通過調(diào)用被注解的方法獲取切入點(diǎn)表達(dá)式
@AfterThrowing("pt1()")
public void printLogAfterThrowing(){
System.out.println("異常通知Logger類中的printLogAfterThrowing方法開始記錄日志了。。。");
}
}
純注解配置AOP
在Spring配置類前添加@EnableAspectJAutoProxy注解,可以使用純注解方式配置AOP
@Configuration
@ComponentScan(basePackages="cn.maoritian")
@EnableAspectJAutoProxy // 允許AOP
public class SpringConfiguration {
// 具體配置
//...
}
使用注解配置AOP的bug
在使用注解配置AOP時(shí),會(huì)出現(xiàn)一個(gè)bug. 四個(gè)通知的調(diào)用順序依次是:前置通知,最終通知,后置通知. 這會(huì)導(dǎo)致一些資源在執(zhí)行最終通知時(shí)提前被釋放掉了,而執(zhí)行后置通知時(shí)就會(huì)出錯(cuò)。