[Hand tear MyBatis source code] plug-in system

overview

Mybatis is an excellent ORM open source framework with a wide range of applications. This framework has great flexibility and provides an easy-to-use plug-in extension mechanism at the four major components (Executor, StatementHandler, ParameterHandler, ResultSetHandler). Mybatis operates on the persistence layer with the help of four core objects. MyBatis supports plug-ins to intercept the four core objects. For mybatis, the plug-in is an interceptor, which is used to enhance the functions of the core objects. The enhanced functions are essentially realized by means of the underlying dynamic proxy. In other words, the four major objects in MyBatis are all proxy objects.
insert image description here

The plug-in mechanism is an entry provided for extending the existing system of MyBatis. The bottom layer is implemented through dynamic proxy. There are four interfaces available for proxy interception:

  • Executor (update, query, commit, rollback and other methods);
  • SQL syntax builder StatementHandler (prepare, parameterize, batch, updates query and other methods);
  • Parameter processor ParameterHandler (getParameterObject, setParameters method);
  • Result set processor ResultSetHandler (handleResultSets, handleOutputParameters and other methods);

These four interfaces have covered the entire process from initiating interface calls to SQl declarations, parameter processing, and result set processing. Any method in the interface can be intercepted to change the original properties and behavior of the method. However, this is a very dangerous behavior. A little carelessness will destroy the core logic of MyBatis without knowing it. So be sure to be very clear about the internal mechanism of MyBatis before using the plugin.

insert image description here

Use of plug-ins

Creating a plug-in is a very simple matter in MyBatis, just implement the Interceptor interface and specify the method signature you want to intercept.

@Intercepts({
    
    @Signature(
  type= Executor.class,
  method = "update",
  args = {
    
    MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
    
    
        // 当执行目标方法时会被方法拦截
    public Object intercept(Invocation invocation) throws Throwable {
    
    
      long begin = System.currentTimeMillis();
        try {
    
    
            return invocation.proceed();// 继续执行原逻辑;
        } finally {
    
    
            System.out.println("执行时间:"+(System.currentTimeMillis() - begin));
        }
    }
        // 生成代理对象,可自定义生成代理对象,这样就无需配置@Intercepts注解。另外需要自行判断是否为拦截目标接口。
    public Object plugin(Object target) {
    
    
        return Plugin.wrap(target,this);// 调用通用插件代理生成机器
    }
}

@Intercepts is an annotation used to declare plug-in interception information in MyBatis.
When we develop a MyBatis plug-in, we need to specify which methods of which objects the plug-in should intercept. We can use the @Intercepts annotation to declare these interception information.
The @Intercepts annotation is defined as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
     
     
 Signature[] value();
}

It contains a Signature array, each Signature represents a piece of interception information, and contains three attributes:

  • type: the interface type to be intercepted
  • method: the method name in the interface
  • args: the parameter type of the method

Therefore, when we use @Intercepts to annotate a plug-in, MyBatis will parse out all the information to be intercepted by the plug-in according to its value attribute, and then generate a proxy object to implement method interception.

Add plugin configuration in config.xml:

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin"/>
</plugins>

​ Through the above configuration, you can monitor the time spent in the modification process.

​Note: The interception will only take effect when the interception target is called from an external class, and it will take effect if the proxy logic is called internally. If there are two Query methods in Executor, the first one will call the second query. If you intercept the second Query, it will not succeed.
insert image description here

Plug-in proxy mechanism

There is an InterceptorChain (interceptor chain) in Configuration that saves all interceptors. When the four major objects are created, the interceptor chain will be called to intercept the target object.

insert image description here
For this plug-in interception implementation is similar to Spring AOP but its implementation is much simpler. The proxy is so light and clear that even the comments are superfluous.

Next, use an automatic pagination plug-in to fully grasp the usage of the plug-in

Automatic pagination plugin

Automatic pagination means that when querying, specify parameters such as page number and size, and the plug-in will automatically perform pagination query and return the total number. This plug-in design needs to meet the following characteristics:

  • Ease of use: No additional configuration is required, just include Page in the parameters. Page is as simple as possible
  • No assumptions about usage scenarios: no restrictions on user usage, such as interface calls or session calls. Or the choice of Executor and StatementHandler. Cannot affect cache business
  • Friendliness: Make friendly user prompts when pagination is not met. Such as paying pagination parameters in the modification operation. Or the user has already included a pagination statement in the query statement, and a prompt should be given in this case.

Intercept target

The next problem to be solved is where is the entry of the plug-in written? What are the targets to intercept?

insert image description here
Parameter processors and result set processors are obviously inappropriate, and Executor.query() needs to consider additional first and second level cache logic. Finally, select StatementHandler. And intercept its prepare method.

@Intercepts(@Signature(type = StatementHandler.class,
        method = "prepare", args = {
    
    Connection.class,
        Integer.class}))

Paging plug-in principle

First, set a Page class, which contains three attributes: total, size, and index. Declaring this parameter in the Mapper interface means that automatic paging logic needs to be executed.

The overall implementation steps include 3 steps:

  • Check if the pagination condition is met
  • Automatically find the total number of rows in the current query
  • Modify the original SQL statement and add the limit offset keyword.

Next we explain each step:

Check if the pagination condition is met

The paging condition is 1. Whether it is a query method, 2. Whether the Page parameter is included in the query parameter. In the intercept method, the interception target StatementHandler can be obtained directly, and the BoundSql contains SQL and parameters through it. The Page can be obtained by traversing the parameters.

// 带上分页参数
StatementHandler target = (StatementHandler) invocation.getTarget();
// SQL包 sql、参数、参数映射
BoundSql boundSql = target.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
Page page = null;
if (parameterObject instanceof Page) {
    
    
    page = (Page) parameterObject;
} else if (parameterObject instanceof Map) {
    
    
    page = (Page) ((Map) parameterObject).values().stream().filter(v -> v instanceof Page).findFirst().orElse(null);
}

Query the total number of rows

The implementation logic is to wrap the original query SQL as a subquery into a subquery, and then use the original parameters to assign values ​​through the original parameter processor. Regarding the implementation, it is realized by using JDBC native API. The MyBatis executor bypasses the primary and secondary caches.

private int selectCount(Invocation invocation) throws SQLException {
    
    
    int count = 0;
    StatementHandler target = (StatementHandler) invocation.getTarget();
    // SQL包 sql、参数、参数映射
    String countSql = String.format("select count(*) from (%s) as _page", target.getBoundSql().getSql());
    // JDBC
    Connection connection = (Connection) invocation.getArgs()[0];
    PreparedStatement preparedStatement = connection.prepareStatement(countSql);
    target.getParameterHandler().setParameters(preparedStatement);
    ResultSet resultSet = preparedStatement.executeQuery();
    if (resultSet.next()) {
    
    
        count = resultSet.getInt(1);
    }
    resultSet.close();
    preparedStatement.close();

    return count;
}

Modify the original SQL

The last item is to modify the original SQL. I can get BoundSql earlier, but it does not provide a method to modify SQL. Here, reflection can be used to assign values ​​to SQL attributes. You can also use the tool class SystemMetaObject provided by MyBatis to assign

String newSql= String.format("%s limit %s offset %s", boundSql.getSql(),page.getSize(),page.getOffset());
SystemMetaObject.forObject(boundSql).setValue("sql",newSql);

Guess you like

Origin blog.csdn.net/zyb18507175502/article/details/131142633