前言
本文是蚯蚓在自己搭建一个通用的springboot后台管理系统框架,且边学习springboot边做的,若有不足之处,希望大家不吝留言,非常感谢。
架构要求:
1.使用全局异常处理,即控制层、业务层、持久层等分层中的方法均不使用try……catch对异常进行捕获处理,所有异常抛出,最终在请求结束后,统一处理。
2.日志需要记录完整的一次请求,包括请求参数:url、请求方式(如get、post等)、类路径、方法、方法参数名、方法入参值等,还有响应结果集、处理的时间等。当然还有一些请求的时间,响应的时间,异常的堆栈信息等等根据具体业务需要定制。
框架基于:
springboot2.x
aop(当要一次性记录整个请求时可用,也可以单独处理请求参数处理前、请求方法执行完之后等等,详见aop知识)
slf4j+logback(日志写入文件,可单独做全局异常记录,功能不如aop+slf4j+logback强大,但是简单的用于日常维护的异常排查还是可以的,但是一定要根据数据量进行存档,不然一个文件太大,打都打不开那就没意思了)
持久层JPA(与本文内容无关)
正文:
1.说明
本文中的aop和slf4j+logback两个是可以自行组合使用的
2.挂载
2.1AOP
maven的pom.xml中加入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2slf4j+logback
maven的pom.xml中加入
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
3.slf4j配置
在src/main/resources/下添加一个logback.xml的文件
这里就贴出来我的配置:信息、警告、异常按日期分离文件
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="true": 打印logback内部状态(默认当logback运行出错时才会打印内部状态 ),配置该属性后打印条件如下(同时满足):
1、找到配置文件 2、配置文件是一个格式正确的xml文件 也可编程实现打印内部状态,例如: LoggerContext lc = (LoggerContext)
LoggerFactory.getILoggerFactory(); StatusPrinter.print(lc); -->
<!-- scan="true": 自动扫描该配置文件,若有修改则重新加载该配置文件 -->
<!-- scanPeriod="30 seconds" : 配置自动扫面时间间隔(单位可以是:milliseconds, seconds, minutes
or hours,默认为:milliseconds), 默认为1分钟,scan="true"时该配置才会生效 -->
<configuration debug="false" scan="false" scanPeriod="30 seconds" packagingData="true">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="D:/Log_test/logs" />
<!-- 设置 logger context 名称,一旦设置不可改变,默认为default -->
<contextName>default</contextName>
<!--控制台日志, 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<!-- 出错日志 appender -->
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<!-- log.dir 在maven profile里配置 -->
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger-%line - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印错误日志 -->
<level>ERROR</level>
<!--
onMatch="ACCEPT" 表示匹配该级别及以上
onMatch="DENY" 表示不匹配该级别及以上
onMatch="NEUTRAL" 表示该级别及以上的,由下一个filter处理,如果当前是最后一个,则表示匹配该级别及以上
onMismatch="ACCEPT" 表示匹配该级别以下
onMismatch="NEUTRAL" 表示该级别及以下的,由下一个filter处理,如果当前是最后一个,则不匹配该级别以下的
onMismatch="DENY" 表示不匹配该级别以下的
-->
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- info日志 appender -->
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger-%line - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印错误日志 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 出错日志 appender -->
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<!-- log.dir 在maven profile里配置 -->
<fileNamePattern>${LOG_HOME}/warn-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger-%line - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印错误日志 -->
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--文件日志, 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 当前活动日志文件名 -->
<file>./my_log.log</file>
<!-- 文件滚动策略根据%d{patter}中的“patter”而定,此处为每天产生一个文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档文件名“.zip或.gz结尾”,表示归档文件自动压缩 -->
<FileNamePattern>./my_log%d{yyyyMMdd}.log.zip</FileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志级别若没显示定义,则继承最近的父logger(该logger需显示定义level,直到rootLogger)的日志级别-->
<!-- logger的appender默认具有累加性(默认日志输出到当前logger的appender和所有祖先logger的appender中),可通过配置 “additivity”属性修改默认行为-->
<logger name="cn.com.startdima" additivity="false">
<level value="DEBUG"/>
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="INFO"/>
<appender-ref ref="WARN"/>
<!-- <appender-ref ref="FILE"/> -->
</logger>
<!--控制台打印资源加载信息-->
<!-- 至多只能配置一个root -->
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
4.全局异常处理
这里描述的是简单的单独的全局异常处理(使用logback写入日志文件)
因为这个不是当前要用的,所以没有深入编码。
import java.io.BufferedReader;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 全局异常日志处理
*
*/
@RestControllerAdvice
public class WebExceptionHandler
{
private final static Logger logger = LoggerFactory.getLogger(WebExceptionHandler.class);
@ExceptionHandler
public RestResult defaultException(Exception e) {
// 记录异常堆栈信息
String strackTrace = ExceptionUtil.getStrackTrace(e);
logger.error("发生了未知异常", strackTrace);
printLog(e);
// 发送邮件通知技术人员
return RestResult.failure(ResultCode.SYSTEM_INNER_EXCEPTION);
}
private void printLog(Exception e)
{
//记录http请求
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
LoggerVo log = new LoggerVo();
log.setHttpMethod(request.getMethod());
log.setUrl(request.getRequestURL().toString());
log.setIp(request.getRemoteAddr());
// log.setLogType(AppConstants.LOG_TYPE_HTTP);
// log.setReqParams(JsonUtil.objectToJson(request.getParameterMap()));
// log.setRespParams(resp);
//logger.error(">>>"+JsonUtil.objectToJson(logEntity));
}
}
5.AOP全局异常处理和日志(重点)
这里直接指定切点为controller包下的所有类。当然也可以自己写一个注解,然后在方法上添加这个注解,指定这个注解来处理。
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import 。。。.comm.util.ExceptionUtil;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
@Aspect
@Component
public class WebControllerAop
{
/**
* 指定切点
* 匹配 。。。.system.controller包及其子包下的所有类的所有方法
*/
@Pointcut("execution(public * 。。。.system.controller.*.*(..))")
public void webLog()
{
}
@Before(value = "webLog()")
public void doBefore(JoinPoint joinPoint)
{
System.out.println("我是前置通知!!!");
}
/**
* 处理完请求返回内容
* @param ret
* @throws Throwable
*/
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
System.out.println("方法的返回值 : " + ret);
}
/**
* 后置异常通知
* @param jp
*/
@AfterThrowing("webLog()")
public void throwss(JoinPoint jp){
System.out.println("方法异常时执行.....");
}
@Around("webLog()")
public Object logHandler(ProceedingJoinPoint joinPoint) throws Throwable
{
long startTime=System.currentTimeMillis();
Object result= null;
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest req = attributes.getRequest();
// 记录下请求内容
String url = req.getRequestURL().toString();
String httpMethod = req.getMethod();
String ip = req.getRemoteAddr();
System.out.println("请求URL : " + url);
System.out.println("HTTP_METHOD : " + httpMethod);
System.out.println("IP : " + ip);
//获取目标方法的参数信息
Object[] obj = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
//AOP代理类的名字
String classPath = signature.getDeclaringTypeName();
System.out.println("类: "+classPath);
//代理的是哪一个方法
String methodName = signature.getName();
System.out.println("方法:"+ methodName);
String suffixM = classPath.replace("cn.com.startdima.", "");
String module = suffixM.substring(0, suffixM.indexOf("."));
System.out.println("模块: " + module);
MethodSignature methodSignature = (MethodSignature) signature;
// 入参
StringBuilder params = new StringBuilder();
// 获取方法参数名称
String[] paramNames = methodSignature.getParameterNames();
Object[] paramValues = joinPoint.getArgs();
int length = paramNames.length;
for(int i=0; i<length; i++)
{
params.append(paramNames[i]).append("=").append(paramValues[i]).append("</br>");
}
long costTime;
try
{
// 执行controller方法
result = joinPoint.proceed();
}
catch (Throwable throwable)
{
String strackTrace = ExceptionUtil.getStrackTrace(throwable);
String exception = throwable.getClass()+":"+throwable.getMessage();
costTime=System.currentTimeMillis()-startTime;
//log.error("请求时间:{},请求耗时:{},请求类名:{},请求方法:{},请求参数:{},请求结果:{}",startTime,costTime,className,methodName,params.toString(),exception);
System.out.println("出现异常");
}
costTime=System.currentTimeMillis()-startTime;
// 存入数据库或日志文件
return result;
}
}
最后附带一个异常堆栈记录的工具类:
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 异常工具类
* @author qiuyin
*
*/
public class ExceptionUtil
{
/**
* 获取异常堆栈信息
* @param throwable
* @return
*/
public static String getStrackTrace(Throwable throwable)
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
try
{
throwable.printStackTrace(pw);
return sw.toString();
} finally
{
pw.close();
}
}
}