SpringBoot AOP介紹


說起spring,我們知道其最核心的兩個功能就是AOP(面向切面)和IOC(控制反轉),這邊文章來總結一下SpringBoot如何整合使用AOP。

一、示例應用場景:對所有的web請求做切面來記錄日志。

1、pom中引入SpringBoot的web模塊和使用AOP相關的依賴:
==============================================

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.11</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1</version>
</dependency>
============================================================


其中:
cglib包是用來動態代理用的,基於類的代理;
aspectjrt和aspectjweaver是與aspectj相關的包,用來支持切面編程的;
aspectjrt包是aspectj的runtime包;
aspectjweaver是aspectj的織入包;

2、實現一個簡單的web請求入口(實現傳入name參數,返回“hello xxx”的功能):


注意:在完成了引入AOP依賴包后,一般來說並不需要去做其他配置。使用過Spring注解配置方式的人會問是否需要在程序主類中增加@EnableAspectJAutoProxy來啟用,實際並不需要。

因為在AOP的默認配置屬性中,spring.aop.auto屬性默認是開啟的,也就是說只要引入了AOP依賴后,默認已經增加了@EnableAspectJAutoProxy。

3、定義切面類,實現web層的日志切面

要想把一個類變成切面類,需要兩步,
① 在類上使用 @Component 注解 把切面類加入到IOC容器中
② 在類上使用 @Aspect 注解 使之成為切面類

package com.example.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 lmb on 2018/9/5.
*/
@Aspect
@Component
public class WebLogAcpect {

private Logger logger = LoggerFactory.getLogger(WebLogAcpect.class);

/**
* 定義切入點,切入點為com.example.aop下的所有函數
*/
@Pointcut("execution(public * com.example.aop..*.*(..))")
public void webLog(){}

/**
* 前置通知:在連接點之前執行的通知
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到請求,記錄請求內容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

// 記錄下請求內容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}

@AfterReturning(returning = "ret",pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 處理完請求,返回內容
logger.info("RESPONSE : " + ret);
}
}

以上的切面類通過 @Pointcut定義的切入點為com.example.aop包下的所有函數做切人,通過 @Before實現切入點的前置通知,通過 @AfterReturning記錄請求返回的對象。

 

二、AOP支持的通知
1、前置通知@Before:在某連接點之前執行的通知,除非拋出一個異常,否則這個通知不能阻止連接點之前的執行流程。

/**
* 前置通知,方法調用前被調用
* @param joinPoint/null
*/
@Before(value = POINT_CUT)
public void before(JoinPoint joinPoint){
logger.info("前置通知");
//獲取目標方法的參數信息
Object[] obj = joinPoint.getArgs();
//AOP代理類的信息
joinPoint.getThis();
//代理的目標對象
joinPoint.getTarget();
//用的最多 通知的簽名
Signature signature = joinPoint.getSignature();
//代理的是哪一個方法
logger.info("代理的是哪一個方法"+signature.getName());
//AOP代理類的名字
logger.info("AOP代理類的名字"+signature.getDeclaringTypeName());
//AOP代理類的類(class)信息
signature.getDeclaringType();
//獲取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//從獲取RequestAttributes中獲取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//如果要獲取Session信息的話,可以這樣寫:
//HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
//獲取請求參數
Enumeration<String> enumeration = request.getParameterNames();
Map<String,String> parameterMap = Maps.newHashMap();
while (enumeration.hasMoreElements()){
String parameter = enumeration.nextElement();
parameterMap.put(parameter,request.getParameter(parameter));
}
String str = JSON.toJSONString(parameterMap);
if(obj.length > 0) {
logger.info("請求的參數信息為:"+str);
}
}

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

環繞通知最強大,也最麻煩,是一個對方法的環繞,具體方法會通過代理傳遞到切面中去,切面中可選擇執行方法與否,執行幾次方法等。環繞通知使用一個代理ProceedingJoinPoint類型的對象來管理目標對象,所以此通知的第一個參數必須是ProceedingJoinPoint類型。在通知體內調用ProceedingJoinPoint的proceed()方法會導致后台的連接點方法執行。proceed()方法也可能會被調用並且傳入一個Object[]對象,該數組中的值將被作為方法執行時的入參。

/**
* 環繞通知:
* 環繞通知非常強大,可以決定目標方法是否執行,什么時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值。
* 環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型
*/
@Around(value = POINT_CUT)
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
logger.info("環繞通知的目標方法名:"+proceedingJoinPoint.getSignature().getName());
try {
Object obj = proceedingJoinPoint.proceed();
return obj;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}

6、有時候我們定義切面的時候,切面中需要使用到目標對象的某個參數,如何使切面能得到目標對象的參數呢?可以使用args來綁定。如果在一個args表達式中應該使用類型名字的地方使用一個參數名字,那么當通知執行的時候對象的參數值將會被傳遞進來。

@Before("execution(* findById*(..)) &&" + "args(id,..)")
public void twiceAsOld1(Long id){
System.err.println ("切面before執行了。。。。id==" + id);

}

注意:任何通知方法都可以將第一個參數定義為org.aspectj.lang.JoinPoint類型(環繞通知需要定義第一個參數為ProceedingJoinPoint類型,它是 JoinPoint 的一個子類)。JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法參數)、getThis()(返回代理對象)、getTarget()(返回目標)、getSignature()(返回正在被通知的方法相關信息)和 toString()(打印出正在被通知的方法的有用信息)。

三、切入點表達式
定義切入點的時候需要一個包含名字和任意參數的簽名,還有一個切入點表達式,如execution(public * com.example.aop...(..))

切入點表達式的格式:execution([可見性]返回類型[聲明類型].方法名(參數)[異常])
其中[]內的是可選的,其它的還支持通配符的使用:
1) *:匹配所有字符
2) ..:一般用於匹配多個包,多個參數
3) +:表示類及其子類
4)運算符有:&&,||,!

切入點表達式關鍵詞用例:
1)execution:用於匹配子表達式。
//匹配com.cjm.model包及其子包中所有類中的所有方法,返回類型任意,方法參數任意
@Pointcut(“execution(* com.cjm.model...(..))”)
public void before(){}

2)within:用於匹配連接點所在的Java類或者包。
//匹配Person類中的所有方法
@Pointcut(“within(com.cjm.model.Person)”)
public void before(){}
//匹配com.cjm包及其子包中所有類中的所有方法
@Pointcut(“within(com.cjm..*)”)
public void before(){}

3) this:用於向通知方法中傳入代理對象的引用。
@Before(“before() && this(proxy)”)
public void beforeAdvide(JoinPoint point, Object proxy){
//處理邏輯
}

4)target:用於向通知方法中傳入目標對象的引用。
@Before(“before() && target(target)
public void beforeAdvide(JoinPoint point, Object proxy){
//處理邏輯
}

5)args:用於將參數傳入到通知方法中。
@Before(“before() && args(age,username)”)
public void beforeAdvide(JoinPoint point, int age, String username){
//處理邏輯
}

6)@within :用於匹配在類一級使用了參數確定的注解的類,其所有方法都將被匹配。
@Pointcut(“@within(com.cjm.annotation.AdviceAnnotation)”)
- 所有被@AdviceAnnotation標注的類都將匹配
public void before(){}

7)@target :和@within的功能類似,但必須要指定注解接口的保留策略為RUNTIME。
@Pointcut(“@target(com.cjm.annotation.AdviceAnnotation)”)
public void before(){}

8)@args :傳入連接點的對象對應的Java類必須被@args指定的Annotation注解標注。
@Before(“@args(com.cjm.annotation.AdviceAnnotation)”)
public void beforeAdvide(JoinPoint point){
//處理邏輯
}

9)@annotation :匹配連接點被它參數指定的Annotation注解的方法。也就是說,所有被指定注解標注的方法都將匹配。
@Pointcut(“@annotation(com.cjm.annotation.AdviceAnnotation)”)
public void before(){}

10)bean:通過受管Bean的名字來限定連接點所在的Bean。該關鍵詞是Spring2.5新增的。
@Pointcut(“bean(person)”)
public void before(){}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM