Springboot2.x 全局异常+日志

前言

本文是蚯蚓在自己搭建一个通用的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();
        }
	}
}

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值