一直以来,都想对logback + aop记录操作日志和参数校验做个总结,今天抽空参考(照抄)了一些他人的文章,加上自己的理解,总结一下。
1.项目结构
项目结构,我是创建了一个父项目,然后创建一个module springboot-demo7
父pom:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.lchtest</groupId>
<artifactId>springboot-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>springboot-demo1</module>
<module>springboot-demo2</module>
<module>springboot-demo3</module>
<module>springboot-demo4</module>
<module>springboot-demo5-mq</module>
<module>springboot-demo6</module>
<module>springboot-demo7</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<packaging>pom</packaging>
<properties>
<java.version>1.8</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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- controller跳转到页面需要 -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!-- 打包依赖,如果不加,执行maven打包,运行后会报错,no main manifest attribute -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
module的pom: 主要是引入 spring-boot-starter-aop 这个依赖,其他 的日志相关的jar包sprinboot都有了
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-demo</artifactId>
<groupId>com.example.lchtest</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springboot-demo7</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
从哪里知道已经有了呢?自己写的父pom里面依赖了spring-boot-starter-parent ,
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
点这个spring-boot-starter-parent进去,会看到
再点进去,可以看到下面这些jar都引入了,因此只需要引入一个spring-boot-starter-aop就可以了
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
在 resources目录下,创建 一个logback-spring.xml, 编写logback配置:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="10 seconds">
<!--<include resource="org/springframework/boot/logging/logback/base.xml" />-->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="D:/logs/app" />
<!--输出到控制台-->
<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>
<!--输出到文件-->
<!-- 时间滚动输出 level为 DEBUG 日志 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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/log-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>
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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/log-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>
<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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/log-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>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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/log-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>
<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、
以及指定<appender>。<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
如果未设置此属性,那么当前logger将会继承上级的级别。
addtivity:是否向上级logger传递打印信息。默认是true。
-->
<!--<logger name="org.springframework.web" level="info"/>-->
<!--<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>-->
<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-->
<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
不能设置为INHERITED或者同义词NULL。默认是DEBUG
可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-->
<!--开发环境:打印控制台-->
<springProfile name="dev">
<logger name="com.nmys.view" level="debug"/>
</springProfile>
<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>
</configuration>
2. 自定义注解
首先要有个启动类
package com.example.aoplogback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* springboot+ logback + aop记录操作日志,请求参数校验结果
*/
@SpringBootApplication
public class AopTest {
public static void main(String[] args) {
SpringApplication.run(AopTest.class,args);
}
}
自定义注解类,用来加在controller上面
package com.example.aoplogback.apspect;
import java.lang.annotation.*;
//注解放置的目标位置,METHOD是可注解在方法级别上
@Target(ElementType.METHOD)
// 注解在哪个阶段生效
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AppLog {
// 日志内容
String value() default "";
}
3. Controller
package com.example.aoplogback.controller;
import com.example.aoplogback.apspect.AppLog;
import com.example.aoplogback.exception.CustomException;
import com.example.aoplogback.pojo.User;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
public class DemoController {
/**
* 模拟新增用户请求;使用bindingResult,必须在切面中处理完参数校验结果之后调用ProceedingJoinPoint.proceed()方法执行真正的业务逻辑
* @param user 请求参数,json格式
* @param bindingResult 参数校验结果,bindingResult必须紧跟在被校验的参数后面,否则无法获得该参数的校验结果
* @return
*/
@AppLog(value = "新增用户") //自定义注解,通过切面记录操作类型
@PostMapping("/test/saveuser")
public Object saveuser(@RequestBody @Validated User user, BindingResult bindingResult){
System.out.println("模拟新增用户");
return user;
}
@AppLog(value = "测试全局异常处理")
@GetMapping("/test/globalexception")
public Object testGloablException(){
int i = 1/0;
return "testGloablException.";
}
@AppLog(value = "测试自定义异常处理")
@GetMapping("/test/customerexception")
public Object testCustomException(){
try{
int i = 1/0;
} catch (Exception e){
throw new CustomException(500, "divide by zero");
}
return "testCustomException.";
}
}
4.用到的pojo 自定义异常类
package com.example.aoplogback.pojo;
import java.io.Serializable;
import java.util.Date;
public class SysLog implements Serializable {
private Long id;
private String username; //用户名
private String operation; //操作
private String method; //方法名
private String params; //参数
private String ip; //ip地址
private Date createDate; //操作时间
@Override
public String toString() {
return "SysLog{" +
"id=" + id +
", username='" + username + '\'' +
", operation='" + operation + '\'' +
", method='" + method + '\'' +
", params='" + params + '\'' +
", ip='" + ip + '\'' +
", createDate=" + createDate +
'}';
} // getter setter 不贴了
}
package com.example.aoplogback.pojo;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
public class User {
@NotBlank(message = "用户名不能为空")
private String name;
@Length(min = 10, max = 300, message = "地址长度必须在10-300之间")
private String address;
@Max(value = 70, message = "年龄必须小于70")
@Min(value = 18, message = "年龄必须大于18")
private int age;
// getter setter 略
}
package com.example.aoplogback.pojo;
public class BaseResponse {
private int rtnCode;
private String rtnDesc;
private Object datas;
public BaseResponse(int rtnCode, String rtnDesc, Object datas) {
this.rtnCode = rtnCode;
this.rtnDesc = rtnDesc;
this.datas = datas;
}
}
自定义异常类:
package com.example.aoplogback.exception;
public class CustomException extends RuntimeException {
private int code;
private String msg;
public CustomException(){ }
public CustomException(int code, String msg){
this.code = code;
this.msg = msg;
}
}
全局异常处理类,要加上@ControllerAdvice注解
package com.example.aoplogback.exception;
import com.example.aoplogback.pojo.BaseResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler extends RuntimeException {
public GlobalExceptionHandler() {
}
/**
* 处理CustomException异常之外的所有异常
* @param e
* @param request
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object handleGloabalException(Exception e, HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
map.put("code", 500);
map.put("msg", e.getMessage());
map.put("url", request.getRequestURL());
return map;
}
/**
* 处理CustomException
* /test/customerexception 会走这个异常处理
* @param e
* @return
*/
@ExceptionHandler(value = CustomException.class)
public @ResponseBody Object handleException(CustomException e){
return new BaseResponse(e.getCode(), e.getMsg(), null);
}
}
5. 切面处理类
package com.example.aoplogback.apspect;
import com.alibaba.fastjson.JSONObject;
import com.example.aoplogback.exception.CustomException;
import com.example.aoplogback.pojo.BaseResponse;
import com.example.aoplogback.pojo.SysLog;
import com.example.aoplogback.utils.IPUtils;
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.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* 切面处理类
*/
@Component
@Aspect
public class AppLogAspect {
private static final Logger LOG = LoggerFactory.getLogger(AppLogAspect.class);
@Pointcut("@annotation(com.example.aoplogback.apspect.AppLog)")
public void appLogPointCut() { }
@Before("appLogPointCut()")
public void appLog(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 获取方法上的注解
AppLog annotation = method.getAnnotation(AppLog.class);
if (Objects.isNull(annotation)) {
return;
}
// 获取注解的值
String value = annotation.value();
SysLog log = new SysLog();
// 保存操作类型
log.setOperation(value);
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求方法名
String methodName = method.getName();
log.setMethod(className + "." + methodName);
// 获取请求参数
Object[] args = joinPoint.getArgs();
List<Object> arguments = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (!(args[i] instanceof ServletRequest || args[i] instanceof ServletResponse ||
args[i] instanceof MultipartFile || args[i] instanceof BindingResult) && args[i] != null) {
arguments.add(args[i]);
}
}
String paramter = "";
if (arguments != null) {
try {
paramter = JSONObject.toJSONString(arguments.toArray());
} catch (Exception e) {
paramter = arguments.toString();
}
}
log.setParams(paramter);
log.setCreateDate(new Date());
log.setIp("0:0:0:0:0:0:0:1");
// 打印日志,此处可以替换为保存到数据库等
LOG.info(log.toString());
}
// 定义参数校验切面的切点
@Pointcut("execution(* com.example.aoplogback.controller..*.*(..))")
public void pointCut() { }
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
BindingResult bindingResult = null;
// 从切点获取bindingResult
for (Object arg : pjp.getArgs()) {
if (arg instanceof BindingResult) {
bindingResult = (BindingResult) arg;
}
}
if (bindingResult != null && bindingResult.hasErrors()) {
StringBuffer sb = new StringBuffer();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
sb.append(fieldError.getField() + ":" + fieldError.getDefaultMessage() + ";");
}
// 参数校验失败打印日志
LOG.error(sb.toString());
// 接口返回错误信息 400 bad request, 通过BaseResponse返回或者直接抛自定义异常
// return new BaseResponse(400,"Parameter error:" + sb.toString(), null);
throw new CustomException(400, "Parameter error: " + sb.toString());
}
//执行目标方法(controller中真正需要执行的业务逻辑)
return pjp.proceed();
}
}
测试参数校验不通过时: 切面处理中抛出CustomException,被GlobalExceptionHandler捕获到,返回BaseResponse
error日志:
测试一个参数校验通过的:
查看切面打印的info日志:
2020-06-25 16:59:39.292 [http-nio-8080-exec-6] INFO com.example.aoplogback.apspect.AppLogAspect - SysLog{id=null, username=‘null’, operation=‘新增用户’, method=‘com.example.aoplogback.controller.DemoController.saveuser’, params=’[{“address”:“NanJing, yuhuatai”,“age”:20,“name”:“jackson”}]’, ip=‘0:0:0:0:0:0:0:1’, createDate=Thu Jun 25 16:59:39 CST 2020}
测试全局异常,可以发现,处理异常的是handleGloabalException 这个方法
代码地址:https://github.com/liuch0228/springboot/tree/master/springbootdemo/springboot-demo7
参考文章:
springboot2.0整合logback日志(详细)
springboot—spring aop 实现系统操作日志记录存储到数据库