After using MyBatis interceptor, the fishing time is long again

Posted by chyan on Mon, 21 Feb 2022 05:27:52 +0100

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);
}
Copy code

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);
}
Copy code

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);

}
Copy code

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
}
Copy code

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) {
    }
}
Copy code

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>
Copy code

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.

Now Java interviews rely on eight part essay, so I spent a long time collecting and sorting out this complete set of Java interview questions. I hope it will be helpful to you. Remember to collect some praise~

Bloggers have compiled the following interview questions into a Java interview manual, which is a PDF version.

After a month's work, I finally completed the # 240000 word # interview manualhttps://mp.weixin.qq.com/s?__biz=MzU4MjgwNjQ0OQ==&mid=2247485336&idx=1&sn=1659e2995ab83d9f8e2ba5df9d5a838d&chksm=fdb3f946cac47050b18b9b517ebecd894f2818926fdb39e1f15b6e78d212c7369dd61d1ddc97#rdIntroduction to Java, including the most complete information package for interview (including download method)https://mp.weixin.qq.com/s?__biz=MzU4MjgwNjQ0OQ==&mid=2247484784&idx=1&sn=4c5a04cf05e4f6a0e78f57dc612ecd42&chksm=fdb3fbaecac472b8b439759fa043477228858fa68bbb015be3f741143bcc6b5dd71734fc6855&token=580539138&lang=zh_CN#rd
Author: Xiao Hei said Java

Topics: Java Spring Spring Boot Programmer