閱讀本文,你將了解到如何使用Spring AOP及AOP的基本原理糟袁,文末還與大家分享AOP的使用情景
在面向?qū)ο缶幊讨校∣OP)中译株,我們編程的關(guān)注點在于某個對象實體有哪些具體功能及其子類功能實現(xiàn)的不同。不同于OOP纷纫,面向切面編程(AOP)更多關(guān)注的業(yè)務(wù)流程叁扫。在不侵入業(yè)務(wù)代碼的前提下穆碎,我們可以通過AOP編程的诵,為業(yè)務(wù)流程某個具體環(huán)節(jié)(連接點)增加業(yè)務(wù)邏輯(通知)欺矫,這些業(yè)務(wù)邏輯可能是打印日志偷厦、安全控制兔综、事務(wù)控制等等涧窒。
使用AOP前必須理解清楚AOP相關(guān)的幾個概念:通知(Advice)慧瘤、切點(pointcut)糖儡、切面(aspect)、金闽。
- 通知(Advice):通知要解決的是通知什么、什么時候通知的問題。通知什么指的我們增加的功能罚随,比如日志打印、事務(wù)控制等潮改。什么時候通知指的是我們在什么時候調(diào)用我們增加的功能脏答。我們可以在方法調(diào)用前調(diào)用通知(Before)阿蝶、方法調(diào)用后調(diào)用通知(After)玷过、方法調(diào)用成功后調(diào)用通知(After-returning)、方法調(diào)用異常后調(diào)用通知(After-throwing)、在方法調(diào)用前和調(diào)用后調(diào)用通知(Around)
- 切點(Piontcut):切點主要定義的是在什么位置上應(yīng)用通知,SpringAOP僅支持方法級別的切面編程(這和其應(yīng)用動態(tài)代理實現(xiàn)有關(guān))。一般我們會指定某個類的某個方法為切點霉囚,或者匹配某一通配符的一個或多個方法為切點闪唆,還可以指定由某一注解修飾的方法為切點等等票顾。
- 切面(aspect):切面是通知和切點定義的結(jié)合番刊,切面定義了在什么時候蝉绷、什么位置執(zhí)行什么操作(何時何地執(zhí)行何種操作)
通過理解這幾個概念沃但,面向切面編程(AOP)就是要解決何時何地執(zhí)行何種操作的問題垂攘。
除了以上的三個概念逸贾,AOP還有其他的概念咪鲜,在這里也簡單說明一下:
- 連接點:目標(biāo)類中某個具體的方法(待增強)颖侄;
- 織入: 織入是將切面加入的目標(biāo)類的過程。在Spring AOP中享郊,織入指的是將切面邏輯應(yīng)用到目標(biāo)類中并生成代理類的過程览祖。
1.使用示例
理解清楚AOP相關(guān)的幾個概念后,我們可以看一個AOP的使用示例炊琉。
創(chuàng)建切面展蒂,其中注解@Pointcut定義了切點信息,@Before("log")和logPrint方法定義了通知信息温自。
//logAOP.java
@Component
@Aspect
public class LogAOP {
//切點信息
@Pointcut("execution(* cn.test.pro.project.GsProjectService.*(..))")
public void log(){
}
//前置增強
@Before("log()")
public void logPrint(JoinPoint joinPoint){
System.out.println("---------logPrint();-------"+joinPoint.getTarget().getClass());
}
}
目標(biāo)類的信息
//GsProjectService.java
@Service
public class GsProjectService {
@Autowired
private GsProjectMapper gsProjectMapper;
public String getById(String id){
return "admin";
}
}
配置文件spring-config.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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- 掃描注解 -->
<context:component-scan base-package="cn.test.pro">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<!--基于aspectj的注解驅(qū)動-->
<aop:aspectj-autoproxy/>
</beans>
啟動類相關(guān)信息
//Main.java
public class Main {
public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
GsProjectService gsProjectService = (GsProjectService) ac.getBean("gsProjectService");
System.out.println(gsProjectService.getById("1"));
}
}
輸出結(jié)果:
---------logPrint();-------class cn.test.pro.project.GsProjectService
cn.test.pro.project.GsProject@3370f42
2. 基本原理
說明:本文提到的AOP的基本原理是主要說明使用注解的AOP玄货,基于XML配置的AOP類似〉棵冢看本節(jié)時建議先閱讀Java 動態(tài)代理機制解析
說起Spring AOP的基本原理松捉,我們要從配置文件中配置說起:
<!--基于aspectj的注解驅(qū)動-->
<aop:aspectj-autoproxy/>
在xml配置文件中增加如上配置后,就開啟了基于注解的AOP功能馆里。我們知道Spring 啟動時會讀取配置文件隘世,并對文件中的配置項進(jìn)行解析可柿。
- 當(dāng)Spring讀取到該配置項后,會根據(jù)該行的命名空間AOP丙者,查找對應(yīng)的命名空間處理器AOPNamespaceHandler复斥;
2.在AOPNamespaceHandler中,我們看到如下的代碼:
public class AopNamespaceHandler extends NamespaceHandlerSupport {
public AopNamespaceHandler() {
}
public void init() {
this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
}
}
在方法init()中械媒,我們看到"aspectj-autoproxy"配置信息的解析交給了類 AspectJAutoProxyBeanDefinition進(jìn)行解析目锭。
- 現(xiàn)在Spring知道要使用類AspectJAutoProxyBeanDefinition進(jìn)行配置解析,類AspectJAutoProxyBeanDefinition是接口BeanDefinitionParser的實現(xiàn)類纷捞,接著Spring調(diào)用該類的parse方法進(jìn)行解析痢虹;
//AspectJAutoProxyBeanDefinition.java
public BeanDefinition parse(Element element, ParserContext parserContext) {
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
this.extendBeanDefinition(element, parserContext);
return null;
}
- 我們特別注意parse方法中的
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
介紹這個方法前,我們還必須要知道AOP是通過動態(tài)代理機制實現(xiàn)的主儡,而類AspectJAnnotationAutoProxyCreator正是完成由目標(biāo)類(target Object)到代理類的轉(zhuǎn)換奖唯,可以說該類是AOP實現(xiàn)的核心類。
我們接著看方法registerAspectJAnnotationAutoProxyCreatorIfNecessary的功能糜值,從方法名上我們可以看出該方法主要完成的是將AspectJAnnotationAutoProxyCreator注冊到Spring容器中的功能丰捷。這樣在合適的時機,Spring就可以使用該類根據(jù)目標(biāo)類動態(tài)生成代理類了寂汇。
-
什么是合適的時機呢病往?根據(jù)動態(tài)代理機制原理(可參考Java 動態(tài)代理機制解析)的介紹,生成代理類必須需要一個實例化的目標(biāo)類健无。
為了知道什么是合適的時機荣恐,我們還要看一下AspectJAnnotationAutoProxyCreator的類結(jié)構(gòu)圖,我們看到該類是接口BeanPostProcessor的實現(xiàn)類累贤。
AspectJAnnotationAutoProxyCreator的類結(jié)構(gòu)圖.png
BeanPostProcessor是一種非常重要的接口叠穆,在創(chuàng)建Bean的過程中會調(diào)用BeanPostProcessor的postProcessAfterInitialization方法。spring的開發(fā)者也可以使用該接口的特性擴展bean的功能臼膏。而代理類的生成也正式在此處硼被。 現(xiàn)在我們看一下AspectJAnnotationAutoProxyCreator的postProcessAfterInitialization方法
//方法的實現(xiàn)在AbstractAutoProxyCreator.java
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(bean != null) {
//如果已經(jīng)生成過代理,則直接從緩存中獲取
Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
if(!this.earlyProxyReferences.contains(cacheKey)) {
//生成代理對象
return this.wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
我們再看一下方法wrapIfNecessary的實現(xiàn)
//方法的實現(xiàn)在AbstractAutoProxyCreator.java
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
//如果當(dāng)前已經(jīng)被代理過渗磅,則直接返回嚷硫;
if(beanName != null && this.targetSourcedBeans.contains(beanName)) {
return bean;
} else if(Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
} else if(!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
//獲取切面的所有信息(包含通知和切點信息)
Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
if(specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
//根據(jù)切面信息和具體bean,創(chuàng)建該bean的代理類
Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
} else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
} else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
}
wrapIfNecessary方法主要分為兩個步驟:首先找到所有切面的信息始鱼,然后根據(jù)切面信息生成代理類仔掸。
- 我們再詳細(xì)看一下Spring是如何創(chuàng)建代理類的?
//方法的實現(xiàn)在AbstractAutoProxyCreator.java
protected Object createProxy(Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
if(this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)this.beanFactory, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if(!proxyFactory.isProxyTargetClass()) {
if(this.shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
} else {
this.evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = this.buildAdvisors(beanName, specificInterceptors);
Advisor[] var7 = advisors;
int var8 = advisors.length;
for(int var9 = 0; var9 < var8; ++var9) {
Advisor advisor = var7[var9];
proxyFactory.addAdvisor(advisor);
}
proxyFactory.setTargetSource(targetSource);
this.customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if(this.advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
return proxyFactory.getProxy(this.getProxyClassLoader());
}
//方法實現(xiàn)在ProxyFactory.java
public Object getProxy(ClassLoader classLoader) {
return this.createAopProxy().getProxy(classLoader);
}
//方法實現(xiàn)在ProxyCreatorSupport.java中
protected final synchronized AopProxy createAopProxy() {
if(!this.active) {
this.activate();
}
return this.getAopProxyFactory().createAopProxy(this);
}
//方法實現(xiàn)在DefaultAopProxyFactory中
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if(!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class<?> targetClass = config.getTargetClass();
if(targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
} else {
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass)?new ObjenesisCglibAopProxy(config):new JdkDynamicAopProxy(config));
}
}
}
經(jīng)歷了多個方法間的調(diào)用医清,我們終于看到了關(guān)注了代碼起暮。在DefaultAopProxyFactory的方法createAopProxy中,我們看到了Spring 是如何選擇JDK和CGLIB兩種動態(tài)代理機制的:
- 如果目標(biāo)對象實現(xiàn)了接口会烙,默認(rèn)情況下會采用JDK的動態(tài)代理實現(xiàn)AOP;
- 如果目標(biāo)對象實現(xiàn)了接口负懦,可以強制使用CGLIB實現(xiàn)AOP(proxy-target-class為true 或 Optimize為true即可筒捺,optimize是CGLIB中的獨有配置項),但是需要保證targetClass不是接口纸厉,并且targetClass不是jdk動態(tài)代理生成的類;
- 如果目標(biāo)對象沒有實現(xiàn)接口系吭,必須采用CGLIB庫;
默認(rèn)情況下颗品,Spring會使用JDK動態(tài)代理肯尺,但是也會根據(jù)實際情況在兩者之間切換。
確定使用哪種動態(tài)機制后抛猫,就可以創(chuàng)建目標(biāo)類的代理了蟆盹。至此 Spring AOP的基本原理就介紹完畢了。
3. 使用場景
了解了Spring AOP的使用示例及基本原理后闺金,我們一塊看兩種Spring AOP的應(yīng)用場景。
(1)增加統(tǒng)一日志
在第一節(jié)使用示例中峰档,為我們展示在方法調(diào)用前增加日志打印败匹。在Web開發(fā)中,我們可以實現(xiàn)Controller層或者Service統(tǒng)一日志打印讥巡,避免重復(fù)性日志打印代碼掀亩。
(2)動態(tài)切換數(shù)據(jù)源
Spring對于多數(shù)據(jù)源有很好的支持。在Spring中欢顷,我們可以通過繼承AbstractRoutingDataSource實現(xiàn)在程序運行時動態(tài)選擇數(shù)據(jù)源槽棍。具體實現(xiàn)方案可以查看spring 動態(tài)切換數(shù)據(jù)源 多數(shù)據(jù)庫
參考:
《Spring實戰(zhàn)》 第三版
《Spring源碼深度解析》
https://docs.spring.io/spring-framework/docs/current/javadoc-api/overview-summary.html