版權聲明:本文爲博主武偉峯原創文章,轉載請註明地址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設小一點,具體到某個細節的就設大一點。