(資料圖)
今天來分享一下我昨天的成果,昨天計劃復(fù)現(xiàn)若依系統(tǒng)的系統(tǒng)日志記載功能,若依的系統(tǒng)日志記載的主要實現(xiàn)使用過自定義注解配合切面類來實現(xiàn)的,這里會把標注@Log的方法在用戶調(diào)用完后,將方法的一部分信息記錄在數(shù)據(jù)庫的指定數(shù)據(jù)表中。因此我們需要java的spring開發(fā)四層結(jié)構(gòu):domain層、mapper層、service層、controller層。到這里項目就大概完成了,注意的是若依中自定義的工具類。本文的項目代碼鏈接:WomPlus: 結(jié)合若依項目對原始工單項目內(nèi)容進行增強 (gitee.com),若依項目鏈接:GitHub - yangzongzhuan/RuoYi-fast: (RuoYi)官方倉庫 基于SpringBoot的權(quán)限管理系統(tǒng) 易讀易懂、界面簡潔美觀。 核心技術(shù)采用Spring、MyBatis、Shiro沒有任何其它重度依賴。直接運行即可用
1.系統(tǒng)日志記載開發(fā)流程五步走朋友們可以根據(jù)自己的項目來調(diào)節(jié)數(shù)據(jù)表結(jié)構(gòu),domain類、mapper接口以及Mapper.xml、Service接口及其實現(xiàn)類,我這里是根據(jù)自己項目需求來編寫的。
1.1 根據(jù)自己項目創(chuàng)建數(shù)據(jù)表wo_operate_logUSE `wom_plus`DROP TABLE IF EXISTS `wo_operate_log`CREATE TABLE `wo_opertae_log`( `operate_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT "日志主鍵", `title` VARCHAR(50) DEFAULT "" COMMENT "模塊標題", `business_type` INT(2) DEFAULT 0 COMMENT "業(yè)務(wù)類型(0其它 1新增 2修改 3刪除)", `method` VARCHAR(100) DEFAULT "" COMMENT "方法名稱", `request_method` VARCHAR(10) DEFAULT "" COMMENT "請求方式", `operator_type` INT(1) DEFAULT 0 COMMENT "操作類別(0其它 1后臺用戶 2手機端用戶)", `operate_name` VARCHAR(50) DEFAULT "" COMMENT "操作人員", `operate_url` VARCHAR(255) DEFAULT "" COMMENT "請求URL", `operate_ip` VARCHAR(128) DEFAULT "" COMMENT "主機地址", `operate_location` VARCHAR(255) DEFAULT "" COMMENT "操作地點", `operate_param` VARCHAR(2000) DEFAULT "" COMMENT "請求參數(shù)", `json_result` VARCHAR(2000) DEFAULT "" COMMENT "返回參數(shù)", `status` INT(1) DEFAULT 0 COMMENT "操作狀態(tài)(0正常 1異常)", `error_msg` VARCHAR(2000) DEFAULT ""COMMENT "錯誤消息", `operate_time` DATETIME COMMENT "操作時間", `cost_time` BIGINT(20) DEFAULT 0 COMMENT "消耗時間", PRIMARY KEY (operate_id), KEY idx_sys_oper_log_bt (`business_type`), KEY idx_sys_oper_log_s (`status`), KEY idx_sys_oper_log_ot (`operate_time`))ENGINE=INNODB COMMENT="操作日志記錄" DEFAULT CHARSET="utf8";wo_operate_log1.2 根據(jù)自己項目編寫Operate實體類
/** * 操作日志記錄表 oper_log * * @author ruoyi */public class OperateLog extends BaseEntity{ private static final long serialVersionUID = 1L; /** 日志主鍵 */ @Excel(name = "操作序號", cellType = Excel.ColumnType.NUMERIC) private Long operateId; /** 操作模塊 */ @Excel(name = "操作模塊") private String title; /** 業(yè)務(wù)類型 */ @Excel(name = "業(yè)務(wù)類型", readConverterExp = "0=其它,1=新增,2=修改,3=刪除,4=授權(quán),5=導(dǎo)出,6=導(dǎo)入,7=強退,8=生成代碼,9=清空數(shù)據(jù)") private Integer businessType; /** 業(yè)務(wù)類型數(shù)組 */ private Integer[] businessTypes; /** 請求方法 */ @Excel(name = "請求方法") private String method; /** 請求方式 */ @Excel(name = "請求方式") private String requestMethod; /** 操作人類別 */ @Excel(name = "操作類別", readConverterExp = "0=其它,1=后臺用戶,2=手機端用戶") private Integer operatorType; /** 操作人員 */ @Excel(name = "操作人員") private String operateName;// /** 部門名稱 */// @Excel(name = "部門名稱")// private String deptName; /** 請求url */ @Excel(name = "請求地址") private String operateUrl; /** 操作地址 */ @Excel(name = "操作地址") private String operateIp; /** 操作地點 */ @Excel(name = "操作地點") private String operateLocation; /** 請求參數(shù) */ @Excel(name = "請求參數(shù)") private String operateParam; /** 返回參數(shù) */ @Excel(name = "返回參數(shù)") private String jsonResult; /** 狀態(tài)0正常 1異常 */ @Excel(name = "狀態(tài)", readConverterExp = "0=正常,1=異常") private Integer status; /** 錯誤消息 */ @Excel(name = "錯誤消息") private String errorMsg; /** 操作時間 */ @Excel(name = "操作時間", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date operTime; /** 消耗時間 */ @Excel(name = "消耗時間", suffix = "毫秒") private Long costTime; public static long getSerialVersionUID() { return serialVersionUID; } public Long getOperateId() { return operateId; } public void setOperateId(Long operateId) { this.operateId = operateId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Integer getBusinessType() { return businessType; } public void setBusinessType(Integer businessType) { this.businessType = businessType; } public Integer[] getBusinessTypes() { return businessTypes; } public void setBusinessTypes(Integer[] businessTypes) { this.businessTypes = businessTypes; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getRequestMethod() { return requestMethod; } public void setRequestMethod(String requestMethod) { this.requestMethod = requestMethod; } public Integer getOperatorType() { return operatorType; } public void setOperatorType(Integer operatorType) { this.operatorType = operatorType; } public String getOperateName() { return operateName; } public void setOperateName(String operateName) { this.operateName = operateName; } public String getOperateUrl() { return operateUrl; } public void setOperateUrl(String operateUrl) { this.operateUrl = operateUrl; } public String getOperateIp() { return operateIp; } public void setOperateIp(String operateIp) { this.operateIp = operateIp; } public String getOperateLocation() { return operateLocation; } public void setOperateLocation(String operateLocation) { this.operateLocation = operateLocation; } public String getOperateParam() { return operateParam; } public void setOperateParam(String operateParam) { this.operateParam = operateParam; } public String getJsonResult() { return jsonResult; } public void setJsonResult(String jsonResult) { this.jsonResult = jsonResult; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } public Date getOperateTime() { return operTime; } public void setOperateTime(Date operateTime) { this.operTime = operateTime; } public Long getCostTime() { return costTime; } public void setCostTime(Long costTime) { this.costTime = costTime; } @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("operateId", getOperateId()) .append("title", getTitle()) .append("businessType", getBusinessType()) .append("businessTypes", getBusinessTypes()) .append("method", getMethod()) .append("requestMethod", getRequestMethod()) .append("operatorType", getOperatorType()) .append("operateName", getOperateName()) .append("operateUrl", getOperateUrl()) .append("operateIp", getOperateIp()) .append("operateLocation", getOperateLocation()) .append("operateParam", getOperateParam()) .append("status", getStatus()) .append("errorMsg", getErrorMsg()) .append("operateTime", getOperateTime()) .append("costTime", getCostTime()) .toString(); }}OperateLog1.3 根據(jù)自己項目編寫OperateMapper和OperateMapper.xml
public interface OperateLogMapper { /** * 新增操作日志 * @param :operateLog 操作日志對象 */ public void insertOperateLog(OperateLog operateLog); /** * 查詢系統(tǒng)操作日志集合 * @param :operateLog 操作日志對象 * @return 操作日志集合 */ public ListMapper接口及其SQL1.4 根據(jù)自己項目編寫IOperateService和OperateServiceImplselectOperateLogList(OperateLog operateLog); /** * 批量刪除系統(tǒng)操作日志 * @param ids 需要刪除的數(shù)據(jù) * @return 結(jié)果 */ public int deleteOperateLogByIds(String[] ids); /** * 查詢操作日志詳細 * @param :operateId 操作ID * @return 操作日志對象 */ public OperateLog selectOperateLogById(Long operateId); /** * 清空操作日志 */ public void cleanOperateLog();} select operate_id, title, business_type, method, request_method, operator_type, operate_name, operate_url, operate_ip, operate_location, operate_param, json_result, status, error_msg, operate_time, cost_time from wom_plus.wo_operate_log insert into wom_plus.wo_operate_log(title, business_type, method, request_method, operator_type, operate_name, operate_url, operate_ip, operate_location, operate_param, json_result, status, error_msg, cost_time, operate_time) values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operateName}, #{operateUrl}, #{operateIp}, #{operateLocation}, #{operateParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) delete from wom_plus.wo_operate_log where operate_id in #{operateId} truncate table wo_operate_log
public interface IOperateLogService { /** * 新增操作日志 * * @param :operateLog 操作日志對象 */ public void insertOperateLog(OperateLog operateLog); /** * 查詢系統(tǒng)操作日志集合 * * @param :operateLog 操作日志對象 * @return 操作日志集合 */ public ListIoperateService及其實現(xiàn)類1.5 Controller方法上的@LogselectOperateLogList(OperateLog operateLog); /** * 批量刪除系統(tǒng)操作日志 * * @param ids 需要刪除的數(shù)據(jù) * @return 結(jié)果 */ public int deleteOperateLogByIds(String ids); /** * 查詢操作日志詳細 * * @param operateId 操作ID * @return 操作日志對象 */ public OperateLog selectOperateLogById(Long operateId); /** * 清空操作日志 */ public void cleanOperateLog();}@Servicepublic class OperateLogServiceImpl implements IOperateLogService { @Autowired(required = false) private OperateLogMapper operateLogMapper; @Override public void insertOperateLog(OperateLog operateLog) { operateLogMapper.insertOperateLog(operateLog); } @Override public List selectOperateLogList(OperateLog operateLog) { return operateLogMapper.selectOperateLogList(operateLog); } @Override public int deleteOperateLogByIds(String ids) { return deleteOperateLogByIds(ids); } @Override public OperateLog selectOperateLogById(Long operateId) { return operateLogMapper.selectOperateLogById(operateId); } @Override public void cleanOperateLog() { operateLogMapper.cleanOperateLog(); }}
@Log(title = "用戶管理", businessType = BusinessType.EXPORT)@PostMapping("/export")@ResponseBodypublic AjaxResult export(@RequestParam(value = "name", required = false) String username){ List2.自定義系統(tǒng)日志記載注解及其切面實現(xiàn)list = userDetailsService.getUserListByUsername(username); ExcelUtil util = new ExcelUtil<>(SysUser.class); return util.exportExcel(list, "用戶數(shù)據(jù)");}
java中注解與AOP的結(jié)合,方便了廣大java程序員對應(yīng)用的開發(fā),能夠?qū)υ蟹椒ǖ脑鰪姕p少很多代碼,原來的我們?nèi)绻诿總€方法上面進行日志記載,那么需要每個方法都調(diào)用日志記載的方法,而現(xiàn)在我們只需要在需要日志加載的方法上面加上@ Log就完美快速簡單地解決了上述繁雜問題。
2.1 自定義@Log/** * 自定義操作日志記錄注解 * * @author ruoyi * */@Target({ ElementType.PARAMETER, ElementType.METHOD })//作用于方法和參數(shù)上面@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Log{ /** * 模塊 */ public String title() default ""; /** * 功能 */ public BusinessType businessType() default BusinessType.OTHER; /** * 操作人類別 */ public OperatorType operatorType() default OperatorType.MANAGE; /** * 是否保存請求的參數(shù) */ public boolean isSaveRequestData() default true; /** * 是否保存響應(yīng)的參數(shù) */ public boolean isSaveResponseData() default true; /** * 排除指定的請求參數(shù) */ public String[] excludeParamNames() default {};}@Log2.2 LogAspect(重點)
該類就是根據(jù)動態(tài)代理來實現(xiàn)的,在Spring中稱之為AOP,面向切面編程,可以很好地實現(xiàn)對軟件中已有方法在安全、日志、監(jiān)控等方面的增強。
@Component@Aspect/** * 操作日志記錄處理 */public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); /** 排除敏感屬性字段 */ public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; /** 計算操作消耗時間 */ private static final ThreadLocalLogAspectTIME_THREADLOCAL = new NamedThreadLocal ("Cost Time"); /** * 處理請求前執(zhí)行 */ @Before(value = "@annotation(controllerLog)") //該方法傳入一個注解類型參數(shù),改參數(shù)被@Before注解中的@annotation作用, // 表示只要該注解作用在哪個方法上,就在該方法上生效 public void before(JoinPoint joinPoint, Log controllerLog){ TIME_THREADLOCAL.set(System.currentTimeMillis()); } /** * 處理完請求后執(zhí)行 * @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 { //本項目沒有使用shiro框架,所以無法根據(jù)登錄獲取用戶信息,以后再完善// // 獲取當(dāng)前的用戶// SysUser currentUser = ShiroUtils.getSysUser(); // *========數(shù)據(jù)庫日志=========*// OperateLog operateLog = new OperateLog(); //ordinal可以返回當(dāng)前枚舉所在的序列,利用這個函數(shù),可以自增長的獲取我們定義的Excel的cell位置, // 然后進行寫入數(shù)據(jù)操作 operateLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 請求的地址// String ip = ShiroUtils.getIp();//這里暫時不使用Shiro相關(guān)類 String ip = IPUtils.getIpAddr(ServletUtils.getRequest()); operateLog.setOperateIp(ip); operateLog.setOperateUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); //后面完善// if (currentUser != null)// {// operateLog.setOperateName(currentUser.getUsername());// } operateLog.setOperateName("xiaoku"); if (e != null) { operateLog.setStatus(BusinessStatus.FAIL.ordinal()); operateLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 設(shè)置方法名稱 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operateLog.setMethod(className + "." + methodName + "()"); // 設(shè)置請求方式 operateLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 處理設(shè)置注解上的參數(shù) getControllerMethodDescription(joinPoint, controllerLog, operateLog, jsonResult); // 設(shè)置消耗時間 operateLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); //我這里不使用異步任務(wù),因此在這里執(zhí)行插入 //這里通過SpringUtils.getBean(Class clz)來獲取所需對象 // 遠程查詢操作地點 operateLog.setOperateLocation(AddressUtils.getRealAddressByIP(operateLog.getOperateIp())); SpringUtils.getBean(IOperateLogService.class).insertOperateLog(operateLog); //暫時不需要// // 保存數(shù)據(jù)庫// AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); } catch (Exception exp) { // 記錄本地異常日志 log.error("異常信息:{}", exp.getMessage()); exp.printStackTrace(); } finally { TIME_THREADLOCAL.remove(); } } /** * 獲取注解中對方法的描述信息 用于Controller層注解 * @param log 日志 * @param :operateLog 操作日志 * @throws Exception */ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperateLog operLog, Object jsonResult) throws Exception { // 設(shè)置action動作 operLog.setBusinessType(log.businessType().ordinal()); // 設(shè)置標題 operLog.setTitle(log.title()); // 設(shè)置操作人類別 operLog.setOperatorType(log.operatorType().ordinal()); // 是否需要保存request,參數(shù)和值 if (log.isSaveRequestData()) { // 獲取參數(shù)的信息,傳入到數(shù)據(jù)庫中。 setRequestValue(joinPoint, operLog, log.excludeParamNames()); } // 是否需要保存response,參數(shù)和值 if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) { operLog.setJsonResult(StringUtils.substring(JSONObject.toJSONString(jsonResult), 0, 2000)); } } /** * 獲取請求的參數(shù),放到log中 * * @param : operateLog * @param : request */ private void setRequestValue(JoinPoint joinPoint, OperateLog operLog, String[] excludeParamNames) { Map map = ServletUtils.getRequest().getParameterMap(); if (StringUtils.isNotEmpty(map)) { String params = JSONObject.toJSONString(map, excludePropertyPreFilter(excludeParamNames)); operLog.setOperateParam(StringUtils.substring(params, 0, 2000)); } else { Object args = joinPoint.getArgs(); if (StringUtils.isNotNull(args)) { String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); operLog.setOperateParam(StringUtils.substring(params, 0, 2000)); } } } /** * 忽略敏感屬性 */ public PropertyPreFilters.MySimplePropertyPreFilter excludePropertyPreFilter(String[] excludeParamNames) { return new PropertyPreFilters().addFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); } /** * 參數(shù)拼裝 */ private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) { String params = ""; if (paramsArray != null && paramsArray.length > 0) { for (Object o : paramsArray) { if (StringUtils.isNotNull(o) && !isFilterObject(o)) { try { Object jsonObj = JSONObject.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); params += jsonObj.toString() + " "; } catch (Exception e) { } } } } return params.trim(); } /** * 判斷是否需要過濾的對象。 * @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; }}
上述代碼中的前置通知@Before和后置通知@After是用來記錄標注有@Log注解的方法運行所花費的時間,環(huán)繞返回注解@AroundReturning是返回系統(tǒng)日志記載信息,環(huán)繞異常注解@AroundThrowing是返回這部分代碼出現(xiàn)異常是返回的異常信息。主要的日志系統(tǒng)信息實現(xiàn)是在handleLog()方法中,根據(jù)切點獲取方法名稱、請求參數(shù)、等等信息。
2.3 若依中的自定義工具類3.項目運行結(jié)果第一張圖的運行結(jié)果是使用@Log注解標注在以Excel形式導(dǎo)出數(shù)據(jù)的export()方法上面,第二張圖片是系統(tǒng)日志記載表wo_operate_log記載的執(zhí)行標有@Log注解的方法的日志記載信息。我這里只截取了后面的內(nèi)容。
關(guān)鍵詞: