spring boot使用AOP统一处理web请求
为了保证服务的高可用,及时发现问题,迅速解决问题,为应用添加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请求的更多相关文章
- spring Boot使用AOP统一处理Web请求日志记录
1.使用spring boot实现一个拦截器 1.引入依赖: <dependency> <groupId>org.springframework.boot</grou ...
- 46. Spring Boot中使用AOP统一处理Web请求日志
在之前一系列的文章中都是提供了全部的代码,在之后的文章中就提供核心的代码进行讲解.有什么问题大家可以给我留言或者加我QQ,进行咨询. AOP为Aspect Oriented Programming的缩 ...
- Springboot中使用AOP统一处理Web请求日志
title: Springboot中使用AOP统一处理Web请求日志 date: 2017-04-26 16:30:48 tags: ['Spring Boot','AOP'] categories: ...
- SpringBoot2.0 使用AOP统一处理Web请求日志(完整版)
一,加入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...
- Spring Boot中使用AOP统一处理Web请求日志
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是Spring框架中的一个重要内容,它通 ...
- (转)Spring Boot中使用AOP统一处理Web请求日志
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是Spring框架中的一个重要内容,它通 ...
- 转:Spring Boot中使用AOP统一处理Web请求日志
在spring boot中,简单几步,使用spring AOP实现一个拦截器: 1.引入依赖: <dependency> <groupId>org.springframewor ...
- Spring Boot使用AOP在控制台打印请求、响应信息
AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等. AOP简介 AOP全称Aspect Oriented Programming,面向切面,AOP主要实现的 ...
- Spring Boot2.0之统一处理web请求日志
试问,你的项目中,如果有几万个方法,你还这么写log.info("name"+name+",age"+age )日志么?low~ 所以用AOP呀 1.首先创建个 ...
随机推荐
- JQuery系列(6) - jQuery设计思想
jQuery是目前使用最广泛的javascript函数库. 据统计,全世界排名前100万的网站,有46%使用jQuery,远远超过其他库.微软公司甚至把jQuery作为他们的官方库. JQuery设计 ...
- jmeter对接口测试入参进行MD5加密的5种方式
在使用jmeter做测试的过程中,经常需要对请求的入参进行加密,下面列举几种常用的方法,以登录请求密码需要MD5加密为例. 虽然可以先把参数化的明文密码都先md5加密,而不是在登录前先执行加密,但是实 ...
- live-pusher属性值的改变
例如:组件推流过程中,切换前后摄像头时,要改变mirror的值并使其生效: LivePusherContext = wx.createLivePusherContext() 1. LivePusher ...
- rxswift的双向绑定
将值域与控件域一同提升为rx的monand域,然后进行绑定. 类型提升. 在之前的文章样例中,所有的绑定都是单向的.但有时候我们需要实现双向绑定.比如将控件的某个属性值与 ViewModel里的某个 ...
- 7-zip命令行详解
一.简介 7z,全称7-Zip, 是一款开源软件.是目前公认的压缩比例最大的压缩解压软件. 主要特征: # 全新的LZMA算法加大了7z格式的压缩比 # 支持格式: * 压缩 / 解压缩:7z, XZ ...
- 异常0xc000041d的抛出过程
为了说明这个过程,我们必须写一个示例程序,如下: #include "stdafx.h" #include <tchar.h> #include <stdio.h ...
- golang 配置goproxy 几个可选的地址
对于golang 语言的开发,对于国内来说有点被动,需要想各种方法,一般的解决方法如下: 使用代理工具(翻墙) 配置goproxy 目前发现的几个不错的goproxy 阿里云 配置如下: expo ...
- MySQL 8.0.18 InnoDB Cluster 主从(MGR)完整安装配置
提示: MySQL InnoDB Cluster底层依赖Group Replication模式,至少3台机器 1. 准备3台 CentOS Linux 7 (Core), 修改各主机名:db-hos ...
- ABP 03 解决 编辑User报错
1.编辑用户时,报错.后面有跟解决方案. 解决方案1: 2.导致出错的原因是这样的,这里的功能是请求服务端的html页面,渲染后显示编辑页面. 关键点是默认参数那儿 路径:\aspnet-core\s ...
- A`>G?~C009
A`>G?~C009 这场怎么才5题...看完猫的提交记录以为猫猫没写这场F A Multiple Array 直接做 B Tournament 直接树d C Division into Two ...