Spring系列__04AOP
AOP簡介
今天來介紹一下AOP。AOP,中文常被翻譯為“面向切面編程”,其作為OOP的擴展,其思想除了在Spring中得到了應用,也是不錯的設計方法。通常情況下,一個軟件系統(tǒng),除了正常的業(yè)務邏輯代碼,往往還有一些功能性的代碼,比如:記錄日志、數(shù)據(jù)校驗等等。最原始的辦法就是直接在你的業(yè)務邏輯代碼中編寫這些功能性代碼,但是,這樣除了當時開發(fā)的時候比較方便以外;代碼的閱讀性、可維護性都會大大降低。而且,當你需要頻繁使用一個功能的時候(比如記錄日志),你還需要重復編寫。而使用AOP的好處,簡單來說就是,它能把這種重復性的功能代碼抽離出來,在需要的時候,通過動態(tài)代理技術,在不修改源代碼的情況下提供增強性功能。
優(yōu)勢:
- 減少重復代碼
- 提高開發(fā)效率
- 代碼更加整潔,提高了可維護性
說了這么多,簡單演示一下,我們假定現(xiàn)在要實現(xiàn)一個賬戶轉(zhuǎn)賬的功能,這里面會涉及到一些事務的控制,從代碼的合理性角度出發(fā),我們將其放在service層。
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;
}
public interface AccountDao {
/**
* 查詢所有
* @return
*/
List<Account> findAllAccount();
/**
* 查詢一個
* @return
*/
Account findAccountById(Integer accountId);
/**
* 保存
* @param account
*/
void saveAccount(Account account);
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 刪除
* @param acccountId
*/
void deleteAccount(Integer acccountId);
/**
* 根據(jù)名稱查詢賬戶
* @param accountName
* @return 如果有唯一的一個結(jié)果就返回,如果沒有結(jié)果就返回null
* 如果結(jié)果集超過一個就拋異常
*/
Account findAccountByName(String accountName);
}
public class AccountServiceImpl_OLD implements AccountService {
private AccountDao accountDao;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public List<Account> findAllAccount() {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執(zhí)行操作
List<Account> accounts = accountDao.findAllAccount();
//3.提交事務
txManager.commit();
//4.返回結(jié)果
return accounts;
}catch (Exception e){
//5.回滾操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.釋放連接
txManager.release();
}
}
@Override
public Account findAccountById(Integer accountId) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執(zhí)行操作
Account account = accountDao.findAccountById(accountId);
//3.提交事務
txManager.commit();
//4.返回結(jié)果
return account;
}catch (Exception e){
//5.回滾操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.釋放連接
txManager.release();
}
}
@Override
public void saveAccount(Account account) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執(zhí)行操作
accountDao.saveAccount(account);
//3.提交事務
txManager.commit();
}catch (Exception e){
//4.回滾操作
txManager.rollback();
}finally {
//5.釋放連接
txManager.release();
}
}
@Override
public void updateAccount(Account account) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執(zhí)行操作
accountDao.updateAccount(account);
//3.提交事務
txManager.commit();
}catch (Exception e){
//4.回滾操作
txManager.rollback();
}finally {
//5.釋放連接
txManager.release();
}
}
@Override
public void deleteAccount(Integer acccountId) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執(zhí)行操作
accountDao.deleteAccount(acccountId);
//3.提交事務
txManager.commit();
}catch (Exception e){
//4.回滾操作
txManager.rollback();
}finally {
//5.釋放連接
txManager.release();
}
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執(zhí)行操作
//2.1根據(jù)名稱查詢轉(zhuǎn)出賬戶
Account source = accountDao.findAccountByName(sourceName);
//2.2根據(jù)名稱查詢轉(zhuǎn)入賬戶
Account target = accountDao.findAccountByName(targetName);
//2.3轉(zhuǎn)出賬戶減錢
source.setMoney(source.getMoney()-money);
//2.4轉(zhuǎn)入賬戶加錢
target.setMoney(target.getMoney()+money);
//2.5更新轉(zhuǎn)出賬戶
accountDao.updateAccount(source);
int i=1/0;
//2.6更新轉(zhuǎn)入賬戶
accountDao.updateAccount(target);
//3.提交事務
txManager.commit();
}catch (Exception e){
//4.回滾操作
txManager.rollback();
e.printStackTrace();
}finally {
//5.釋放連接
txManager.release();
}
}
在這里,我們看見了很惡心的代碼:大量重復性的記錄日志的代碼,而且,當你更改的時候,你發(fā)現(xiàn)并不方便。后續(xù)我們會對這個代碼進行改寫。
AOP的實現(xiàn)方式
AOP通過動態(tài)代理的來實現(xiàn)。
動態(tài)代理簡介
在這里先簡單介紹一下動態(tài)代理:使用一個代理將對象包裝起來, 然后用該代理對象取代原始對象.。任何對原始對象的調(diào)用都要通過代理. 代理對象決定是否以及何時將方法調(diào)用轉(zhuǎn)到原始對象上。其調(diào)用過程如下圖所示:

特點:
- 字節(jié)碼隨用隨創(chuàng)建,隨用隨加載。
- 它與靜態(tài)代理的區(qū)別也在于此。因為靜態(tài)代理是字節(jié)碼一上來就創(chuàng)建好,并完成加載。
- 裝飾者模式就是靜態(tài)代理的一種體現(xiàn)。
動態(tài)代理有兩種形式
- 基于接口的動態(tài)代理
提供者: JDK 官方的 Proxy 類。
要求:被代理類最少實現(xiàn)一個接口 - 基于子類的動態(tài)代理
提供者:第三方的 CGLib,如果報 asmxxxx 異常,需要導入 asm.jar。
要求:被代理類不能用 final 修飾的類(最終類)。
下面結(jié)合示例來解釋一下這兩種動態(tài)代理的實現(xiàn)方式:
筆者最近更換了一臺新的電腦,就以買電腦來舉個例子吧。現(xiàn)在大家買電腦,已經(jīng)很少去實體店了,多半是通過電商渠道。不管是什么,都是從中間商來買,這一行為,在無形中就體現(xiàn)了代理模式的思想。
電腦生產(chǎn)商最開始的時候,除了生產(chǎn)和組裝電腦,同時還可以將電腦出售給消費者或者經(jīng)銷商(代理商),而他對顧客來說,需要完成兩種服務:銷售商品和售后服務。當行業(yè)發(fā)展到一定階段,電腦生產(chǎn)商不斷增多,人們就會制定一些行業(yè)規(guī)范來讓大家共同遵守(也就是抽象出來的接口)。而且,電腦生產(chǎn)商為了節(jié)約成本,不再提供直接和消費者銷售的服務,我們消費者也因此只能從代理商那里購買新的電腦。這便是典型的代理模式。
使用 JDK 官方的 Proxy 類創(chuàng)建代理對象
public interface IProducer {
public void saleProduct(float money);
public void afterService(float money);
}
public class Producer implements IProducer {
@Override
public void saleProduct(float money) {
System.out.println("銷售產(chǎn)品,并拿到錢:" + money);
}
@Override
public void afterService(float money) {
System.out.println("提供售后服務,并拿到錢:" + money);
}
}
//消費者
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 如何創(chuàng)建代理對象:
* 使用Proxy類中的newProxyInstance方法
* 創(chuàng)建代理對象的要求:
* 被代理類最少實現(xiàn)一個接口,如果沒有則不能使用
* newProxyInstance方法的參數(shù):
* ClassLoader:類加載器
* 它是用于加載代理對象字節(jié)碼的。和被代理對象使用相同的類加載器。固定寫法。
* Class[]:字節(jié)碼數(shù)組
* 它是用于讓代理對象和被代理對象有相同方法。固定寫法。
* InvocationHandler:用于提供增強的代碼
* 它是讓我們寫如何代理。我們一般都是些一個該接口的實現(xiàn)類,通常情況下都是匿名內(nèi)部類,但不是必須的。
* 此接口的實現(xiàn)類都是誰用誰寫。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(), new InvocationHandler() {
/**
* 作用:執(zhí)行被代理對象的任何接口方法都會經(jīng)過該方法
* 方法參數(shù)的含義
* @param proxy 代理對象的引用
* @param method 當前執(zhí)行的方法
* @param args 當前執(zhí)行方法所需的參數(shù)
* @return 和被代理對象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增強的代碼
Object returnValue = null;
//1.獲取方法執(zhí)行的參數(shù)
Float money = (Float)args[0];
//2.判斷當前方法是不是銷售,如果是的話,打八折
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
使用jdk提供的Proxy來創(chuàng)建代理對象的時候,要求別代理對象至少要實現(xiàn)一個接口,代理類需要實現(xiàn)同樣的接口并由同一類加載器加載。如果沒有這樣,就不能使用這種方式了。其他具體內(nèi)容,請參考官方文檔。
cglib方式來實現(xiàn)動態(tài)代理
其實,說AOP是OOP的延伸,還是很容易證明的:jdk提供動態(tài)代理的方式是實現(xiàn)接口,而cglib的實現(xiàn)方式就是利用了OOP的繼承。原理大同小異,主要區(qū)別就是不用實現(xiàn)接口而是改用繼承,也因此具備繼承的限制:被代理的類不能是被final修飾。
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* create方法的參數(shù):
* Class:字節(jié)碼
* 它是用于指定被代理對象的字節(jié)碼。
*
* Callback:用于提供增強的代碼
* 它是讓我們寫如何代理。我們一般都是些一個該接口的實現(xiàn)類,通常情況下都是匿名內(nèi)部類,但不是必須的。
* 此接口的實現(xiàn)類都是誰用誰寫。
* 我們一般寫的都是該接口的子接口實現(xiàn)類:MethodInterceptor
*/
Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
Object result = null;
Float price = (Float) objects[0];
if ("saleProduct".equals(method.getName())) {
result = method.invoke(o, price * 0.8f);
}
return result;
}
});
proxyProducer.saleProduct(10000f);
}
}
ublic class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* create方法的參數(shù):
* Class:字節(jié)碼
* 它是用于指定被代理對象的字節(jié)碼。
*
* Callback:用于提供增強的代碼
* 它是讓我們寫如何代理。我們一般都是些一個該接口的實現(xiàn)類,通常情況下都是匿名內(nèi)部類,但不是必須的。
* 此接口的實現(xiàn)類都是誰用誰寫。
* 我們一般寫的都是該接口的子接口實現(xiàn)類:MethodInterceptor
*/
Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
Object result = null;
Float price = (Float) objects[0];
if ("saleProduct".equals(method.getName())) {
result = method.invoke(o, price * 0.8f);
}
return result;
}
});
proxyProducer.saleProduct(10000f);
}
}
Spring中的AOP
以上所述的兩種生成代理對象的方法,在Spring中都會應用:默認優(yōu)先使用jdk自帶的方式,當發(fā)現(xiàn)別代理類沒有實現(xiàn)接口時改用cglib方式。
專業(yè)術語直白翻譯
- Joinpoint(連接點):
所謂的連擊點,就是你的業(yè)務邏輯中的每一個方法,都被稱作連接點。而且,和AspectJ和JBoss不同,Spring不支持字段和構造器連接點,只支持方法級別的連接點。 - Pointcut(切入點):
當你要對一個連接點進行額外的功能添加時,這個連接點就是切入點。 - Advice(通知/增強):
通知就是你攔截了切點后要做的事情。根據(jù)你要做的時機,分為:前置通知、后置通知、返回通知、異常通知和環(huán)繞通知。 - Introduction(引介):
引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運行期為類動態(tài)地添加一些方法或 Field。 - Target(目標對象):
代理的目標對象。 - Weaving(織入):
是指把增強應用到目標對象來創(chuàng)建新的代理對象的過程。spring 采用動態(tài)代理織入,而 AspectJ 采用編譯期織入和類裝載期織入。 - Proxy(代理) :
一個類被 AOP 織入增強后,就產(chǎn)生一個結(jié)果代理類。 - Aspect(切面):
是切入點和通知(引介)的結(jié)合。
實戰(zhàn)演練
這次我們打算做一個簡單一點的功能:實現(xiàn)一個能夠進行加減乘除運算的計算器,并進行相應的日志記錄
過程主要是以下幾步:
1.開發(fā)業(yè)務邏輯代碼
2.開發(fā)切面代碼
3.配置ioc,將計算器和切面配置到Spring容器中
4.切面配置,開啟AOP
對于配置的方式,主要是還是兩種方式:
Java配置:
public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
package com.spring.demo.springaop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 可以使用 @Order 注解指定切面的優(yōu)先級, 值越小優(yōu)先級越高
*/
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(public int com.spring.demo.springaop.ArithmeticCalculator.*(..))")
public void declareJoinPoint() {}
@Before("declareJoinPoint()")
public void beforeMehtod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("the " + methodName + " begins with " + Arrays.asList(args));
}
@AfterReturning(value = "declareJoinPoint()", returning = "result")
public void afterMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("the " + methodName + " ends successfully with result is " + result);
}
@AfterThrowing(value = "declareJoinPoint()", throwing = "e")
public void afterException(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("the " + methodName + "occurs a Exception by" + e.getMessage());
}
/**
* 環(huán)繞通知需要攜帶 ProceedingJoinPoint 類型的參數(shù).
* 環(huán)繞通知類似于動態(tài)代理的全過程: ProceedingJoinPoint 類型的參數(shù)可以決定是否執(zhí)行目標方法.
* 且環(huán)繞通知必須有返回值, 返回值即為目標方法的返回值
*/
/*
@Around("execution(public int com.spring.demo.springaop.ArithmeticCalculator.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;
String methodName = pjd.getSignature().getName();
try {
//前置通知
System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
//執(zhí)行目標方法
result = pjd.proceed();
//返回通知
System.out.println("The method " + methodName + " ends with " + result);
} catch (Throwable e) {
//異常通知
System.out.println("The method " + methodName + " occurs exception:" + e);
throw new RuntimeException(e);
}
//后置通知
System.out.println("The method " + methodName + " ends");
return result;
}
*/
}
@Order(1)
@Aspect
@Component
public class VlidationAspect {
@Before("com.spring.demo.springaop.LoggingAspect.declareJoinPoint()")
public void validateArgs(JoinPoint joinPoint){
System.out.println("-->validate:" + Arrays.asList(joinPoint.getArgs()));
}
}
@EnableAspectJAutoProxy
@Configuration
@ComponentScan
public class MainConcig {
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.spring.demo" +
".springaop");
ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) context.getBean("arithmeticCalculator");
int add = arithmeticCalculator.add(100, 200);
}
}
xml文件配置
JavaBean還是這些,只是將各個注解刪除即可,而bean的配置和aop功能的開啟,由配置文件來聲明。要引入aop命名空間。
<!-- 配置 bean -->
<bean id="arithmeticCalculator"
class="com.spring.demo.springaop.xml.ArithmeticCalculatorImpl"></bean>
<!-- 配置切面的 bean. -->
<bean id="loggingAspect"
class="com.spring.demo.springaop.xml.LoggingAspect"></bean>
<bean id="vlidationAspect"
class="com.spring.demo.springaop.xml.VlidationAspect"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切點表達式 -->
<aop:pointcut expression="execution(* com.spring.demo.springaop.ArithmeticCalculator.*(int, int))"
id="pointcut"/>
<!-- 配置切面及通知 -->
<aop:aspect ref="loggingAspect" order="2">
<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
<!--
<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
-->
</aop:aspect>
<aop:aspect ref="vlidationAspect" order="1">
<aop:before method="validateArgs" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
切入點表達式說明(引用別人的,懶得寫了)
execution:匹配方法的執(zhí)行(常用)
execution(表達式)
表達式語法: execution([修飾符] 返回值類型 包名.類名.方法名(參數(shù)))
寫法說明:
全匹配方式:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
訪問修飾符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
返回值可以使用號,表示任意返回值
*
com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
包名可以使用號,表示任意包,但是有幾級包,需要寫幾個*
- ....AccountServiceImpl.saveAccount(com.itheima.domain.Account)
使用..來表示當前包,及其子包 - com..AccountServiceImpl.saveAccount(com.itheima.domain.Account)
類名可以使用*號,表示任意類 - com...saveAccount(com.itheima.domain.Account)
方法名可以使用號,表示任意方法 - com...( com.itheima.domain.Account)
參數(shù)列表可以使用*,表示參數(shù)可以是任意數(shù)據(jù)類型,但是必須有參數(shù) - com...(*)
參數(shù)列表可以使用..表示有無參數(shù)均可,有參數(shù)可以是任意類型 - com...(..)
全通配方式: - ...(..)
注:
通常情況下,我們都是對業(yè)務層的方法進行增強,所以切入點表達式都是切到業(yè)務層實現(xiàn)類。
execution( com.itheima.service.impl..(..))
補充說明: 引入通知
引入通知是一種特殊的通知類型. 它通過為接口提供實現(xiàn)類, 允許對象動態(tài)地實現(xiàn)接口, 就像對象已經(jīng)在運行時擴展了實現(xiàn)類一樣。

引入通知可以使用兩個實現(xiàn)類 MaxCalculatorImpl 和 MinCalculatorImpl, 讓 ArithmeticCalculatorImpl 動態(tài)地實現(xiàn) MaxCalculator 和 MinCalculator 接口. 而這與從 MaxCalculatorImpl 和 MinCalculatorImpl 中實現(xiàn)多繼承的效果相同. 但卻不需要修改 ArithmeticCalculatorImpl 的源代碼。
引入通知也必須在切面中聲明。
代碼演示



浙公網(wǎng)安備 33010602011771號