前段时间做了一个支线任务,现在还是回到我们的主线继续完成天津项目。朋友们知道松哥最近在录制TienChin项目视频教程,这是一个基于若一-Vue脚手架的项目。用过这个脚手架的朋友可能知道这个脚手架有一个功能,就是如果你需要记录一个接口操作信息,只需要在Controller接口上加上@Log注解即可,非常方便。最终记录在数据库中的日志类似下面这样:可能有小伙伴要吐槽,这不应该记录在Elasticsearch中吗?去麋鹿!怎么说呢,如果你能把日志存到数据库里,以后存到Elasticsearch里其实也很容易。结合宋哥录制的es视频(公众号后台回复es),相信你可以自己解决这个问题。今天我们主要分析一下这个脚手架中@Log注解的玩法。1.日志表设计下面我们来看一下日志表的设计。CREATETABLE`sys_oper_log`(`oper_id`bigint(20)NOTNULLAUTO_INCREMENTCOMMENT'logprimarykey',`title`varchar(50)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'moduletitle',`business_type`int(2)DEFAULT'0'COMMENT'业务类型(0other1new2modify3delete)',`method`varchar(100)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'方法名',`request_method`varchar(10)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'请求方法',`operator_type`int(1)DEFAULT'0'COMMENT'操作类别(0其他1后台用户2移动端用户)',`oper_name`varchar(50)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'operator',`dept_name`varchar(50)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'部门名称',`oper_url`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'请求URL',`oper_ip`varchar(128)COLLATEutf8mb4_unicode_ciDEFAULT'主机地址',`oper_location`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'操作位置',`oper_param`varchar(2000)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'请求参数',`json_result`varchar(2000)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'返回参数',`status`int(1)DEFAULT'0'COMMENT'运行状态(0正常1异常)',`error_msg`varchar(2000)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'错误消息',`oper_time`datetimeDEFAULTNULLCOMMENT'操作时间',PRIMARYKEY(`oper_id`))ENGINE=InnoDBAUTO_INCREMENT=280DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ciCOMMENT='操作记录';这里先解释一下各个字段的含义:oper_id:这个是日志的主键,自增标题:这个标题一般用来说明操作是干什么的,比如删除一个用户,增加一个线程和等待。business_type:这里指的是业务类型,一般来说:增、改、删、导入、导出等。method:要执行的接口方法名。request_method:这是指请求的方法类型,比如GET、POST、PUT、DELETE等。operator_type:这是指操作的类型,分为后台用户、手机用户和其他三种。oper_name:运算符的名称。dept_name:操作员所属的部门。oper_url:请求的URL地址。oper_ip:请求的IP地址。oper_location:请求IP地址所属的区域。oper_param:请求的参数。json_result:响应的JSON参数。status:操作的状态,成功或失败。error_msg:如果是失败,失败的内容是什么。oper_time:操作的时间。这里给出了这几个字段,基本可以满足项目的需要。如果不够,您也可以自己添加。2.注解的定义我们看一下@Log注解的定义,位于org.javaboy.tienchin.common.annotation.Log:@Target({ElementType.PARAMETER,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceLog{/***Module*/publicStringtitle()default"";/***函数*/publicBusinessTypebusinessType()defaultBusinessType.OTHER;/***运算符类型*/publicOperatorTypeoperatorType()defaultOperatorType.MANAGE;/***是否保存请求参数*/publicbooleanisSaveRequestData()defaulttrue;/***是否保存响应参数*/publicbooleanisSaveResponseData()defaulttrue;}这个注解一共有五个属性,结合上表的定义,五个属性的含义就很容易理解了,所以我不会说太多。3、注解解析经典组合:自定义注解+AOP切面。解析这个注解的AOP切面是LogAspect,位于org.javaboy.tienchin.framework.aspectj.LogAspect:@Aspect@ComponentpublicclassLogAspect{privatestaticfinalLoggerlog=LoggerFactory.getLogger(LogAspect.class);/***完成请求后执行**@paramjoinPoint切点*/@AfterReturning(pointcut="@annotation(controllerLog)",returning="jsonResult")publicvoiddoAfterReturning(JoinPointjoinPoint,LogcontrollerLog,ObjectjsonResult){handleLog(joinPoint,controllerLog,null,jsonResult);}/***拦截异常操作**@paramjoinPoint切点*@parame异常*/@AfterThrowing(value="@annotation(controllerLog)",throwing="e")publicvoiddoAfterThrowing(JoinPointjoinPoint,LogcontrollerLog,异常e){handleLog(joinPoint,controllerLog,e,null);}protectedvoidhandleLog(finalJoinPointjoinPoint,LogcontrollerLog,finalExceptione,ObjectjsonResult){try{//获取当前用户登录用户loginUser=SecurityUtils.getLoginUser();//*========数据库日志=========*//SysOperLogoperLog=newSysOperLog();operLog.setStatus(BusinessStatus.SUCCESS.ordinal());//请求地址Stringip=IpUtils.getIpAddr(ServletUtils.getRequest());operLog.setOperIp(ip);operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());if(loginUser!=null){operLog.setOperName(loginUser.getUsername());}if(e!=null){operLog.setStatus(BusinessStatus.FAIL.ordinal());operLog.setErrorMsg(StringUtils.substring(e.getMessage(),0,2000));}//设置方法名StringclassName=joinPoint.getTarget().getClass().getName();StringmethodName=joinPoint.getSignature().getName();operLog.setMethod(className+"."+methodName+"()");//设置请求方式operLog.setRequestMethod(ServletUtils.getRequest().getMethod());//设置注解的流程参数getControllerMethodDescription(joinPoint,controllerLog,operLog,jsonResult);//保存数据库AsyncManager.me().execute(AsyncFactory.recordOper(operLog));}catch(Exceptionexp){//记录本地异常日志log.error("==pre-notificationexception==");log.error("异常信息:{}",exp.getMessage());exp.printStackTrace();}}/***Controller层注解获取注解中方法的描述信息**@paramloglog*@paramoperLog运行日志*@throwsException*/publicvoidgetControllerMethodDescription(JoinPointjoinPoint,Loglog,SysOperLogoperLog,ObjectjsonResult)throwsException{//设置动作actionoperLog.setBusinessType(log.businessType().ordinal());//设置标题operLog.setTitle(log.title());//设置算子类型operLog.setOperatorType(log.operatorType().ordinal());//是否需要保存请求、参数和值if(log.isSaveRequestData()){//获取参数的信息并传入数据库setRequestValue(joinPoint,operLog);}//是否需要保存响应、参数和值if(log.isSaveResponseData()&&StringUtils.isNotNull(jsonResult)){operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult),0,2000));}}/***获取请求的参数并放入日志中**@paramoperLog操作日志*@throwsException异常*/privatevoidsetRequestValue(JoinPointjoinPoint,SysOperLogoperLog)throwsException{StringrequestMethod=operLog.getRequestMethod();如果(HttpMethod.PUT.name().equals(requestMethod)||HttpMethod.POST.name().equals(requestMethod)){Stringparams=argsArrayToString(joinPoint.getArgs());操作日志。setOperParam(StringUtils.substring(params,0,2000));}else{Map,?>paramsMap=(Map,?>)ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);操作日志.setOperParam(StringUtils.substring(paramsMap.toString(),0,2000));}}/***参数组装*/privateStringargsArrayToString(Object[]paramsArray){Stringparams="";if(paramsArray!=null&¶msArray.length>0){for(Objecto:paramsArray){if(StringUtils.isNotNull(o)&&!isFilterObject(o)){try{ObjectjsonObj=JSON.toJSON(o);参数+=jsonObj.toString()+"";}catch(Exceptione){}}}}returnparams.trim();}/***判断是否过滤对象**@paramo对象信息。*@return如果是需要过滤的对象,则返回true;否则返回假。*/publicbooleanisFilterObject(finalObjecto){Class>clazz=o.getClass();如果(clazz.isArray()){返回clazz.getComponentType().isAssignableFrom(MultipartFile.class);}elseif(Collection.class.isAssignableFrom(clazz)){集合collection=(Collection)o;for(Objectvalue:collection){返回值instanceofMultipartFile;}}elseif(Map.class.isAssignableFrom(clazz)){Mapmap=(Map)o;for(Objectvalue:map.entrySet()){Map.Entryentry=(Map.Entry)value;返回entry.getValue()instanceofMultipartFile;}}返回oinstanceofMultipartFile||oHttpServletRequest实例||oHttpServletResponse实例||oBindingResult实例;}}大概念跟小伙伴们拍下这个切面的递专辑。首先,定义了两种不同类型的通知:返回通知和异常通知。正常的流程是在返回通知中处理日志写操作,但是如果系统不幸抛出异常,则在异常通知中处理日志写操作(此时只是多了一个异常对象)。日志数据保存在SysOperLog对象中,收集各种日志数据是常规操作,不多说。在收集接口参数的时候,有两点需要注意:如果请求类型是PUT或者POST,直接从接口参数中获取想要的数据,但是可能有些接口参数不需要记录内容,比如如HttpServletRequest、HttpServletResponse或者文件上传对象MultipartFile等,这些类型的内容不需要记录在日志中。这里通过一个isFilterObject方法完成数据过滤操作;如果请求类型为GET或DELETE,请求参数直接从请求对象中提取。为什么要这样设计?显然,直接从请求对象中提取参数是最方便的,一行代码就可以搞定,但是如果请求类型是PUT或者POST,就意味着请求参数在请求体中,而请求参数可能是二进制数据(比如上传文件),二进制数据不易保存,所以对于POST和PUT,还是从接口参数中提取出来,然后过滤掉二进制数据即可。数据采集??完成后,下一步就是写入数据库。因为我们现在使用的SpringMVC是线程阻塞的,即在服务端处理完之后,接口才会响应客户端,而写日志是业务无关的操作,所以可以直接放在一个子在线程中完成。在若一-Vue脚手架中,JavaJUC中的ScheduledExecutorService就是用来完成这个延时任务的。AsyncManager.me().execute方法其实就是执行一个延时任务。这个延迟任务是向数据库写入一条记录。4、日志完成后,日志注解的具体用法如下:@Log(title="参数管理",businessType=BusinessType.EXPORT)@PreAuthorize("@ss.hasPermi('system:config:export')")@PostMapping("/export")publicvoidexport(HttpServletResponseresponse,SysConfigconfig){List
