spring Aop

一、AOP的基本概念

Spring 框架的一個關鍵組件是面向方面的編程(AOP)框架。面向切面的編程需要把程序邏輯分解成不同的部分稱爲所謂的關注點。跨一個應用程序的多個點的功能被稱爲橫切關注點,這些橫切關注點在概念上獨立於應用程序的業務邏輯。有各種各樣的常見的很好的方面的例子,如日誌記錄、審計、聲明式事務、安全性和緩存等。

在 OOP 中,關鍵單元模塊度是類,而在 AOP 中單元模塊度是切面。依賴注入幫助你對應用程序對象相互解耦和 AOP 可以幫助你從它們所影響的對象中對橫切關注點解耦。AOP 是像編程語言的觸發物,如 Perl,.NET,Java 或者其他。

Spring AOP 模塊提供攔截器來攔截一個應用程序,例如,當執行一個方法時,你可以在方法執行之前或之後添加額外的功能。

什麼是AOP?

 AOP即Aspect-Oriented Programming的縮寫,中文意思是面向切面(或方面)編程。它是一種思想,可在不改變程序源碼的情況下爲程序添加額外的功能;

AOP的發展階段?

  • 靜態AOP:Aspect形式,通過特定的編譯器,將實現後的Aspect編譯並織入到系統的靜態類中;
  • 動態AOP:AOP的織入過程在系統運行開始之後進行,而不是預先編譯到系統中;

AOP的設計意圖?

允許通過分離應用的業務邏輯系統級服務進行內聚性的開發。應用對象只實現業務邏輯即可,並不負責其它的系統級關注點;

AOP的常見使用?

日誌記錄、跟蹤、監控和優化,性能統計、優化,安全、權限控制,應用系統的異常捕捉及處理,事務處理,緩存,持久化,懶加載(Lazy loading),內容傳遞,調試,資源池,同步等等,都可以使用AOP來完成。

AOP的實現(簡介)

  • AspectJ: AspectJ是目前最完善的AOP語言,對Java編程語言的進行了擴展,定義了AOP語法,能夠在編譯期提供橫切代碼的織入。AspectJ提供了兩種橫切實現機制,一種稱爲動態橫切(Dynamic Crosscutting),另一種稱爲靜態橫切(Static Crosscutting)。
  • AspectWerkz: 基於Java的簡單、動態和輕量級的AOP框架,支持運行期或類裝載期織入橫切代碼,它擁有一個特殊的類裝載器。它與AspectJ項目已經合併,第一個發佈版本是AspectJ5:擴展AspectJ語言,以基於註解的方式支持類似AspectJ的代碼風格。
  • JBoss AOP: JBoss是一個開源的符合J2EE規範的應用服務器,作爲J2EE規範的補充,JBoss中引入了AOP框架,爲普通Java類提供了J2EE服務,而無需遵循EJB規範。JBoss通過類載入時,使用Javassist框架對字節碼操作實現動態AOP框架。
  • Spring AOP: Spring AOP使用純Java實現,不需要專門的編譯過程,不需要特殊的類裝載器,它在運行期通過代理方式向目標類織入增強代碼。Spring並不嘗試提供最完整的AOP實現,主要側重於提供一種和Spring IoC容器整合的AOP實現,以解決企業級開發中常見問題。

  我們這裏關注於Spring AOP

AOP中的常見概念

  • 連接點(Join Point):程序執行的某個位置,比如類初始化前,類初始化後,方法調用前,方法調用後等等。
  • 切點(Point cut): 通過切點來定位特定的連接點, 這是一組一個或多個連接點,通知應該被執行。你可以使用表達式或模式指定切入點正如我們將在 AOP 的例子中看到的。。
  • 增強(Advice):織入到目標類連接點上的一段程序代碼。
  • 目標對象(Target Object): 增強邏輯的織入目標類。
  • 引介(Introduction):引介是一種特殊的增強,它爲類添加一些屬性和方法。
  • 織入(Weaving): 將增強添加對目標類的具體連接點上的過程。
  • 代理:一個類被AOP織入增強後,會產生一個結果類,該類融合了原類和增強邏輯的代理類。
  • 切面:由切點和增強組成,既包括了橫切邏輯的定義,也包括了連接點的定義。

因此,AOP的一般過程就是:

  向目標對象(Target Object)中
  選取Pointcut(由一系列的Join Point組成)
  織入Advice
  而切面就是Pointcut+Advice,這個Weaving(織入過程)中可能使用了引介,也可能生成了代理對象。

二、使用代理實現AOP

首先,我們使用jdk的原生api實現動態代理。

jdk的動態代理只能支持接口...,因此創建接口MyService

public interface MyService {
    public void hello();
}

創建切面接口MyAdvice,定義Pointcut爲方法執行前,和方法調用後

public interface MyAdvice {

    void beforeMethod(Method method);

    void afterMethod(Method method);
}

演示如下:

複製代碼
public class ProxyUtils {

    // 適用jdk原生的方式
    public static <T> T createProxy(final T target, final MyAdvice advice) {
        @SuppressWarnings("unchecked") /*jdk返回的一定可以轉換爲這個類型*/
                T result = (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                (Object proxy, Method method, Object[] args) -> {
                    advice.beforeMethod(method);
                    Object res = method.invoke(target, args);
                    advice.afterMethod(method);
                    return res;
                }
        );
        return result;
    }

    public static void main(String[] args) {
        //1. 創建原生對象
        MyService targetObject = () -> {
            System.out.println("hello....");
        };
        //2. 創建增強類,即Advice
        MyAdvice advice = new MyAdvice() {
            @Override
            public void beforeMethod(Method method) {
                System.out.println(String.format("before日誌: 執行了%s方法...", method.getName()));
            }

            @Override
            public void afterMethod(Method method) {
                System.out.println(String.format("after日誌:執行了%s方法...", method.getName()));
            }
        };
        //3. 創建代理Proxy
        MyService proxy = createProxy(targetObject, advice);
        //4. 執行方法
        proxy.hello();
    }
  
    //...
    
}
複製代碼

打印結果爲:

before日誌: 執行了hello方法...
hello
after日誌:執行了hello方法...

同樣,可以使用cglib的方式生成動態代理,cglib還支持繼承類的方式生成代理,代碼如下:

複製代碼
/**
     * <B>方法說明:</B>生成cglib代理類<BR>
     *
     * @param target 被代理對象
     * @param advice 切入邏輯
     * @return
     */
    public static <T> T createCglibProxy(final T target, final MyAdvice advice, final boolean extend) {
        Enhancer enhancer = new Enhancer();
        //使用接口或者使用父類
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object res = null;
                advice.beforeMethod(method);
                res = method.invoke(target, objects);
                advice.afterMethod(method);
                return res;
            }
        });
        //通過superClass或者interfaces來判斷如何實現
        if (extend) {
            enhancer.setSuperclass(target.getClass());
        } else {
            enhancer.setInterfaces(target.getClass().getInterfaces());
        }
        @SuppressWarnings("unchecked") //jdk返回的一定可以轉換爲這個類型
                T result = (T) enhancer.create();
        return result;
    }
複製代碼

前面提到JBoss AOP是使用了javasist這個框架

複製代碼
 /**
     * javassist利用工廠創建動態代理
     */
    public static <T> T createJavassitProxy(final T target,final MyAdvice advice) {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setSuperclass(target.getClass());
        Class<?> clz = proxyFactory.createClass();
        Object proxy = null;
        try {
            proxy= clz.newInstance();
            ((ProxyObject)proxy).setHandler(new MethodHandler() {
                @Override
                public Object invoke(Object o, Method method, Method method1, Object[] objects) throws Throwable {
                    advice.beforeMethod(method);
                    Object res = method.invoke(target, objects);
                    advice.afterMethod(method);
                    return res;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T) proxy;
    }
複製代碼

三、Spring boot中使用AOP

spring boot應該會成爲主流,因此這裏不考慮使用xml的方式進行aop配置,而是使用註解的方式。

文件圖:

 

pom文件:

複製代碼
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>demo</groupId>
    <artifactId>demo.aop</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>aop</name>
    <description>aop</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
複製代碼

切面類:

複製代碼
package demo.aop.advice;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * Created by carl.yu on 2016/12/9.
 */
@Aspect
@Component
public class WebLogAspect {
    //1. demo.aop.controller..* 表示demo.aop.controller下任何子包中的任何類
    //2. demo.aop.controller..*後面的.* 表示任何方法名
    //3. (..)表示任何參數的方法
    //4. public * 表示public修飾的方,返回參數爲任何
    @Pointcut("execution(public * demo.aop.controller..*.*(..))")
    public void webLog() {
    }

    //環繞切面配置
    @Around("webLog()")
    public Object printAroundAdvice(ProceedingJoinPoint joinPoint)
            throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        long start = System.currentTimeMillis();
        HttpServletRequest request = attributes.getRequest();
        System.out.println("-----------------------------");
        System.out.println("URL : " + request.getRequestURL().toString());
        System.out.println("HTTP_METHOD : " + request.getMethod());
        System.out.println("IP : " + request.getRemoteAddr());
        System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));
        Object result = joinPoint.proceed();
        System.out.println("Spend time:" + (System.currentTimeMillis() - start));
        return result;
    }
}
複製代碼

測試Controller

複製代碼
@RestController
public class HelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello() {
        return "Hello";
    }

    @RequestMapping(value = "/welcome", method = RequestMethod.GET)
    public String welcome() {
        return "welcome";
    }
}
複製代碼

訪問請求,結果如下所示:

複製代碼
URL : http://localhost:8080/hello
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : demo.aop.controller.HelloController.hello
ARGS : []
Spend time:6
-----------------------------
URL : http://localhost:8080/welcome
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : demo.aop.controller.HelloController.welcome
ARGS : []
Spend time:0
複製代碼

上面使用了execution的表達式,spring aop支持表達式非常多,這裏簡單介紹幾種:

  • execution - 匹配方法執行的連接點,這是你將會用到的Spring的最主要的切入點指定者。

  • within - 限定匹配特定類型的連接點(在使用Spring AOP的時候,在匹配的類型中定義的方法的執行)。

  • this - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中bean reference(Spring AOP 代理)是指定類型的實例。

  • target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中目標對象(被代理的appolication object)是指定類型的實例。

  • args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中參數是指定類型的實例。

  • @target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中執行的對象的類已經有指定類型的註解。

  • @args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中實際傳入參數的運行時類型有指定類型的註解。

  • @within - 限定匹配特定的連接點,其中連接點所在類型已指定註解(在使用Spring AOP的時候,所執行的方法所在類型已指定註解)。

  • @annotation - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中連接點的主題有某種給定的註解。

具體使用方式請參照官方文檔。

四、Spring aop中Advice類型以及順序問題

AspectJ提供了五種定義通知的標註:

  • @Before:前置通知,在調用目標方法之前執行通知定義的任務
  • @After:後置通知,在目標方法執行結束後,無論執行結果如何都執行通知定義的任務
  • @After-returning:後置通知,在目標方法執行結束後,如果執行成功,則執行通知定義的任務
  • @After-throwing:異常通知,如果目標方法執行過程中拋出異常,則執行通知定義的任務
  • @Around:環繞通知,在目標方法執行前和執行後,都需要執行通知定義的任務

 如果我們有多個通知想要在同一連接點執行,那執行順序如何確定呢?Spring AOP使用AspectJ的優先級規則來確定通知執行順序。總共有兩種情況:同一切面中通知執行順序、不同切面中的通知執行順序。

在同一個切面中:

而如果在同一切面中定義兩個相同類型通知(如同是前置通知或環繞通知(proceed之前))並在同一連接點執行時,其執行順序是未知的,如果確實需要指定執行順序需要將通知重構到兩個切面,然後定義切面的執行順序。

不同的切面中的話則可以通過Order定義順序,Order越小,則優先級越高。

定義方式:

五、Spring aop原理淺析

Spring的aop框架,一般而言有三種方式:

• Using ProxyFactoryBean: In Spring AOP, ProxyFactoryBean provides a declarative way for configuring Spring’s ApplicationContext (and hence the underlying BeanFactory) in creating AOP proxies based on defined Spring beans.
• Using the Spring aop namespace: Introduced in Spring 2.0, the aop namespace provides a simplified way (when compared to ProxyFactoryBean) for defining aspects and their DI requirements in Spring applications. However, the aop namespace also uses ProxyFactoryBean behind the scenes.
• Using @AspectJ-style annotations: Besides the XML-based aop namespace, you can use the @AspectJ-style annotations within your classes for configuring Spring AOP. Although the syntax it uses is based on AspectJ and you need to include some AspectJ libraries when using this option, Spring still uses the proxy mechanism (that is, creates proxied objects for the targets) when bootstrapping ApplicationContext

上面的例子中主要都是使用了第三種方式:AsepctJ風格的註解。因此,這裏也主要介紹這種方式的實現。

首先,根據配置會調用AspectJAutoProxyRegistrar 根據@EnableAspectJAutoProxy或<aop:aspectj-autoproxy/>註冊AnnotationAwareAspectJAutoProxyCreator.

複製代碼
@Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
        }
        if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
        }
    }
複製代碼

該方法主要是根據配置或者註解來註冊proxy代理創建的一些規則

而創建代理使用的是AnnotationAwareAspectJAutoProxyCreator

類圖如下所示:

 

 

 

 真正創建proxy的父類是AbstractAutoProxyCreatorcreateProxy方法..

複製代碼
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());
    }
複製代碼

可以發現,這裏已經開始創建Advisor,你定義的Advice在這裏會被創建並且加入, debug截圖如下:

最後proxyFactory.getProxy()方法會調用DefaultAopProxyFactory的createAopProxy方法

複製代碼
@Override
    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);
        }
    }
複製代碼

從而生成代理對象...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章