After using the MyBatis interceptor, the fishing time is long again.

scene

In the development of back-end services, the popular framework combination is SSM (SpringBoot + Spring + MyBatis). When we develop some business systems, there will be many business data tables, and the information in the tables will be inserted again, and many operations may be carried out in the whole life cycle.

For example, when we purchase a commodity on a website, an order record will be generated. After the amount is paid, the order status will change to paid. Finally, when we receive the ordered commodity, the order status will change to completed, etc.

Suppose our order form t_ The order result is as follows:

When creating an order, you need to set insert_by,insert_time,update_by,update_ Value of time;

When updating the order status, you only need to update the update_by,update_ The value of time.

How should we deal with it?

Muggle practice

The simplest and easiest way to think of is to process the relevant fields in each business processing code.

For example, in the order creation method, the processing is as follows:

public void create(Order order){
    // ... Other codes
    // Set audit fields
    Date now = new Date();
    order.setInsertBy(appContext.getUser());
    order.setUpdateBy(appContext.getUser());
    order.setInsertTime(now);
    order.setUpdateTime(now);
    orderDao.insert(order);
}

For the order update method, only updateBy and updateTime are set:

public void update(Order order){
    // ... Other codes

    // Set audit fields
    Date now = new Date();
    order.setUpdateBy(appContext.getUser());
    order.setUpdateTime(now);
    orderDao.insert(order);
}

Although this method can complete the function, there are some problems:

  • You need to decide which fields to set according to different business logic in each method;
  • After there are many business models, the business methods of each model must be set, and there are too many duplicate codes.

After we know that there are problems with this method, we have to find out if there are any good methods, right? Look down!

Elegant approach

Because our persistence layer framework uses MyBatis more, we use the interceptor of MyBatis to complete our functions.

First of all, let's understand what is an interceptor?

What is an interceptor?

MyBatis interceptor, as its name suggests, intercepts certain operations. The interceptor can intercept some methods before and after execution, and add some processing logic.

The interceptor of MyBatis can intercept the interfaces of Executor, StatementHandler, pagerhandler and ResultSetHandler, that is, it will proxy these four objects.

The original intention of interceptor design is to enable users to integrate into the whole execution process in the form of plug-ins without modifying the source code of MyBatis.

For example, the executors in MyBatis include BatchExecutor, reuseexecution, simpleexecution and cacheingexecution. If these query methods can not meet your needs, we can intercept the query method of the Executor interface by establishing an interceptor instead of directly modifying the source code of MyBatis, and implement our own query method logic after interception.

The Interceptor in MyBatis is represented by the Interceptor interface, which has three methods.

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

The plugin method is used by the interceptor to encapsulate the target object. Through this method, we can return the target object itself or its proxy.

When the proxy is returned, we can intercept the methods to call the intercept method. Of course, we can also call other methods.

The setProperties method is used to specify some properties in the Mybatis configuration file.

Update audit fields using interceptors

So how can we use interceptors to realize the function of assigning values to audit fields?

When we create and modify orders, we essentially execute insert and update statements through MyBatis, which is processed through the Executor.

We can intercept the Executor through the interceptor, and then set the insert for the data object to be inserted according to the executed statement in the interceptor_ by,insert_ time,update_ by,update_ Time and other attribute values are OK.

custom interceptor

The most important thing to customize the Interceptor is to implement the plugin method and intercept method.

In the plugin method, we can decide whether to intercept, and then decide what kind of target object to return.

The intercept method is the method to be executed when intercepting.

For the plugin method, in fact, Mybatis has provided us with an implementation. There is a class called plugin in Mybatis, which has a static method wrap(Object target,Interceptor interceptor). Through this method, you can decide whether the object to be returned is the target object or the corresponding agent.

However, there is still a problem here, that is, how do we know that the table to be inserted has audit fields to be processed in the interceptor?

Because not all tables in our table are business tables, some dictionary tables or definition tables may have no audit fields. We do not need to process such tables in the interceptor.

In other words, we should be able to distinguish which objects need to update the audit field.

Here, we can define an interface to make the models that need to update the audit field implement the interface uniformly. This interface acts as a marker.

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
}

Next, we can implement our custom interceptor.

@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 {
        // Get user name from context
        String userName = AppContext.getUser();
        
        Object[] args = invocation.getArgs();
        SqlCommandType sqlCommandType = null;
        
        for (Object object : args) {
            // Get operation type from MappedStatement parameter
            if (object instanceof MappedStatement) {
                MappedStatement ms = (MappedStatement) object;
                sqlCommandType = ms.getSqlCommandType();
                logger.debug("Operation type: {}", sqlCommandType);
                continue;
            }
            // Judge whether the parameter is BaseDO type
            // One parameter
            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 compatible 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;
                    }
                }
            }
            // Compatible batch insert
            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) {
    }
}

From the above code, we can see that our customized Interceptor IbatisAuditDataInterceptor implements the Interceptor interface.

In the @ Intercepts annotation on our interceptor, the type parameter specifies that the intercepted class is the implementation of the Executor interface, and the method parameter specifies to intercept the update method in the Executor, because the addition, deletion and modification of database operations are performed through the update method.

Configure interceptor plug-in

After the interceptor is defined, the interceptor needs to be specified in the plugins of SqlSessionFactoryBean to take effect. Therefore, it should be configured as follows.

<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>
            <!-- Process audit fields -->
            <ref bean="ibatisAuditDataInterceptor" />
        </array>
    </property>

Here, our customized interceptor takes effect. Through the test, you will find that there is no need to manually set the value of the audit field in the business code. After the transaction is submitted, the audit field will be automatically assigned through the interceptor plug-in.

Summary

In this issue, Xiaohei introduces how to deal with the frequent update of audit fields in our daily development gracefully.

By customizing the interceptor of MyBatis, automatically assign values to some business models with audit fields in the form of plug-ins, so as to avoid writing boring and repetitive code.

After all, life is short. Write less code and fish more.

If this article is helpful to you, give Xiaohei a praise and encouragement.

I'm Xiao Hei, a programmer who "lingers" on the Internet

Running water doesn't compete first. It's important to keep talking

Keywords: Java Mybatis

Added by dibyajyotig on Mon, 21 Feb 2022 12:32:37 +0200