在开发以及调试过程中,程序员对日志的需求是非常大的,出了什么问题,都要通过日志去进行排查,但是如果日志不清或者杂乱无章,则不利于维护
这边就比较详细的列举几种类型的日志,供大家参考
首先明白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() + " "); StackTraceElement[] messages = e.getStackTrace(); int length = messages.length; for (int i = 0; i < length; i++) { stringBuffer.append(" "+messages[i].toString()+" "); } 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() + " "); StackTraceElement[] messages = e.getStackTrace(); int length = messages.length; for (int i = 0; i < length; i++) { stringBuffer.append(" "+messages[i].toString()+" "); } return stringBuffer.toString(); } }
这个切面里未捕获到的异常也全部做特定处理