這篇文章主要是對(duì)AOP實(shí)現(xiàn)原理以及Spring使用AOP進(jìn)行描述
還不了解AOP的請(qǐng)先看此文章Spring AOP就是這么簡(jiǎn)單啦
動(dòng)態(tài)代理
在這里我們的案例所用的思想為下圖

以此思想設(shè)計(jì)一個(gè)代理
創(chuàng)建
IProducer.java作為接口,代表生產(chǎn)商需要有的特征
package com.ygg.proxy;
/**
* 對(duì)生產(chǎn)廠家要求的接口
*/
public interface IProducer {
/**
* 銷售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}
接口實(shí)現(xiàn)類
package com.ygg.proxy;
/**
* 一個(gè)生產(chǎn)者
*/
public class Producer implements IProducer{
/**
* 銷售
* @param money
*/
public void saleProduct(float money){
System.out.println("銷售產(chǎn)品,并拿到錢:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服務(wù),并拿到錢:"+money);
}
}
動(dòng)態(tài)代理實(shí)現(xiàn)
package com.ygg.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 模擬一個(gè)消費(fèi)者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 動(dòng)態(tài)代理:
* 特點(diǎn):字節(jié)碼隨用隨創(chuàng)建,隨用隨加載
* 作用:不修改源碼的基礎(chǔ)上對(duì)方法增強(qiáng)
* 分類:
* 基于接口的動(dòng)態(tài)代理
* 基于子類的動(dòng)態(tài)代理
* 基于接口的動(dòng)態(tài)代理:
* 涉及的類:Proxy
* 提供者:JDK官方
* 如何創(chuàng)建代理對(duì)象:
* 使用Proxy類中的newProxyInstance方法
* 創(chuàng)建代理對(duì)象的要求:
* 被代理類最少實(shí)現(xiàn)一個(gè)接口,如果沒有則不能使用
* newProxyInstance方法的參數(shù):
* ClassLoader:類加載器
* 它是用于加載代理對(duì)象字節(jié)碼的。和被代理對(duì)象使用相同的類加載器。固定寫法。
* Class[]:字節(jié)碼數(shù)組
* 它是用于讓代理對(duì)象和被代理對(duì)象有相同方法。固定寫法。
* InvocationHandler:用于提供增強(qiáng)的代碼
* 它是讓我們寫如何代理。我們一般都是些一個(gè)該接口的實(shí)現(xiàn)類,通常情況下都是匿名內(nèi)部類,但不是必須的。
* 此接口的實(shí)現(xiàn)類都是誰(shuí)用誰(shuí)寫。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:執(zhí)行被代理對(duì)象的任何接口方法都會(huì)經(jīng)過(guò)該方法
* 方法參數(shù)的含義
* @param proxy 代理對(duì)象的引用
* @param method 當(dāng)前執(zhí)行的方法
* @param args 當(dāng)前執(zhí)行方法所需的參數(shù)
* @return 和被代理對(duì)象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增強(qiáng)的代碼
Object returnValue = null;
//1.獲取方法執(zhí)行的參數(shù)
Float money = (Float)args[0];
//2.判斷當(dāng)前方法是不是銷售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
接口代理實(shí)現(xiàn)方法缺點(diǎn):沒有相應(yīng)接口將無(wú)法實(shí)現(xiàn)
基于子類動(dòng)態(tài)代理
實(shí)現(xiàn)此代理方式需導(dǎo)入cglib.jar、asm.jar
實(shí)現(xiàn)方法與上雷同
package com.ygg.cglib;
/**
* 一個(gè)生產(chǎn)者
*/
public class Producer {
/**
* 銷售
* @param money
*/
public void saleProduct(float money){
System.out.println("銷售產(chǎn)品,并拿到錢:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服務(wù),并拿到錢:"+money);
}
}
package com.ygg.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 模擬一個(gè)消費(fèi)者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 動(dòng)態(tài)代理:
* 特點(diǎn):字節(jié)碼隨用隨創(chuàng)建,隨用隨加載
* 作用:不修改源碼的基礎(chǔ)上對(duì)方法增強(qiáng)
* 分類:
* 基于接口的動(dòng)態(tài)代理
* 基于子類的動(dòng)態(tài)代理
* 基于子類的動(dòng)態(tài)代理:
* 涉及的類:Enhancer
* 提供者:第三方cglib庫(kù)
* 如何創(chuàng)建代理對(duì)象:
* 使用Enhancer類中的create方法
* 創(chuàng)建代理對(duì)象的要求:
* 被代理類不能是最終類
* create方法的參數(shù):
* Class:字節(jié)碼
* 它是用于指定被代理對(duì)象的字節(jié)碼。
*
* Callback:用于提供增強(qiáng)的代碼
* 它是讓我們寫如何代理。我們一般都是些一個(gè)該接口的實(shí)現(xiàn)類,通常情況下都是匿名內(nèi)部類,但不是必須的。
* 此接口的實(shí)現(xiàn)類都是誰(shuí)用誰(shuí)寫。
* 我們一般寫的都是該接口的子接口實(shí)現(xiàn)類:MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 執(zhí)行北地阿里對(duì)象的任何方法都會(huì)經(jīng)過(guò)該方法
* @param proxy
* @param method
* @param args
* 以上三個(gè)參數(shù)和基于接口的動(dòng)態(tài)代理中invoke方法的參數(shù)是一樣的
* @param methodProxy :當(dāng)前執(zhí)行方法的代理對(duì)象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增強(qiáng)的代碼
Object returnValue = null;
//1.獲取方法執(zhí)行的參數(shù)
Float money = (Float)args[0];
//2.判斷當(dāng)前方法是不是銷售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
Spring中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">
<!-- 配置srping的Ioc,把service對(duì)象配置進(jìn)來(lái)-->
<bean id="accountService" class="com.ygg.service.impl.AccountServiceImpl"></bean>
<!--spring中基于XML的AOP配置步驟
1、把通知Bean也交給spring來(lái)管理
2、使用aop:config標(biāo)簽表明開始AOP的配置
3、使用aop:aspect標(biāo)簽表明配置切面
id屬性:是給切面提供一個(gè)唯一標(biāo)識(shí)
ref屬性:是指定通知類bean的Id。
4、在aop:aspect標(biāo)簽的內(nèi)部使用對(duì)應(yīng)標(biāo)簽來(lái)配置通知的類型
我們現(xiàn)在示例是讓printLog方法在切入點(diǎn)方法執(zhí)行之前之前:所以是前置通知
aop:before:表示配置前置通知
method屬性:用于指定Logger類中哪個(gè)方法是前置通知
pointcut屬性:用于指定切入點(diǎn)表達(dá)式,該表達(dá)式的含義指的是對(duì)業(yè)務(wù)層中哪些方法增強(qiáng)
切入點(diǎn)表達(dá)式的寫法:
關(guān)鍵字:execution(表達(dá)式)
表達(dá)式:
訪問(wèn)修飾符 返回值 包名.包名.包名...類名.方法名(參數(shù)列表)
標(biāo)準(zhǔn)的表達(dá)式寫法:
public void com.ygg.service.impl.AccountServiceImpl.saveAccount()
訪問(wèn)修飾符可以省略
void com.ygg.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.ygg.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有幾級(jí)包,就需要寫幾個(gè)*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示當(dāng)前包及其子包
* *..AccountServiceImpl.saveAccount()
類名和方法名都可以使用*來(lái)實(shí)現(xiàn)通配
* *..*.*()
參數(shù)列表:
可以直接寫數(shù)據(jù)類型:
基本類型直接寫名稱 int
引用類型寫包名.類名的方式 java.lang.String
可以使用通配符表示任意類型,但是必須有參數(shù)
可以使用..表示有無(wú)參數(shù)均可,有參數(shù)可以是任意類型
全通配寫法:
* *..*.*(..)
實(shí)際開發(fā)中切入點(diǎn)表達(dá)式的通常寫法:
切到業(yè)務(wù)層實(shí)現(xiàn)類下的所有方法
* com.ygg.service.impl.*.*(..)
-->
<!-- 配置Logger類 -->
<bean id="logger" class="com.ygg.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的類型,并且建立通知方法和切入點(diǎn)方法的關(guān)聯(lián)-->
<aop:before method="printLog" pointcut="execution(* com.ygg.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
SpringAOP常用的通知
<!--配置AOP-->
<aop:config>
<!-- 配置切入點(diǎn)表達(dá)式 id屬性用于指定表達(dá)式的唯一標(biāo)識(shí)。expression屬性用于指定表達(dá)式內(nèi)容
此標(biāo)簽寫在aop:aspect標(biāo)簽內(nèi)部只能當(dāng)前切面使用。
它還可以寫在aop:aspect外面,此時(shí)就變成了所有切面可用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入點(diǎn)方法執(zhí)行之前執(zhí)行
<aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>-->
<!-- 配置后置通知:在切入點(diǎn)方法正常執(zhí)行之后值。它和異常通知永遠(yuǎn)只能執(zhí)行一個(gè)
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->
<!-- 配置異常通知:在切入點(diǎn)方法執(zhí)行產(chǎn)生異常之后執(zhí)行。它和后置通知永遠(yuǎn)只能執(zhí)行一個(gè)
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>-->
<!-- 配置最終通知:無(wú)論切入點(diǎn)方法是否正常執(zhí)行它都會(huì)在其后面執(zhí)行
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>-->
<!-- 配置環(huán)繞通知 詳細(xì)的注釋請(qǐng)看Logger類中-->
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
SpirngAOP注解開發(fā)
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"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring創(chuàng)建容器時(shí)要掃描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 配置spring開啟注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
//一些注解的作用
@Component("logger")
@Aspect//表示當(dāng)前類是一個(gè)切面類
/**
* 前置通知
*/
// @Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger類中的beforePrintLog方法開始記錄日志了。。。");
}
/**
* 后置通知
*/
// @AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger類中的afterReturningPrintLog方法開始記錄日志了。。。");
}
/**
* 異常通知
*/
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("異常通知Logger類中的afterThrowingPrintLog方法開始記錄日志了。。。");
}
/**
* 最終通知
*/
// @After("pt1()")
public void afterPrintLog(){
System.out.println("最終通知Logger類中的afterPrintLog方法開始記錄日志了。。。");
}
/**
* 環(huán)繞通知
* 問(wèn)題:
* 當(dāng)我們配置了環(huán)繞通知之后,切入點(diǎn)方法沒有執(zhí)行,而通知方法執(zhí)行了。
* 分析:
* 通過(guò)對(duì)比動(dòng)態(tài)代理中的環(huán)繞通知代碼,發(fā)現(xiàn)動(dòng)態(tài)代理的環(huán)繞通知有明確的切入點(diǎn)方法調(diào)用,而我們的代碼中沒有。
* 解決:
* Spring框架為我們提供了一個(gè)接口:ProceedingJoinPoint。該接口有一個(gè)方法proceed(),此方法就相當(dāng)于明確調(diào)用切入點(diǎn)方法。
* 該接口可以作為環(huán)繞通知的方法參數(shù),在程序執(zhí)行時(shí),spring框架會(huì)為我們提供該接口的實(shí)現(xiàn)類供我們使用。
*
* spring中的環(huán)繞通知:
* 它是spring框架為我們提供的一種可以在代碼中手動(dòng)控制增強(qiáng)方法何時(shí)執(zhí)行的方式。
*/
@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法執(zhí)行所需的參數(shù)
System.out.println("Logger類中的aroundPringLog方法開始記錄日志了。。。前置");
rtValue = pjp.proceed(args);//明確調(diào)用業(yè)務(wù)層方法(切入點(diǎn)方法)
System.out.println("Logger類中的aroundPringLog方法開始記錄日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger類中的aroundPringLog方法開始記錄日志了。。。異常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger類中的aroundPringLog方法開始記錄日志了。。。最終");
}
}