效果展示
自定义日志格式和日志管理,使用 Spring 切面注解来对用户操作轨迹的记录。
整合 Logback 日志框架
日志是追溯系统使用记录、跟踪问题的的依据,是系统不可缺少的重要组成部分。在 Spring Boot 中,默认使用的是 Logback 日志。如果创建一个项目没有引用其他的日志框架的话,默认使用 Logback 打印日志
默认的日志格式
目前我们还没有对日志信息做任何设置,直接运行 Spring Boot 项目后,在控制台中就会生成 Logback 默认的日志格式
默认格式内容如下。
时间日期:日志打印时间,精确到毫秒。
日志级别:ERROR,WARN,INFO,DEBUG,TRACE。
进程 ID:进程 ID 指的是当前应用对应的 PID。
分隔符:–标识实际日志的开始。
线程名:方括号括起来(可能会阶段控制台输出)。
Logger 名:通常使用源代码的类名。
日志内容:日志正文。
配置日志文件
日志都是打印到控制台的,如果关闭项目或者系统,日志就会消失,这样不便于后期对问题的分析。所以除了将日志打印在控制台便于实时查看外,也需要将日志做定期归档,便于以后查找问题。针对这些问题, Spring Boot 默认支持使用 xml 自定义日志格式,只需要在 upms/src/main/resources 目录下新建 logback-spring.xml。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!-- 输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}-[%line] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按天生成日志 -->
<appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件命名 -->
<FileNamePattern>
/IdeaProjects/applog/%d{yyyy-MM-dd}/common-all-%d{yyyy-MM-dd}-%i.log
</FileNamePattern>
<!-- 保存最近 10 天的日志,防止磁盘被占满 -->
<maxHistory>10</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 设置最大日志文件大小 -->
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %logger{20}-[%line] - %msg%n
</Pattern>
</layout>
</appender>
<!-- 输出指定包文件的日志 -->
<logger name="com.ming.bms" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="logFile" />
</logger>
<!-- Spring 日志级别控制 -->
<logger name="org.springframework" level="warn" />
<!-- 日志处理级别 -->
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="logFile" />
</root>
</configuration>
logback-spring.xml 配置完成后,重新启动项目,此时可以看到自定义的控制台日志格式如下。
通过 logback-spring.xml 配置文件的设置,我们主要对控制台输出和打印到日志文件做了如下调整。
1、控制台输出中,输出格式为 时:分:秒.毫秒 [线程名] 日志级别 类名-日志信息。
2、打印到日志文件,日志文件的根目录为 D:\IdeaProjects\applog 日志按天进行归档,且单个日志文件最大为 100M,如果超过将生产两个文件。
用户操作行为记录
日志可以让作为开发人员排除问题的工具,但是对于系统的管理员,他们根本不会如何查看日志文件。如何才能让他们在页面上就可以看到用户的操作历史呢。此时就需要将用户的操作行为线性地记录在日志表中。记录下用户所访问的方法内容。
新增用户操作日志表
package com.example.bms.system.dao;
import com.example.bms.system.domain.BmsLog;
import java.util.List;
import java.util.Map;
public interface BmsLogMapper {
int deleteByPrimaryKey(Integer logId);
int insert(BmsLog record);
int insertSelective(BmsLog record);
BmsLog selectByPrimaryKey(Integer logId);
int updateByPrimaryKeySelective(BmsLog record);
int updateByPrimaryKeyWithBLOBs(BmsLog record);
int updateByPrimaryKey(BmsLog record);
/**
* 分页查询
*/
List<BmsLog> list(Map<String,Object> map);
/**
* 总数
*/
int count(Map<String,Object> map);
}
Mapper 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.bms.system.dao.BmsLogMapper">
<resultMap id="BaseResultMap" type="com.example.bms.system.domain.BmsLog">
<id column="log_id" jdbcType="INTEGER" property="logId" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="permissions" jdbcType="VARCHAR" property="permissions" />
<result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
<result column="spend_time" jdbcType="INTEGER" property="spendTime" />
<result column="base_path" jdbcType="VARCHAR" property="basePath" />
<result column="uri" jdbcType="VARCHAR" property="uri" />
<result column="method" jdbcType="VARCHAR" property="method" />
<result column="user_agent" jdbcType="VARCHAR" property="userAgent" />
<result column="ip" jdbcType="VARCHAR" property="ip" />
<result column="result" jdbcType="TINYINT" property="result" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.example.bms.system.domain.BmsLog">
<result column="parameter" jdbcType="LONGVARCHAR" property="parameter" />
</resultMap>
<sql id="Base_Column_List">
log_id, description, username, permissions, start_time, spend_time, base_path, uri,
method, user_agent, ip, result
</sql>
<sql id="Blob_Column_List">
parameter
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="ResultMapWithBLOBs">
select
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from bms_log
where log_id = #{logId,jdbcType=INTEGER}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
delete from bms_log
where log_id = #{logId,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="com.example.bms.system.domain.BmsLog">
insert into bms_log (log_id, description, username,
permissions, start_time, spend_time,
base_path, uri, method,
user_agent, ip, result,
parameter)
values (#{logId,jdbcType=INTEGER}, #{description,jdbcType=VARCHAR}, #{username,jdbcType=VARCHAR},
#{permissions,jdbcType=VARCHAR}, #{startTime,jdbcType=TIMESTAMP}, #{spendTime,jdbcType=INTEGER},
#{basePath,jdbcType=VARCHAR}, #{uri,jdbcType=VARCHAR}, #{method,jdbcType=VARCHAR},
#{userAgent,jdbcType=VARCHAR}, #{ip,jdbcType=VARCHAR}, #{result,jdbcType=TINYINT},
#{parameter,jdbcType=LONGVARCHAR})
</insert>
<insert id="insertSelective" parameterType="com.example.bms.system.domain.BmsLog">
insert into bms_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="logId != null">
log_id,
</if>
<if test="description != null">
description,
</if>
<if test="username != null">
username,
</if>
<if test="permissions != null">
permissions,
</if>
<if test="startTime != null">
start_time,
</if>
<if test="spendTime != null">
spend_time,
</if>
<if test="basePath != null">
base_path,
</if>
<if test="uri != null">
uri,
</if>
<if test="method != null">
method,
</if>
<if test="userAgent != null">
user_agent,
</if>
<if test="ip != null">
ip,
</if>
<if test="result != null">
result,
</if>
<if test="parameter != null">
parameter,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="logId != null">
#{logId,jdbcType=INTEGER},
</if>
<if test="description != null">
#{description,jdbcType=VARCHAR},
</if>
<if test="username != null">
#{username,jdbcType=VARCHAR},
</if>
<if test="permissions != null">
#{permissions,jdbcType=VARCHAR},
</if>
<if test="startTime != null">
#{startTime,jdbcType=TIMESTAMP},
</if>
<if test="spendTime != null">
#{spendTime,jdbcType=INTEGER},
</if>
<if test="basePath != null">
#{basePath,jdbcType=VARCHAR},
</if>
<if test="uri != null">
#{uri,jdbcType=VARCHAR},
</if>
<if test="method != null">
#{method,jdbcType=VARCHAR},
</if>
<if test="userAgent != null">
#{userAgent,jdbcType=VARCHAR},
</if>
<if test="ip != null">
#{ip,jdbcType=VARCHAR},
</if>
<if test="result != null">
#{result,jdbcType=TINYINT},
</if>
<if test="parameter != null">
#{parameter,jdbcType=LONGVARCHAR},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.example.bms.system.domain.BmsLog">
update bms_log
<set>
<if test="description != null">
description = #{description,jdbcType=VARCHAR},
</if>
<if test="username != null">
username = #{username,jdbcType=VARCHAR},
</if>
<if test="permissions != null">
permissions = #{permissions,jdbcType=VARCHAR},
</if>
<if test="startTime != null">
start_time = #{startTime,jdbcType=TIMESTAMP},
</if>
<if test="spendTime != null">
spend_time = #{spendTime,jdbcType=INTEGER},
</if>
<if test="basePath != null">
base_path = #{basePath,jdbcType=VARCHAR},
</if>
<if test="uri != null">
uri = #{uri,jdbcType=VARCHAR},
</if>
<if test="method != null">
method = #{method,jdbcType=VARCHAR},
</if>
<if test="userAgent != null">
user_agent = #{userAgent,jdbcType=VARCHAR},
</if>
<if test="ip != null">
ip = #{ip,jdbcType=VARCHAR},
</if>
<if test="result != null">
result = #{result,jdbcType=TINYINT},
</if>
<if test="parameter != null">
parameter = #{parameter,jdbcType=LONGVARCHAR},
</if>
</set>
where log_id = #{logId,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="com.example.bms.system.domain.BmsLog">
update bms_log
set description = #{description,jdbcType=VARCHAR},
username = #{username,jdbcType=VARCHAR},
permissions = #{permissions,jdbcType=VARCHAR},
start_time = #{startTime,jdbcType=TIMESTAMP},
spend_time = #{spendTime,jdbcType=INTEGER},
base_path = #{basePath,jdbcType=VARCHAR},
uri = #{uri,jdbcType=VARCHAR},
method = #{method,jdbcType=VARCHAR},
user_agent = #{userAgent,jdbcType=VARCHAR},
ip = #{ip,jdbcType=VARCHAR},
result = #{result,jdbcType=TINYINT},
parameter = #{parameter,jdbcType=LONGVARCHAR}
where log_id = #{logId,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="com.example.bms.system.domain.BmsLog">
update bms_log
set description = #{description,jdbcType=VARCHAR},
username = #{username,jdbcType=VARCHAR},
permissions = #{permissions,jdbcType=VARCHAR},
start_time = #{startTime,jdbcType=TIMESTAMP},
spend_time = #{spendTime,jdbcType=INTEGER},
base_path = #{basePath,jdbcType=VARCHAR},
uri = #{uri,jdbcType=VARCHAR},
method = #{method,jdbcType=VARCHAR},
user_agent = #{userAgent,jdbcType=VARCHAR},
ip = #{ip,jdbcType=VARCHAR},
result = #{result,jdbcType=TINYINT}
where log_id = #{logId,jdbcType=INTEGER}
</update>
<select id="list" resultType="com.example.bms.system.domain.BmsLog">
select `log_id`,`description`,`username`,`start_time`,`spend_time`,`base_path`,`uri`,`method`,`parameter`,
`user_agent`,`ip`,`result`,`permissions` from bms_log
<where>
<if test="logId != null and logId != ''"> and log_id = #{logId} </if>
<if test="description != null and description != ''"> and description = #{description} </if>
<if test="username != null and username != ''"> and username = #{username} </if>
<if test="startTime != null and startTime != ''"> and start_time = #{startTime} </if>
<if test="spendTime != null and spendTime != ''"> and spend_time = #{spendTime} </if>
<if test="basePath != null and basePath != ''"> and base_path = #{basePath} </if>
<if test="uri != null and uri != ''"> and uri = #{uri} </if>
<if test="method != null and method != ''"> and method = #{method} </if>
<if test="parameter != null and parameter != ''"> and parameter = #{parameter} </if>
<if test="userAgent != null and userAgent != ''"> and user_agent = #{userAgent} </if>
<if test="ip != null and ip != ''"> and ip = #{ip} </if>
<if test="result != null and result != ''"> and result = #{result} </if>
<if test="permissions != null and permissions != ''"> and permissions = #{permissions} </if>
</where>
<choose>
<when test="sort != null and sort.trim() != ''">
order by ${sort} ${order}
</when>
<otherwise>
order by log_id desc
</otherwise>
</choose>
<if test="offset != null and limit != null">
limit #{offset}, #{limit}
</if>
</select>
<select id="count" resultType="int">
select count(*) from bms_log
<where>
<if test="logId != null and logId != ''"> and log_id = #{logId} </if>
<if test="description != null and description != ''"> and description = #{description} </if>
<if test="username != null and username != ''"> and username = #{username} </if>
<if test="startTime != null and startTime != ''"> and start_time = #{startTime} </if>
<if test="spendTime != null and spendTime != ''"> and spend_time = #{spendTime} </if>
<if test="basePath != null and basePath != ''"> and base_path = #{basePath} </if>
<if test="uri != null and uri != ''"> and uri = #{uri} </if>
<if test="method != null and method != ''"> and method = #{method} </if>
<if test="parameter != null and parameter != ''"> and parameter = #{parameter} </if>
<if test="userAgent != null and userAgent != ''"> and user_agent = #{userAgent} </if>
<if test="ip != null and ip != ''"> and ip = #{ip} </if>
<if test="result != null and result != ''"> and result = #{result} </if>
<if test="permissions != null and permissions != ''"> and permissions = #{permissions} </if>
</where>
</select>
</mapper>
在添加相关的管理类,service controller
新增日志查询页面
添加log页面
使用 Spring 切面注解记录日志
1、添加依赖
需要在 pom.xml 中添加 aop 相关的依赖
<!-- Spring AOP 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、定义 @Log 注解
package com.example.bms.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author pangxie
* @data 2020/11/15 15:52
*
* log 注解类
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
3、添加 Spring 切面注解逻辑
package com.example.bms.common.aspect;
import com.example.bms.common.annotation.Log;
import com.example.bms.common.utils.HttpContextUtils;
import com.example.bms.common.utils.IPUtils;
import com.example.bms.common.utils.JSONUtils;
import com.example.bms.common.utils.ShiroUtils;
import com.example.bms.system.domain.BmsLog;
import com.example.bms.system.domain.BmsUser;
import com.example.bms.system.service.BmsLogService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;
/**
* @author pangxie
* @data 2020/11/15 15:54
*
* 切面注解
*/
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Autowired
BmsLogService logService;
@Pointcut("@annotation(com.example.bms.common.annotation.Log)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = point.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
// 异步保存日志
saveLog(point, time);
return result;
}
void saveLog(ProceedingJoinPoint joinPoint, long time) throws InterruptedException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
BmsLog sysLog = new BmsLog();
Log syslog = method.getAnnotation(Log.class);
if (syslog != null) {
// 注解上的描述
sysLog.setDescription(syslog.value());
}
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
// 设置根路径
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path;
sysLog.setBasePath(basePath);
// 获取uri
sysLog.setUri(request.getRequestURI());
// 请求方式
sysLog.setMethod(request.getMethod());
// 获取用户设备信息
sysLog.setUserAgent(request.getHeader("User-Agent"));
// 用户名
BmsUser currUser = ShiroUtils.getUser();
// 请求的参数
Map<String, String[]> paramMap = request.getParameterMap();
try {
if (paramMap != null) {
String params = JSONUtils.beanToJson(paramMap);
params = params.substring(0, params.length() < 1000 ? params.length(): 999);
sysLog.setParameter(params);
}
} catch (Exception e) {
logger.error("error: "+ e.getMessage(),e);
}
if (null == currUser) {
if (null != sysLog.getParameter()) {
sysLog.setUsername(sysLog.getParameter());
} else {
sysLog.setUsername("获取用户信息为空");
}
} else {
sysLog.setUsername(ShiroUtils.getUser().getUsername());
}
sysLog.setSpendTime((int) time);
// 系统当前时间
Date date = new Date();
sysLog.setStartTime(date);
// 相应状态 0:成功, 1:失败
sysLog.setResult(0);
// 保存系统日志
logService.insert(sysLog);
}
}
改进版
上面的老方式如果业务发生异常是记录不了日志的。把保存日志的方法放到 finally 中,当业务发生异常时也会记录日志
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = null;
try {
result = point.proceed();
} catch (Throwable throwable) {
// 为了不影响业务本身的异常框架捕捉此处需要把异常抛出去
throw throwable;
} finally {
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//日志实体, 距离上次测试太久了,日志实体是新建的,但是字段一致
SysLog sysLog = new SysLog();
saveLog(point, time, sysLog);
// 异步保存日志
System.out.println("系统日志数据: " + sysLog);
}
return result;
}
4、使用注解
打开 BmsUserController.java 类,在 /main 控制器的方法上添加注解。这样,所有用户访问 {。。。}/bms/user/main 时,系统都会记录下用户的操作信息。如下
可以在其他的请求页面中添加 @Log 注解,这样其他的方法也会纳入到我们的记录范围。这样我们就可以实现对用户的管理,可以起到对用户操作的监管追溯。也可以对系统执行效率进行分析,针对长请求可以做针对分析。
日志是追溯系统使用、问题跟踪的依据。日志的收集和归档对于开发者维护系统、BUG 定位起到至关重要的作用。