关于后端:使用MyBatis拦截器后摸鱼时间又长了🐟

场景
在后端服务开发时,当初很风行的框架组合个别都是SSM(Spring MVC + Spring + MyBatis)或者SpringBoot集成Mybatis,JPA等长久层框架,在咱们进行一些业务零碎开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很屡次的操作。
比方,咱们在某网站购买一件商品,会生成一条订单记录,在领取完金额后订单状态会变为已领取,等最初咱们收到订单商品,这个订单状态会变成已实现等。
假如咱们的订单表t_order后果如下:

当订单创立时,须要设置insert_by,insert_time,update_by,update_time的值;
在进行订单状态更新时,则只须要更新update_by,update_time的值。
那应该如何解决呢?
麻瓜做法
最简略的做法,也是最容易想到的做法,就是在每个业务解决的代码中,对相干的字段进行解决。
比方订单创立的办法中,如下解决:
public void create(Order order){

// ...其余代码
// 设置审计字段
Date now = new Date();
order.setInsertBy(appContext.getUser());
order.setUpdateBy(appContext.getUser());
order.setInsertTime(now);
order.setUpdateTime(now);
orderDao.insert(order);

}
复制代码
订单更新办法则只设置updateBy和updateTime:
public void update(Order order){

// ...其余代码

// 设置审计字段
Date now = new Date();
order.setUpdateBy(appContext.getUser());
order.setUpdateTime(now);
orderDao.insert(order);

}
复制代码
这种形式尽管能够实现性能,然而存在一些问题:

须要在每个办法中依照不同的业务逻辑决定设置哪些字段;
在业务模型变多后,每个模型的业务办法中都要进行设置,反复代码太多。

那咱们晓得这种形式存在问题当前,就得找找有什么好办法对不对,往下看!
优雅做法
因为咱们长久层框架更多地应用MyBatis,那咱们就借助于MyBatis的拦截器来实现咱们的性能。
首先咱们来理解一下,什么是拦截器?
什么是拦截器?
MyBatis的拦截器顾名思义,就是对某些操作进行拦挡。通过拦截器能够对某些办法执行前后进行拦挡,增加一些解决逻辑。
MyBatis的拦截器能够对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦挡,也就是说会对这4种对象进行代理。
拦截器设计的初衷就是为了让用户在MyBatis的解决流程中不用去批改MyBatis的源码,可能以插件的形式集成到整个执行流程中。
比方MyBatis中的Executor有BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor,如果这几种实现的query办法都不能满足你的需要,咱们能够不必去间接批改MyBatis的源码,而通过建设拦截器的形式,拦挡Executor接口的query办法,在拦挡之后,实现本人的query办法逻辑。
在MyBatis中的拦截器通过Interceptor接口示意,该接口中有三个办法。
public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);

}
复制代码
plugin办法是拦截器用于封装指标对象的,通过该办法咱们能够返回指标对象自身,也能够返回一个它的代理。
当返回的是代理的时候咱们能够对其中的办法进行拦挡来调用intercept办法,当然也能够调用其余办法。
setProperties办法是用于在Mybatis配置文件中指定一些属性的。
应用拦截器更新审计字段
那么咱们应该如何通过拦截器来实现咱们对审计字段赋值的性能呢?
在咱们进行订单创立和批改时,实质上是通过MyBatis执行insert、update语句,MyBatis是通过Executor来解决的。
咱们能够通过拦截器拦挡Executor,而后在拦截器中对要插入的数据对象依据执行的语句设置insert_by,insert_time,update_by,update_time等属性值就能够了。
自定义拦截器
自定义Interceptor最重要的是要实现plugin办法和intercept办法。
在plugin办法中咱们能够决定是否要进行拦挡进而决定要返回一个什么样的指标对象。
在intercept办法就是要进行拦挡的时候要执行的办法。
对于plugin办法而言,其实Mybatis曾经为咱们提供了一个实现。Mybatis中有一个叫做Plugin的类,外面有一个静态方法wrap(Object target,Interceptor interceptor),通过该办法能够决定要返回的对象是指标对象还是对应的代理。
然而这里还存在一个问题,就是咱们如何在拦截器中晓得要插入的表有审计字段须要解决呢?
因为咱们的表中并不是所有的表都是业务表,可能有一些字典表或者定义表是没有审计字段的,这样的表咱们不须要在拦截器中进行解决。
也就是说咱们要可能辨别出哪些对象须要更新审计字段。
这里咱们能够定义一个接口,让须要更新审计字段的模型都对立实现该接口,这个接口起到一个标记的作用。
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

}
复制代码
接下来,咱们就能够实现咱们的自定义拦截器了。
@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 {
    // 从上下文中获取用户名
    String userName = AppContext.getUser();
    
    Object[] args = invocation.getArgs();
    SqlCommandType sqlCommandType = null;
    
    for (Object object : args) {
        // 从MappedStatement参数中获取到操作类型
        if (object instanceof MappedStatement) {
            MappedStatement ms = (MappedStatement) object;
            sqlCommandType = ms.getSqlCommandType();
            logger.debug("操作类型: {}", sqlCommandType);
            continue;
        }
        // 判断参数是否是BaseDO类型
        // 一个参数
        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) {
}

}
复制代码
通过下面的代码能够看到,咱们自定义的拦截器IbatisAuditDataInterceptor实现了Interceptor接口。
在咱们拦截器上的@Intercepts注解,type参数指定了拦挡的类是Executor接口的实现,method 参数指定拦挡Executor中的update办法,因为数据库操作的增删改操作都是通过update办法执行。
配置拦截器插件
在定义好拦截器之后,须要将拦截器指定到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>
        <!-- 解决审计字段 -->
        <ref bean="ibatisAuditDataInterceptor" />
    </array>
</property>

复制代码
到这里,咱们自定义的拦截器就失效了,通过测试你会发现,不必在业务代码中手动设置审计字段的值,会在事务提交之后,通过拦截器插件主动对审计字段进行赋值。
小结
在本期内容中给大家介绍了对于咱们日常开发中很频繁的审计字段的更新操作,应该如何优雅地解决。
通过自定义MyBatis的拦截器,以插件的模式对一些有审计字段的业务模型主动赋值,防止反复编写干燥的反复代码。
毕竟人生苦短,少写代码,多摸鱼。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理