近况
这周主要在看若依的微服务后台管理系统,看了别人的项目,感觉自己很多地方不太懂,
多线程方面知识严重欠缺,某些技术学的不扎实,包括一些代码设计思路吧,总之,我太菜了
12月份的主要目标,学习若依微服务系统,可以让我学习很多东西,B站上也有相关视频,
12月份看完项目代码+视频,能完成后端部分就已经很满足了。
进入正题,来看看本篇博客具体内容
系统日志
若依项目中通过注解的方式来做日志,并将日志持久化到数据库中,方便后面展示到页面中
若依系统中把日志分为操作日志和登录日志,来看看具体是怎么实现的吧
操作日志
基本执行流程
操作日志指的是当请求调用某个方法的时候,我们记录调用方法时的形参,返回值,用户数据信息 所以操作日志实现是通过配置自定义注解,标注在方法上,然后通过AOP拦截请求,从而获取到请求的一些信息,并持久化数据库中。 日志注解及AOP切面都在ruoyi-common-log模块中,在该模块的AsyncLogService类中, 使用ruoyi-api-system模块中的RemoteLogService接口,feign调用ruoyi-modules-system模块的controller 如果是登录日志,则调用该模块的SysLogininforController,如果是操作日志,调用的是SysOperlogController 然后执行之后的service,dao层进行持久化,持久化到数据库便可从中获取并前端展示出来。
当然,我们看的是具体实现,而不是大致流程,操作日志配置都放在
ruoyi-common-log
模块中自定义Log注解
/** * 自定义操作日志记录注解 */ @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; }
自定义AOP切面,LogAspect
package com.ruoyi.common.log.aspect; /** * 操作日志记录处理 */ @Aspect @Component public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); @Autowired private AsyncLogService asyncLogService; /** * 处理完请求后执行 * * @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{ // *========数据库日志=========*// SysOperLog operLog = new SysOperLog(); //数据库表的实体类 operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 请求的地址 String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); operLog.setOperIp(ip); operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); String username = SecurityUtils.getUsername(); if (StringUtils.isNotBlank(username)){ operLog.setOperName(username); } if (e != null){ operLog.setStatus(BusinessStatus.FAIL.ordinal()); operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); // 保存数据库 asyncLogService.saveSysLog(operLog); } catch (Exception exp){ // 记录本地异常日志 log.error("==前置通知异常=="); log.error("异常信息:{}", exp.getMessage()); exp.printStackTrace(); } } /** * 获取注解中对方法的描述信息 用于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); } // 是否需要保存response,参数和值 if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)){ operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求的参数,放到log中 * * @param operLog 操作日志 * @throws Exception 异常 */ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception{ String requestMethod = operLog.getRequestMethod(); if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)){ String params = argsArrayToString(joinPoint.getArgs()); operLog.setOperParam(StringUtils.substring(params, 0, 2000)); } } /** * 参数拼装 */ private String argsArrayToString(Object[] paramsArray){ String params = ""; if (paramsArray != null && paramsArray.length > 0){ for (Object o : paramsArray){ if (StringUtils.isNotNull(o) && !isFilterObject(o)){ try{ Object jsonObj = JSON.toJSON(o); 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; } }
该类中需要关注以下几点:
切入点的annotation注解
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult){ handleLog(joinPoint, controllerLog, null, jsonResult); }
@annotation是匹配拥有指定注解的方法的,也就是说只要标注了该注解的方法,都能匹配到。
上述:pointcut = "@annotation(controllerLog)"
,表示将匹配的方法上的注解赋值给参数controllerLog
这样我们就拿到了,自定义注解上的一些属性值,例如Log注解上有title,businessType等属性值。获取当前请求及IP地址
IpUtils.getIpAddr(ServletUtils.getRequest());
如何在任何地方都能拿到当前请求中的一些信息,例如请求类型,请求路径,请求ip地址?
通过RequestContextHolder就可以完成,上面工具类底层就是使用了该类,工具类具体内容已放在博客最后面。枚举类的ordinal方法
operLog.setBusinessType(log.businessType().ordinal());
该方法用于获取枚举属性的索引值,索引从0开始。
字节码对象的getComponentType方法,isAssignableFrom方法的使用
clazz.getComponentType().isAssignableFrom(MultipartFile.class); Map.class.isAssignableFrom(clazz)
getComponentType方法用来获取数组中元素的Class对象,如果不是Class对象那么返回null
String [] arr = new String[10]; String str = ""; System.out.println(arr.getClass().getComponentType()); // String类 System.out.println(str.getClass().getComponentType()); // 得到null值,因为str不是数组
isAssignableFrom方法
isAssignableFrom()方法与instanceof关键字的区别总结为以下两个点: isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。 isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类。 使用方式: 父类.class.isAssignableFrom(子类.class) 子类实例 instanceof 父类类型 isAssignableFrom()方法的调用者和参数都是Class对象,调用者为父类,参数为本身或者其子类。 instanceof关键字两个参数,前一个为类的实例,后一个为其本身或者父类的类型。
三个表示状态的枚举类
package com.ruoyi.common.log.enums; /** * 操作状态 */ public enum BusinessStatus { //成功 SUCCESS, //失败 FAIL, } //************************************************************** package com.ruoyi.common.log.enums; /** * 业务操作类型 */ public enum BusinessType { //其它 OTHER, //新增 INSERT, //修改 UPDATE, //删除 DELETE, //授权 GRANT, //导出 EXPORT, //导入 IMPORT, //强退 FORCE, //生成代码 GENCODE, //清空数据 CLEAN, } //************************************************************** package com.ruoyi.common.log.enums; /** * 操作人类别 */ public enum OperatorType { //其它 OTHER, //后台用户 MANAGE, //移动用户 MOBILE }
持久化的Service,AsyncLogService
/** * 异步调用日志服务 */ @Service public class AsyncLogService { @Autowired private RemoteLogService remoteLogService; /** * 保存系统日志记录 */ @Async public void saveSysLog(SysOperLog sysOperLog) { remoteLogService.saveLog(sysOperLog, SecurityConstants.INNER); } }
以上便是ruoyi-common-log模块的主要内容,AsyncLogService类中使用了ruoyi-api-system模块中的
RemoteLogService接口远程调用ruoyi-modules-system模块的SysOperlogController,
后续便是进入service,进入dao层使用mybatis持久化。操作日志表结构
CREATE TABLE `sys_oper_log` ( `oper_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键', `title` varchar(50) DEFAULT '' COMMENT '模块标题', `business_type` int DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)', `method` varchar(100) DEFAULT '' COMMENT '方法名称', `request_method` varchar(10) DEFAULT '' COMMENT '请求方式', `operator_type` int DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2手机端用户)', `oper_name` varchar(50) DEFAULT '' COMMENT '操作人员', `dept_name` varchar(50) DEFAULT '' COMMENT '部门名称', `oper_url` varchar(255) DEFAULT '' COMMENT '请求URL', `oper_ip` varchar(128) DEFAULT '' COMMENT '主机地址', `oper_location` varchar(255) DEFAULT '' COMMENT '操作地点', `oper_param` varchar(2000) DEFAULT '' COMMENT '请求参数', `json_result` varchar(2000) DEFAULT '' COMMENT '返回参数', `status` int DEFAULT '0' COMMENT '操作状态(0正常 1异常)', `error_msg` varchar(2000) DEFAULT '' COMMENT '错误消息', `oper_time` datetime DEFAULT NULL COMMENT '操作时间', PRIMARY KEY (`oper_id`) ) ENGINE=InnoDB AUTO_INCREMENT=104 DEFAULT CHARSET=utf8 COMMENT='操作日志记录'
操作日志表结构对应的实体类
package com.ruoyi.system.api.domain; /** * 操作日志记录表 oper_log */ public class SysOperLog extends BaseEntity { private static final long serialVersionUID = 1L; /** 日志主键 */ @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) private Long operId; /** 操作模块 */ @Excel(name = "操作模块") private String title; /** 业务类型(0其它 1新增 2修改 3删除) */ @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; /** 操作类别(0其它 1后台用户 2手机端用户) */ @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") private Integer operatorType; /** 操作人员 */ @Excel(name = "操作人员") private String operName; /** 部门名称 */ @Excel(name = "部门名称") private String deptName; /** 请求url */ @Excel(name = "请求地址") private String operUrl; /** 操作地址 */ @Excel(name = "操作地址") private String operIp; /** 请求参数 */ @Excel(name = "请求参数") private String operParam; /** 返回参数 */ @Excel(name = "返回参数") private String jsonResult; /** 操作状态(0正常 1异常) */ @Excel(name = "状态", readConverterExp = "0=正常,1=异常") private Integer status; /** 错误消息 */ @Excel(name = "错误消息") private String errorMsg; /** 操作时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date operTime; //已省略getter,setter,@Excel注解是自定义注解,用来做导入导出功能的。 }
以上便是若依微服务操作日志功能的具体实现,只要将@Log注解标注在方法上,便可以收集一些数据。
登录日志
基本执行流程
请求最先来到网关模块,之后被转发到ruoyi-auth认证模块,在该模块的SysLoginService类 中有一个recordLogininfor方法,该方法专门做登录日志的持久化操作,即将登录日志保存在数据库中 该方法内仍然是使用ruoyi-api-system模块中的RemoteLogService接口,调用ruoyi-modules-system模块的controller,并执行后续操作。
ruoyi-auth认证模块的SysLoginService类
package com.ruoyi.auth.service; /** * 登录校验方法 */ @Component public class SysLoginService { //ruoyi-api-system模块中的feign接口 @Autowired private RemoteLogService remoteLogService; @Autowired private RemoteUserService remoteUserService; /** * 登录 */ public LoginUser login(String username, String password) { // 用户名或密码为空 错误 if (StringUtils.isAnyBlank(username, password)) { recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写"); throw new ServiceException("用户/密码必须填写"); } // 密码如果不在指定范围内 错误 if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围"); throw new ServiceException("用户密码不在指定范围"); } // 用户名不在指定范围内 错误 if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) { recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围"); throw new ServiceException("用户名不在指定范围"); } // 查询用户信息 R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); if (R.FAIL == userResult.getCode()) { throw new ServiceException(userResult.getMsg()); } if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData())) { recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在"); throw new ServiceException("登录用户:" + username + " 不存在"); } LoginUser userInfo = userResult.getData(); SysUser user = userResult.getData().getSysUser(); if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除"); throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); } if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员"); throw new ServiceException("对不起,您的账号:" + username + " 已停用"); } if (!SecurityUtils.matchesPassword(password, user.getPassword())) { recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码错误"); throw new ServiceException("用户不存在/密码错误"); } recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功"); return userInfo; } public void logout(String loginName) { recordLogininfor(loginName, Constants.LOGOUT, "退出成功"); } /** * 注册 */ public void register(String username, String password) { // 用户名或密码为空 错误 if (StringUtils.isAnyBlank(username, password)) { throw new ServiceException("用户/密码必须填写"); } if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) { throw new ServiceException("账户长度必须在2到20个字符之间"); } if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { throw new ServiceException("密码长度必须在5到20个字符之间"); } // 注册用户信息 SysUser sysUser = new SysUser(); sysUser.setUserName(username); sysUser.setNickName(username); sysUser.setPassword(SecurityUtils.encryptPassword(password)); R<?> registerResult = remoteUserService.registerUserInfo(sysUser, SecurityConstants.INNER); if (R.FAIL == registerResult.getCode()) { throw new ServiceException(registerResult.getMsg()); } recordLogininfor(username, Constants.REGISTER, "注册成功"); } /** * 记录登录信息 * * @param username 用户名 * @param status 状态 * @param message 消息内容 * @return */ public void recordLogininfor(String username, String status, String message) { //登录日志表的实体类 SysLogininfor logininfor = new SysLogininfor(); logininfor.setUserName(username); logininfor.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest())); logininfor.setMsg(message); // 日志状态 if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) { logininfor.setStatus("0"); } else if (Constants.LOGIN_FAIL.equals(status)) { logininfor.setStatus("1"); } remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER); } }
登录日志表结构
CREATE TABLE `sys_logininfor` ( `info_id` bigint NOT NULL AUTO_INCREMENT COMMENT '访问ID', `user_name` varchar(50) DEFAULT '' COMMENT '用户账号', `ipaddr` varchar(128) DEFAULT '' COMMENT '登录IP地址', `status` char(1) DEFAULT '0' COMMENT '登录状态(0成功 1失败)', `msg` varchar(255) DEFAULT '' COMMENT '提示信息', `access_time` datetime DEFAULT NULL COMMENT '访问时间', PRIMARY KEY (`info_id`) ) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8 COMMENT='系统访问记录'
登录日志表实体类,SysLogininfor
package com.ruoyi.system.api.domain; /** * 系统访问记录表 sys_logininfor */ public class SysLogininfor extends BaseEntity { private static final long serialVersionUID = 1L; /** ID */ @Excel(name = "序号", cellType = ColumnType.NUMERIC) private Long infoId; /** 用户账号 */ @Excel(name = "用户账号") private String userName; /** 状态 0成功 1失败 */ @Excel(name = "状态", readConverterExp = "0=成功,1=失败") private String status; /** 地址 */ @Excel(name = "地址") private String ipaddr; /** 描述 */ @Excel(name = "描述") private String msg; /** 访问时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date accessTime; }
相关文章