Principle and Application of MyBatis Interceptor

  • 2021-09-20 20:25:39
  • OfStack

Directory 1. Intercept Object and Interface Implementation Sample
2. Three ways of interceptor registration 1. XML registration 2. Configuration class registration 3. Annotation mode 3. ParameterHandler parameter rewriting-modify time and modify person system 1 insert 4. Rewrite SQL through StatementHandler

1. Interception object and interface implementation example

The function of the MyBatis interceptor is to perform additional processing between Dao and DB. In most cases, configuring sql through xml of mybatis can achieve the desired DB operation effect. However, there are 1 similar or identical query conditions or query requirements, which can improve development efficiency through the implementation of interceptors, such as paging, inserting and updating time/person, data authority, SQL monitoring log, etc.

Mybatis supports four object interceptions Executor, StatementHandler, PameterHandler, and ResultSetHandler Executor: Method to intercept an actuator. StatementHandler: Intercepts processing of Sql syntax builds. ParameterHandler: Handling of interception parameters. ResultHandler: Intercept processing of result sets.

public interface Executor {
    ResultHandler NO_RESULT_HANDLER = null;
    int update(MappedStatement var1, Object var2) throws SQLException;
    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;
    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
    <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
    List<BatchResult> flushStatements() throws SQLException;
    void commit(boolean var1) throws SQLException;
    void rollback(boolean var1) throws SQLException;
    CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);
    boolean isCached(MappedStatement var1, CacheKey var2);
    void clearLocalCache();
    void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);
    Transaction getTransaction();
    void close(boolean var1);
    boolean isClosed();
    void setExecutorWrapper(Executor var1);
}
public interface StatementHandler {
    Statement prepare(Connection var1, Integer var2) throws SQLException;
    void parameterize(Statement var1) throws SQLException;
    void batch(Statement var1) throws SQLException;
    int update(Statement var1) throws SQLException;
    <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
    <E> Cursor<E> queryCursor(Statement var1) throws SQLException;
    BoundSql getBoundSql();
    ParameterHandler getParameterHandler();
}
public interface ParameterHandler {
    Object getParameterObject();
    void setParameters(PreparedStatement var1) throws SQLException;
}
public interface ResultHandler<T> {
    void handleResult(ResultContext<? extends T> var1);
}

The order of execution of the interception is Executor- > StatementHandler- > ParameterHandler- > ResultHandler

Interceptor interface provided by MyBatis:

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    default void setProperties(Properties properties) {}
}

Implementation of Interceptor Based on Object intercept Method;

The Object plugin method is used to judge the type of interceptor executed;

The void setProperties method is used to get the properties of the configuration item.

Combination of interceptor object and interceptor interface. Custom interceptor class needs to implement interceptor interface, and declare the object to be intercepted by annotation @ Intercepts and parameter @ Signature.

@ Signature parameter type is the interception object, method is the interception method, that is, the method corresponding to the above four classes, and args is the parameter corresponding to the interception method (the method has overload, so it is necessary to specify the number and type of parameters)

@ Intercepts can have multiple @ Signature, that is, one interceptor implementation class can intercept multiple objects and methods at the same time, as shown below:

Executor- > intercept StatementHandler- > intercept ParameterHandler- > intercept ResultHandler- > intercept

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
})
public class SelectPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof Executor) {
            System.out.println("SelectPlugin");
        }
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    @Override
    public void setProperties(Properties properties) {}
}
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class StatementPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof StatementHandler) {
            System.out.println("StatementPlugin");
        }
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    @Override
    public void setProperties(Properties properties) {}
}
@Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})})
public class ParameterPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof ParameterHandler) {
            System.out.println("ParameterPlugin");
        }
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        if (target instanceof ParameterHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    @Override
    public void setProperties(Properties properties) {}
}
@Intercepts({@Signature(type = ResultHandler.class,method = "handleResult",args = {ResultContext.class})})
public class ResultPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof ResultHandler) {
            System.out.println("ResultPlugin");
        }
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        if (target instanceof ResultHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    @Override
    public void setProperties(Properties properties) {}
}

2. Three Ways to Register Interceptors

The implementation of Mybatis interceptor object and its interface was introduced before, so how to register interceptor in the project? Three registration methods are given in this paper.

1. XML registration

xml registration is the most basic way to register through the plugins element in the Mybatis configuration file. One plugin corresponds to one interceptor, and property sub-element can be specified in plugin element. When registering and defining interceptors, all property corresponding to interceptors are injected into interceptors through setProperties method of Interceptor. Therefore, the interceptor registers xml as follows:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- ...... -->
    <plugins>
       <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor">
           <property name="prop1" value="prop1"/>
           <property name="prop2" value="prop2"/>
       </plugin>
    </plugins>
    <!-- ...... -->
</configuration>

2. Configure class registration

Configuration class registration means that the interceptor is declared in the configuration class of Mybatis, and configuration class registration can also inject parameters into setProperties method of Interceptor through Properties class. Specific reference is as follows:


@Configuration
public class MyBatisConfig {
    @Bean
    public String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) {
        UpdatePlugin executorInterceptor = new UpdatePlugin();
        Properties properties = new Properties();
        properties.setProperty("prop1", "value1");
        //  Add custom parameters to interceptors 
        executorInterceptor.setProperties(properties);
        sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor);
        sqlSessionFactory.getConfiguration().addInterceptor(new StatementPlugin());
        sqlSessionFactory.getConfiguration().addInterceptor(new ResultPlugin());
        sqlSessionFactory.getConfiguration().addInterceptor(new ParameterPlugin());
        // sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());
        return "interceptor";
    }

    //  And sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin()); Effect 1 To 
    @Bean
    public SelectPlugin SelectInterceptor() {
        SelectPlugin interceptor = new SelectPlugin();
        Properties properties = new Properties();
        //  Call properties.setProperty Method to set custom parameters to the interceptor 
        interceptor.setProperties(properties);
        return interceptor;
    }
}

3. Annotation method

Annotation through @ Component is the simplest way, which can be used when there is no need to transfer custom parameters, which is convenient and quick.


@Component
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
})
public class SelectPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof Executor) {
            System.out.println("SelectPlugin");
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

3. ParameterHandler parameter rewriting-modification time and modification person system 1 insertion

The specific interceptor implementation is described. In the daily coding requirements, it is necessary to insert the modification time and personnel when modifying. It is very troublesome to write in the way of xml, but the global insertion and modification time and personnel can be quickly realized by interceptor. Look at the code first:


@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
})
public class MyBatisInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //  Parameter agent 
        if (invocation.getTarget() instanceof ParameterHandler) {
            System.out.println("ParameterHandler");
            //  Automatically add operator information 
            autoAddOperatorInfo(invocation);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     *  Automatically add operator information 
     *
     * @param invocation  Proxy object 
     * @throws Throwable  Anomaly 
     */
    private void autoAddOperatorInfo(Invocation invocation) throws Throwable {
        System.out.println("autoInsertCreatorInfo");
        //  Gets the parameter object of the proxy ParameterHandler
        ParameterHandler ph = (ParameterHandler) invocation.getTarget();
        //  Pass MetaObject Get ParameterHandler Reflection content of 
        MetaObject metaObject = MetaObject.forObject(ph,
                SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                new DefaultReflectorFactory());
        //  Pass MetaObject Reflected content acquisition MappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement");
        //  When sql Type is INSERT Or UPDATE Automatically insert operator information when 
        if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT ||
                mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) {
            //  Get Parameter Object 
            Object obj = ph.getParameterObject();
            if (null != obj) {
                //  Get the properties of parameter objects by reflection 
                Field[] fields = obj.getClass().getDeclaredFields();
                //  Traversing the properties of parameter objects 
                for (Field f : fields) {
                    //  If sql Yes INSERT, And exist createdAt Attribute 
                    if ("createdAt".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {
                        //  Set the Allow Access Reflection property 
                        f.setAccessible(true);
                        //  If there is no setting createdAt Property, the createdAt Property to add the current time 
                        if (null == f.get(obj)) {
                            //  Settings createdAt Property is the current time 
                            f.set(obj, LocalDateTime.now());
                        }
                    }
                    //  If sql Yes INSERT, And exist createdBy Attribute 
                    if ("createdBy".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {
                        //  Set the Allow Access Reflection property 
                        f.setAccessible(true);
                        //  If there is no setting createdBy Property, the createdBy Property to add the currently logged-in person 
                        if (null == f.get(obj)) {
                            //  Settings createdBy Property is the person currently logged on 
                            f.set(obj, 0);
                        }
                    }
                    // sql For INSERT Or UPDATE Need to be set at all times updatedAt Attribute 
                    if ("updatedAt".equals(f.getName())) {
                        f.setAccessible(true);
                        if (null == f.get(obj)) {
                            f.set(obj, LocalDateTime.now());
                        }
                    }
                    // sql For INSERT Or UPDATE Need to be set at all times updatedBy Attribute 
                    if ("updatedBy".equals(f.getName())) {
                        f.setAccessible(true);
                        if (null == f.get(obj)) {
                            f.set(obj, 0);
                        }
                    }
                }

                //  Acquisition by reflection ParameterHandler Adj. parameterObject Attribute 
                Field parameterObject = ph.getClass().getDeclaredField("parameterObject");
                //  Set Allow Access parameterObject Attribute 
                parameterObject.setAccessible(true);
                //  Set the new parameter object set above to the ParameterHandler Adj. parameterObject Attribute 
                parameterObject.set(ph, obj);
            }
        }
    }
}

Interceptor interface implementation reference above, here focuses on the autoAddOperatorInfo method in the related classes.

1.ParameterHandler

Interface source code:


 public interface ParameterHandler {
     Object getParameterObject();
     void setParameters(PreparedStatement var1) throws SQLException;
 }

Provides two methods:

getParameterObject is the get parameter object, null may exist, and note the null pointer.

setParameters controls how the SQL parameter is set, that is, the relationship between the java object configured in the sql statement and the jdbc type, such as # {id, jdbcType = INTEGER}, and the default type of id is javaType = class java. lang. Integer.

This interface has a default implementation class, and the source code is as follows:


public class DefaultParameterHandler implements ParameterHandler {
    private final TypeHandlerRegistry typeHandlerRegistry;
    private final MappedStatement mappedStatement;
    private final Object parameterObject;
    private final BoundSql boundSql;
    private final Configuration configuration;

    public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        this.mappedStatement = mappedStatement;
        this.configuration = mappedStatement.getConfiguration();
        this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
        this.parameterObject = parameterObject;
        this.boundSql = boundSql;
    }

    public Object getParameterObject() {
        return this.parameterObject;
    }

    public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
        if (parameterMappings != null) {
            for(int i = 0; i < parameterMappings.size(); ++i) {
                ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if (this.boundSql.hasAdditionalParameter(propertyName)) {
                        value = this.boundSql.getAdditionalParameter(propertyName);
                    } else if (this.parameterObject == null) {
                        value = null;
                    } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {
                        value = this.parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    JdbcType jdbcType = parameterMapping.getJdbcType();
                    if (value == null && jdbcType == null) {
                        jdbcType = this.configuration.getJdbcTypeForNull();
                    }

                    try {
                        typeHandler.setParameter(ps, i + 1, value, jdbcType);
                    } catch (SQLException | TypeException var10) {
                        throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);
                    }
                }
            }
        }

    }
}

Through the DefaultParameterHandler implementation classes we know which properties and methods are available through ParameterHandler, including our next important class, MappedStatement.

2.MappedStatement

Each select/update/insert/delete tag in the mapper file of MyBatis is parsed by the parser into a corresponding MappedStatement object, that is, an MappedStatement object describes an SQL statement. The MappedStatement object properties are as follows:


// mapper Configuration file name 
    private String resource;
    // mybatis Global information of, such as jdbc
    private Configuration configuration;
    //  Node's id Attribute plus namespace , Such as: com.example.mybatis.dao.UserMapper.selectByExample
    private String id;
    private Integer fetchSize;
    private Integer timeout;
    private StatementType statementType;
    private ResultSetType resultSetType;
    private SqlSource sqlSource;
    private Cache cache;
    private ParameterMap parameterMap;
    private List<ResultMap> resultMaps;
    private boolean flushCacheRequired;
    private boolean useCache;
    private boolean resultOrdered;
    // sql Type of statement: select , update , delete , insert
    private SqlCommandType sqlCommandType;
    private KeyGenerator keyGenerator;
    private String[] keyProperties;
    private String[] keyColumns;
    private boolean hasNestedResultMaps;
    private String databaseId;
    private Log statementLog;
    private LanguageDriver lang;
    private String[] resultSets;

In this example, the sqlCommandType of the MappedStatement object is used to judge that the current sql type is insert and update for the next step.

4. Rewrite SQL through StatementHandler

StatementHandler is used to encapsulate JDBC Statement operations, and is responsible for operations on JDBC Statement, such as setting parameters, and converting Statement result sets into List sets.

The implementation code is as follows:

Remove annotation markers


public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    default void setProperties(Properties properties) {}
}
0

The Dao layer adds a deletion annotation, and does not add a deletion flag when it is false


public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    default void setProperties(Properties properties) {}
}
1

The interceptor judges whether to add the deletion flag by deleting the annotation flag


public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    default void setProperties(Properties properties) {}
}
2

It is necessary to identify the content to be replaced in SQL statement replacement, so a special sign "9=9" is added to sql statement of xml, which does not affect the execution result of the original SQL. Different filtering conditions can set different signs, which is a clever replacement method.

The above is the MyBatis interceptor principle and the use of the details, more information about the MyBatis interceptor please pay attention to other related articles on this site!


Related articles: