系统用户操作日志(记录用户操作并定时保存到表中)
客户需求: 要对几个关键的业务功能进行操作日志记录,即什么人在什么时间操作了哪个功能,操作前的数据报文是什么、操作后的数据报文是什么,必要的时候可以一键回退。
设计思路: ruoyi中使用Spring AOP来实现操作日志
1、定义业务操作日志注解,注解内可以定义一些属性,如操作功能名称、功能的描述等;
2、把业务操作日志注解标记在需要进行业务操作记录的方法上(在实际业务中,一些简单的业务查询行为通常没有必要记录);
3、定义切入点,编写切面:切入点就是标记了业务操作日志注解的目标方法;切面的主要逻辑就是保存业务操作日志信息;
Spring Aop 详解 以及示例
ruoyi实现方案,直接上核心日志切面类
/*** 操作日志记录处理 注意这里是 操作日志 而不是 输出日志* * @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(){@Overridepublic void run(){// 远程查询操作地点
// operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));SpringUtils.getBean(ISysOperLogService.class).save(operLog);}};}}
这样就可以将带有Log注解的控制层的操作保存到数据库中
系统日志
springboot默认提供logback日志来保存控制台输出日志。
在 src/main/resources/logback.xml 中定义相应的日志输出格式即可
ruoyi 示例
<?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 文件的同级目录下。
# log
logging:level:# 你自己的包名称com.example: debugorg.springframework: warn
在 application.yml中配置日志
Logback日志路径保存配置
配置1
<property name="LOG_HOME" value="log" />
若项目未打成.jar文件, 运行项目, 日志文件会保存在项目的根目录下
若项目打成.jar文件, 运行.jar文件, 日志文件会保存在.jar文件同级目录下
此时 在根目录的文件夹下面会生成相应的logs文件夹并且生成带有日期的日志文件
按天保存系统日志到文件
操作日志持久化(保存到表中)