Spring Boot与Logback的运用(自定义异常+AOP)

在开发以及调试过程中,程序员对日志的需求是非常大的,出了什么问题,都要通过日志去进行排查,但是如果日志不清或者杂乱无章,则不利于维护

这边就比较详细的列举几种类型的日志,供大家参考

首先明白logback日志是Spring Boot自带的,不需要引入额外的包

            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-access</artifactId>
                <version>${logback.version}</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>${logback.version}</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
                <version>${logback.version}</version>
            </dependency>

点进pom里的核心依赖,就能看见上面几个,是由Spring Boot自动依赖配置好的,我们只要直接使用就好了

比较简单的是直接在application的配置文件里 写参数配置就行了,他提供了日志级别,日志输出路径等,也能满足基本的日志输出

我们这通过xml文件进行配置 logback-spring.xml

这样就能直接引用到xml了,但是为什么能引用到了

就是在logback里有个默认的机制,内部会有几种标准的文件格式,在LogbackLoggingSystem里标注了

@Override
    protected String[] getStandardConfigLocations() {
        return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
                "logback.xml" };
    }

所以最为标准的为这里面的四种文件格式,但是如果项目中没有,他还提供了扩展文件格式 就是在后面拼上-spring,例如logback.xml 扩展为logback-spring.xml

ok

下面看下xml里面的内容:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--定义日志文件的存储地址 可以在LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="logs" />
    

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <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}}" />
    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/category-server-log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>DENY</onMatch>
            <onMismatch>NEUTRAL</onMismatch>
        </filter>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>NEUTRAL</onMismatch>
        </filter>

    </appender>

    <!-- 出错日志 appender  -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <!-- log.dir 在maven profile里配置 -->
            <FileNamePattern>${LOG_HOME}/category-server-error-log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 自己打印的日志文件,用于记录重要日志信息 -->
    <appender name="MY_INFO_FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/category-server-myinfo-log.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>15</MaxHistory>
            <!--日志文件最大的大小-->
            <MaxFileSize>10MB</MaxFileSize>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>  
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <logger name="my_info" additivity="true">
        <appender-ref ref="MY_INFO_FILE"/>
    </logger>
    <!--myibatis log configure-->
    <logger name="com.example.demo" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>
        <!-- 日志输出级别 -->
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="FILE" />
            <appender-ref ref="ERROR_FILE" />
        </root>
</configuration>

这里一共有四块内容,第一是console的日志输出,第二是系统运行日志,第三是警告以上的日志输出(基本上是程序出错日志),第四种是自定义日志

每一块日志由一个appender标签引入

CONSOLE是控制台日志输出,只要规定个格式就行了

FILE是系统运行日志,系统的所有运行信息都会保留,正常我们会把这部分信息保存在硬盘日志文件中,按天按文件大小保存,因为这个内容实在是比较多

ERROR_FILE是WARN级别以上的日志,这块是开发人员和运维人员最多关注的,因为基本上所有的bug都会在这个里面体现

MY_INFO_FILE是自定义日志,想定义自己的日志文件,记录一些重要的信息

这里的日志都是以文件的形式保存在本地,当然像WARN级别以上日志可以异步保存到数据库

 

日志文件定义好后,接下来就要开始定义业务逻辑了

在针对一些异常日志,我们想尽可能完整准确的抛出异常,一眼就能知道是什么问题,这里我们就需要自定义异常,最多的就是像空指针,数组越界等常见异常

定义基础异常类BaseException继承他的父类RuntimeException

public class BaseException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public BaseException() {
        super();
        // TODO Auto-generated constructor stub
    }

    public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
        // TODO Auto-generated constructor stub
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
        // TODO Auto-generated constructor stub
    }

    public BaseException(String message) {
        super(message);
        // TODO Auto-generated constructor stub
    }

    public BaseException(Throwable cause) {
        super(cause);
        // TODO Auto-generated constructor stub
    }
    
    
    
}

然后全局异常处理类:GlobalExceptionHandler

@CrossOrigin
@RestControllerAdvice
public class GlobalExceptionHandler{

    private static Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    private static final String APPLICATION_JSON = "application/json";
    private static final String UTF_8 = "UTF-8";
     
    /**
     * BaseException 处理类
    * @Title: HandleBaseException  
    * @Description: TODO
    * @param @param e
    * @param @return
    * @return ResponseMsg
    * @throws
     */
    @ExceptionHandler(BaseException.class)
    @ResponseBody
    public ResponseMsg HandleBaseException(RuntimeException e){
        //只能输出捕获到的异常,未捕获到的异常不输出到日志,或者通过aop拦截器拦截所有方法
        LOGGER.error(getExceptionDetail(e));
        //返回失败信息
        Route route = new Route();
        ResponseMsg responseMsg = new ResponseMsg(route,ReturnMsgEnum.INTERNAL_ERROR.getCode(),
                 ReturnMsgEnum.INTERNAL_ERROR.getMsg(), "");
        return responseMsg;
    }
    
    @ExceptionHandler(GlobalException.class)
    @ResponseBody
    public ResponseMsg HandleGlobalException(Exception e){
        //只能输出捕获到的异常,未捕获到的异常不输出到日志,或者通过aop拦截器拦截所有方法
        LOGGER.error(getExceptionDetail(e));
        //返回失败信息
        Route route = new Route();
        ResponseMsg responseMsg = new ResponseMsg(route,ReturnMsgEnum.INTERNAL_ERROR.getCode(),
                 ReturnMsgEnum.INTERNAL_ERROR.getMsg(), "系统未捕获该异常");
        return responseMsg;
    }
    
    public String getExceptionDetail(Exception e) {
        StringBuffer stringBuffer = new StringBuffer(e.toString() + "\n");
        StackTraceElement[] messages = e.getStackTrace();
        int length = messages.length;
        for (int i = 0; i < length; i++) {
            stringBuffer.append("\t"+messages[i].toString()+"\n");
        }
        return stringBuffer.toString();
    }
    
}
@RestControllerAdvice:表明他是一个Controller 并且是异常拦截的统一处理类
定义针对自定义异常的处理方法:用
@ExceptionHandler(BaseException.class)注解标注
BaseException就是刚才的自定义异常
之后所有抛出的BaseException都会由他处理

自定义异常我们都能轻松捕获到了,并且输出到日志里了

如果有些异常我们没有捕获到,我们就可以定义一个切面,让所有方法都经过这个切面处理
/**
 * 处理未捕获到的异常
* @ClassName: SpringAOP  
* @author Mr.Chengjq
* @date 2018年10月17日  
* @Description: TODO
 */
@Aspect
@Configuration
public class SpringAOP {
    private static final Logger logger = LoggerFactory.getLogger(SpringAOP.class);
     
    /**
     * 定义切点Pointcut
     * 第一个*号:表示返回类型, *号表示所有的类型
     * 第二个*号:表示类名,*号表示所有的类
     * 第三个*号:表示方法名,*号表示所有的方法
     * 后面括弧里面表示方法的参数,两个句点表示任何参数
     */
    @Pointcut("execution(* com.example.demo..*.*(..))")
    public void executionService() {
 
    }
 
 
    /**
     * 方法调用之前调用
     * @param joinPoint
     */
    @Before(value = "executionService()")
    public void doBefore(JoinPoint joinPoint){
 
        //添加日志打印
        String requestId = String.valueOf(UUID.randomUUID());
        MDC.put("requestId",requestId);
        logger.info("=====>@Before:请求参数为:{}",Arrays.toString(joinPoint.getArgs()));
 
    }
 
    /**
     * 方法之后调用
     * @param joinPoint
     * @param returnValue 方法返回值
     */
    @AfterReturning(pointcut = "executionService()",returning="returnValue")
    public void  doAfterReturning(JoinPoint joinPoint,Object returnValue){
 
        logger.info("=====>@AfterReturning:响应参数为:{}",returnValue);
        // 处理完请求,返回内容
        MDC.clear();
    }
 
    /**
     * 统计方法执行耗时Around环绕通知
     * @param joinPoint
     * @return
     */
    @Around("executionService()")
    public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable{
 
        //获取开始执行的时间
        long startTime = System.currentTimeMillis();
 
        // 定义返回对象、得到方法需要的参数
        Object obj = null;
        //Object[] args = joinPoint.getArgs();
        try {
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            // TODO: handle exception
            logger.error(getExceptionDetail(e));
            throw new GlobalException();
        }
        
        // 获取执行结束的时间
        long endTime = System.currentTimeMillis();
        //MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
        // 打印耗时的信息
        logger.info("=====>处理本次请求共耗时:{} ms",endTime-startTime);
        return obj;
    }
    
    
    public String getExceptionDetail(Throwable e) {
        StringBuffer stringBuffer = new StringBuffer(e.toString() + "\n");
        StackTraceElement[] messages = e.getStackTrace();
        int length = messages.length;
        for (int i = 0; i < length; i++) {
            stringBuffer.append("\t"+messages[i].toString()+"\n");
        }
        return stringBuffer.toString();
    }

}
这个切面里未捕获到的异常也全部做特定处理



 

posted on 2018-10-22 14:21  Mr.chengJQ  阅读(1809)  评论(0编辑  收藏  举报

导航