Spring AOP

1、AOP concepts(AOP術語)

  • Aspect/Advisors(切面)
    一個關注點的模塊化,這個關注點可能會橫切多個對象。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式來實現。

  • Join point(連接點)
    在程序執行期間的一點。在Spring AOP中,連接點總是表示方法執行。

  • Advice(通知)
    在切面的某個特定的連接點上執行的動作。許多AOP框架(包括Spring)都是以攔截器做通知模型,并維護一個以連接點為中心的攔截器鏈。

  • Pointcut(切入點)
    查找連接點的條件。通知和一個切入點表達式關聯,并在滿足這個切入點的連接點上運行。

  • Introduction(引入)
    給一個類型聲明額外的方法或屬性。Spring允許引入新的接口(以及一個對應的實現)到任何被代理的對象。

  • Target object(目標對象)
    被一個或者多個切面所通知的對象。也被稱做被通知(advised)對象。 既然Spring AOP是通過運行時代理實現的,這個對象永遠是一個被代理(proxied)對象。

  • AOP proxy
    AOP框架創建的對象,用來實現切面契約(例如通知方法執行等等)。在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。

  • Weaving(織入)
    織入是一個過程,是將切面應用到目標對象從而創建出AOP代理對象的過程,織入可以在編譯期、類裝載期、運行期進行。

1.1 通知類型

  • Before advice(前置通知):在某連接點之前執行的通知,但這個通知不能阻止連接點之前的執行流程(除非它拋出一個異常)。

  • After returning advice(后置通知):在某連接點正常完成后執行的通知:例如,一個方法沒有拋出任何異常,正常返回。

  • After throwing advice(異常通知):在方法拋出異常退出時執行的通知。

  • After (finally) advice(最終通知):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。

  • Around Advice(環繞通知):包圍一個連接點的通知,如方法調用。這是最強大的一種通知類型。環繞通知可以在方法調用前后完成自定義的行為。它也會選擇是否繼續執行連接點或直接返回它自己的返回值或拋出異常來結束執行。

2、Spring AOP

1、Spring AOP使用純Java實現,它不需要專門的編譯過程。Spring AOP不需要控制類加載器層次結構,因此適用于Servlet容器或應用程序服務器。

2、Spring AOP目前僅支持方法執行連接點。

3、Spring實現AOP的方法跟其他的框架不同。Spring并不是要提供最完整的AOP實現(盡管Spring AOP有這個能力),相反的,它其實側重于提供一種AOP實現和Spring IoC容器之間的整合,用于幫助解決在企業級開發中的常見問題。

4、Spring AOP從來沒有打算通過提供一種全面的AOP解決方案來與AspectJ競爭。我們相信無論是基于代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是很有價值的,他們之間應該是互補而不是競爭的關系。

2.1、Spring AOP基于XML的應用程序

1、Jar包依賴

<dependencies>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    </dependencies> 

2、定義切面和需要被攔截的對象

public class Student {

    private Integer age;
    private String name;

    public void setAge(Integer age) {
        this.age = age;
    }
    public Integer getAge() {
        System.out.println("Age : " + age );
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        System.out.println("Name : " + name );
        return name;
    }

    public void printThrowException(){
        System.out.println("Exception raised");
        throw new IllegalArgumentException();
    }
}
public class Logging {

    public void beforeAdvice(){
        System.out.println("beforeAdvice.");
    }

    public void afterAdvice(){
        System.out.println("afterAdvice");
    }

    public void afterReturningAdvice(Object retVal){
        System.out.println("afterReturningAdvice:" + retVal.toString() );
    }

    public void afterThrowingAdvice(IllegalArgumentException ex){
        System.out.println("afterThrowingAdvice: " + ex.toString());
    }

}

3、配置XML

    <aop:config>
        <aop:aspect id="log" ref="logging">
            <aop:pointcut id="all" expression="execution(* com.codersm.study.spring.aop.*.*(..))"/>
            <aop:before method="beforeAdvice" pointcut-ref="all"/>
            <aop:after method="afterAdvice" pointcut-ref="all"/>
            <aop:after-returning method="afterReturningAdvice" pointcut-ref="all" returning="retVal"/>
            <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="all" throwing="ex"/>
        </aop:aspect>
    </aop:config>

    <bean id="student" class="com.codersm.study.spring.aop.Student">
        <property name="name" value="zhangsan"/>
        <property name="age" value="21"/>
    </bean>

    <bean id="logging" class="com.codersm.study.spring.aop.Logging"/>

4、測試

ApplicationContext context = 
    new ClassPathXmlApplicationContext("classpath:spring-aop.xml");
    Student student = (Student) context.getBean("student");
    student.getAge();

2.3、Spring AOP基于@Aspect的應用程序

1、定義切面

@Aspect
@Component
public class LoggingAspect {

    /**
     * 單獨定義切入點,可復用
     */
    @Pointcut("execution(* com.codersm.study.spring.aop.*.*(..))")
    public void pointcut() {
    }

    @Before(value = "pointcut()")
    public void before() {
        System.out.println("Before advice");
    }


    @Around(value = "pointcut()")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("Around advice begin");
        Object ret = proceedingJoinPoint.proceed();
        System.out.println("Around advice end,execute method result is " + ret);
    }

    @After(value = "pointcut()")
    public void after() {
        System.out.println("After advice");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("afterThrowing advice exception is " + ex);
    }

    @AfterReturning(value = "pointcut()", returning = "ret")
    public void AfterReturning(Object ret) {
        System.out.println("AfterReturning advice result is :" + ret);
    }
}

2、配置xml文件開啟@Aspect

 <context:component-scan base-package="com.codersm.study.spring.*"/>
 <aop:aspectj-autoproxy/>

3、測試

 ApplicationContext applicationContext = null;

    @Before
    public void before() {
        applicationContext = 
            new ClassPathXmlApplicationContext("classpath:spring-aop-annotation.xml");
    }

    @Test
    public void testAop() {
        Student student = (Student) applicationContext.getBean("student");
        student.setName("hello world");
        System.out.println("---------------------------------");
        student.printThrowException();
    }

2.4、 通知類型小結

通知 描述
前置通知 權限控制(少用)
后置通知 少用
環繞通知 權限控制/性能監控/緩存實現/事務管理
異常通知 發生異常后,記錄錯誤日志
最終通知 釋放資源

3、獲取通知參數

  • 任何通知聲明JoinPoint作為通知方法第一個參數,JoinPoint提供一些有用的方法。
    around advice is required to declare a first parameter of type ProceedingJoinPoint, which is a subclass of JoinPoint.

** ProceedingJoinPoint is only supported for around advice.**

  • 傳遞參數給通知
    To make argument values available to the advice body, you can use the binding form of args.
@Before("execution(* com.codersm.study.spring.aop.*.*(..)) && args(name,..)")
public void before(String name) {
        System.out.println("Before advice,name = " + name);
}

另外一種定義方式:

@Pointcut("execution(* com.codersm.study.spring.aop.*.*(..))  && args(name,..)")
public void pointcut(String name) {
}
@Before(value = "pointcut(name)")
public void before(String name) {
       System.out.println("Before advice,name = " + name);
}

4、AOP proxies

4.1、 AOP介紹

Spring AOP使用JDK動態代理或CGLIB創建目標類的代理對象,如果目標類實現了至少一個接口,則使用JDK動態代理;否則,使用CGLIB代理。如果強制使用CGLIB代理,需要考慮這些問題:

  • final methods cannot be advised, as they cannot be overridden.
  • As of Spring 3.2, it is no longer necessary to add CGLIB to your project classpath, as CGLIB classes are repackaged under org.springframework and included directly in the spring-core JAR. This means that CGLIB-based proxy support 'just works' in the same way that JDK dynamic proxies always have.
  • As of Spring 4.0, the constructor of your proxied object will NOT be called twice anymore since the CGLIB proxy instance will be created via Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.

4.2、理解AOP代理

any method calls that it may make on itself, such as this.bar() or this.foo(), are going to be invoked against the this reference, and not the proxy. This has important implications. It means that self-invocation is not going to result in the advice associated with a method invocation getting a chance to execute.

solution

  • refactor your code such that the self-invocation does not happen.
  • You can (choke!) totally tie the logic within your class to Spring AOP by doing this
public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}
 public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.adddInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }

5、AOP源碼分析

spring.handlers

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler
public class AopNamespaceHandler extends NamespaceHandlerSupport {

    /**
     * Register the {@link BeanDefinitionParser BeanDefinitionParsers} for the
     * '{@code config}', '{@code spring-configured}', '{@code aspectj-autoproxy}'
     * and '{@code scoped-proxy}' tags.
     */
    @Override
    public void init() {
        // In 2.0 XSD as well as in 2.1 XSD.
        registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
        registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
        registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());

        // Only in 2.0 XSD: moved to context namespace as of 2.1
        registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
    }

}

ConfigBeanDefinitionParser.parse( )方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {
        CompositeComponentDefinition compositeDef =
                new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element));
        parserContext.pushContainingComponent(compositeDef);

        configureAutoProxyCreator(parserContext, element);

        List<Element> childElts = DomUtils.getChildElements(element);
        for (Element elt: childElts) {
            String localName = parserContext.getDelegate().getLocalName(elt);
            if (POINTCUT.equals(localName)) {
                parsePointcut(elt, parserContext);
            }
            else if (ADVISOR.equals(localName)) {
                parseAdvisor(elt, parserContext);
            }
            else if (ASPECT.equals(localName)) {
                parseAspect(elt, parserContext);
            }
        }

        parserContext.popAndRegisterContainingComponent();
        return null;
    }

configureAutoProxyCreator(parserContext, element)

private void configureAutoProxyCreator(ParserContext parserContext, Element element) {
        AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(parserContext, element);
    }
    public static void registerAspectJAutoProxyCreatorIfNecessary(
            ParserContext parserContext, Element sourceElement) {

        BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAutoProxyCreatorIfNecessary(
                parserContext.getRegistry(), parserContext.extractSource(sourceElement));
        useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
        registerComponentIfNecessary(beanDefinition, parserContext);
    }

Spring對XML文件解析

/**
     * Parse the elements at the root level in the document:
     * "import", "alias", "bean".
     * @param root the DOM root element of the document
     */
    protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    Element ele = (Element) node;
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }
public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
        return registerOrEscalateApcAsRequired(AspectJAwareAdvisorAutoProxyCreator.class, registry, source);
    }

AspectJAwareAdvisorAutoProxyCreator


AspectJAwareAdvisorAutoProxyCreator層次結構.png
public interface BeanPostProcessor {

    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

BeanPostProcessor接口定義回調方法,允許修改新的實例化Bean,例如檢查標記接口或用代理進行包裝。

  • postProcessBeforeInitialization
    在任何bean初始化回調(如InitializingBean的afterPropertiesSet或自定義init方法)之前,將此BeanPostProcessor應用于給定的新bean實例。
  • postProcessAfterInitialization
    在任何bean初始化回調之后,將此BeanPostProcessor應用于給定的新Bean實例(如InitializingBean的afterPropertiesSet或自定義init方法)。

ApplicationContext 會自動檢測由 BeanPostProcessor 接口的實現定義的 bean,注冊這些 bean 為后置處理器,然后通過在容器中創建 bean,在適當的時候調用它。

AspectJAwareAdvisorAutoProxyCreator這兩個方法的實現:

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.contains(cacheKey)) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }

繼續跟蹤源碼,發現了createProxy方法:

    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }

        // Create proxy if we have advice.
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }

        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

createProxy方法

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 (shouldProxyTargetClass(beanClass, beanName)) {
                proxyFactory.setProxyTargetClass(true);
            }
            else {
                evaluateProxyInterfaces(beanClass, proxyFactory);
            }
        }

        Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
        for (Advisor advisor : advisors) {
            proxyFactory.addAdvisor(advisor);
        }

        proxyFactory.setTargetSource(targetSource);
        customizeProxyFactory(proxyFactory);

        proxyFactory.setFrozen(this.freezeProxy);
        if (advisorsPreFiltered()) {
            proxyFactory.setPreFiltered(true);
        }

        return proxyFactory.getProxy(getProxyClassLoader());
    }

創建AopProxy代理對象,具體流程:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            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.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }

ObjenesisCglibAopProxy繼承CglibAopProxy。方法調用原理可以查看CglibAopProxy和JdkDynamicAopProxy。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容