spring boot如何使用AOP統(tǒng)一處理web請求
為了保證服務的高可用,及時發(fā)現(xiàn)問題,迅速解決問題,為應用添加log是必不可少的。
但是隨著項目的增大,方法增多,每個方法加單獨加日志處理會有很多冗余
那在SpringBoot項目中如何統(tǒng)一的處理Web請求日志?
基本思想:
采用AOP的方式,攔截請求,寫入日志
AOP 是面向切面的編程,就是在運行期通過動態(tài)代理的方式對代碼進行增強處理
基于AOP不會破壞原來程序邏輯,因此它可以很好的對業(yè)務邏輯的各個部分進行隔離,從而使得業(yè)務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。
1.添加依賴
<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>
引入spring-boot-starter-web 依賴之后無需在引入相關的日志依賴,spring-boot-starter-web中已經集成了slf4j 的依賴
引入spring-boot-starter-aop 依賴之后,AOP 的功能即是啟動狀態(tài)
2.配置
application.properties添加
# AOP spring.aop.auto=true spring.aop.proxy-target-class=true
logback-spring.xml,主要是ControllerRequest那部分
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="log.path" value="logs" />
<!--0. 日志格式和顏色渲染 -->
<!-- 彩色日志依賴的渲染類 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!--1. 輸出到控制臺-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是為開發(fā)使用,只配置最底級別,控制臺輸出的日志級別是大于或等于此級別的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>info</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 設置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--2. 輸出到文檔-->
<!-- 2.1 level為 DEBUG 日志,時間滾動輸出 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日志文檔的路徑及文檔名 -->
<file>${log.path}/debug/debug.log</file>
<!--日志文檔輸出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 設置字符集 -->
</encoder>
<!-- 日志記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志歸檔 -->
<fileNamePattern>${log.path}/debug/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文檔保留天數(shù)-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文檔只記錄debug級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>debug</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 2.2 level為 INFO 日志,時間滾動輸出 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日志文檔的路徑及文檔名 -->
<file>${log.path}/info/info.log</file>
<!--日志文檔輸出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志歸檔路徑以及格式 -->
<fileNamePattern>${log.path}/info/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文檔保留天數(shù)-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文檔只記錄info級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 2.3 level為 WARN 日志,時間滾動輸出 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日志文檔的路徑及文檔名 -->
<file>${log.path}/warn/warn.log</file>
<!--日志文檔輸出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此處設置字符集 -->
</encoder>
<!-- 日志記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文檔保留天數(shù)-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文檔只記錄warn級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 2.4 level為 ERROR 日志,時間滾動輸出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日志文檔的路徑及文檔名 -->
<file>${log.path}/error/error.log</file>
<!--日志文檔輸出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此處設置字符集 -->
</encoder>
<!-- 日志記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文檔保留天數(shù)-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文檔只記錄ERROR級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<springProfile name="dev">
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>
<springProfile name="prod">
<root level="info">
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>
<appender name="ControllerRequest" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/request/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${log.path}/request/info.log.%d{yyyy-MM-dd}</FileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<logger name="ControllerRequest" level="DEBUG" additivity="false">
<appender-ref ref="ControllerRequest"/>
</logger>
</configuration>
3..實現(xiàn)
實現(xiàn)切面的注解
(1)類注解
A. @Aspect 將一個java類定義為切面類
B. @order(i) 標記切面類的處理優(yōu)先級,i值越小,優(yōu)先級別越高。可以注解類,也能注解到方法上
(2)方法注解
A. @Pointcut 定義一個切入點,可以是一個表達式
execution表達式,eg:
任意公共方法的執(zhí)行 execution(public * *(..)) 任何一個以“set”開始的方法的執(zhí)行 execution(* set*(..)) 定義在controller包里的任意方法的執(zhí)行 execution(public * com.example.demo.controller.*(..)) 定義在controller包里的任意方法的執(zhí)行 execution(public * com.example.demo.controller.*.*(..)) 定義在controller包和所有子包里的任意類的任意方法的執(zhí)行 execution(public * com.example.demo.controller..*.*(..))
B. 實現(xiàn)在不同的位置切入
- @Before 在切點前執(zhí)行方法,內容為指定的切點
- @After 在切點后,return前執(zhí)行
- @AfterReturning 切入點在 return內容之后(可用作處理返回值)
- @Around 切入點在前后切入內容,并自己控制何時執(zhí)行切入的內容
- @AfterThrowing 處理當切入部分拋出異常后的邏輯
C.@order(i) 標記切點的優(yōu)先級,i越小,優(yōu)先級越高
@order(i)注解說明
注解類,i值是,值越小,優(yōu)先級越高
注解方法,分兩種情況
注解的是 @Before 是i值越小,優(yōu)先級越高
注解的是 @After或@AfterReturning 中,i值越大,優(yōu)先級越高
具體實現(xiàn)
package com.example.demo.configure;
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.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
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.lang.reflect.Method;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class WebRequestLogAspect {
private final Logger loggerController = LoggerFactory.getLogger("ControllerRequest");
private final Logger logger = LoggerFactory.getLogger(WebRequestLogAspect.class);
ThreadLocal<Long> startTime = new ThreadLocal<>();
ThreadLocal<String> beanName = new ThreadLocal<>();
ThreadLocal<String> user = new ThreadLocal<>();
ThreadLocal<String> methodName = new ThreadLocal<>();
ThreadLocal<String> params = new ThreadLocal<>();
ThreadLocal<String> remoteAddr = new ThreadLocal<>();
ThreadLocal<String> uri = new ThreadLocal<>();
private static Map<String, Object> getFieldsName(ProceedingJoinPoint joinPoint) {
// 參數(shù)值
Object[] args = joinPoint.getArgs();
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String[] parameterNames = pnd.getParameterNames(method);
Map<String, Object> paramMap = new HashMap<>(32);
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i] + "(" + args[i].getClass().getSimpleName() + ")");
}
return paramMap;
}
@Pointcut("execution(public * com.example.demo.controller..*.*(..))")
public void webRequestLog() {
}
/**
* 前置通知,方法調用前被調用
* @param joinPoint
*/
@Before("webRequestLog()")
public void doBefore(JoinPoint joinPoint) {
try {
startTime.set(System.currentTimeMillis());
// 接收到請求,記錄請求內容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
beanName.set(joinPoint.getSignature().getDeclaringTypeName());
methodName.set(joinPoint.getSignature().getName());
uri.set(request.getRequestURI());
remoteAddr.set(getIpAddr(request));
user.set((String) request.getSession().getAttribute("user"));
} catch (Exception e) {
logger.error("***操作請求日志記錄失敗doBefore()***", e);
}
}
/**
* 環(huán)繞通知,環(huán)繞增強,相當于MethodInterceptor
* @param thisJoinPoint
*/
@Around("webRequestLog()")
public Object proceed(ProceedingJoinPoint thisJoinPoint) throws Throwable {
Object object = thisJoinPoint.proceed();
Map<String, Object> fieldsName = getFieldsName(thisJoinPoint);
params.set(fieldsName.toString());
return object;
}
/**
* 處理完請求返回內容
* @param result
*/
@AfterReturning(returning = "result", pointcut = "webRequestLog()")
public void doAfterReturning(Object result) {
try {
long requestTime = (System.currentTimeMillis() - startTime.get()) / 1000;
loggerController.info("請求耗時:" + requestTime + ", uri=" + uri.get() + "; beanName=" + beanName.get() + "; remoteAddr=" + remoteAddr.get() + "; user=" + user.get()
+ "; methodName=" + methodName.get() + "; params=" + params.get() + "; RESPONSE : " + result);
} catch (Exception e) {
logger.error("***操作請求日志記錄失敗doAfterReturning()***", e);
}
}
/**
* 獲取登錄用戶遠程主機ip地址
*
* @param request
* @return
*/
private String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")) {
//根據網卡取本機配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
ip = inet.getHostAddress();
}
}
// 多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
}
4.測試類
package com.example.demo.controller;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.dao.UserRepository;
import com.example.demo.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
public class Demo {
@RequestMapping (value = "test1")
public String test1(@RequestParam(defaultValue = "0") Integer id,@RequestParam(defaultValue = "0")String name){
return id+name;
}
@RequestMapping("hello")
public String hello() {
return "Hello World!";
}
@PostMapping("/updateStatus")
public Object updateStatus(@RequestBody JSONObject jsonParam) {
return jsonParam;
}
}
輸出到logs/request/info.log內容
2019-09-11 13:31:45.729 [http-nio-8080-exec-4] INFO ControllerRequest - 請求耗時:0, uri=/test1; beanName=com.example.demo.controller.Demo; remoteAddr=172.27.0.17; user=null; methodName=test1; params={name=abcdef(String), id=123(Integer)}; RESPONSE : 123abcdef
2019-09-11 13:32:16.692 [http-nio-8080-exec-5] INFO ControllerRequest - 請求耗時:0, uri=/updateStatus; beanName=com.example.demo.controller.Demo; remoteAddr=172.27.0.17; user=null; methodName=updateStatus; params={jsonParam={"id":"17","type":3,"status":2}(JSONObject)}; RESPONSE : {"id":"17","type":3,"status":2}
2019-09-11 13:33:32.584 [http-nio-8080-exec-7] INFO ControllerRequest - 請求耗時:0, uri=/hello; beanName=com.example.demo.controller.Demo; remoteAddr=172.27.0.17; user=null; methodName=hello; params={}; RESPONSE : Hello World!
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
淺談Ribbon、Feign和OpenFeign的區(qū)別
這篇文章主要介紹了淺談Ribbon、Feign和OpenFeign的區(qū)別。具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
Java結構型設計模式之組合模式Composite Pattern詳解
組合模式,又叫部分整體模式,它創(chuàng)建了對象組的數(shù)據結構組合模式使得用戶對單個對象和組合對象的訪問具有一致性。本文將通過示例為大家詳細介紹一下組合模式,需要的可以參考一下2022-11-11
JAVA中JSONObject對象和Map對象之間的相互轉換
這篇文章主要介紹了JAVA中JSONObject對象和Map對象之間的相互轉換,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01

