系統用戶操作日志(記錄用戶操作并定時保存到表中)
客戶需求: 要對幾個關鍵的業務功能進行操作日志記錄,即什么人在什么時間操作了哪個功能,操作前的數據報文是什么、操作后的數據報文是什么,必要的時候可以一鍵回退。
設計思路: 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文件夾并且生成帶有日期的日志文件
按天保存系統日志到文件
操作日志持久化(保存到表中)