系统用户操作日志(记录用户操作并定时保存到表中)
客户需求: 要对几个关键的业务功能进行操作日志记录,即什么人在什么时间操作了哪个功能,操作前的数据报文是什么、操作后的数据报文是什么,必要的时候可以一键回退。
设计思路: ruoyi中使用Spring AOP来实现操作日志1、定义业务操作日志注解,注解内可以定义一些属性,如操作功能名称、功能的描述等;
2、把业务操作日志注解标记在需要进行业务操作记录的方法上(在实际业务中,一些简单的业务查询行为通常没有必要记录);
3、定义切入点,编写切面:切入点就是标记了业务操作日志注解的目标方法;切面的主要逻辑就是保存业务操作日志信息;
ruoyi实现方案,直接上核心日志切面类
java
/**
* 操作日志记录处理 注意这里是 操作日志 而不是 输出日志
*
* @author ruoyi
*/
@Aspect
@Component
@Slf4j
public class LogAspect
{
/** 排除敏感属性字段 */
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/** 计算操作消耗时间 */
private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
/**
* 处理请求前执行
*/
@Before(value = "@annotation(controllerLog)")
public void boBefore(JoinPoint joinPoint, Log controllerLog)
{
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
{
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
{
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
{
try
{
// 获取当前的用户
// LoginUser loginUser = SecurityUtils.getLoginUser();
// 暂时使用admin来代替操作人
String user= "admin";
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
// String ip = IpUtils.getIpAddr();
String ip = "127.0.0.1";
operLog.setOperIp(ip);
operLog.setOperUrl(substring(getRequest().getRequestURI(), 0, 255));
// if (loginUser != null)
// {
operLog.setOperName(loginUser.getUsername());
// operLog.setOperName(user);
// SysUser currentUser = loginUser.getUser();
// if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept()))
// {
// operLog.setDeptName(currentUser.getDept().getDeptName());
// }
// }
if (e != null)
{
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 设置消耗时间
operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
// // 保存数据库 这一步ruoyi 是放到定期执行任务线程池中
// AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
AsyncManager.me().execute(recordOper(operLog));
}
catch (Exception exp)
{
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
finally
{
TIME_THREADLOCAL.remove();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
{
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData())
{
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog, log.excludeParamNames());
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && jsonResult!=null)
{
operLog.setJsonResult(substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception
{
Map<?, ?> paramsMap = getParamMap(getRequest());
String requestMethod = operLog.getRequestMethod();
if (paramsMap.isEmpty() && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)))
{
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(substring(params, 0, 2000));
}
else
{
// operLog.setOperParam(substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames)
{
StringBuilder params = new StringBuilder();
if (paramsArray != null && paramsArray.length > 0)
{
for (Object o : paramsArray)
{
if (o!=null && !isFilterObject(o))
{
try
{
// String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
String jsonObj = JSON.toJSONString(o);
params.append(jsonObj).append(" ");
}
catch (Exception e)
{
}
}
}
}
return params.toString().trim();
}
// /**
// * 忽略敏感属性
// */
// public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames)
// {
// return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
// }
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o)
{
Class<?> clazz = o.getClass();
if (clazz.isArray())
{
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
}
else if (Collection.class.isAssignableFrom(clazz))
{
Collection collection = (Collection) o;
for (Object value : collection)
{
return value instanceof MultipartFile;
}
}
else if (Map.class.isAssignableFrom(clazz))
{
Map map = (Map) o;
for (Object value : map.entrySet())
{
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @param end 结束
* @return 结果
*/
public static String substring(final String str, int start, int end)
{
if (str == null)
{
return "";
}
if (end < 0)
{
end = str.length() + end;
}
if (start < 0)
{
start = str.length() + start;
}
if (end > str.length())
{
end = str.length();
}
if (start > end)
{
return "";
}
if (start < 0)
{
start = 0;
}
if (end < 0)
{
end = 0;
}
return str.substring(start, end);
}
/**
* 获取request
*/
public static HttpServletRequest getRequest()
{
return getRequestAttributes().getRequest();
}
public static ServletRequestAttributes getRequestAttributes()
{
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 获得所有请求参数
*
* @param request 请求对象{@link ServletRequest}
* @return Map
*/
public static Map<String, String> getParamMap(ServletRequest request)
{
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : getParams(request).entrySet())
{
params.put(entry.getKey(), join(entry.getValue(), ","));
}
return params;
}
/**
* 获得所有请求参数
*
* @param request 请求对象{@link ServletRequest}
* @return Map
*/
public static Map<String, String[]> getParams(ServletRequest request)
{
final Map<String, String[]> map = request.getParameterMap();
return Collections.unmodifiableMap(map);
}
public static String join(Object[] array, String delimiter) {
return array == null ? null : join((Object[])array, delimiter, 0, array.length);
}
public static String join(Object[] array, String delimiter, int startIndex, int endIndex) {
if (array == null) {
return null;
} else if (endIndex - startIndex <= 0) {
return "";
} else {
StringJoiner joiner = new StringJoiner(toStringOrEmpty(delimiter));
for(int i = startIndex; i < endIndex; ++i) {
joiner.add(toStringOrEmpty(array[i]));
}
return joiner.toString();
}
}
private static String toStringOrEmpty(Object obj) {
return Objects.toString(obj, "");
}
/**
* 操作日志记录
*
* @param operLog 操作日志信息
* @return 任务task
*/
public static TimerTask recordOper(final SysOperLog operLog)
{
return new TimerTask()
{
@Override
public void run()
{
// 远程查询操作地点
// operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
SpringUtils.getBean(ISysOperLogService.class).save(operLog);
}
};
}
}
这样就可以将带有Log注解的控制层的操作保存到数据库中
系统日志
springboot默认提供logback日志来保存控制台输出日志。
在 src/main/resources/logback.xml 中定义相应的日志输出格式即可
ruoyi 示例
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 直接存在根目录下-->
<property name="log.path" value="logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- <file>${log.path}/sys-info.log</file>-->
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- <file>${log.path}/sys-error.log</file>-->
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- <file>${log.path}/sys-user.log</file>-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.example" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
<!--系统用户操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>
以上配置会在项目根目录的同级目录下生成 log/{日期}.log 的日志文件, 若打成 .jar包运行, 则日志文件会生成在 . jar 文件的同级目录下。
yml
# log
logging:
level:
# 你自己的包名称
com.example: debug
org.springframework: warn
在 application.yml中配置日志
Logback日志路径保存配置
配置1
xml
<property name="LOG_HOME" value="log" />
若项目未打成.jar文件, 运行项目, 日志文件会保存在项目的根目录下
若项目打成.jar文件, 运行.jar文件, 日志文件会保存在.jar文件同级目录下
此时 在根目录的文件夹下面会生成相应的logs文件夹并且生成带有日期的日志文件