場(chǎng)景
在后端服務(wù)開(kāi)發(fā)時(shí),現(xiàn)在很流行的框架組合就是SSM(SpringBoot + Spring + MyBatis)太惠,在我們進(jìn)行一些業(yè)務(wù)系統(tǒng)開(kāi)發(fā)時(shí)磨淌,會(huì)有很多的業(yè)務(wù)數(shù)據(jù)表,而表中的信息從新插入開(kāi)始凿渊,整個(gè)生命周期過(guò)程中可能會(huì)進(jìn)行很多次的操作伦糯。
比如,我們?cè)谀尘W(wǎng)站購(gòu)買(mǎi)一件商品嗽元,會(huì)生成一條訂單記錄,在支付完金額后訂單狀態(tài)會(huì)變?yōu)橐阎Ц段够鳎茸詈笪覀兪盏接唵紊唐芳涟@個(gè)訂單狀態(tài)會(huì)變成已完成等。
假設(shè)我們的訂單表t_order
結(jié)果如下:
當(dāng)訂單創(chuàng)建時(shí)翰绊,需要設(shè)置insert_by
佩谷,insert_time
旁壮,update_by
,update_time
的值谐檀;
在進(jìn)行訂單狀態(tài)更新時(shí)抡谐,則只需要更新update_by
,update_time
的值桐猬。
那應(yīng)該如何處理呢麦撵?
麻瓜做法
最簡(jiǎn)單的做法,也是最容易想到的做法溃肪,就是在每個(gè)業(yè)務(wù)處理的代碼中免胃,對(duì)相關(guān)的字段進(jìn)行處理。
比如訂單創(chuàng)建的方法中惫撰,如下處理:
public void create(Order order){
// ...其他代碼
// 設(shè)置審計(jì)字段
Date now = new Date();
order.setInsertBy(appContext.getUser());
order.setUpdateBy(appContext.getUser());
order.setInsertTime(now);
order.setUpdateTime(now);
orderDao.insert(order);
}
訂單更新方法則只設(shè)置updateBy
和updateTime
:
public void update(Order order){
// ...其他代碼
// 設(shè)置審計(jì)字段
Date now = new Date();
order.setUpdateBy(appContext.getUser());
order.setUpdateTime(now);
orderDao.insert(order);
}
這種方式雖然可以完成功能羔沙,但是存在一些問(wèn)題:
- 需要在每個(gè)方法中按照不同的業(yè)務(wù)邏輯決定設(shè)置哪些字段;
- 在業(yè)務(wù)模型變多后厨钻,每個(gè)模型的業(yè)務(wù)方法中都要進(jìn)行設(shè)置扼雏,重復(fù)代碼太多。
那我們知道這種方式存在問(wèn)題以后夯膀,就得找找有什么好方法對(duì)不對(duì)诗充,往下看!
優(yōu)雅做法
因?yàn)槲覀兂志脤涌蚣芨嗟厥褂肕yBatis棍郎,那我們就借助于MyBatis的攔截器來(lái)完成我們的功能其障。
首先我們來(lái)了解一下,什么是攔截器涂佃?
什么是攔截器抬吟?
MyBatis的攔截器顧名思義,就是對(duì)某些操作進(jìn)行攔截樟结。通過(guò)攔截器可以對(duì)某些方法執(zhí)行前后進(jìn)行攔截政冻,添加一些處理邏輯。
MyBatis的攔截器可以對(duì)Executor伯病、StatementHandler造烁、PameterHandler和ResultSetHandler 接口進(jìn)行攔截,也就是說(shuō)會(huì)對(duì)這4種對(duì)象進(jìn)行代理午笛。
攔截器設(shè)計(jì)的初衷就是為了讓用戶(hù)在MyBatis的處理流程中不必去修改MyBatis的源碼惭蟋,能夠以插件的方式集成到整個(gè)執(zhí)行流程中。
比如MyBatis中的Executor
有BatchExecutor
药磺、ReuseExecutor
告组、SimpleExecutor
和CachingExecutor
,如果這幾種實(shí)現(xiàn)的query
方法都不能滿(mǎn)足你的需求癌佩,我們可以不用去直接修改MyBatis的源碼木缝,而通過(guò)建立攔截器的方式便锨,攔截Executor
接口的query
方法,在攔截之后我碟,實(shí)現(xiàn)自己的query方法邏輯放案。
在MyBatis中的攔截器通過(guò)Interceptor接口表示,該接口中有三個(gè)方法矫俺。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
復(fù)制代碼
plugin方法是攔截器用于封裝目標(biāo)對(duì)象的吱殉,通過(guò)該方法我們可以返回目標(biāo)對(duì)象本身,也可以返回一個(gè)它的代理恳守。
當(dāng)返回的是代理的時(shí)候我們可以對(duì)其中的方法進(jìn)行攔截來(lái)調(diào)用intercept方法考婴,當(dāng)然也可以調(diào)用其他方法。
setProperties方法是用于在Mybatis配置文件中指定一些屬性的催烘。
使用攔截器更新審計(jì)字段
那么我們應(yīng)該如何通過(guò)攔截器來(lái)實(shí)現(xiàn)我們對(duì)審計(jì)字段賦值的功能呢沥阱?
在我們進(jìn)行訂單創(chuàng)建和修改時(shí),本質(zhì)上是通過(guò)MyBatis執(zhí)行insert伊群、update語(yǔ)句考杉,MyBatis是通過(guò)Executor來(lái)處理的。
我們可以通過(guò)攔截器攔截Executor舰始,然后在攔截器中對(duì)要插入的數(shù)據(jù)對(duì)象根據(jù)執(zhí)行的語(yǔ)句設(shè)置insert_by,insert_time,update_by,update_time等屬性值就可以了崇棠。
自定義攔截器
自定義Interceptor
最重要的是要實(shí)現(xiàn)plugin
方法和intercept
方法。
在plugin
方法中我們可以決定是否要進(jìn)行攔截進(jìn)而決定要返回一個(gè)什么樣的目標(biāo)對(duì)象丸卷。
在intercept
方法就是要進(jìn)行攔截的時(shí)候要執(zhí)行的方法枕稀。
對(duì)于plugin
方法而言,其實(shí)Mybatis已經(jīng)為我們提供了一個(gè)實(shí)現(xiàn)谜嫉。Mybatis中有一個(gè)叫做Plugin
的類(lèi)萎坷,里面有一個(gè)靜態(tài)方法wrap(Object target,Interceptor interceptor)
,通過(guò)該方法可以決定要返回的對(duì)象是目標(biāo)對(duì)象還是對(duì)應(yīng)的代理沐兰。
但是這里還存在一個(gè)問(wèn)題哆档,就是我們?nèi)绾卧跀r截器中知道要插入的表有審計(jì)字段需要處理呢?
因?yàn)槲覀兊谋碇胁⒉皇撬械谋矶际菢I(yè)務(wù)表住闯,可能有一些字典表或者定義表是沒(méi)有審計(jì)字段的瓜浸,這樣的表我們不需要在攔截器中進(jìn)行處理。
也就是說(shuō)我們要能夠區(qū)分出哪些對(duì)象需要更新審計(jì)字段比原。
這里我們可以定義一個(gè)接口插佛,讓需要更新審計(jì)字段的模型都統(tǒng)一實(shí)現(xiàn)該接口,這個(gè)接口起到一個(gè)標(biāo)記的作用量窘。
public interface BaseDO {
}
public class Order implements BaseDO{
private Long orderId;
private String orderNo;
private Integer orderStatus;
private String insertBy;
private String updateBy;
private Date insertTime;
private Date updateTime;
//... getter ,setter
}
復(fù)制代碼
接下來(lái)雇寇,我們就可以實(shí)現(xiàn)我們的自定義攔截器了。
@Component("ibatisAuditDataInterceptor")
@Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})})
public class IbatisAuditDataInterceptor implements Interceptor {
private Logger logger = LoggerFactory.getLogger(IbatisAuditDataInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 從上下文中獲取用戶(hù)名
String userName = AppContext.getUser();
Object[] args = invocation.getArgs();
SqlCommandType sqlCommandType = null;
for (Object object : args) {
// 從MappedStatement參數(shù)中獲取到操作類(lèi)型
if (object instanceof MappedStatement) {
MappedStatement ms = (MappedStatement) object;
sqlCommandType = ms.getSqlCommandType();
logger.debug("操作類(lèi)型: {}", sqlCommandType);
continue;
}
// 判斷參數(shù)是否是BaseDO類(lèi)型
// 一個(gè)參數(shù)
if (object instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(object, "insertedBy", userName);
BeanUtils.setProperty(object, "insertTimestamp", insertTime);
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", insertTime);
continue;
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", updateTime);
continue;
}
}
// 兼容MyBatis的updateByExampleSelective(record, example);
if (object instanceof ParamMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
ParamMap<Object> parasMap = (ParamMap<Object>) object;
String key = "record";
if (!parasMap.containsKey(key)) {
continue;
}
Object paraObject = parasMap.get(key);
if (paraObject instanceof BaseDO) {
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(paraObject, "updatedBy", userName);
BeanUtils.setProperty(paraObject, "updateTimestamp", updateTime);
continue;
}
}
}
// 兼容批量插入
if (object instanceof DefaultSqlSession.StrictMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
DefaultSqlSession.StrictMap<ArrayList<Object>> map = (DefaultSqlSession.StrictMap<ArrayList<Object>>) object;
String key = "collection";
if (!map.containsKey(key)) {
continue;
}
ArrayList<Object> objs = map.get(key);
for (Object obj : objs) {
if (obj instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(obj, "insertedBy", userName);
BeanUtils.setProperty(obj, "insertTimestamp", insertTime);
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", insertTime);
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", updateTime);
}
}
}
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
復(fù)制代碼
通過(guò)上面的代碼可以看到,我們自定義的攔截器IbatisAuditDataInterceptor
實(shí)現(xiàn)了Interceptor
接口谢床。
在我們攔截器上的@Intercepts
注解,type
參數(shù)指定了攔截的類(lèi)是Executor
接口的實(shí)現(xiàn)厘线,method
參數(shù)指定攔截Executor
中的update
方法识腿,因?yàn)閿?shù)據(jù)庫(kù)操作的增刪改操作都是通過(guò)update
方法執(zhí)行。
配置攔截器插件
在定義好攔截器之后造壮,需要將攔截器指定到SqlSessionFactoryBean
的plugins
中才能生效渡讼。所以要按照如下方式配置。
<bean id="transSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="transDataSource" />
<property name="mapperLocations">
<array>
<value>classpath:META-INF/mapper/*.xml</value>
</array>
</property>
<property name="plugins">
<array>
<!-- 處理審計(jì)字段 -->
<ref bean="ibatisAuditDataInterceptor" />
</array>
</property>
復(fù)制代碼
到這里耳璧,我們自定義的攔截器就生效了成箫,通過(guò)測(cè)試你會(huì)發(fā)現(xiàn),不用在業(yè)務(wù)代碼中手動(dòng)設(shè)置審計(jì)字段的值旨枯,會(huì)在事務(wù)提交之后蹬昌,通過(guò)攔截器插件自動(dòng)對(duì)審計(jì)字段進(jìn)行賦值。
小結(jié)
在本期內(nèi)容中小黑給大家介紹了對(duì)于我們?nèi)粘i_(kāi)發(fā)中很頻繁的審計(jì)字段的更新操作攀隔,應(yīng)該如何優(yōu)雅地處理皂贩。
通過(guò)自定義MyBatis的攔截器,以插件的形式對(duì)一些有審計(jì)字段的業(yè)務(wù)模型自動(dòng)賦值昆汹,避免重復(fù)編寫(xiě)枯燥的重復(fù)代碼明刷。
畢竟人生苦短,少寫(xiě)代碼满粗,多摸魚(yú)辈末。
作者:小黑說(shuō)Java
鏈接:https://juejin.cn/post/7061250661828001800