後端筆記07-AopLog
SpringBoot7-AopLog
使用AOP切面對請求進行日誌記錄,同時記錄UserAgent資訊
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- aop --> <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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.0</version> </dependency> <!-- 解析 UserAgent 資訊 --> <dependency> <groupId>eu.bitwalker</groupId> <artifactId>UserAgentUtils</artifactId> <version>1.21</version> </dependency> </dependencies>
AopLog.java
@Aspect @Component @Slf4j public class AopLog { private static final String START_TIME = "request-start"; /** * 切入點 */ //Pointcut表示式 @Pointcut("execution(public * com.study.aoplog.controller.*Controller.*(..))") //Pointcut簽名 public void log() { } /** * 前置操作 * * @param point 切入點 */ //AOP通知中可以用JoinPoint獲取資料 //Around通知比較特殊,是ProceedingJoinPoint @Before("log()") public void beforeLog(JoinPoint point) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(attributes).getRequest(); log.info("【請求 URL】:{}",request.getRequestURL()); log.info("【請求 IP】:{}",request.getRemoteAddr()); log.info("【請求類名】:{},【請求方法名】:{}",point.getSignature().getDeclaringTypeName(),point.getSignature().getName()); Map<String,String[]> parameterMap = request.getParameterMap(); log.info("【請求引數】:{}", JSONUtil.toJsonStr(parameterMap)); Long start = System.currentTimeMillis(); request.setAttribute(START_TIME, start); } /** * 環繞操作 * * @param point 切入點 * @return 原方法返回值 * @throws Throwable 異常資訊 */ @Around("log()") public Object aroundLog(ProceedingJoinPoint point) throws Throwable { Object result = point.proceed(); log.info("【返回值】:{}",JSONUtil.toJsonStr(result)); return result; } /** * 後置操作 */ @AfterReturning("log()") public void afterReturning() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(attributes).getRequest(); Long start = (Long) request.getAttribute(START_TIME); Long end = System.currentTimeMillis(); log.info("【請求耗時】:{}毫秒",end - start); String header = request.getHeader("User-Agent"); UserAgent userAgent = UserAgent.parseUserAgentString(header); log.info("【瀏覽器型別】:{},【作業系統】:{},【原始User-Agent】:{}", userAgent.getBrowser().toString(), userAgent.getOperatingSystem().toString(), header); } }
AOP:面向切面程式設計,相對於OOP面向物件程式設計 Spring的AOP的存在目的是為了解耦。AOP可以讓一組類共享相同的行為。在OOP中只能繼承和實現介面,且類繼承只能單繼承,阻礙更多行為新增到一組類上,AOP彌補了OOP的不足。
AOP術語
1.通知(有的地方叫增強)(Advice)
需要完成的工作叫做通知,就是你寫的業務邏輯中需要比如事務、日誌等先定義好,然後需要的地方再去用。
2.連線點(Join point)
對應的是具體被攔截的物件,因為Spring只支援方法,所以被攔截的物件往往就是指特定的方法。
3.切點(Poincut)
其實就是篩選出的連線點,一個類中的所有方法都是連線點,但又不全需要,會篩選出某些作為連線點做為切點。如果說通知定義了切面的動作或者執行時機的話,切點則定義了執行的地點。
有時候,我們的切面不單單應用於單個方法,也可以是多個類的不同方法,這時,可以通過正則表示式和指示器的規則去定義。
4.切面(Aspect)
其實就是通知和切點的結合,通知和切點共同定義了切面的全部內容,它是幹什麼的,什麼時候在哪執行。
5.引入(Introduction)
在不改變一個現有類程式碼的情況下,為該類新增屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行為和狀態。其實就是把切面(也就是新方法屬性:通知定義的)用到目標類中去。
6.目標(target)
被通知的物件。也就是需要加入額外程式碼的物件,也就是真正的業務邏輯被組織織入切面。
7.織入(Weaving)
把切面加入程式程式碼的過程。切面在指定的連線點被織入到目標物件中,在目標物件的生命週期裡有多個點可以進行織入:
- 編譯期:切面在目標類編譯時被織入,這種方式需要特殊的編譯器。
- 類載入期:切面在目標類載入到JVM時被織入,這種方式需要特殊的類載入器,它可以在目標類被引入應用之前增強該目標類的位元組碼。
- 執行期:切面在應用執行的某個時刻被織入,一般情況下,在織入切面時,AOP容器會為目標物件動態建立一個代理物件,Spring AOP就是以這種方式織入切面的。
public class UserService{
void save(){}
List list(){}
....
}
以UserService為例,在UserService中的save()方法前需要開啟事務,在方法後關閉事務,在拋異常時回滾事務。
那麼,UserService中的所有方法都是連線點(JoinPoint),save()方法就是切點(Poincut)。需要在save()方法前後執行的方法就是通知(Advice),切點和通知合起來就是一個切面(Aspect)。save()方法就是目標(target)。把想要執行的程式碼動態的加入到save()方法前後就是織入(Weaving)。
AOP通知
1.before(前置通知): 在方法開始執行前執行
2.after(後置通知): 在方法執行後執行
3.afterReturning(返回後通知): 在方法返回後執行
4.afterThrowing(異常通知): 在丟擲異常時執行
5.around(環繞通知): 在方法執行前和執行後都會執行
執行順序 around > before > 【方法執行】>around > after > afterReturning
定義簡單切面
@Aspect // 使用@Aspect註解將一個java類定義為切面類
@Component
public class MyAspect {
// 使用@Pointcut定義一個切入點,可以是一個規則表示式,也可以是一個註解等
// Pointcut的定義包括兩個部分:Pointcut表示式(expression)和Pointcut簽名(signature)
// Pointcut表示式
// execution(* *(..)):表示匹配所有方法
// execution(public * com.test.TestController.*(..)):表示匹配com.test.TestController類中所有的公有方法
// execution(* com.test..*.*(..)):表示匹配com.test包中所有的方法
@Pointcut("execution(public * com.test.TestController.testFunc(..))")
// Pointcut簽名
public void pointCut() {}
// 使用@Before在切入點開始處切入內容
@Before("pointCut()")
public void before() {
log.info("MyAspect before ...");
}
// 使用@After在切入點結尾處切入內容
@After("pointCut()")
public void after() {
log.info("MyAspect after ...");
}
//使用@AfterReturning在切入點return內容之後切入內容(可以用來對處理返回值做一些加工處理)
@AfterReturning("pointCut()")
public void afterReturning() {
log.info("MyAspect after returning ...");
}
//使用@AfterThrowing用來處理當切入內容部分丟擲異常之後的處理邏輯
@AfterThrowing("pointCut()")
public void afterThrowing() {
log.info("MyAspect after throwing ...");
}
//使用@Around在切入點前後切入內容,並自己控制何時執行切入點自身的內容
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect around before ...");
joinPoint.proceed();
log.info("MyAspect around after ...");
}
}
Spring AOP提供使用org.aspectj.lang.JoinPoint型別獲取連線點資料,任何通知方法的第一個引數都可以是JoinPoint(環繞通知是ProceedingJoinPoint,JoinPoint子類)。
- JoinPoint:提供訪問當前被通知方法的目標物件、代理物件、方法引數等資料
- ProceedingJoinPoint:只用於環繞通知,使用proceed()方法來執行目標方法
如引數型別是JoinPoint、ProceedingJoinPoint型別,可以從“argNames”屬性省略掉該引數名(可選,寫上也對),這些型別物件會自動傳入的,但必須作為第一個引數。
例子-切面中使用JoinPoint和ProceedingJoinPoint
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(public * com.test.TestController.testFunc(..))")
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
log.info("MyAspect before Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("ClientIP:{}", request.getRemoteAddr());
}
@After("pointCut()")
public void after(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
log.info("MyAspect after Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
}
@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
log.info("MyAspect after returning Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
}
@AfterThrowing("pointCut()")
public void afterThrowing(JoinPoint joinPoint) {
log.info("MyAspect after throwing ...");
}
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect around before ...");
joinPoint.proceed();// 用proceed執行原方法 可用OBJ物件接收原方法返回值
log.info("MyAspect around after ...");
}
}
優化:AOP切面的優先順序
由於通過AOP實現,程式得到了很好的解耦,但是也會帶來一些問題,比如:我們可能會對Web層做多個切面,校驗使用者,校驗頭資訊等等,這個時候經常會碰到切面的處理順序問題。
所以,我們需要定義每個切面的優先順序,我們需要@Order(i)
註解來標識切面的優先順序。i的值越小,優先順序越高。假設我們還有一個切面是CheckNameAspect
用來校驗name必須為didi,我們為其設定@Order(10)
,假設另外有一個WebLogAspect設定為@Order(5)
,所以WebLogAspect有更高的優先順序,這個時候執行順序是這樣的:
- 在
@Before
中優先執行@Order(5)
的內容,再執行@Order(10)
的內容 - 在
@After
和@AfterReturning
中優先執行@Order(10)
的內容,再執行@Order(5)
的內容
所以我們可以這樣子總結:
- 在切入點前的操作,按order的值由小到大執行
- 在切入點後的操作,按order的值由大到小執行
JoinPoint getSignature() 返回屬性詳解
public interface Signature {
String toString();
String toShortString();
String toLongString();
String getName();// 請求方法名
int getModifiers();
Class getDeclaringType();
String getDeclaringTypeName();// 請求類名
}
tips
@Around("log()")
public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
log.info("【返回值】:{}",JSONUtil.toJsonStr(result));
return result;
}
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect around before ...");
joinPoint.proceed();
log.info("MyAspect around after ...");
}
在實際使用中,如果@Around下的方法是void,無返回型別,介面返回值將會被阻塞,需要使用第一種方法,才能使介面正確返回資料
參考網址:
https://juejin.im/post/6844903766035005453
RequestContextHolder 分析
持有上下文的Request容器 在Controller層外取用Request Response時使用 獲取方法如下
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
HttpServletResponse response = Objects.requireNonNull(attributes).getResponse();
參考:
UserAgentUtils
UserAgentUtils是一個處理user-agent 字元的一個工具。可以用來實時地處理http請求和分析http請求日誌檔案。這裡可以使用UserAgentUtils對訪問使用者的瀏覽器型別、作業系統、裝置種類等進行統計分析。
基本用法
UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
Browser browser = userAgent.getBrowser();
String browserName = browser.getName();// 瀏覽器名稱
String group = browser.getGroup().getName();// 瀏覽器大類
Version browserVersion = userAgent.getBrowserVersion();// 詳細版本
String version = browserVersion.getMajorVersion();// 瀏覽器主版本
System.out.println(browserName);
System.out.println(group);
System.out.println(browserVersion);
System.out.println(version);
System.out.println(userAgent.getOperatingSystem());// 訪問裝置系統
System.out.println(userAgent.getOperatingSystem().getDeviceType());// 訪問裝置型別
System.out.println(userAgent.getOperatingSystem().getManufacturer());// 訪問裝置製造廠商
TestController.java
@RestController
public class TestController {
@GetMapping("/test")
public Dict test(String who) {
return Dict.create().set("who", StrUtil.isBlank(who) ? "me" : who);
}
}
搬運自我的 Git:https://github.com/miles-rush/StudyNote
SpringBoot-Demo:https://github.com/xkcoding/spring-boot-demo 的學習筆記