Springboot注解操作日志 此组件解决的问题是: 谁在什么时间对什么做了什么事 本组件目前针对Springboot做了Autoconfig,如果是SpringMVC,也可自己在xml初始化bean使用方式基本使用maven依赖添加SDK依赖dependencygroupIdio。github。mouztgroupIdbizlogsdkartifactIdversion1。0。4versiondependency复制代码SpringBoot入口打开开关,添加EnableLogRecord注解 tenant是代表租户的标识,一般一个服务或者一个业务下的多个服务都写死一个tenant就可以SpringBootApplication(excludeDataSourceAutoConfiguration。class)EnableTransactionManagementEnableLogRecord(tenantcom。mzt。test)publicclassMain{publicstaticvoidmain(String〔〕args){SpringApplication。run(Main。class,args);}}复制代码日志埋点1。普通的记录日志pefix:是拼接在bizNo上作为log的一个标识。避免bizNo都为整数ID的时候和其他的业务中的ID重复。比如订单ID、用户ID等bizNo:就是业务的ID,比如订单ID,我们查询的时候可以根据bizNo查询和它相关的操作日志success:方法调用成功后把success记录在日志的内容中SpEL表达式:其中用双大括号包围起来的(例如:{{order。purchaseName}})order。purchaseName是SpEL表达式。Spring中支持的它都支持的。比如调用静态方法,三目表达式。SpEL可以使用方法中的任何参数LogRecordAnnotation(success{{order。purchaseName}}下了一个订单,购买商品{{order。productName}},下单结果:{{ret}},prefixLogRecordType。ORDER,bizNo{{order。orderNo}})publicbooleancreateOrder(Orderorder){log。info(【创建订单】orderNo{},order。getOrderNo());dbinsertorderreturntrue;}复制代码 此时会打印操作日志张三下了一个订单,购买商品超值优惠红烧肉套餐,下单结果:true2。期望记录失败的日志,如果抛出异常则记录fail的日志,没有抛出记录success的日志LogRecordAnnotation(fail创建订单失败,失败原因:{{errorMsg}},success{{order。purchaseName}}下了一个订单,购买商品{{order。productName}},下单结果:{{ret}},prefixLogRecordType。ORDER,bizNo{{order。orderNo}})publicbooleancreateOrder(Orderorder){log。info(【创建订单】orderNo{},order。getOrderNo());dbinsertorderreturntrue;}复制代码 其中的errorMsg是取的方法抛出异常后的异常的errorMessage。3。日志支持种类 比如一个订单的操作日志,有些操作日志是用户自己操作的,有些操作是系统运营人员做了修改产生的操作日志,我们系统不希望把运营的操作日志暴露给用户看到, 但是运营期望可以看到用户的日志以及运营自己操作的日志,这些操作日志的bizNo都是订单号,所以为了扩展添加了类型字段,主要是为了对日志做分类,查询方便,支持更多的业务。LogRecordAnnotation(fail创建订单失败,失败原因:{{errorMsg}},categoryMANAGER,success{{order。purchaseName}}下了一个订单,购买商品{{order。productName}},下单结果:{{ret}},prefixLogRecordType。ORDER,bizNo{{order。orderNo}})publicbooleancreateOrder(Orderorder){log。info(【创建订单】orderNo{},order。getOrderNo());dbinsertorderreturntrue;}复制代码4。支持记录操作的详情或者额外信息 如果一个操作修改了很多字段,但是success的日志模版里面防止过长不能把修改详情全部展示出来,这时候需要把修改的详情保存到detail字段, detail是一个String,需要自己序列化。这里的order。toString()是调用了Order的toString()方法。 如果保存JSON,自己重写一下Order的toString()方法就可以。LogRecordAnnotation(fail创建订单失败,失败原因:{{errorMsg}},categoryMANAGERVIEW,detail{{order。toString()}},success{{order。purchaseName}}下了一个订单,购买商品{{order。productName}},下单结果:{{ret}},prefixLogRecordType。ORDER,bizNo{{order。orderNo}})publicbooleancreateOrder(Orderorder){log。info(【创建订单】orderNo{},order。getOrderNo());dbinsertorderreturntrue;}复制代码5。如何指定操作日志的操作人是什么?框架提供了两种方法第一种:手工在LogRecord的注解上指定。这种需要方法参数上有operatorLogRecordAnnotation(fail创建订单失败,失败原因:{{errorMsg}},categoryMANAGERVIEW,detail{{order。toString()}},operator{{currentUser}},success{{order。purchaseName}}下了一个订单,购买商品{{order。productName}},下单结果:{{ret}},prefixLogRecordType。ORDER,bizNo{{order。orderNo}})publicbooleancreateOrder(Orderorder,StringcurrentUser){log。info(【创建订单】orderNo{},order。getOrderNo());dbinsertorderreturntrue;}复制代码 这种方法手工指定,需要方法参数上有operator参数,或者通过SpEL调用静态方法获取当前用户。第二种:通过默认实现类来自动的获取操作人,由于在大部分web应用中当前的用户都是保存在一个线程上下文中的,所以每个注解都加一个operator获取操作人显得有些重复劳动,所以提供了一个扩展接口来获取操作人 框架提供了一个扩展接口,使用框架的业务可以implements这个接口自己实现获取当前用户的逻辑, 对于使用Springboot的只需要实现IOperatorGetService接口,然后把这个Service作为一个单例放到Spring的上下文中。使用SpringMvc的就需要自己手工装配这些bean了。ConfigurationpublicclassLogRecordConfiguration{BeanpublicIOperatorGetServiceoperatorGetService(){return()Optional。of(OrgUserUtils。getCurrentUser())。map(anewOperatorDO(a。getMisId()))。orElseThrow(()newIllegalArgumentException(userisnull));}}也可以这么搞:ServicepublicclassDefaultOperatorGetServiceImplimplementsIOperatorGetService{OverridepublicOperatorDOgetUser(){OperatorDOoperatorDOnewOperatorDO();operatorDO。setOperatorId(SYSTEM);returnoperatorDO;}}复制代码6。日志文案调整 对于更新等方法,方法的参数上大部分都是订单ID、或者产品ID等, 比如下面的例子:日志记录的success内容是:更新了订单{{orderId}},更新内容为,这种对于运营或者产品来说难以理解,所以引入了自定义函数的功能。 使用方法是在原来的变量的两个大括号之间加一个函数名称例如{ORDER{orderId}}其中ORDER是一个函数名称。只有一个函数名称是不够的,需要添加这个函数的定义和实现。可以看下面例子 自定义的函数需要实现框架里面的IParseFunction的接口,需要实现两个方法:functionName()方法就返回注解上面的函数名;apply()函数参数是{ORDER{orderId}}中SpEL解析的orderId的值,这里是一个数字1223110,接下来只需要在实现的类中把ID转换为可读懂的字符串就可以了, 一般为了方便排查问题需要把名称和ID都展示出来,例如:订单名称(ID)的形式。 这里有个问题:加了自定义函数后,框架怎么能调用到呢? 答:对于Springboot应用很简单,只需要把它暴露在Spring的上下文中就可以了,可以加上Spring的Component或者Service很方便。Springmvc应用需要自己装配Bean。没有使用自定义函数LogRecordAnnotation(success更新了订单{{orderId}},更新内容为。。。。,prefixLogRecordType。ORDER,bizNo{{order。orderNo}},detail{{order。toString()}})publicbooleanupdate(LongorderId,Orderorder){returnfalse;}使用了自定义函数,主要是在{{orderId}}的大括号中间加了functionNameLogRecordAnnotation(success更新了订单ORDER{orderId}},更新内容为。。。,prefixLogRecordType。ORDER,bizNo{{order。orderNo}},detail{{order。toString()}})publicbooleanupdate(LongorderId,Orderorder){returnfalse;}还需要加上函数的实现ComponentpublicclassOrderParseFunctionimplementsIParseFunction{ResourceLazy为了避免类加载顺序的问题最好为Lazy,没有问题也可以不加privateOrderQueryServiceorderQueryService;OverridepublicStringfunctionName(){函数名称为ORDERreturnORDER;}Override这里的value可以吧Order的JSON对象的传递过来,然后反解析拼接一个定制的操作ahrefhttps:www。bs178。comrizhitargetblankclassinfotextkey日志a内容publicStringapply(Stringvalue){if(StringUtils。isEmpty(value)){returnvalue;}OrderorderorderQueryService。queryOrder(Long。parseLong(value));把订单产品名称加上便于理解,加上ID便于查问题returnorder。getProductName()。concat(()。concat(value)。concat());}}复制代码7。日志文案调整使用SpEL三目表达式LogRecordAnnotation(prefixLogRecordTypeConstant。CUSTOMATTRIBUTE,bizNo{{businessLineId}},success{{disable?停用:启用}}了自定义属性{ATTRIBUTE{attributeId}})publicCustomAttributeVOdisableAttribute(LongbusinessLineId,LongattributeId,booleandisable){returnxxx;}复制代码8。日志文案调整模版中使用方法参数之外的变量 可以在方法中通过LogRecordContext。putVariable(variableName,Object)的方法添加变量,第一个对象为变量名称,后面为变量的对象, 然后我们就可以使用SpEL使用这个变量了,例如:例子中的{{innerOrder。productName}}是在方法中设置的变量OverrideLogRecordAnnotation(success{{order。purchaseName}}下了一个订单,购买商品{{order。productName}},测试变量{{innerOrder。productName}},下单结果:{{ret}},prefixLogRecordType。ORDER,bizNo{{order。orderNo}})publicbooleancreateOrder(Orderorder){log。info(【创建订单】orderNo{},order。getOrderNo());dbinsertorderOrderorder1newOrder();order1。setProductName(内部变量测试);LogRecordContext。putVariable(innerOrder,order1);returntrue;}复制代码9。函数中使用LogRecordContext的变量 使用LogRecordContext。putVariable(variableName,Object)添加的变量除了可以在注解的SpEL表达式上使用,还可以在自定义函数中使用,这种方式比较复杂,下面例子中示意了列表的变化,比如从〔A,B,C〕改到〔B,D〕那么日志显示:删除了A,增加了DLogRecord(success{DIFFLIST{文档地址}},bizNo{{id}},prefixREQUIREMENT)publicvoidupdateRequirementDocLink(StringcurrentMisId,Longid,ListStringdocLinks){RequirementDOrequirementDOgetRequirementDOById(id);LogRecordContext。putVariable(oldList,requirementDO。getDocLinks());LogRecordContext。putVariable(newList,docLinks);requirementModule。updateById(docLinks,RequirementUpdateDO。builder()。id(id)。docLinks(docLinks)。updater(currentMisId)。updateTime(newDate())。build());}ComponentpublicclassDiffListParseFunctionimplementsIParseFunction{OverridepublicStringfunctionName(){returnDIFFLIST;}SuppressWarnings(unchecked)OverridepublicStringapply(Stringvalue){if(StringUtils。isBlank(value)){returnvalue;}ListStringoldList(ListString)LogRecordContext。getVariable(oldList);ListStringnewList(ListString)LogRecordContext。getVariable(newList);oldListoldListnull?Lists。newArrayList():oldList;newListnewListnull?Lists。newArrayList():newList;SetStringdeletedSetsSets。difference(Sets。newHashSet(oldList),Sets。newHashSet(newList));SetStringaddSetsSets。difference(Sets。newHashSet(newList),Sets。newHashSet(oldList));StringBuilderstringBuildernewStringBuilder();if(CollectionUtils。isNotEmpty(addSets)){stringBuilder。append(新增了b)。append(value)。append(b:);for(Stringitem:addSets){stringBuilder。append(item)。append(,);}}if(CollectionUtils。isNotEmpty(deletedSets)){stringBuilder。append(删除了b)。append(value)。append(b:);for(Stringitem:deletedSets){stringBuilder。append(item)。append(,);}}returnStringUtils。isBlank(stringBuilder)?null:stringBuilder。substring(0,stringBuilder。length()1);}}复制代码框架的扩展点重写OperatorGetServiceImpl通过上下文获取用户的扩展,例子如下ServicepublicclassDefaultOperatorGetServiceImplimplementsIOperatorGetService{OverridepublicOperatorgetUser(){returnOptional。ofNullable(UserUtils。getUser())。map(anewOperator(a。getName(),a。getLogin()))。orElseThrow(()newIllegalArgumentException(userisnull));}}复制代码ILogRecordService保存查询日志的例子,使用者可以根据数据量保存到合适的存储介质上,比如保存在数据库或者ES。自己实现保存和删除就可以了 也可以只实现查询的接口,毕竟已经保存在业务的存储上了,查询业务可以自己实现,不走ILogRecordService这个接口,毕竟产品经理会提一些千奇百怪的查询需求。ServicepublicclassDbLogRecordServiceImplimplementsILogRecordService{ResourceprivateLogRecordMapperlogRecordMapper;OverrideTransactional(propagationPropagation。REQUIRESNEW)publicvoidrecord(LogRecordlogRecord){log。info(【logRecord】log{},logRecord);LogRecordPOlogRecordPOLogRecordPO。toPo(logRecord);logRecordMapper。insert(logRecordPO);}OverridepublicListLogRecordqueryLog(StringbizKey,CollectionStringtypes){returnLists。newArrayList();}OverridepublicPageDOLogRecordqueryLogByBizNo(StringbizNo,CollectionStringtypes,PageRequestDOpageRequestDO){returnlogRecordMapper。selectByBizNoAndCategory(bizNo,types,pageRequestDO);}}复制代码IParseFunction自定义转换函数的接口,可以实现IParseFunction实现对LogRecord注解中使用的函数扩展 例子:ComponentpublicclassUserParseFunctionimplementsIParseFunction{privatefinalSplittersplitterSplitter。on(,)。trimResults();ResourceLazyprivateUserQueryServiceuserQueryService;OverridepublicStringfunctionName(){returnUSER;}Override11,12返回11(小明),12(张三)publicStringapply(Stringvalue){if(StringUtils。isEmpty(value)){returnvalue;}ListStringuserIdsLists。newArrayList(splitter。split(value));ListUsermisDOListuserQueryService。getUserList(userIds);MapString,UseruserMapStreamUtil。extractMap(misDOList,User::getId);StringBuilderstringBuildernewStringBuilder();for(StringuserId:userIds){stringBuilder。append(userId);if(userMap。get(userId)!null){stringBuilder。append(()。append(userMap。get(userId)。getUsername())。append());}stringBuilder。append(,);}returnstringBuilder。toString()。replaceAll(,34;,);}}复制代码变量相关 LogRecordAnnotation可以使用的变量出了参数也可以使用返回值ret变量,以及异常的错误信息errorMsg,也可以通过SpEL的T方式调用静态方法噢ChangeLogTODO 注意点: 整体日志拦截是在方法执行之后记录的,所以对于方法内部修改了方法参数之后,LogRecordAnnotation的注解上的SpEL对变量的取值是修改后的值哦源码 https:github。commouztmztbizlog