springboot aop的使用 學習總結(二)

版權聲明:本文爲博主武偉峯原創文章,轉載請註明地址http://blog.csdn.net/tianyaleixiaowu。

aop是spring的兩大功能模塊之一,功能非常強大,爲解耦提供了非常優秀的解決方案。

現在就以springboot中aop的使用來了解一下aop。

一:使用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>com.example</groupId>  
    <artifactId>testaop</artifactId>  
    <version>0.0.1-SNAPSHOT</version>  
    <packaging>jar</packaging>  
  
    <name>testaop</name>  
    <description>Demo project for Spring Boot</description>  
  
    <parent>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-parent</artifactId>  
        <version>1.5.9.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-aop</artifactId>  
        </dependency>  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-web</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>  

創建個controller

package com.example.controller;  
  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
/** 
 * Created by wuwf on 17/4/27. 
 * 
 */  
@RestController  
public class FirstController {  
  
    @RequestMapping("/first")  
    public Object first() {  
        return "first controller";  
    }  
  
    @RequestMapping("/doError")  
    public Object error() {  
        return 1 / 0;  
    }  
}  

創建一個aspect切面類

package com.example.aop;  
  
import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.ProceedingJoinPoint;  
import org.aspectj.lang.annotation.*;  
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 wuwf on 17/4/27. 
 * 日誌切面 
 */  
@Aspect  
@Component  
public class LogAspect {  
    @Pointcut("execution(public * com.example.controller.*.*(..))")  
    public void webLog(){}  
  
    @Before("webLog()")  
    public void deBefore(JoinPoint joinPoint) throws Throwable {  
        // 接收到請求,記錄請求內容  
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
        HttpServletRequest request = attributes.getRequest();  
        // 記錄下請求內容  
        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()));  
  
    }  
  
    @AfterReturning(returning = "ret", pointcut = "webLog()")  
    public void doAfterReturning(Object ret) throws Throwable {  
        // 處理完請求,返回內容  
        System.out.println("方法的返回值 : " + ret);  
    }  
  
    //後置異常通知  
    @AfterThrowing("webLog()")  
    public void throwss(JoinPoint jp){  
        System.out.println("方法異常時執行.....");  
    }  
  
    //後置最終通知,final增強,不管是拋出異常或者正常退出都會執行  
    @After("webLog()")  
    public void after(JoinPoint jp){  
        System.out.println("方法最後執行.....");  
    }  
  
    //環繞通知,環繞增強,相當於MethodInterceptor  
    @Around("webLog()")  
    public Object arround(ProceedingJoinPoint pjp) {  
        System.out.println("方法環繞start.....");  
        try {  
            Object o =  pjp.proceed();  
            System.out.println("方法環繞proceed,結果是 :" + o);  
            return o;  
        } catch (Throwable e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}  

啓動項目

模擬正常執行的情況,訪問http://localhost:8080/first,看控制檯結果:

 

方法環繞start.....
URL : http://localhost:8080/first
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.first
ARGS : []
方法環繞proceed,結果是 :first controller
方法最後執行.....
方法的返回值 : first controller

/****************************分割線****************************/

模擬出現異常時的情況,訪問http://localhost:8080/doError,看控制檯結果:
方法環繞start.....
URL : http://localhost:8080/doError
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.error
ARGS : []
java.lang.ArithmeticException: / by zero

......

方法最後執行.....
方法的返回值 : null

/****************************分割線****************************/

通過上面的簡單的例子,可以看到aop的執行順序。知道了順序後,就可以在相應的位置做切面處理了。

二: 切面方法說明

@Aspect

作用是把當前類標識爲一個切面供容器讀取

@Before
標識一個前置增強方法,相當於BeforeAdvice的功能

@AfterReturning

後置增強,相當於AfterReturningAdvice,方法退出時執行

@AfterThrowing

異常拋出增強,相當於ThrowsAdvice

@After

final增強,不管是拋出異常或者正常退出都會執行

@Around

環繞增強,相當於MethodInterceptor

/****************************分割線****************************/

各方法參數說明:

除了@Around外,每個方法裏都可以加或者不加參數JoinPoint,如果有用JoinPoint的地方就加,不加也可以,JoinPoint裏包含了類名、被切面的方法名,參數等屬性,可供讀取使用。@Around參數必須爲ProceedingJoinPoint,pjp.proceed相應於執行被切面的方法。@AfterReturning方法裏,可以加returning = “XXX”,XXX即爲在controller裏方法的返回值,本例中的返回值是“first controller”。@AfterThrowing方法裏,可以加throwing = "XXX",供讀取異常信息,如本例中可以改爲:

//後置異常通知  
    @AfterThrowing(throwing = "ex", pointcut = "webLog()")  
    public void throwss(JoinPoint jp, Exception ex){  
        System.out.println("方法異常時執行.....");  
    }  

一般常用的有before和afterReturn組合,或者單獨使用Around,即可獲取方法開始前和結束後的切面。

三:關於切面PointCut的切入點

轉載請註明來源-作者@loongshawn:http://blog.csdn.net/loongshawn/article/details/72303040

1.execution表達式

<aop:config>
        <aop:pointcut id="pointcut" expression="execution(* com.loongshawn.method.ces..*.*    
                                                          (..))" />
        <aop:aspect ref="logAspect">
            <aop:before pointcut-ref="pointcut" method="beforeAdvice"/>
            <aop:after-returning pointcut-ref="pointcut" arg-names="joinPoint,retValue" 
                 returning="retValue" method="afterAdvice"/>
        </aop:aspect>
</aop:config>
  • 上述配置爲AOP配置代碼片段,其中expression部分爲定義切點的表達式部分,如下:

execution(* com.loongshawn.method.ces..*.*(..))

  注意:markdown中符號“*”是加粗,因此輸出“*”符號需要進行轉義“*”。

  表達式結構解釋如下:

標識符 含義
execution() 表達式的主體
第一個“*”符號 表示返回值的類型任意
com.loongshawn.method.ces AOP所切的服務的包名,即,需要進行橫切的業務類
包名後面的“..” 表示當前包及子包
第二個“*” 表示類名,*即所有類
.*(..) 表示任何方法名,括號表示參數,兩個點表示任何參數類型

2.官方文檔介紹

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) 
throws-pattern?)

  上述表達式結構是Spring官方文檔說明,翻譯爲中文如下,其中除了返回類型模式、方法名模式和參數模式外,其它項都是可選的。

execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數模式>)<異常模式>?)

  接下來,結合官方文檔中的例子從不同的維度來了解execution表達式。表達式上方的中文說明爲對該表達式的解釋,請注意閱讀方式。

1.通過方法修飾符定義切點

  匹配所有的public修飾符的方法:

execution(public * *(..))

2.通過方法名定義切點

  匹配所有”set”開頭的方法:

execution(* set*(..))

3.通過類定義切點

  匹配AccountService 接口的所有方法:

execution(* com.xyz.service.AccountService.*(..))

4.通過包定義切點

  匹配service包中的所有方法:

execution(* com.xyz.service..(..))

  匹配service包及其子包中的所有方法:

execution(* com.xyz.service...(..))

5.通過方法入參定義切點

  切點表達式中方法入參部分比較複雜,可以使用”*”和“..”通配符,其中“*”表示任意類型的參數,而“..”表示任意類型參數且參數個數不限。

  匹 配joke(String,int)方法,且joke()方法的第一個入參是String,第二個入參是int。如果方法中的入參類型是Java.lang包下的類,可以直接使用類名,否則必須使用全限定類名,如joke(java.util.List,int);

execution(* joke(String,int))

  匹 配目標類中的joke()方法,該方法第一個入參爲String,第二個入參可以是任意類型,如joke(Strings1,String s2)和joke(String s1,double d2)都匹配,但joke(String s1,doubled2,String s3)則不匹配;

execution(* joke(String,*))

  匹配目標類中的joke()方法,該方法第 一個入參爲String,後面可以有任意個入參且入參類型不限,如joke(Strings1)、joke(String s1,String s2)和joke(String s1,double d2,Strings3)都匹配。

execution(* joke(String,..))

  匹 配目標類中的joke()方法,方法擁有一個入參,且入參是Object類型或該類的子類。它匹配joke(Strings1)和joke(Client c)。如果我們定義的切點是execution(*joke(Object)),則只匹配joke(Object object)而不匹配joke(Stringcc)或joke(Client c)。

execution(* joke(Object+))

3.官方文檔截圖

Aspect Oriented Programming with Spring–>11.2.3 Declaring a pointcut 
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

這裏寫圖片描述

本文僅對官方文檔做了翻譯及簡要說明,需要在實際使用過程中加深印象及理解。

四:自定義註解

一般多用於某些特定的功能,比較零散的切面,譬如特定的某些方法需要處理,就可以單獨在方法上加註解切面。

我們來自定義一個註解:

package com.example.aop;  
  
import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
  
/** 
 * Created by wuwf on 17/4/27. 
 */  
@Target({ElementType.METHOD, ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface UserAccess {  
    String desc() default "無信息";  
}  

註解裏提供了一個desc的方法,供被切面的地方傳參,如果不需要傳參可以不寫。

在Controller里加個方法

@RequestMapping("/second")  
    @UserAccess(desc = "second")  
    public Object second() {  
        return "second controller";  
    }  

切面類:

package com.example.aop;  
  
import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.ProceedingJoinPoint;  
import org.aspectj.lang.annotation.*;  
import org.springframework.stereotype.Component;  
  
/** 
 * Created by wuwf on 17/4/27. 
 */  
@Component  
@Aspect  
public class UserAccessAspect {  
  
    @Pointcut(value = "@annotation(com.example.aop.UserAccess)")  
    public void access() {  
  
    }  
  
    @Before("access()")  
    public void deBefore(JoinPoint joinPoint) throws Throwable {  
        System.out.println("second before");  
    }  
  
    @Around("@annotation(userAccess)")  
    public Object around(ProceedingJoinPoint pjp, UserAccess userAccess) {  
        //獲取註解裏的值  
        System.out.println("second around:" + userAccess.desc());  
        try {  
            return pjp.proceed();  
        } catch (Throwable throwable) {  
            throwable.printStackTrace();  
            return null;  
        }  
    }  
}  

主要看一下@Around註解這裏,如果需要獲取在controller註解中賦給UserAccess的desc裏的值,就需要這種寫法,這樣UserAccess參數就有值了。

/****************************分割線****************************/

啓動項目,訪問http://localhost:8080/second,看控制檯:

 

方法環繞start.....
URL : http://localhost:8080/second
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.second
ARGS : []
second around:second
second before
方法環繞proceed,結果是 :second controller
方法最後執行.....
方法的返回值 : second controller

/****************************分割線****************************/

通知結果可以看到,兩個aop切面類都工作了,順序呢就是下面的

 

spring aop就是一個同心圓,要執行的方法爲圓心,最外層的order最小。從最外層按照AOP1、AOP2的順序依次執行doAround方法,doBefore方法。然後執行method方法,最後按照AOP2、AOP1的順序依次執行doAfter、doAfterReturn方法。也就是說對多個AOP來說,先before的,一定後after。
對於上面的例子就是,先外層的就是對所有controller的切面,內層就是自定義註解的。
那不同的切面,順序怎麼決定呢,尤其是同格式的切面處理,譬如兩個execution的情況,那spring就是隨機決定哪個在外哪個在內了。
所以大部分情況下,我們需要指定順序,最簡單的方式就是在Aspect切面類上加上@Order(1)註解即可,order越小最先執行,也就是位於最外層。像一些全局處理的就可以把order設小一點,具體到某個細節的就設大一點。

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