3.自定义注解实现系统日志记载
前言
今天来分享一下我昨天的成果,昨天计划复现若依系统的系统日志记载功能,若依的系统日志记载的主要实现使用过自定义注解配合切面类来实现的,这里会把标注@Log的方法在用户调用完后,将方法的一部分信息记录在数据库的指定数据表中。因此我们需要java的spring开发四层结构:domain层、mapper层、service层、controller层。到这里项目就大概完成了,注意的是若依中自定义的工具类。本文的项目代码链接:WomPlus: 结合若依项目对原始工单项目内容进行增强 (gitee.com),若依项目链接:GitHub - yangzongzhuan/RuoYi-fast: (RuoYi)官方仓库 基于SpringBoot的权限管理系统 易读易懂、界面简洁美观。 核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖。直接运行即可用
1.系统日志记载开发流程五步走
朋友们可以根据自己的项目来调节数据表结构,domain类、mapper接口以及Mapper.xml、Service接口及其实现类,我这里是根据自己项目需求来编写的。
1.1 根据自己项目创建数据表wo_operate_log

USE `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 '业务类型(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 '请求参数', `json_result` VARCHAR(2000) DEFAULT '' COMMENT '返回参数', `status` INT(1) DEFAULT 0 COMMENT '操作状态(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';
1.2 根据自己项目编写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; /** 业务类型 */ @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") private Integer businessType; /** 业务类型数组 */ 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; /** 请求参数 */ @Excel(name = "请求参数") private String operateParam; /** 返回参数 */ @Excel(name = "返回参数") private String jsonResult; /** 状态0正常 1异常 */ @Excel(name = "状态", 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(); } }
1.3 根据自己项目编写OperateMapper和OperateMapper.xml

public interface OperateLogMapper { /** * 新增操作日志 * @param :operateLog 操作日志对象 */ public void insertOperateLog(OperateLog operateLog); /** * 查询系统操作日志集合 * @param :operateLog 操作日志对象 * @return 操作日志集合 */ public List<OperateLog> selectOperateLogList(OperateLog operateLog); /** * 批量删除系统操作日志 * @param ids 需要删除的数据 * @return 结果 */ public int deleteOperateLogByIds(String[] ids); /** * 查询操作日志详细 * @param :operateId 操作ID * @return 操作日志对象 */ public OperateLog selectOperateLogById(Long operateId); /** * 清空操作日志 */ public void cleanOperateLog(); } <?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.ku.wo.project.system.monitor.operlog.mapper.OperateLogMapper"> <resultMap id="OperateLogResult" type="com.ku.wo.project.system.monitor.operlog.domain.OperateLog" > <id property="operateId" column="operate_id"/> <result property="title" column="title"/> <result property="businessType" column="business_type"/> <result property="method" column="method"/> <result property="requestMethod" column="request_method"/> <result property="operatorType" column="operator_type"/> <result property="operateName" column="oper_name"/> <result property="operateUrl" column="oper_url"/> <result property="operateIp" column="oper_ip"/> <result property="operateLocation" column="oper_location"/> <result property="operateParam" column="oper_param"/> <result property="jsonResult" column="json_result"/> <result property="status" column="status"/> <result property="errorMsg" column="error_msg"/> <result property="operateTime" column="operate_time"/> <result property="costTime" column="cost_time"/> </resultMap> <sql id="selectOperateLogVo"> 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 </sql> <!--keyProperty="operateId"中的值为属性值--> <insert id="insertOperateLog" useGeneratedKeys="true" keyProperty="operateId"> 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()) </insert> <select id="selectOperateLogList" resultMap="OperateLogResult"> <include refid="selectOperateLogVo"/> <where> <if test="title != null and title != ''"> AND title like concat('%', #{title}, '%') </if> <if test="businessType != null"> AND business_type = #{businessType} </if> <if test="businessTypes != null and businessTypes.length > 0"> AND business_type in <foreach collection="businessTypes" item="businessType" open="(" separator="," close=")"> #{businessType} </foreach> </if> <if test="status != null"> AND status = #{status} </if> <if test="operateName != null and operateName != ''"> AND operate_name like concat('%', #{operateName}, '%') </if> <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 --> AND operate_time >= #{params.beginTime} </if> <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 --> AND operate_time <= #{params.endTime} </if> </where> </select> <delete id="deleteOperateLogByIds"> delete from wom_plus.wo_operate_log where operate_id in <foreach collection="array" item="operateId" open="(" separator="," close=")"> #{operateId} </foreach> </delete> <select id="selectOperateLogById" resultMap="OperateLogResult"> <include refid="selectOperateLogVo"/> where operate_id = #{operateId} </select> <!--清空一个表--> <update id="cleanOperateLog"> truncate table wo_operate_log </update> </mapper>
1.4 根据自己项目编写IOperateService和OperateServiceImpl

public interface IOperateLogService { /** * 新增操作日志 * * @param :operateLog 操作日志对象 */ public void insertOperateLog(OperateLog operateLog); /** * 查询系统操作日志集合 * * @param :operateLog 操作日志对象 * @return 操作日志集合 */ public List<OperateLog> selectOperateLogList(OperateLog operateLog); /** * 批量删除系统操作日志 * * @param ids 需要删除的数据 * @return 结果 */ public int deleteOperateLogByIds(String ids); /** * 查询操作日志详细 * * @param operateId 操作ID * @return 操作日志对象 */ public OperateLog selectOperateLogById(Long operateId); /** * 清空操作日志 */ public void cleanOperateLog(); } @Service public class OperateLogServiceImpl implements IOperateLogService { @Autowired(required = false) private OperateLogMapper operateLogMapper; @Override public void insertOperateLog(OperateLog operateLog) { operateLogMapper.insertOperateLog(operateLog); } @Override public List<OperateLog> 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(); } }
1.5 Controller方法上的@Log
@Log(title = "用户管理", businessType = BusinessType.EXPORT) @PostMapping("/export") @ResponseBody public AjaxResult export(@RequestParam(value = "name", required = false) String username){ List<SysUser> list = userDetailsService.getUserListByUsername(username); ExcelUtil<SysUser> util = new ExcelUtil<>(SysUser.class); return util.exportExcel(list, "用户数据"); }
2.自定义系统日志记载注解及其切面实现
java中注解与AOP的结合,方便了广大java程序员对应用的开发,能够对原有方法的增强减少很多代码,原来的我们如果要在每个方法上面进行日志记载,那么需要每个方法都调用日志记载的方法,而现在我们只需要在需要日志加载的方法上面加上@ Log就完美快速简单地解决了上述繁杂问题。
2.1 自定义@Log

/** * 自定义操作日志记录注解 * * @author ruoyi * */ @Target({ ElementType.PARAMETER, ElementType.METHOD })//作用于方法和参数上面 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 模块 */ public String title() default ""; /** * 功能 */ public BusinessType businessType() default BusinessType.OTHER; /** * 操作人类别 */ public OperatorType operatorType() default OperatorType.MANAGE; /** * 是否保存请求的参数 */ public boolean isSaveRequestData() default true; /** * 是否保存响应的参数 */ public boolean isSaveResponseData() default true; /** * 排除指定的请求参数 */ public String[] excludeParamNames() default {}; }
2.2 LogAspect(重点)
该类就是根据动态代理来实现的,在Spring中称之为AOP,面向切面编程,可以很好地实现对软件中已有方法在安全、日志、监控等方面的增强。

@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 ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time"); /** * 处理请求前执行 */ @Before(value = "@annotation(controllerLog)") //该方法传入一个注解类型参数,改参数被@Before注解中的@annotation作用, // 表示只要该注解作用在哪个方法上,就在该方法上生效 public void before(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 { //本项目没有使用shiro框架,所以无法根据登录获取用户信息,以后再完善 // // 获取当前的用户 // SysUser currentUser = ShiroUtils.getSysUser(); // *========数据库日志=========*// OperateLog operateLog = new OperateLog(); //ordinal可以返回当前枚举所在的序列,利用这个函数,可以自增长的获取我们定义的Excel的cell位置, // 然后进行写入数据操作 operateLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 请求的地址 // String ip = ShiroUtils.getIp();//这里暂时不使用Shiro相关类 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)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operateLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 operateLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, operateLog, jsonResult); // 设置消耗时间 operateLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); //我这里不使用异步任务,因此在这里执行插入 //这里通过SpringUtils.getBean(Class<T>clz)来获取所需对象 // 远程查询操作地点 operateLog.setOperateLocation(AddressUtils.getRealAddressByIP(operateLog.getOperateIp())); SpringUtils.getBean(IOperateLogService.class).insertOperateLog(operateLog); //暂时不需要 // // 保存数据库 // 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 { // 设置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() && StringUtils.isNotNull(jsonResult)) { operLog.setJsonResult(StringUtils.substring(JSONObject.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求的参数,放到log中 * * @param : operateLog * @param : request */ private void setRequestValue(JoinPoint joinPoint, OperateLog operLog, String[] excludeParamNames) { Map<String, String[]> 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)); } /** * 参数拼装 */ 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注解的方法运行所花费的时间,环绕返回注解@AroundReturning是返回系统日志记载信息,环绕异常注解@AroundThrowing是返回这部分代码出现异常是返回的异常信息。主要的日志系统信息实现是在handleLog()方法中,根据切点获取方法名称、请求参数、等等信息。
2.3 若依中的自定义工具类
3.项目运行结果
第一张图的运行结果是使用@Log注解标注在以Excel形式导出数据的export()方法上面,第二张图片是系统日志记载表wo_operate_log记载的执行标有@Log注解的方法的日志记载信息。我这里只截取了后面的内容。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
2021-04-09 四月九号java知识