项目简介:
Java framework for enterprise web applications
- Vue
- SpringBoot 2.1.6
- …
[第二篇 日志的管理]
项目使用的是SpringBoot自带的Logback日志, SpringBoot的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
或
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
均带有坐标spring-boot-starter-logging也就是Logback的依赖;
先介绍一下Maven多环境打包,项目中使用了本地,开发,测试等多个环境,为了开发测试方便,使用了maven多环境打包插件,在各个子模块项目的pom.xml中配置了以及,打包时候-P命令后指定环境即可,如
- mvn clean package -DskipTests -Plocal
- mvn clean package -DskipTests -Ptest
<profiles>
<profile>
<id>local</id>
<activation>
<!--activeByDefault为true表示,默认激活id为local的profile-->
<activeByDefault>true</activeByDefault>
</activation>
<!-- properties里面可以添加自定义节点如下添加了一个env节点-->
<properties>
<!-- 这个节点的值可以在maven的其他地方引用,可以简单理解为定义了一个叫profileActive的变量 -->
<profileActive>local</profileActive>
</properties>
</profile>
<profile>
<id>dev</id>
<properties>
<profileActive>dev</profileActive>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<id>xs</id>
<properties>
<profileActive>xs</profileActive>
</properties>
</profile>
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<!-- 打包时排除文件 -->
<exclude>application*.yml</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<!--filtering 需要设置为 true,这样在include的时候,才会把
配置文件中的@profileActive@ 这个maven`变量`替换成当前环境的对应值 -->
<filtering>true</filtering>
<!-- 打包时所包含得文件 -->
<includes>
<include>application-common.yml</include>
<include>application-common-${profileActive}.yml</include>
<include>application.yml</include>
<include>application-appcommon.yml</include>
<include>application-${profileActive}.yml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Logback日志配置如下(以开发环境为例):
Logback_spring.xml ,使用了默认的彩色日志,在Pattern标签下可以修改日志的格式颜色等(比如将%clr改成%yellow),设置Mybatis执行的日志为Debug就可以在控制台看到sql的执行过程,想要自定义某个包下的日志级别,可以使用logger标签或者在yml配置文件中直接配置,还可以定义每日生成html日志等等,实用性不大不再贴出.
/**
* logger标签自定义日志级别
*/
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="ERROR"/>
<logger name="org.springframework.cloud.sleuth" level="ERROR"/>
<logger name="com.springms" level="ERROR"/>
<logger name="zipkin" level="ERROR"/>
<logger name="自己定义的包路径" level="ERROR"/>
/**
* yml配置文件自定义日志级别
*/
logging:
config: classpath:config/logback-config.xml
level:
com:
xxx:
index: debug(指定某个包下日志级别)
/**
* @Description Logback_spring.xml
*/
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="LOG_PATH" source="custom-log.path" defaultValue="/home/log"/>
<springProperty scope="context" name="LOG_FILENAME" source="custom-log.filename" defaultValue="log.log"/>
<springProperty scope="context" name="ROOT_LEVEL" source="custom-log.root.level" defaultValue="info"/>
<springProperty scope="context" name="MYBATIS_LEVEL" source="custom-log.mybatis.level" defaultValue="info"/>
<springProperty scope="context" name="MYBATIS_DAO_PATH" source="custom-log.mybatis.dao-path" defaultValue="com.talkweb.edu.*.dao"/>
<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"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr(%-80.80logger{79}){cyan} %clr(:){faint} %m%n%wEx</pattern>
</encoder>
</appender>
<!--文件输出的格式设置 -->
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志日常打印文件 -->
<file>${LOG_PATH}/${LOG_FILENAME}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>
${LOG_PATH}/${LOG_FILENAME}-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<!-- 如果按天来回滚,则最大保存时间为60天,60天之前的都将被清理掉 -->
<maxHistory>5</maxHistory>
<!-- 日志总保存量为10GB -->
<totalSizeCap>10GB</totalSizeCap>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!--文件达到 最大128MB时会被压缩和切割 -->
<maxFileSize>128MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 文件输出的日志 的格式 -->
<encoder>
<pattern>
%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%file:%line] - %msg%n
</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- Safely log to the same file from multiple JVMs. Degrades performance! -->
<prudent>false</prudent>
</appender>
<!--myibatis log configure -->
<logger name="${MYBATIS_DAO_PATH}" level="${MYBATIS_LEVEL}"/>
<!-- 日志输出级别 -->
<root level="${ROOT_LEVEL}">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
日志配置文件已述完,项目中日志用的是Aop注解日志日志,定义了一个日志的切面SystemLogAspect ,使用注解@SystemControllerLog和@SystemServiceLog可以完成控制层和服务层基本访问日志,其中还包含了对异常的处理,在服务层类上加上@Slf4j按需使用log.info(“日志信息”).
@Aspect
@Component
public class SystemLogAspect {
// 本地异常日志记录对象
private static final ServerLogger logger = new ServerLogger(SystemLogAspect.class);
// Service层切点
@Pointcut("@annotation(com.xxx.log.SystemServiceLog)")
public void serviceAspect() {
}
// Controller层切点
@Pointcut("@annotation(com.xxx.log.SystemControllerLog)")
public void controllerAspect() {
}
private String project = "index";
/**
* 前置通知 用于拦截Controller层记录用户的操作
*
* @param joinPoint
* 切点
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) {
try {
if (null == RequestContextHolder.getRequestAttributes()) {
return;
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String parentLogId = request.getHeader(LoggerConstants.HTTP_HEAD_PARENT_LOG_ID);
String tenant_org_id = request.getHeader(Constants.HTTP_HEAD_TENANT_ORG_ID);
// 获取用户请求方法的参数并序列化为JSON格式字符串
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for (int i = 0; i < joinPoint.getArgs().length; i++) {
params += GsonUtil.toJson(joinPoint.getArgs()[i]) + ";";
}
}
String logId = UUID.randomUUID().toString();
// 读取session中的用户
// 请求的IP
String ip = request.getRemoteAddr();
SystemLog log = new SystemLog();
log.setLogId(logId);
log.setLogClass(joinPoint.getTarget().getClass().getName());
log.setMethod(joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()");
log.setQuestIp(ip);
log.setLogType(LoggerConstants.CONTROLL_LOG_TYPE);
log.setDescription(getControllerMethodDescription(joinPoint));
log.setCreatetime(new Date());
log.setProject(project);
log.setQuestParam(params);
if (StringUtils.isNotBlank(parentLogId)) {
log.setParentLogId(parentLogId);
}
String logJson = GsonUtil.toJson(log);
// 存放到本地线程,以便下级请求获取到改日志ID
ThreadLocalHelper.put(LoggerConstants.HTTP_HEAD_PARENT_LOG_ID, logId);
// 本地线程,存放租户对应的单位Id
ThreadLocalHelper.put(Constants.HTTP_HEAD_TENANT_ORG_ID,tenant_org_id);
logger.info(logJson);
} catch (Exception e) {
// 记录本地异常日志
logger.error(e.toString(), e);
}
}
/**
* 前置通知 用于拦截Controller层记录用户的操作
*
* @param proceedingJoinPoint
* 切点
*/
@Around("controllerAspect()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object result = null;
try {
result = proceedingJoinPoint.proceed();
} catch (Exception e) {
Response<String> resultDTO = new Response<String>();
MessageResult messageResult = new MessageResult();
if (e instanceof ParamException) {
ParamException errorCodeException = (ParamException) e;
messageResult.setResultCode(errorCodeException.getCode());
messageResult.setResultMsg(errorCodeException.getMessage());
messageResult.setErrorParam(errorCodeException.getParaName());
} else if (e instanceof MicroServiceException) {
messageResult.setResultCode(IStateCode.SQL_ERORR);
messageResult.setResultMsg(LoggerConstants.DB_EXCEPTION_ERROR);
} else {
messageResult.setResultCode(IStateCode.SYSTEM_ERORR);
messageResult.setResultMsg(LoggerConstants.SYSTEM_ERROR);
}
resultDTO.setServerResult(messageResult);
logger.error(e.getMessage(),e);
String params = "";
if (proceedingJoinPoint.getArgs() != null && proceedingJoinPoint.getArgs().length > 0) {
for (int i = 0; i < proceedingJoinPoint.getArgs().length; i++) {
params += GsonUtil.toJson(proceedingJoinPoint.getArgs()[i]) + ";";
}
}
/* ==========记录本地异常日志========== */
logger.error("异常方法:{" + proceedingJoinPoint.getTarget().getClass().getName() +"." + proceedingJoinPoint.getSignature().getName()
+ "}异常信息:{" + e.getMessage() + "}参数:{" + params + "}", e);
return resultDTO;
}
return result;
}
/**
* 异常通知 用于拦截service层记录异常日志
*
* @param joinPoint
* @param e
* @throws Throwable
*/
@AfterThrowing(pointcut = "serviceAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) throws Throwable {
// 获取用户请求方法的参数并序列化为JSON格式字符串
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for (int i = 0; i < joinPoint.getArgs().length; i++) {
params += GsonUtil.toJson(joinPoint.getArgs()[i]) + ";";
}
}
try {
if (null == RequestContextHolder.getRequestAttributes()) {
return;
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
// 获取请求ip
String ip = request.getRemoteAddr();
Object parentLogIdStr = ThreadLocalHelper.getValue(LoggerConstants.HTTP_HEAD_PARENT_LOG_ID);
String parentLogId = null == parentLogIdStr ? null : (String) parentLogIdStr;
String logId = UUID.randomUUID().toString();
SystemLog log = new SystemLog();
log.setLogId(logId);
log.setLogClass(e.getClass().getName());
log.setExcepMessage(e.getMessage());
log.setMethod(joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()");
log.setQuestIp(ip);
log.setQuestParam(params);
log.setDescription(getServiceMthodDescription(joinPoint));
log.setLogType(LoggerConstants.SERVICE_LOG_TYPE);
log.setCreatetime(new Date());
log.setProject(project);
if (StringUtils.isNotBlank(parentLogId)) {
log.setParentLogId(parentLogId);
}
String logJson = GsonUtil.toJson(log);
logger.error(logJson);
} catch (Exception ex) {
// 记录本地异常日志
logger.error(ex.getMessage(), ex);
}
/* ==========记录本地异常日志========== */
logger.error("异常方法:{" + joinPoint.getTarget().getClass().getName() +"." +joinPoint.getSignature().getName()
+ "}异常信息:{" + e.getMessage() + "}参数:{" + params + "}", e);
throw e;
}
/**
* 获取注解中对方法的描述信息 用于service层注解
*
* @param joinPoint
* 切点
* @return 方法描述
* @throws Exception
*/
public static String getServiceMthodDescription(JoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length && null != method.getAnnotation(SystemServiceLog.class)) {
description = method.getAnnotation(SystemServiceLog.class).description();
break;
}
}
}
return description;
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint
* 切点
* @return 方法描述
* @throws Exception
*/
public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length && null != method.getAnnotation(SystemControllerLog.class)) {
description = method.getAnnotation(SystemControllerLog.class).description();
break;
}
}
}
return description;
}
}
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemServiceLog
{
/**
* 描述信息
* <功能详细描述>
* @return [参数说明]
*
* @return String [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
String description() default "";
}
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemControllerLog
{
/**
* 描述信息
* <功能详细描述>
* @return [参数说明]
*
* @return String [返回类型说明]
* @exception throws [违例类型] [违例说明]
* @see [类、类#方法、类#成员]
*/
String description() default "";
}
此外还有一个aop切点日志可以计算Controller的毫秒损耗
@Aspect
@Component
@Slf4j
public class TkControllerRequestAdvice {
private static final ThreadLocal<Long> timeThreadLocal = new ThreadLocal<Long>();
/**
* 定义切点
*/
@Pointcut("execution(public * com.xxx.*.controller..*.*(..))")
public void log() {
}
/**
* 处理请求前处理
*
* @param joinPoint 连接点
*/
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
String client = request.getRemoteAddr();
String method = request.getMethod();
String requestURI = request.getRequestURI();
String token = request.getHeader("token");
long currentTimeMillis = System.currentTimeMillis();
timeThreadLocal.set(currentTimeMillis);
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
String parameterName = parameterNames[i];
Object parameterValue = args[i];
if (parameterValue instanceof MultipartFile) {
Map<String, Object> f = getFileParam((MultipartFile) parameterValue);
params.put(parameterName, f);
continue;
}
params.put(parameterName, parameterValue);
}
StringBuffer sb = new StringBuffer();
sb.append("Request_").append(currentTimeMillis).append(" ");
sb.append("<").append(client).append(">");
sb.append(" ").append(method).append(" ");
sb.append("\"").append(requestURI).append("\"");
sb.append(" token:").append(token).append(" ");
sb.append("class_method : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
log.info(sb.toString());
}
/**
* 处理请求后返回
*
* @param obj 返回值
*/
@AfterReturning(pointcut = "log()", returning = "obj")
public void afterReturning(Object obj) {
log.info("Response_" + timeThreadLocal.get() + " => " + JsonUtil.toJson(obj));
log.info("耗时(毫秒) : " + (System.currentTimeMillis() - timeThreadLocal.get()));
}
private Map<String, Object> getFileParam(MultipartFile file) {
Map<String, Object> params = new HashMap<>();
params.put("文件名", file.getOriginalFilename());
params.put("文件类型", file.getContentType());
params.put("文件大小", file.getSize());
return params;
}
}