- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章Springboot-注解-操作日志的实现方式由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
此组件解决的问题是:
「谁」在「什么时间」对「什么」做了「什么事」 。
本组件目前针对 Spring-boot 做了 Autoconfig,如果是 SpringMVC,也可自己在 xml 初始化 bean 。
使用方式 。
maven依赖添加SDK依赖 。
1
2
3
4
5
|
<
dependency
>
<
groupId
>io.github.mouzt</
groupId
>
<
artifactId
>bizlog-sdk</
artifactId
>
<
version
>1.0.1</
version
>
</
dependency
>
|
SpringBoot入口打开开关,添加 @EnableLogRecord 注解 。
tenant是代表租户的标识,一般一个服务或者一个业务下的多个服务都写死一个 tenant 就可以 。
1
2
3
4
5
6
7
8
|
@SpringBootApplication
(exclude = DataSourceAutoConfiguration.
class
)
@EnableTransactionManagement
@EnableLogRecord
(tenant =
"com.mzt.test"
)
public
class
Main {
public
static
void
main(String[] args) {
SpringApplication.run(Main.
class
, args);
}
}
|
pefix:是拼接在 bizNo 上作为 log 的一个标识。避免 bizNo 都为整数 ID 的时候和其他的业务中的 ID 重复。比如订单 ID、用户 ID 等 。
bizNo:就是业务的 ID,比如订单ID,我们查询的时候可以根据 bizNo 查询和它相关的操作日志 。
success:方法调用成功后把 success 记录在日志的内容中 。
SpEL 表达式:其中用双大括号包围起来的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL表达式。Spring中支持的它都支持的。比如调用静态方法,三目表达式。SpEL 可以使用方法中的任何参数 。
1
2
3
4
5
6
7
|
@LogRecordAnnotation
(success =
"{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}"
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
)
public
boolean
createOrder(Order order) {
log.info(
"【创建订单】orderNo={}"
, order.getOrderNo());
// db insert order
return
true
;
}
|
此时会打印操作日志 “张三下了一个订单,购买商品「超值优惠红烧肉套餐」,下单结果:true” 。
1
2
3
4
5
6
7
8
9
|
@LogRecordAnnotation
(
fail =
"创建订单失败,失败原因:「{{#_errorMsg}}」"
,
success =
"{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}"
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
)
public
boolean
createOrder(Order order) {
log.info(
"【创建订单】orderNo={}"
, order.getOrderNo());
// db insert order
return
true
;
}
|
其中的 #_errorMsg 是取的方法抛出异常后的异常的 errorMessage.
比如一个订单的操作日志,有些操作日志是用户自己操作的,有些操作是系统运营人员做了修改产生的操作日志,我们系统不希望把运营的操作日志暴露给用户看到, 。
但是运营期望可以看到用户的日志以及运营自己操作的日志,这些操作日志的bizNo都是订单号,所以为了扩展添加了类型字段,主要是为了对日志做分类,查询方便,支持更多的业务.
1
2
3
4
5
6
7
8
9
10
|
@LogRecordAnnotation
(
fail =
"创建订单失败,失败原因:「{{#_errorMsg}}」"
,
category =
"MANAGER"
,
success =
"{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}"
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
)
public
boolean
createOrder(Order order) {
log.info(
"【创建订单】orderNo={}"
, order.getOrderNo());
// db insert order
return
true
;
}
|
如果一个操作修改了很多字段,但是success的日志模版里面防止过长不能把修改详情全部展示出来,这时候需要把修改的详情保存到 detail 字段, 。
detail 是一个 String ,需要自己序列化。这里的 #order.toString() 是调用了 Order 的 toString() 方法.
如果保存 JSON,自己重写一下 Order 的 toString() 方法就可以.
1
2
3
4
5
6
7
8
9
10
11
|
@LogRecordAnnotation
(
fail =
"创建订单失败,失败原因:「{{#_errorMsg}}」"
,
category =
"MANAGER_VIEW"
,
detail =
"{{#order.toString()}}"
,
success =
"{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}"
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
)
public
boolean
createOrder(Order order) {
log.info(
"【创建订单】orderNo={}"
, order.getOrderNo());
// db insert order
return
true
;
}
|
第一种:手工在LogRecord的注解上指定。这种需要方法参数上有operator 。
1
2
3
4
5
6
7
8
9
10
11
12
|
@LogRecordAnnotation
(
fail =
"创建订单失败,失败原因:「{{#_errorMsg}}」"
,
category =
"MANAGER_VIEW"
,
detail =
"{{#order.toString()}}"
,
operator =
"{{#currentUser}}"
,
success =
"{{#order.purchaseName}}下了一个订单,购买商品「{{#order.productName}}」,下单结果:{{#_ret}}"
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
)
public
boolean
createOrder(Order order, String currentUser) {
log.info(
"【创建订单】orderNo={}"
, order.getOrderNo());
// db insert order
return
true
;
}
|
这种方法手工指定,需要方法参数上有 operator 参数,或者通过 SpEL 调用静态方法获取当前用户.
第二种: 通过默认实现类来自动的获取操作人,由于在大部分web应用中当前的用户都是保存在一个线程上下文中的,所以每个注解都加一个operator获取操作人显得有些重复劳动,所以提供了一个扩展接口来获取操作人 。
框架提供了一个扩展接口,使用框架的业务可以 implements 这个接口自己实现获取当前用户的逻辑, 。
对于使用 Springboot 的只需要实现 IOperatorGetService 接口,然后把这个 Service 作为一个单例放到 Spring 的上下文中。使用 Spring Mvc 的就需要自己手工装配这些 bean 了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Configuration
public
class
LogRecordConfiguration {
@Bean
public
IOperatorGetService operatorGetService() {
return
() -> Optional.of(OrgUserUtils.getCurrentUser())
.map(a ->
new
OperatorDO(a.getMisId()))
.orElseThrow(() ->
new
IllegalArgumentException(
"user is null"
));
}
}
//也可以这么搞:
@Service
public
class
DefaultOperatorGetServiceImpl
implements
IOperatorGetService {
@Override
public
OperatorDO getUser() {
OperatorDO operatorDO =
new
OperatorDO();
operatorDO.setOperatorId(
"SYSTEM"
);
return
operatorDO;
}
}
|
对于更新等方法,方法的参数上大部分都是订单ID、或者产品ID等, 。
比如下面的例子:日志记录的success内容是:“更新了订单{{#orderId}},更新内容为…”,这种对于运营或者产品来说难以理解,所以引入了自定义函数的功能.
使用方法是在原来的变量的两个大括号之间加一个函数名称 例如 “{ORDER{#orderId}}” 其中 ORDER 是一个函数名称。只有一个函数名称是不够的,需要添加这个函数的定义和实现。可以看下面例子 。
自定义的函数需要实现框架里面的IParseFunction的接口,需要实现两个方法:
functionName() 方法就返回注解上面的函数名; 。
apply()函数参数是 "{ORDER{#orderId}}"中SpEL解析的#orderId的值,这里是一个数字1223110,接下来只需要在实现的类中把 ID 转换为可读懂的字符串就可以了, 。
一般为了方便排查问题需要把名称和ID都展示出来,例如:"订单名称(ID)"的形式.
这里有个问题:加了自定义函数后,框架怎么能调用到呢?
答:对于Spring boot应用很简单,只需要把它暴露在Spring的上下文中就可以了,可以加上Spring的 @Component 或者 @Service 很方便。Spring mvc 应用需要自己装配 Bean.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 没有使用自定义函数
@LogRecordAnnotation
(success =
"更新了订单{{#orderId}},更新内容为...."
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
,
detail =
"{{#order.toString()}}"
)
public
boolean
update(Long orderId, Order order) {
return
false
;
}
//使用了自定义函数,主要是在 {{#orderId}} 的大括号中间加了 functionName
@LogRecordAnnotation
(success =
"更新了订单ORDER{#orderId}},更新内容为..."
,
prefix = LogRecordType.ORDER, bizNo =
"{{#order.orderNo}}"
,
detail =
"{{#order.toString()}}"
)
public
boolean
update(Long orderId, Order order) {
return
false
;
}
// 还需要加上函数的实现
@Component
public
class
OrderParseFunction
implements
IParseFunction {
@Resource
@Lazy
//为了避免类加载顺序的问题 最好为Lazy,没有问题也可以不加
private
OrderQueryService orderQueryService;
@Override
public
String functionName() {
// 函数名称为 ORDER
return
"ORDER"
;
}
@Override
//这里的 value 可以吧 Order 的JSON对象的传递过来,然后反解析拼接一个定制的操作日志内容
public
String apply(String value) {
if
(StringUtils.isEmpty(value)){
return
value;
}
Order order = orderQueryService.queryOrder(Long.parseLong(value));
//把订单产品名称加上便于理解,加上 ID 便于查问题
return
order.getProductName().concat(
"("
).concat(value).concat(
")"
);
}
}
|
1
2
3
4
5
|
@LogRecordAnnotation
(prefix = LogRecordTypeConstant.CUSTOM_ATTRIBUTE, bizNo =
"{{#businessLineId}}"
,
success =
"{{#disable ? '停用' : '启用'}}了自定义属性{ATTRIBUTE{#attributeId}}"
)
public
CustomAttributeVO disableAttribute(Long businessLineId, Long attributeId,
boolean
disable) {
return
xxx;
}
|
重写OperatorGetServiceImpl通过上下文获取用户的扩展,例子如下 。
1
2
3
4
5
6
7
8
9
10
|
@Service
public
class
DefaultOperatorGetServiceImpl
implements
IOperatorGetService {
@Override
public
Operator getUser() {
return
Optional.ofNullable(UserUtils.getUser())
.map(a ->
new
Operator(a.getName(), a.getLogin()))
.orElseThrow(()->
new
IllegalArgumentException(
"user is null"
));
}
}
|
ILogRecordService 保存/查询日志的例子,使用者可以根据数据量保存到合适的存储介质上,比如保存在数据库/或者ES。自己实现保存和删除就可以了 。
也可以只实现查询的接口,毕竟已经保存在业务的存储上了,查询业务可以自己实现,不走 ILogRecordService 这个接口,毕竟产品经理会提一些千奇百怪的查询需求.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Service
public
class
DbLogRecordServiceImpl
implements
ILogRecordService {
@Resource
private
LogRecordMapper logRecordMapper;
@Override
@Transactional
(propagation = Propagation.REQUIRES_NEW)
public
void
record(LogRecord logRecord) {
log.info(
"【logRecord】log={}"
, logRecord);
LogRecordPO logRecordPO = LogRecordPO.toPo(logRecord);
logRecordMapper.insert(logRecordPO);
}
@Override
public
List<LogRecord> queryLog(String bizKey, Collection<String> types) {
return
Lists.newArrayList();
}
@Override
public
PageDO<LogRecord> queryLogByBizNo(String bizNo, Collection<String> types, PageRequestDO pageRequestDO) {
return
logRecordMapper.selectByBizNoAndCategory(bizNo, types, pageRequestDO);
}
}
|
IParseFunction 自定义转换函数的接口,可以实现IParseFunction 实现对LogRecord注解中使用的函数扩展 。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Component
public
class
UserParseFunction
implements
IParseFunction {
private
final
Splitter splitter = Splitter.on(
","
).trimResults();
@Resource
@Lazy
private
UserQueryService userQueryService;
@Override
public
String functionName() {
return
"USER"
;
}
@Override
// 11,12 返回 11(小明),12(张三)
public
String apply(String value) {
if
(StringUtils.isEmpty(value)) {
return
value;
}
List<String> userIds = Lists.newArrayList(splitter.split(value));
List<User> misDOList = userQueryService.getUserList(userIds);
Map<String, User> userMap = StreamUtil.extractMap(misDOList, User::getId);
StringBuilder stringBuilder =
new
StringBuilder();
for
(String userId : userIds) {
stringBuilder.append(userId);
if
(userMap.get(userId) !=
null
) {
stringBuilder.append(
"("
).append(userMap.get(userId).getUsername()).append(
")"
);
}
stringBuilder.append(
","
);
}
return
stringBuilder.toString().replaceAll(
",$"
,
""
);
}
}
|
LogRecordAnnotation 可以使用的变量出了参数也可以使用返回值#_ret变量,以及异常的错误信息#_errorMsg,也可以通过SpEL的 T 方式调用静态方法噢 。
实现一个 Log的 Context,可以解决方法参数中没有的变量但是想使用的问题,初步想法是可以通过在方法中 add 变量的形式实现,很快就可以实现了 。
整体日志拦截是在方法执行之后记录的,所以对于方法内部修改了方法参数之后,LogRecordAnnotation 的注解上的 SpEL 对变量的取值是修改后的值哦~ 。
源码 。
https://github.com/mouzt/mzt-biz-log 。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我。如有错误或未考虑完全的地方,望不吝赐教.
原文链接:https://blog.csdn.net/weixin_43954303/article/details/113781801 。
最后此篇关于Springboot-注解-操作日志的实现方式的文章就讲到这里了,如果你想了解更多关于Springboot-注解-操作日志的实现方式的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在努力做到这一点 在我的操作中从数据库获取对象列表(确定) 在 JSP 上打印(确定) 此列表作为 JSP 中的可编辑表出现。我想修改然后将其提交回同一操作以将其保存在我的数据库中(失败。当我使用
我有以下形式的 Linq to Entities 查询: var x = from a in SomeData where ... some conditions ... select
我有以下查询。 var query = Repository.Query() .Where(p => !p.IsDeleted && p.Article.ArticleSections.Cou
我正在编写一个应用程序包,其中包含一个主类,其中主方法与GUI类分开,GUI类包含一个带有jtabbedpane的jframe,它有两个选项卡,第一个选项卡包含一个jtable,称为jtable1,第
以下代码产生错误 The nested query is not supported. Operation1='Case' Operation2='Collect' 问题是我做错了什么?我该如何解决?
我已经为 HA redis 集群(2 个副本、1 个主节点、3 个哨兵)设置了本地 docker 环境。只有哨兵暴露端口(10021、10022、10023)。 我使用的是 stackexchange
我正在 Desk.com 中构建一个“集成 URL”,它使用 Shopify Liquid 模板过滤器语法。对于开始日期为 7 天前而结束日期为现在的查询,此 URL 需要包含“开始日期”和“结束日期
你一定想过。然而情况却不理想,python中只能使用类似于 i++/i--等操作。 python中的自增操作 下面代码几乎是所有程序员在python中进行自增(减)操作的常用
我需要在每个使用 github 操作的手动构建中显示分支。例如:https://gyazo.com/2131bf83b0df1e2157480e5be842d4fb 我应该显示分支而不是一个。 最佳答
我有一个关于 Perl qr 运算符的问题: #!/usr/bin/perl -w &mysplit("a:b:c", /:/); sub mysplit { my($str, $patt
我已经使用 ArgoUML 创建了一个 ERD(实体关系图),我希望在一个类中创建两个操作,它们都具有 void 返回类型。但是,我只能创建一个返回 void 类型的操作。 例如: 我能够将 book
Github 操作仍处于测试阶段并且很新,但我希望有人可以提供帮助。我认为可以在主分支和拉取请求上运行 github 操作,如下所示: on: pull_request push: b
我正在尝试创建一个 Twilio 工作流来调用电话并记录用户所说的内容。为此,我正在使用 Record,但我不确定要在 action 参数中放置什么。 尽管我知道 Twilio 会发送有关调用该 UR
我不确定这是否可行,但值得一试。我正在使用模板缓冲区来减少使用此算法的延迟渲染器中光体积的过度绘制(当相机位于体积之外时): 使用廉价的着色器,将深度测试设置为 LEQUAL 绘制背面,将它们标记在模
有没有聪明的方法来复制 和 重命名 文件通过 GitHub 操作? 我想将一些自述文件复制到 /docs文件夹(:= 同一个 repo,不是远程的!),它们将根据它们的 frontmatter 重命名
我有一个 .csv 文件,其中第一列包含用户名。它们采用 FirstName LastName 的形式。我想获取 FirstName 并将 LastName 的第一个字符添加到它上面,然后删除空格。然
Sitecore 根据 Sitecore 树中定义的项目名称生成 URL, http://samplewebsite/Pages/Sample Page 但我们的客户有兴趣降低所有 URL(页面/示例
我正在尝试进行一些计算,但是一旦我输入金额,它就会完成。我只是希望通过单击按钮而不是自动发生这种情况。 到目前为止我做了什么: Angular JS - programming-fr
我的公司创建了一种在环境之间移动文件的复杂方法,现在我们希望将某些构建的 JS 文件(已转换和缩小)从一个 github 存储库移动到另一个。使用 github 操作可以实现这一点吗? 最佳答案 最简
在我的代码中,我创建了一个 JSONArray 对象。并向 JSONArray 对象添加了两个 JSONObject。我使用的是 json-simple-1.1.jar。我的代码是 package j
我是一名优秀的程序员,十分优秀!