如何使用Spring AOP及基本原理

閱讀本文,你將了解到如何使用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)行解析可柿。

  1. 當(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)行解析目锭。

  1. 現(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;
    }
  1. 我們特別注意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)生成代理類了寂汇。

  1. 什么是合適的時機呢病往?根據(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的功能臼膏。而代理類的生成也正式在此處硼被。

  2. 現(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ù)切面信息生成代理類仔掸。

  1. 我們再詳細(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抬驴,隨后出現(xiàn)的幾起案子炼七,更是在濱河造成了極大的恐慌,老刑警劉巖布持,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件豌拙,死亡現(xiàn)場離奇詭異,居然都是意外死亡题暖,警方通過查閱死者的電腦和手機按傅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胧卤,“玉大人唯绍,你說我怎么就攤上這事≈μ埽” “怎么了况芒?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長侧啼。 經(jīng)常有香客問我牛柒,道長堪簿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任皮壁,我火速辦了婚禮椭更,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蛾魄。我一直安慰自己虑瀑,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布滴须。 她就那樣靜靜地躺著舌狗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扔水。 梳的紋絲不亂的頭發(fā)上痛侍,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音魔市,去河邊找鬼主届。 笑死,一個胖子當(dāng)著我的面吹牛待德,可吹牛的內(nèi)容都是我干的君丁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼将宪,長吁一口氣:“原來是場噩夢啊……” “哼绘闷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起较坛,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤印蔗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后燎潮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喻鳄,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年确封,在試婚紗的時候發(fā)現(xiàn)自己被綠了除呵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡爪喘,死狀恐怖颜曾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秉剑,我是刑警寧澤泛豪,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響诡曙,放射性物質(zhì)發(fā)生泄漏臀叙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一价卤、第九天 我趴在偏房一處隱蔽的房頂上張望劝萤。 院中可真熱鬧,春花似錦慎璧、人聲如沸床嫌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厌处。三九已至,卻和暖如春岁疼,著一層夾襖步出監(jiān)牢的瞬間阔涉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工五续, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洒敏,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓疙驾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親郭毕。 傳聞我的和親對象是個殘疾皇子它碎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 本文是我自己在秋招復(fù)習(xí)時的讀書筆記,整理的知識點显押,也是為了防止忘記扳肛,尊重勞動成果,轉(zhuǎn)載注明出處哦乘碑!如果你也喜歡挖息,那...
    波波波先森閱讀 12,294評論 6 86
  • 本章內(nèi)容: 面向切面編程的基本原理 通過POJO創(chuàng)建切面 使用@AspectJ注解 為AspectJ切面注入依賴 ...
    謝隨安閱讀 3,149評論 0 9
  • 一、AOP的基礎(chǔ) 1.1兽肤、AOP是什么套腹??资铡? 考慮這樣一個問題:需要對系統(tǒng)中的某些業(yè)務(wù)做日志記錄电禀,比如支付系統(tǒng)中的...
    聶叼叼閱讀 2,116評論 2 17
  • 不要做壞事,你以為誰都傷害可以傷害你吧笤休?你吃的像烏龜里面的嫩肉一樣尖飞,你缺少一個殼,你連烏龜都不是 是這樣嗎?蝸牛之...
    不像話的故事閱讀 69評論 0 0
  • 我是日記星球125號星寶寶吳翊靈政基,正在參加孫老師的日記星球21天蛻變之旅和21天親子英語啟蒙的學(xué)習(xí)贞铣,這是我的第篇5...
    wu溧蕙閱讀 255評論 0 2