十一、AOP開發
1、Spring Boot實現 AOP
11.1.1、SpringBootAop簡介
Spring Boot的AOP編程和Spring框架中AOP編程的唯一區別是:引入依賴的方式不同,其他內容完全一樣
Spring Boot中AOP編程需要引入aop啟動器:
<!--aop啟動器-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
可以看到當引入aop啟動器
之后會引入aop依賴
和aspectj依賴
- aop依賴:如果只有這一個依賴,也可以實現AOP編程,這種方式表示使用了純Spring AOP實現aop編程
- aspectj依賴:一個獨立的可以完成AOP編程的AOP框架,屬于第三方的,不屬于Spring框架(通常用它,因為它的功能更加強大)
11.1.2、SpringBootAop實現
實現功能:項目中很多service,要求執行任何service中的任何方法之前
記錄日志
(1)、引入依賴
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.longdidi</groupId><artifactId>springboot-11-001</artifactId><version>0.0.1-SNAPSHOT</version><properties><java.version>21</java.version></properties><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></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId></plugin></plugins></build></project>
(2)、編寫service
package com.longdidi.service;public interface UserService {/*** 保存用戶信息** @param id 用戶id* @param name 用戶名*/void save(Long id, String name);/*** 根據id刪除用戶** @param id 用戶id*/void deleteById(Long id);
}
(3)、編寫service實現類
package com.longdidi.service.impl;import com.longdidi.service.UserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl implements UserService {@Overridepublic void save(Long id, String name) {System.out.println("正在保存用戶信息:" + name);}@Overridepublic void deleteById(Long id) {System.out.println("正在刪除用戶" + id + "信息");}
}
(4)、編寫切面
package com.longdidi.component;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;@Component // 納入IoC容器
@Aspect // 指定該類為切面類
public class LoggingAspect {// 日期格式化器private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS");// 定義切入點:匹配所有以 "service" 結尾的包下的所有方法// 切入點表達式@Pointcut("execution(* com.longdidi.service..*(..))")public void serviceMethods() {}// 前置通知// 切入點表達式:service包下任意類的任意方法@Before("execution(* com.longdidi.service..*.*(..))")public void sysLog(JoinPoint joinPoint) throws Throwable {StringBuilder log = new StringBuilder();LocalDateTime now = LocalDateTime.now();String strNow = formatter.format(now);// 追加日期log.append(strNow);// 追加冒號log.append(":");// 追加方法簽名log.append(joinPoint.getSignature().getName());// 追加方法參數log.append("(");Object[] args = joinPoint.getArgs();for (int i = 0; i < args.length; i++) {log.append(args[i]);if (i < args.length - 1) {log.append(",");}}log.append(")");System.out.println(log);}
}
(5)、編寫測試類
package com.longdidi.controller;import com.longdidi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class UserController {@AutowiredUserService userService;@RequestMapping("/testUser")public void testUser() {userService.save(1L, "jack");userService.deleteById(1L);}
}
(6)、測試
訪問http://localhost:8080/testUser
查看控制臺輸出
2、統一異常處理
11.2.1、異常處理
在controller層如果程序出現了異常并且這個異常未被捕獲,springboot提供的異常處理機制將生效
Spring Boot 提供異常處理機制主要是為了提高應用的健壯性和用戶體驗
它的好處包括
- 統一錯誤響應:可以定義全局異常處理器來統一處理各種異常,確保返回給客戶端的錯誤信息格式一致,便于前端解析。
- 提升用戶體驗:能夠優雅地處理異常情況,避免直接將技術性錯誤信息暴露給用戶,而是顯示更加友好的提示信息。
- 簡化代碼:開發者不需要在每個可能拋出異常的方法中重復編寫異常處理邏輯,減少冗余代碼,使業務代碼更加清晰簡潔。
- 增強安全性:通過控制異常信息的輸出,防止敏感信息泄露,增加系統的安全性。
11.2.2、自適應異常機制
springboot會根據請求頭的Accept字段來決定錯誤的響應格式
這種機制的好處就是:客戶端設備自適應,提高用戶的體驗
在springboot-11-002中測試
不用編寫任何代碼,直接啟動程序訪問
測試返回html錯誤
測試返回json格式錯誤
11.2.3、SpringMVC的異常
重點:如果程序員使用了SpringMVC的錯誤處理方案,SpringBoot的錯誤處理方案不生效
(1)、局部控制
局部異常控制需要在方法上使用@ExceptionHandler注解進行標注
凡是這個控制器當中出現了對應的異常,則走這個方法來進行異常的處理,只在當前控制器局部生效
【示例】在springboot-11-002模塊中測試
UserController.java
在控制器當中編寫一個方法,方法使用@ExceptionHandler注解進行標注
package com.longdidi.controller;import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;@RestController
public class UserController {@GetMapping("/resource/{id}")public String getResource(@PathVariable Long id) {if (id == 1) {throw new IllegalArgumentException("無效ID:" + id);}return "ID = " + id;}@ExceptionHandler(IllegalArgumentException.class)public String handler(IllegalArgumentException e) {return "錯誤信息:" + e.getMessage();}
}
再編寫一個OtherController,讓它也發生IllegalArgumentException
異常,看看它會不會走局部的錯誤處理機制
package com.longdidi.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;@RestController
public class OtherController {@GetMapping("/resource2/{id}")public String getResource(@PathVariable Long id) {if (id == 1) {throw new IllegalArgumentException("無效ID:" + id);}return "ID = " + id;}
}
測試
訪問http://localhost:8080/resource/1
訪問http://localhost:8080/resource2/1
測試通過
(2)、全局控制
全局控制使用@ControllerAdvice + @ExceptionHandler
可以把局部生效的方法單獨放到一個類當中,這個類使用@ControllerAdvice注解標注,凡是任何控制器當中出現了對應的異常,則走這個方法來進行異常的處理
編寫全局異常處理類
package com.longdidi.config;import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(IllegalArgumentException.class)@ResponseBodypublic String handler(IllegalArgumentException e) {return "錯誤信息:" + e.getMessage();}
}
編寫測試類
package com.longdidi.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;@RestController
public class GlobalController {@GetMapping("/resource3/{id}")public String getResource(@PathVariable Long id) {if (id == 1) {throw new IllegalArgumentException("無效ID:" + id);}return "ID = " + id;}
}
測試http://localhost:8080/resource3/1
11.2.4、SpringBoot的異常
(1)、處理順序
重點:如果SpringMVC沒有對應的處理方案,會開啟SpringBoot默認的錯誤處理方案
SpringBoot默認的錯誤處理方案如下
- 如果客戶端要的是json,則直接響應json格式的錯誤信息
- 如果客戶端要的是html頁面,則按照下面方案
-
第一步(精確錯誤碼文件)
去
classpath:/templates/error/
目錄下找404.html``500.html
等精確錯誤碼.html
文件如果找不到則去靜態資源目錄下的/error目錄下找
如果還是找不到進入第二步查找
-
第二步(模糊錯誤碼文件)
去
classpath:/templates/error/
目錄下找4xx.html``5xx.html
等模糊錯誤碼.html
文件如果找不到則去靜態資源目錄下的/error目錄下找
如果還是找不到進入第三步
-
第三步(通用錯誤頁面)
去找
classpath:/templates/error.html
位置查找如果找不到則進入第四步
-
第四步(默認錯誤處理)
如果上述所有步驟都未能找到合適的錯誤頁面,Spring Boot 會使用內置的默認錯誤處理機制,即
/error
端點
【示例】springboot-11-003
項目結構
訪問任意接口,該接口實際上不存在,因此出現404錯誤
訪問http://localhost:8080/test
修改/templates/error/404.html頁面名為任意名后繼續測試
修改/static/error/404.html頁面名為任意名后繼續測試
修改/templates/error/4xx.html頁面名為任意名后繼續測試
修改/static/error/4xx.html頁面名為任意名后繼續測試
修改/templates/error.html頁面名為任意名后繼續測試
(2)、獲取錯誤信息
Spring Boot 默認會在模型Model中放置以下信息
- timestamp: 錯誤發生的時間戳
- status: HTTP 狀態碼
- error: 錯誤類型(如 “Not Found”)
- exception: 異常類名
- message: 錯誤消息
- trace: 堆棧跟蹤
在thymeleaf中使用 ${message}
即可取出信息
注意:springboot3.3.5版本默認只向Model對象中綁定了timestamp``status``error
。如果要保存exception``message``trace
,需要開啟以下三個配置:
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always
【示例】在模塊springboot-11-004中測試
引入依賴
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies>
在templates文件夾創建error.html頁面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>error</title>
</head>
<body>
<h1>error通用錯誤</h1>
異常發生時間:<span th:text="${timestamp}"></span><br>
HTTP狀態碼:<span th:text="${status}"></span><br>
錯誤類型:<span th:text="${error}"></span><br>
異常類名:<span th:text="${exception}"></span><br>
錯誤信息:<span th:text="${message}"></span><br>
堆棧跟蹤:<span th:text="${trace}"></span><br>
</body>
</html>
測試訪問http://localhost:8080/test
添加如下配置
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always
再次訪問測試
(3)、前后端分離異常
統一使用SpringMVC的錯誤處理方案,定義全局的異常處理機制:@ControllerAdvice + @ExceptionHandler
返回json格式的錯誤信息,其它的就不需要管了,因為前端接收到錯誤信息怎么處理是他自己的事兒。
(4)、服務器端錯誤處理方案
建議使用SpringBoot的錯誤處理方案
- 如果發生的異常是HTTP錯誤狀態碼
- 建議常見的錯誤碼給定
精確錯誤碼.html
- 建議不常見的錯誤碼給定
模糊錯誤碼.html
- 建議常見的錯誤碼給定
- 如果發生的異常不是HTTP錯誤狀態碼而是業務相關異常
- 在程序中處理具體的業務異常,自己通過程序來決定跳轉到哪個錯誤頁面
- 建議提供
classpath:/templates/error.html
來處理通用錯誤
11.2.5、實用異常示例
11.2.5.1、基于AOP的異常
項目結構圖
引入依賴
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.longdidi</groupId><artifactId>springboot-11-005</artifactId><version>0.0.1-SNAPSHOT</version><properties><java.version>21</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
開啟aop配置
spring.aop.proxy-target-class=true
定義異常處理
package com.longdidi.config;import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
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 java.io.IOException;
import java.io.PrintWriter;@Aspect
@Component
public class GlobalAspect {private static final Logger logger = LoggerFactory.getLogger(GlobalAspect.class);//定義切入點//凡是注解了RequestMapping的方法都被攔截//@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")@Pointcut("execution(public * com.longdidi..*.*(..))")private void webPointcut() {}/*** 攔截web層異常、記錄異常日志并返回友好信息到前端目前只攔截Exception,是否要攔截Error需再做考慮** @parame異常對象*/@AfterThrowing(pointcut = "webPointcut()", throwing = "e")public void handleThrowing(Exception e) {if (e != null) {e.printStackTrace();logger.error("發現異常!" + e.getMessage());//這里輸入友好性信息writeContent("出現異常");}}/*** 將內容輸出到瀏覽器** @paramcontent輸出內容*/private void writeContent(String content) {HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();response.reset();response.setCharacterEncoding("UTF-8");response.setHeader("Content-Type", "text/plain;charset=UTF-8");response.setHeader("icop-content-type", "exception");PrintWriter writer = null;try {writer = response.getWriter();} catch (IOException e) {e.printStackTrace();}writer.print(content);writer.flush();writer.close();}}
添加測試類
package com.longdidi.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class HelloController {@RequestMapping("/")public String hello() {int i = 0;if (1 / i == 2) {System.out.println(111);}return "hello";}
}
測試
http://localhost:8080/
11.2.5.2、基于注解的異常
(1)、項目結構
(2)、添加依賴
添加web依賴、lombok依賴、fastjson2依賴
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.longdidi</groupId><artifactId>springboot-11-006</artifactId><version>0.0.1-SNAPSHOT</version><properties><java.version>21</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 引入Lombock依賴 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2-extension-spring6</artifactId><version>2.0.55</version></dependency><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.55</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
(3)、全局異常處理類
package com.longdidi.exceptionHander;import com.longdidi.result.ExceptionCodeEnum;
import com.longdidi.result.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;@ControllerAdvice
public class ExceptionHander {private static final String logExceptionFormat = "Capture Exception By GlobalExceptionHandler: Code: %s Detail: %s";private static Logger log = LoggerFactory.getLogger(ExceptionHander.class);//運行時異常@ExceptionHandler(RuntimeException.class)@ResponseBodypublic R runtimeExceptionHandler(RuntimeException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED1, ex);}//空指針異常@ExceptionHandler(NullPointerException.class)@ResponseBodypublic R nullPointerExceptionHandler(NullPointerException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED2, ex);}//類型轉換異常@ExceptionHandler(ClassCastException.class)@ResponseBodypublic R classCastExceptionHandler(ClassCastException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED3, ex);}//IO異常@ExceptionHandler(IOException.class)@ResponseBodypublic R iOExceptionHandler(IOException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED4, ex);}//未知方法異常@ExceptionHandler(NoSuchMethodException.class)@ResponseBodypublic R noSuchMethodExceptionHandler(NoSuchMethodException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED5, ex);}//數組越界異常@ExceptionHandler(IndexOutOfBoundsException.class)@ResponseBodypublic R indexOutOfBoundsExceptionHandler(IndexOutOfBoundsException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED6, ex);}//400錯誤@ExceptionHandler({HttpMessageNotReadableException.class})@ResponseBodypublic R requestNotReadable(HttpMessageNotReadableException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED7, ex);}//400錯誤@ExceptionHandler({TypeMismatchException.class})@ResponseBodypublic R requestTypeMismatch(TypeMismatchException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED8, ex);}//400錯誤@ExceptionHandler({MissingServletRequestParameterException.class})@ResponseBodypublic R requestMissingServletRequest(MissingServletRequestParameterException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED9, ex);}//405錯誤@ExceptionHandler({HttpRequestMethodNotSupportedException.class})@ResponseBodypublic R request405(HttpRequestMethodNotSupportedException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED10, ex);}//406錯誤@ExceptionHandler({HttpMediaTypeNotAcceptableException.class})@ResponseBodypublic R request406(HttpMediaTypeNotAcceptableException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED11, ex);}//500錯誤@ExceptionHandler({ConversionNotSupportedException.class, HttpMessageNotWritableException.class})@ResponseBodypublic R server500(RuntimeException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED12, ex);}//棧溢出@ExceptionHandler({StackOverflowError.class})@ResponseBodypublic R requestStackOverflow(StackOverflowError ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED13, ex);}//除數不能為0@ExceptionHandler({ArithmeticException.class})@ResponseBodypublic R arithmeticException(ArithmeticException ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED14, ex);}//其他錯誤@ExceptionHandler({Exception.class})@ResponseBodypublic R exception(Exception ex) {return resultFormat(ExceptionCodeEnum.EXCEPTION_FAILED15, ex);}private R resultFormat(ExceptionCodeEnum codeEnum, Throwable ex) {ex.printStackTrace();//log.error(String.format(logExceptionFormat, ex.getMessage()));return R.FAIL(codeEnum);}
}
(4)、錯誤枚舉類
package com.longdidi.result;import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;@NoArgsConstructor
@AllArgsConstructor
public enum ExceptionCodeEnum {OK(200, "成功"),FAIL(400, "失敗"),BAD_REQUEST(400, "請求錯誤"),NOT_FOUND(404, "未找到資源"),INTERNAL_ERROR(500, "內部服務器錯誤"),MODIFICATION_FAILED(400, "修改失敗"),DELETION_FAILED(400, "刪除失敗"),CREATION_FAILED(400, "創建失敗"),EXCEPTION_FAILED1(1, "異常1"),EXCEPTION_FAILED2(2, "異常2"),EXCEPTION_FAILED3(3, "異常3"),EXCEPTION_FAILED4(4, "異常4"),EXCEPTION_FAILED5(5, "異常5"),EXCEPTION_FAILED6(6, "異常6"),EXCEPTION_FAILED7(7, "異常7"),EXCEPTION_FAILED8(8, "異常8"),EXCEPTION_FAILED9(9, "異常9"),EXCEPTION_FAILED10(10, "異常10"),EXCEPTION_FAILED11(11, "異常11"),EXCEPTION_FAILED12(12, "異常11"),EXCEPTION_FAILED13(13, "異常13"),EXCEPTION_FAILED14(14, "異常14"),EXCEPTION_FAILED15(15, "異常15");@Getter@Setterprivate int code;@Getter@Setterprivate String msg;}
(5)、統一數據返回類
package com.longdidi.result;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class R<T> {private int code; // 響應的狀態碼private String msg; // 響應的消息private T data; // 響應的數據體// 用于構建成功的響應,不攜帶數據public static <T> R<T> OK() {return R.<T>builder().code(ExceptionCodeEnum.OK.getCode()).msg(ExceptionCodeEnum.OK.getMsg()).build();}// 用于構建成功的響應,攜帶數據public static <T> R<T> OK(T data) {return R.<T>builder().code(ExceptionCodeEnum.OK.getCode()).msg(ExceptionCodeEnum.OK.getMsg()).data(data).build();}// 用于構建失敗的響應,不帶任何參數,默認狀態碼為400,消息為"失敗"public static <T> R<T> FAIL() {return R.<T>builder().code(ExceptionCodeEnum.FAIL.getCode()).msg(ExceptionCodeEnum.FAIL.getMsg()).build();}// 用于構建失敗的響應,自定義狀態碼和消息public static <T> R<T> FAIL(ExceptionCodeEnum codeEnum) {return R.<T>builder().code(codeEnum.getCode()).msg(codeEnum.getMsg()).build();}
}
(6)、JSON配置
#spring.web.resources.add-mappings=falsefastjson.date-format=yyyy-MM-dd HH:mm:ss
fastjson.charset=UTF-8
(7)、測試類
package com.longdidi.controller;import com.alibaba.fastjson2.JSONObject;
import com.longdidi.result.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class JSONController {/*** 測試正常返回** @return*/@GetMapping("/json/getStudent")public R getStudent() {JSONObject student = new JSONObject();student.put("age", "11");student.put("name", "學習筆記");return R.builder().data(student).build();}/*** 測試拋出RuntimeException異常** @return*/@RequestMapping("/json/getUserException")public R getUserException() {throw new RuntimeException();}/*** 測試拋出全局異常處理** @return*/@RequestMapping("/json/getAllException")public R getAllException() throws Exception {throw new Exception();}
}
(8)、測試
訪問http://localhost:8080/json/getStudent
訪問http://localhost:8080/json/getUserException
訪問http://localhost:8080/json/getAllException
3、統一日志處理
11.3.1、WEB日志
(1)、項目結構
(2)、引入依賴
因為需要對web請求做切面來記錄日志,所以引入web模塊和aop模塊,當然一些工具的依賴也需要引入
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.longdidi</groupId><artifactId>springboot-11-007</artifactId><version>0.0.1-SNAPSHOT</version><properties><java.version>21</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--引用工具--><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2-extension-spring6</artifactId><version>2.0.55</version></dependency><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.55</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.36</version></dependency><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency><!--引用AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId></plugin></plugins></build></project>
(3)、開啟代理
開啟AOP的配置,其中spring.aop.auto屬性默認是開啟的,也就是說只要引入了AOP依賴后,默認已經增加了@EnableAspectJAutoProxy
spring.aop.auto=true
當需要使用CGLIB來實現AOP的時候,需要配置spring.aop.proxy-target-class=true
spring.aop.proxy-target-class=true
(4)、WEB接口
package com.longdidi.controller;import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.util.Map;@RestController
public class LogController {@RequestMapping("/hello")public String hello() {int i = 0;if (1 / i == 2) {System.out.println(111);}return "hello";}@RequestMapping(value = "/login", method = RequestMethod.POST)public String login() {return "success";}@RequestMapping(value = "/echo", method = RequestMethod.GET)public String login(String name) {return "hello," + name;}@RequestMapping("/test")public String hello(@RequestBody Map<String, Object> str) {Object aaa = str.get("aaa");Object bbb = str.get("bbb");System.out.println("第一個參數:" + aaa);System.out.println("第二個參數:" + bbb);int i = 1;if (1 / i == 2) {System.out.println(111);}return "hello";}
}
(5)、日志切面
package com.longdidi.config;import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.longdidi.utils.IpUtils;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;@Aspect
@Component
@Order(1)
public class WebLogAspect {private static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);ThreadLocal<Long> startTime = new ThreadLocal<>();/*** 此處定義切入點*/@Pointcut("execution(public * com.longdidi.controller..*.*(..))")public void webLog() {}/*** 前置通知:方法調用前被調用* 通過JoinPoint 獲取通知的簽名信息:如目標方法名、目標方法參數信息等* 把接口中的requestMsg參數重新設置進request** @param joinPoint*/@Before("webLog()")public void doBefore(JoinPoint joinPoint) throws Throwable {logger.info("aop Before");//設置方法開始執行時間startTime.set(System.currentTimeMillis());//獲取requestServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();Map<String, String[]> parameterMap = request.getParameterMap();StringBuffer sb = new StringBuffer();// ip地址String ipAddr = IpUtils.getIpAddr(request);sb.append("\n【請求 URL】:").append(request.getRequestURL().toString());sb.append("\n【請求 IP】:").append(ipAddr);sb.append("\n【請求類名】:").append(joinPoint.getSignature().getDeclaringTypeName());sb.append("\n【請求方法名】:").append(joinPoint.getSignature().getName());sb.append("\n【Http方法】:").append(request.getMethod());Object[] args = joinPoint.getArgs();Object[] arguments = new Object[args.length];for (int i = 0; i < args.length; i++) {if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {//ServletRequest不能序列化,從入參里排除,否則報異常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)//ServletResponse不能序列化 從入參里排除,否則報異常:java.lang.IllegalStateException: getOutputStream() has already been called for this responsecontinue;}arguments[i] = args[i];}String paramter = "";if (arguments != null) {try {paramter = JSONObject.toJSONString(arguments);} catch (Exception e) {paramter = arguments.toString();}}//構造參數組集合List<Object> argList = new ArrayList<>();for (Object arg : joinPoint.getArgs()) {//request、response無法使用toJsonif (arg instanceof HttpServletRequest) {argList.add("request");} else if (arg instanceof HttpServletResponse) {argList.add("response");} else {argList.add(JSON.toJSON(arg));}}}/*** 后置最終通知(目標方法只要執行完了就會執行后置通知方法)** @After注解表示在方法執行之后執行*/@After("webLog()")public void after(JoinPoint joinPoint) {logger.info("aop after");}@AfterReturning(pointcut = "webLog()")public void doAfterReturning(JoinPoint joinPoint) throws Throwable {logger.info("aop AfterReturning");//獲取requestServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();//從切面植入點處通過反射機制獲取植入點處的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();//獲取切點所在的類String className = joinPoint.getSignature().getDeclaringTypeName();//獲取切點所在的方法Method method = signature.getMethod();//IPString ipAddr = IpUtils.getIpAddr(request);//URLString url = request.getRequestURL().toString();StringBuffer sb = new StringBuffer();sb.append("\n【請求 URL】:").append(request.getRequestURL().toString());sb.append("\n【請求 IP】:").append(ipAddr);sb.append("\n【請求類名】:").append(joinPoint.getSignature().getDeclaringTypeName());sb.append("\n【請求方法名】:").append(joinPoint.getSignature().getName());sb.append("\n【Http方法】:").append(request.getMethod());sb.append("\n[耗時]:").append((System.currentTimeMillis() - startTime.get()) + "毫秒");}/*** 環繞操作** @param proceedingJoinPoint 切入點* @return 原方法返回值* @throws Throwable 異常信息*/@Around("webLog()")public Object aroundLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//設置方法開始執行時間startTime.set(System.currentTimeMillis());//從切面植入點處通過反射機制獲取植入點處的方法MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();//獲取切點所在的方法Method method = signature.getMethod();//請求類名String className = proceedingJoinPoint.getTarget().getClass().getName();//獲取請求的方法名String methodName = method.getName();//請求參數Object[] args = proceedingJoinPoint.getArgs();//將參數所在的數組轉換成json//Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.asList(args);//String params = JSON.toJSONString(args);//獲取requestServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();// ip地址String ipAddr = IpUtils.getIpAddr(request);Map<String, String[]> parameterMap = request.getParameterMap();StringBuffer sb = new StringBuffer();sb.append("\n【請求 URL】:").append(request.getRequestURL().toString());sb.append("\n【請求 IP】:").append(ipAddr);sb.append("\n【請求類名】:").append(className);sb.append("\n【請求方法名】:").append(methodName);sb.append("\n【Http方法】:").append(request.getMethod());//sb.append("\n【請求參數】:").append(params);//開始調用時間logger.info(sb.toString());try {Object result = proceedingJoinPoint.proceed();logger.info("請求類:" + className + ";方法:" + methodName + ";執行時間:" + (System.currentTimeMillis() - startTime.get()) + "毫秒");return result;} catch (Exception e) {e.printStackTrace();return null;}}/*** 攔截web層異常,記錄異常日志,并返回友好信息到前端目前只攔截Exception,是否要攔截Error需再做考慮* 當目標方法拋出異常返回后,將把目標方法拋出的異常傳給通知方法;** @parame異常對象*/@AfterThrowing(pointcut = "webLog()", throwing = "exception")public void handleThrowing(JoinPoint joinPoint, Throwable exception) {if (exception != null) {exception.printStackTrace();logger.error("發現異常!" + exception.getMessage());//這里輸入友好性信息writeContent("出現異常");}}/*** 將內容輸出到瀏覽器** @paramcontent輸出內容*/private void writeContent(String content) {HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();response.reset();response.setCharacterEncoding("UTF-8");response.setHeader("Content-Type", "text/plain;charset=UTF-8");response.setHeader("icop-content-type", "exception");PrintWriter writer = null;try {writer = response.getWriter();} catch (IOException e) {e.printStackTrace();}writer.print(content);writer.flush();writer.close();}
}
(6)、IP工具類
package com.longdidi.utils;import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Pattern;public class IpUtils {private static final Logger logger = LoggerFactory.getLogger(IpUtils.class);public static final String _255 = "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";public static final Pattern pattern = Pattern.compile("^(?:" + _255 + "\\.){3}" + _255 + "$");public static String longToIpV4(long longIp) {int octet3 = (int) ((longIp >> 24) % 256);int octet2 = (int) ((longIp >> 16) % 256);int octet1 = (int) ((longIp >> 8) % 256);int octet0 = (int) ((longIp) % 256);return octet3 + "." + octet2 + "." + octet1 + "." + octet0;}public static long ipV4ToLong(String ip) {String[] octets = ip.split("\\.");return (Long.parseLong(octets[0]) << 24) + (Integer.parseInt(octets[1]) << 16)+ (Integer.parseInt(octets[2]) << 8) + Integer.parseInt(octets[3]);}public static boolean isIPv4Private(String ip) {long longIp = ipV4ToLong(ip);return (longIp >= ipV4ToLong("10.0.0.0") && longIp <= ipV4ToLong("10.255.255.255"))|| (longIp >= ipV4ToLong("172.16.0.0") && longIp <= ipV4ToLong("172.31.255.255"))|| longIp >= ipV4ToLong("192.168.0.0") && longIp <= ipV4ToLong("192.168.255.255");}public static boolean isIPv4Valid(String ip) {return pattern.matcher(ip).matches();}/*** https://blog.csdn.net/shanchahua123456/article/details/84773292*//*** 獲取請求主機IP地址,如果通過代理進來,則透過防火墻獲取真實IP地址;* <p>* 以下整理了各個代理服務器自己開發的轉發服務請求頭,這些請求頭都不是標準的http請求頭,不一定所有的代理都會帶上這些請求頭,所以通過這方式只能盡可能的獲取到真實ip,但不能保證一定可以獲取到真實ip,而且代理服務器請求頭中獲取的ip是可偽造的。* 參數:* <p>* X-Forwarded-For:Squid 服務代理* <p>* Proxy-Client-IP:apache 服務代理* <p>* WL-Proxy-Client-IP:weblogic 服務代理* <p>* HTTP_CLIENT_IP:有些代理服務器* <p>* X-Real-IP:nginx服務代理** @param request* @return*//*** request.getRemoteAddr() 獲取的值為0:0:0:0:0:0:0:1的原因及解決辦法* <p>* 0:0:0:0:0:0:0:1 是IPV6 相當于127.0.0.1* <p>* 遇到了request.getRemoteAddr()獲取的值為0:0:0:0:0:0:0:1,這是為什么呢,* 照道理講,應該是127.0.0.1才對,為什么這個獲取的值變成了ipv6了呢,而且我發現這種情況只有在服務器和客戶端都在同一臺電腦上才會出現* (例如用localhost訪問的時候才會出現),* 后來上網查了查原因,原來是/etc/hosts這個東西作怪(在windows上應該是C:\Windows\System32\drivers\etc\hosts這個文件),* 只需要注釋掉文件中的 # ::1 localhost 這一行即可解決問題。另外localhost這個文件很有用,這里你可以添加自己的條目,* 例如添加 192.168.0.212 myweb 這樣子,在瀏覽器中原來只能使用192.168.0.212來訪問的,并可以使用myweb來進行替換。** @param request* @return*/public static String getIpAddr(HttpServletRequest request) {//X-Forwarded-For:Squid 服務代理String ipAddress = request.getHeader("x-forwarded-for");//Proxy-Client-IP:apache 服務代理if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("Proxy-Client-IP");}//WL-Proxy-Client-IP:weblogic 服務代理if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("WL-Proxy-Client-IP");}//HTTP_CLIENT_IP:有些代理服務器if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("HTTP_CLIENT_IP");}//X-Real-IP:nginx服務代理if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("X-Real-IP");}//還是不能獲取到,最后再通過request.getRemoteAddr();獲取if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getRemoteAddr();String localIp = "127.0.0.1";String localIpv6 = "0:0:0:0:0:0:0:1";if (ipAddress.equals(localIp) || ipAddress.equals(localIpv6)) {// 根據網卡取本機配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();ipAddress = inet.getHostAddress();} catch (UnknownHostException e) {//輸出異常信息e.printStackTrace();}}}//有些網絡通過多層代理,那么獲取到的ip就會有多個,一般都是通過逗號(,)分割開來,并且第一個ip為客戶端的真實IPString ipSeparate = ",";int ipLength = 15;if (ipAddress != null && ipAddress.length() > ipLength) {if (ipAddress.indexOf(ipSeparate) > 0) {ipAddress = ipAddress.substring(0, ipAddress.indexOf(ipSeparate));//ipAddress = ipAddress.split(",")[0];}}return ipAddress;}}
(7)、測試請求
http://localhost:8080/echo?name=%E5%BC%A0%E4%B8%89
(8)、AOP切面的同步問題
在WebLogAspect切面中,分別通過doBefore和doAfterReturning兩個獨立函數實現了切點頭部和切點返回后執行的內容,若想統計請求的處理時間,就需要在doBefore處記錄時間,并在doAfterReturning處通過當前時間與開始處記錄的時間計算得到請求處理的消耗時間。
可以在WebLogAspect切面中定義一個成員變量來給doBefore和doAfterReturning一起訪問,為了解決同步提可以引入ThreadLocal對象
像下面這樣進行記錄
@Aspect
@Component
@Order(1)
public class WebLogAspect {private static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);ThreadLocal<Long> startTime = new ThreadLocal<>();/*** 此處定義切入點*/@Pointcut("execution(public * com.longdidi.controller..*.*(..))")public void webLog() {}@AfterReturning(pointcut = "webLog()")public void doAfterReturning(JoinPoint joinPoint) throws Throwable {logger.info("aop AfterReturning");//獲取requestServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();//從切面植入點處通過反射機制獲取植入點處的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();//獲取切點所在的類String className = joinPoint.getSignature().getDeclaringTypeName();//獲取切點所在的方法Method method = signature.getMethod();//IPString ipAddr = IpUtils.getIpAddr(request);//URLString url = request.getRequestURL().toString();StringBuffer sb = new StringBuffer();sb.append("\n【請求 URL】:").append(request.getRequestURL().toString());sb.append("\n【請求 IP】:").append(ipAddr);sb.append("\n【請求類名】:").append(joinPoint.getSignature().getDeclaringTypeName());sb.append("\n【請求方法名】:").append(joinPoint.getSignature().getName());sb.append("\n【Http方法】:").append(request.getMethod());sb.append("\n[耗時]:").append((System.currentTimeMillis() - startTime.get()) + "毫秒");}
}
(9)、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的值由大到小執行
11.3.2、Service日志
(1)、項目結構
(2)、引入依賴
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.longdidi</groupId><artifactId>springboot-11-008</artifactId><version>0.0.1-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--引用工具--><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2-extension-spring6</artifactId><version>2.0.55</version></dependency><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.55</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.36</version></dependency><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency><!--引用AOP--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId></plugin></plugins></build></project>
(3)、定義監控注解
package com.longdidi.monitor;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 系統日志記錄監控器*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMonitor {/*** 模塊信息*/String value();
}
(4)、配置日志切面
package com.longdidi.config;import com.longdidi.monitor.LogMonitor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import java.util.Date;
import java.text.SimpleDateFormat;@Component
@Aspect
public class LogAspect {private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());@Pointcut("@annotation(com.longdidi.monitor.LogMonitor)")public void pointCut() {}@Around(value = "pointCut() && @annotation(systemLogRecordMonitor)")public Object recordSystemLog(ProceedingJoinPoint joinPoint, LogMonitor systemLogRecordMonitor) throws Throwable {long startTime = System.currentTimeMillis();String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(startTime));Object result = joinPoint.proceed(joinPoint.getArgs());long endTime = System.currentTimeMillis();String value = systemLogRecordMonitor.value();String userName = "參數應從result中獲取";// 權限框架使用的是SpringSecurity//var authentication = SecurityContextHolder.getContext().getAuthentication();//var username = authentication.getName();LOGGER.info("用戶 [ {} ] 于 [ {} ] 訪問了 [{}] 模塊 耗時 {}/MS.", userName, format, value, (endTime - startTime));return result;}}
(5)、定義Service
package com.longdidi.service;import com.longdidi.monitor.LogMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;@Service
public class UserService {private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());@LogMonitor("刪除")public void delete() {LOGGER.info("一級 delete");}@LogMonitor("創建")public void create() {LOGGER.info("一級 create");}@LogMonitor("更新")public void update() {LOGGER.info("一級 update");}@LogMonitor("獲取全部")public void getAll() {LOGGER.info("一級 getAll");}
}
(6)、定義測試Controller
package com.longdidi.controller;import com.longdidi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class LogController {@AutowiredUserService userService;@RequestMapping("/delete")public void delete() {userService.delete();}@RequestMapping("/create")public void create() {userService.create();}@RequestMapping("/update")public void update() {userService.update();}@RequestMapping("/getAll")public void getAll() {userService.getAll();}
}
(7)、測試訪問
http://localhost:8080/create
lt中獲取";
// 權限框架使用的是SpringSecurity
//var authentication = SecurityContextHolder.getContext().getAuthentication();
//var username = authentication.getName();
LOGGER.info(“用戶 [ {} ] 于 [ {} ] 訪問了 [{}] 模塊 耗時 {}/MS.”, userName, format, value, (endTime - startTime));
return result;
}
}
#### (5)、定義Service```java
package com.longdidi.service;import com.longdidi.monitor.LogMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;@Service
public class UserService {private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());@LogMonitor("刪除")public void delete() {LOGGER.info("一級 delete");}@LogMonitor("創建")public void create() {LOGGER.info("一級 create");}@LogMonitor("更新")public void update() {LOGGER.info("一級 update");}@LogMonitor("獲取全部")public void getAll() {LOGGER.info("一級 getAll");}
}
(6)、定義測試Controller
package com.longdidi.controller;import com.longdidi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class LogController {@AutowiredUserService userService;@RequestMapping("/delete")public void delete() {userService.delete();}@RequestMapping("/create")public void create() {userService.create();}@RequestMapping("/update")public void update() {userService.update();}@RequestMapping("/getAll")public void getAll() {userService.getAll();}
}
(7)、測試訪問
http://localhost:8080/create