为了保证服务的高可用,及时发现问题,迅速解决问题,为应用添加log是必不可少的。

但是随着项目的增大,方法增多,每个方法加单独加日志处理会有很多冗余

那在SpringBoot项目中如何统一的处理Web请求日志?

基本思想:

  采用AOP的方式,拦截请求,写入日志

AOP 是面向切面的编程,就是在运行期通过动态代理的方式对代码进行增强处理

基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

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 的功能即是启动状态

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是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<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>
<!--日志文档保留天数-->
<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>
<!--日志文档保留天数-->
<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>
<!--日志文档保留天数-->
<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>
<!--日志文档保留天数-->
<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..实现

实现切面的注解

  (1)类注解

    A. @Aspect 将一个java类定义为切面类

    B. @order(i) 标记切面类的处理优先级,i值越小,优先级别越高。可以注解类,也能注解到方法上

  (2)方法注解

    A. @Pointcut 定义一个切入点,可以是一个表达式

execution表达式,eg:

任意公共方法的执行
execution(public * *(..)) 任何一个以“set”开始的方法的执行
execution(* set*(..)) 定义在controller包里的任意方法的执行
execution(public * com.example.demo.controller.*(..)) 定义在controller包里的任意方法的执行
execution(public * com.example.demo.controller.*.*(..)) 定义在controller包和所有子包里的任意类的任意方法的执行
execution(public * com.example.demo.controller..*.*(..))

    B. 实现在不同的位置切入

@Before   在切点前执行方法,内容为指定的切点

@After    在切点后,return前执行

@AfterReturning  切入点在 return内容之后(可用作处理返回值)

@Around       切入点在前后切入内容,并自己控制何时执行切入的内容

@AfterThrowing  处理当切入部分抛出异常后的逻辑

    C.@order(i) 标记切点的优先级,i越小,优先级越高

@order(i)注解说明

注解类,i值是,值越小,优先级越高

注解方法,分两种情况

注解的是 @Before 是i值越小,优先级越高

注解的是 @After或@AfterReturning 中,i值越大,优先级越高

具体实现

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) {
// 参数值
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);
}
} /**
* 环绕通知,环绕增强,相当于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!

spring boot使用AOP统一处理web请求的更多相关文章

  1. spring Boot使用AOP统一处理Web请求日志记录

    1.使用spring boot实现一个拦截器 1.引入依赖: <dependency>   <groupId>org.springframework.boot</grou ...

  2. 46. Spring Boot中使用AOP统一处理Web请求日志

    在之前一系列的文章中都是提供了全部的代码,在之后的文章中就提供核心的代码进行讲解.有什么问题大家可以给我留言或者加我QQ,进行咨询. AOP为Aspect Oriented Programming的缩 ...

  3. Springboot中使用AOP统一处理Web请求日志

    title: Springboot中使用AOP统一处理Web请求日志 date: 2017-04-26 16:30:48 tags: ['Spring Boot','AOP'] categories: ...

  4. SpringBoot2.0 使用AOP统一处理Web请求日志(完整版)

    一,加入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...

  5. Spring Boot中使用AOP统一处理Web请求日志

    AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是Spring框架中的一个重要内容,它通 ...

  6. (转)Spring Boot中使用AOP统一处理Web请求日志

    AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是Spring框架中的一个重要内容,它通 ...

  7. 转:Spring Boot中使用AOP统一处理Web请求日志

    在spring boot中,简单几步,使用spring AOP实现一个拦截器: 1.引入依赖: <dependency> <groupId>org.springframewor ...

  8. Spring Boot使用AOP在控制台打印请求、响应信息

    AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等. AOP简介 AOP全称Aspect Oriented Programming,面向切面,AOP主要实现的 ...

  9. Spring Boot2.0之统一处理web请求日志

    试问,你的项目中,如果有几万个方法,你还这么写log.info("name"+name+",age"+age )日志么?low~ 所以用AOP呀 1.首先创建个 ...

随机推荐

  1. Bootstrap 学习笔记1

    <img src="..." class="img-responsive" alt="响应式图像"> 通过添加 img-resp ...

  2. 基于变分自编码器(VAE)利用重建概率的异常检测

    本文为博主翻译自:Jinwon的Variational Autoencoder based Anomaly Detection using Reconstruction Probability,如侵立 ...

  3. 【Linux】linux ln文件夹的链接(转)

    1 文件的链接ln -s a.py b.pya.py 软链接到b.py (无论 b.py 文件存在与否 都可以成功)2 文件夹的链接ln -s a ba文件夹软链接到b文件夹(b文件夹不能提前创建) ...

  4. 验证码破解 | Selenium模拟登录简书

    使用超级鹰打码平台处理登录的文字点击验证码 import time from io import BytesIO from PIL import Image from selenium import ...

  5. 什么是JSON Web Token

    JSON Web Token(JWT)是一个基于 RFC 7519 的开放数据标准,它定义了一种宽松且紧凑的数据组合方式,使用 JSON  对象在各应用之间传输加密信息.该 JSON 对象可以通过数字 ...

  6. 洛谷 P5408 【模板】第一类斯特林数·行

    传送门 首先,有 \[ x^{\overline n}=\sum_k\begin{bmatrix}{n\\ k}\end{bmatrix}x^{k}\\ \] 那么我们只需要求出\(x^{\overl ...

  7. 05-树9 Huffman Codes (30 分)

    In 1953, David A. Huffman published his paper "A Method for the Construction of Minimum-Redunda ...

  8. shell for循环输出目录下的文件名

    test.sh文件: #!/bin/bash for file in `ls`;do echo $file done 输出 bogon:test macname$ chmod +x test.sh b ...

  9. MySql的执行计划

    一.什么是数据库执行计划: MySQL执行计划是sql语句经过查询优化器后,查询优化器会根据用户的sql语句所包含的字段和内容数量等统计信息,选择出一个执行效率最优(MySQL系统认为最优)的执行计划 ...

  10. Debian系Linux 发行版 源配置说明

    概述: 本文是在逛论坛是的发现,借鉴过来,以便学习.源列表主文件 /etc/apt/sources.list同时也可创建独立的源配置文件到 /etc/apt/sources.list.d/* 下 so ...