前言

aop面向切面程式設計,是程式設計中一個很重要的思想本篇文章主要介紹的是SpringBoot切面Aop的使用和案例

什麼是aop

AOP(Aspect OrientedProgramming):面向切面程式設計,面向切面程式設計(也叫面向方面程式設計),是目前軟體開發中的一個熱點,也是Spring框架中的一個重要內容。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

使用場景

利用AOP可以對我們邊緣業務進行隔離,降低無關業務邏輯耦合性。提高程式的可重用性,同時提高了開發的效率。一般用於日誌記錄,效能統計,安全控制,許可權管理,事務處理,異常處理,資源池管理。使用場景

為什麼需要面向切面程式設計

面向物件程式設計(OOP)的好處是顯而易見的,缺點也同樣明顯。當需要為多個不具有繼承關係的物件新增一個公共的方法的時候,例如日誌記錄、效能監控等,如果採用面向物件程式設計的方法,需要在每個物件裡面都新增相同的方法,這樣就產生了較大的重複工作量和大量的重複程式碼,不利於維護。面向切面程式設計(AOP)是面向物件程式設計的補充,簡單來說就是統一處理某一“切面”的問題的程式設計思想。如果使用AOP的方式進行日誌的記錄和處理,所有的日誌程式碼都集中於一處,不需要再每個方法裡面都去新增,極大減少了重複程式碼。

技術要點

  1. 通知(Advice)包含了需要用於多個應用物件的橫切行為,完全聽不懂,沒關係,通俗一點說就是定義了“什麼時候”和“做什麼”。

  2. 連線點(Join Point)是程式執行過程中能夠應用通知的所有點。

  3. 切點(Poincut)是定義了在“什麼地方”進行切入,哪些連線點會得到通知。顯然,切點一定是連線點。

  4. 切面(Aspect)是通知和切點的結合。通知和切點共同定義了切面的全部內容——是什麼,何時,何地完成功能。

  5. 引入(Introduction)允許我們向現有的類中新增新方法或者屬性。

  6. 織入(Weaving)是把切面應用到目標物件並建立新的代理物件的過程,分為編譯期織入、類載入期織入和執行期織入。

整合使用

匯入依賴

在springboot中使用aop要導aop依賴

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

注意這裡版本依賴於spring-boot-start-parent父pom中的spring-boot-dependencies

編寫攔截的bean

這裡我們定義一個controller用於攔截所有請求的記錄

@RestController
public class AopController { @RequestMapping("/hello")
public String sayHello(){
System.out.println("hello");
return "hello";
}
}

定義切面

SpringBoot在使用切面的時候採用@Aspect註解對POJO進行標註,該註解表明該類不僅僅是一個POJO,還是一個切面容器

定義切點

切點是通過@Pointcut註解和切點表示式定義的。

@Pointcut註解可以在一個切面內定義可重用的切點。

由於Spring切面粒度最小是達到方法級別,而execution表示式可以用於明確指定方法返回型別,類名,方法名和引數名等與方法相關的部件,並且實際中,大部分需要使用AOP的業務場景也只需要達到方法級別即可,因而execution表示式的使用是最為廣泛的。如圖是execution表示式的語法:

execution表示在方法執行的時候觸發。以“”開頭,表明方法返回值型別為任意型別。然後是全限定的類名和方法名,“”可以表示任意類和任意方法。對於方法引數列表,可以使用“..”表示引數為任意型別。如果需要多個表示式,可以使用“&&”、“||”和“!”完成與、或、非的操作。

定義通知

通知有五種型別,分別是:

  1. 前置通知(@Before):在目標方法呼叫之前呼叫通知
  2. 後置通知(@After):在目標方法完成之後呼叫通知
  3. 環繞通知(@Around):在被通知的方法呼叫之前和呼叫之後執行自定義的方法
  4. 返回通知(@AfterReturning):在目標方法成功執行之後呼叫通知
  5. 異常通知(@AfterThrowing):在目標方法丟擲異常之後呼叫通知

程式碼中定義了三種類型的通知,使用@Before註解標識前置通知,列印“beforeAdvice...”,使用@After註解標識後置通知,列印“AfterAdvice...”,使用@Around註解標識環繞通知,在方法執行前和執行之後分別列印“before”和“after”。這樣一個切面就定義好了,程式碼如下:

@Aspect
@Component
public class AopAdvice { @Pointcut("execution (* com.shangguan.aop.controller.*.*(..))")
public void test() { } @Before("test()")
public void beforeAdvice() {
System.out.println("beforeAdvice...");
} @After("test()")
public void afterAdvice() {
System.out.println("afterAdvice...");
} @Around("test()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("before");
try {
proceedingJoinPoint.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("after");
} }

執行結果

案例場景

這裡我們通過一個日誌記錄場景來完整的使用Aop切面業務層只需關心程式碼邏輯實現而不用關心請求引數和響應引數的日誌記錄

那麼首先我們需要自定義一個全域性日誌記錄的切面類GlobalLogAspect

然後在該類新增@Aspect註解,然後在定義一個公共的切入點(Pointcut),指向需要處理的包,然後在定義一個前置通知(新增@Before註解),後置通知(新增@AfterReturning)和環繞通知(新增@Around)方法實現即可

日誌資訊類

package cn.soboys.core;

import lombok.Data;

/**
* @author kenx
* @version 1.0
* @date 2021/6/18 18:48
* 日誌資訊
*/
@Data
public class LogSubject {
/**
* 操作描述
*/
private String description; /**
* 操作使用者
*/
private String username; /**
* 操作時間
*/
private String startTime; /**
* 消耗時間
*/
private String spendTime; /**
* URL
*/
private String url; /**
* 請求型別
*/
private String method; /**
* IP地址
*/
private String ip; /**
* 請求引數
*/
private Object parameter; /**
* 請求返回的結果
*/
private Object result; /**
* 城市
*/
private String city; /**
* 請求裝置資訊
*/
private String device; }

全域性日誌攔截

package cn.soboys.core;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature; import java.lang.reflect.Method; /**
* @author kenx
* @version 1.0
* @date 2021/6/18 14:52
* 切面
*/
public class BaseAspectSupport {
public Method resolveMethod(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature)point.getSignature();
Class<?> targetClass = point.getTarget().getClass(); Method method = getDeclaredMethod(targetClass, signature.getName(),
signature.getMethod().getParameterTypes());
if (method == null) {
throw new IllegalStateException("無法解析目標方法: " + signature.getMethod().getName());
}
return method;
} private Method getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
return getDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
}

GlobalLogAspect

package cn.soboys.core;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.json.JSONUtil;
import cn.soboys.core.utils.HttpContextUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
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.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; /**
* @author kenx
* @version 1.0
* @date 2021/6/18 15:22
* 全域性日誌記錄器
*/
@Slf4j
@Aspect
@Component
public class GlobalLogAspect extends BaseAspectSupport {
/**
* 定義切面Pointcut
*/
@Pointcut("execution(public * cn.soboys.mallapi.controller.*.*(..))")
public void log() { } /**
* 環繞通知
*
* @param joinPoint
* @return
*/
@Around("log()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { LogSubject logSubject = new LogSubject();
//記錄時間定時器
TimeInterval timer = DateUtil.timer(true);
//執行結果
Object result = joinPoint.proceed();
logSubject.setResult(result);
//執行消耗時間
String endTime = timer.intervalPretty();
logSubject.setSpendTime(endTime);
//執行引數
Method method = resolveMethod(joinPoint);
logSubject.setParameter(getParameter(method, joinPoint.getArgs())); HttpServletRequest request = HttpContextUtil.getRequest();
// 介面請求時間
logSubject.setStartTime(DateUtil.now());
//請求連結
logSubject.setUrl(request.getRequestURL().toString());
//請求方法GET,POST等
logSubject.setMethod(request.getMethod());
//請求裝置資訊
logSubject.setDevice(HttpContextUtil.getDevice());
//請求地址
logSubject.setIp(HttpContextUtil.getIpAddr());
//介面描述
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
logSubject.setDescription(apiOperation.value());
} String a = JSONUtil.toJsonPrettyStr(logSubject);
log.info(a);
return result; } /**
* 根據方法和傳入的引數獲取請求引數
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < parameters.length; i++) {
//將RequestBody註解修飾的引數作為請求引數
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
//將RequestParam註解修飾的引數作為請求引數
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
String key = parameters[i].getName();
if (requestBody != null) {
argList.add(args[i]);
} else if (requestParam != null) {
map.put(key, args[i]);
} else {
map.put(key, args[i]);
}
}
if (map.size() > 0) {
argList.add(map);
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}