SQL injector for source code analysis of mybatis plus

Time:2020-1-14

The WeChat public is “back-end”, focusing on the sharing of back-end technologies: Java, Golang, WEB framework, distributed middleware, service governance, and so on.
The old driver gave you all the money and took you all the way to the top. It’s too late to explain. Get on the bus!

Mybatis plus is an enhancement tool developed entirely based on mybatis. Its design concept is to only enhance on the basis of mybatis without making any changes, to simplify development and improve efficiency. It adds many practical functions on the basis of mybatis, such as optimistic lock plug-in, automatic field filling function, paging plug-in, condition builder, SQL injector, etc, These are very practical functions in the development process. Mybatis plus stands on the shoulders of giants and makes a series of innovations, which I highly recommend. Next, I will analyze in detail how to realize the principle of SQL auto injection from the source point of view.

New

Let’s review the mapper registration and binding process of mybatis. I also wrote a “mapper registration and binding of mybatis source code analysis”. In this article, I explained in detail that the ultimate purpose of mapper binding is to register the SQL information on XML or annotation and its corresponding mapper class into mappedstatement. Since the design concept of mybatis plus is based on mybatis If we only make enhancements and don’t make changes, then the SQL injector must also be registering our pre-defined SQL and pre-defined mapper into mappedstatement.

Now I will comb mapper’s registration and binding process again with the sequence diagram:

SQL injector for source code analysis of mybatis plus

Analyze the functions of these classes:

  • Sqlsessionfactorybean: inherits the factorybean and initializingbean, and conforms to the basic specification of spring LOC container bean. When getting the bean, you can call the getobject() method to sqlsessionfactory.
  • Xmlmapperbuilder: XML file parser, which parses the XML file information corresponding to mapper, and registers the XML file information into configuration.
  • Xmlstatementbuilder: XML node parser, used to build select / insert / update / delete node information.
  • Mapperbuilder assistant: mapper build assistant, which encapsulates mapper node information as statement and adds it to mappedstatement.
  • Mapperregistry: mapper registers and binds the class information of mapper to mapperproxyfactory.
  • Mapperannotationbuilder: mapper annotation resolution builder, which is why mybatis can directly add annotation information in mapper method without writing SQL information in XML. This builder is specially used for parsing mapper method annotation information and encapsulating these information into statement to add to mappedstatement.

It can be seen from the sequence diagram that the configuration configuration class stores all mapper registration and binding information, and then injects configuration when creating sqlsessionfactory. Finally, the sqlsession session created by sqlsessionfactory can interact with the database according to the configuration information, and mapperproxyfactory will create a mapperproxy proxy class for each mapper Mapperproxy contains all the details of mapper’s sqlsession operation, so we can directly use mapper’s method to interact with sqlsession.

After a round, I found that I haven’t talked about the source code analysis of SQL injector yet. You don’t need to panic. You need to show the maturity and stability of the old driver. I told you the principle of SQL injector before. Only the source code analysis is left. At this time, we should do the foreplay before the source code analysis. The foreplay is enough to tear, pull, pull and strip the source code. It’s too late to explain. Hurry up Car!

Source code analysis

From the timing chart of mapper registration and binding process, to add SQL injector to mybatis seamlessly, you need to add it from mapper registration step. Sure enough, MP inherits mapperregistry class and rewrites addmapper method

com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (hasMapper(type)) {
            //Todo if the previous injection returns directly
            return;
            // throw new BindingException("Type " + type +
            // " is already known to the MybatisPlusMapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            //Todo custom no XML injection
            MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

Method to replace mapperannotationbuilder with mybatismap perannotationbuilder,In particular, in order not to change the original logic of mybatis, MP will use inheritance or directly roughly copy it, and then prefix the original class name with “mybatis”.

com.baomidou.mybatisplus.MybatisMapperAnnotationBuilder#parse:

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
        loadXmlResource();
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        Method[] methods = type.getMethods();
        //Todo injection curd dynamic SQL (should be injected before annotation)
        if (BaseMapper.class.isAssignableFrom(type)) {
            GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
        }
        for (Method method : methods) {
            try {
                // issue #237
                if (!method.isBridge()) {
                    parseStatement(method);
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

SQL injector is added from this method. First, judge whether mapper is the superclass or super interface of basemapper. Basemapper is the basic mapper of MP. There are many default basic methods defined in it, which means that once we use MP, many basic database operations can be directly inherited from basemapper through SQL injector. The development efficiency is very high Wood has!

com.baomidou.mybatisplus.toolkit.GlobalConfigUtils#getSqlInjector:

public static ISqlInjector getSqlInjector(Configuration configuration) {
  // fix #140
  GlobalConfiguration globalConfiguration = getGlobalConfig(configuration);
  ISqlInjector sqlInjector = globalConfiguration.getSqlInjector();
  if (sqlInjector == null) {
    sqlInjector = new AutoSqlInjector();
    globalConfiguration.setSqlInjector(sqlInjector);
  }
  return sqlInjector;
}

Globalconfiguration is the global cache class of MP, which is used to store some functions of MP. Obviously, SQL injector is stored in globalconfiguration.

This method first obtains the custom SQL injector from the global cache class. If the custom SQL injector is not found in the global configuration, an MP default SQL injector autosqlinjector will be set.

SQL injector interface:

//SQL auto injector interface
public interface ISqlInjector {
  
  //Inject SQL according to mapperclass
  void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);

  //Check whether SQL is injected (it has been injected and will not be injected again)
  void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);

  //Inject sqlrunner correlation
  void injectSqlRunner(Configuration configuration);

}

All custom SQL injectors need to implement isqlinjector interface. MP has implemented some basic injectors for us by default:

  • com.baomidou.mybatisplus.mapper.AutoSqlInjector
  • com.baomidou.mybatisplus.mapper.LogicSqlInjector

Among them, autosqlinjector provides the most basic SQL injection, as well as some general SQL injection and assembly logic. Logicsqlinjector replicates the deletion logic based on autosqlinjector, because the data deletion of our database is essentially soft deletion, not real deletion.

SQL injector for source code analysis of mybatis plus

com.baomidou.mybatisplus.mapper.AutoSqlInjector#inspectInject:

public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    String className = mapperClass.toString();
    Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
    if (!mapperRegistryCache.contains(className)) {
        inject(builderAssistant, mapperClass);
        mapperRegistryCache.add(className);
    }
}

This method is the entrance of SQL injector, and the judgment function of no injection after injection is added at the entrance.

//Inject single point crudsql
@Override
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
  this.configuration = builderAssistant.getConfiguration();
  this.builderAssistant = builderAssistant;
  this.languageDriver = configuration.getDefaultScriptingLanguageInstance();

  //Hump setup plus configuration > original configuration
  GlobalConfiguration globalCache = this.getGlobalConfig();
  if (!globalCache.isDbColumnUnderline()) {
    globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase());
  }
  Class<?> modelClass = extractModelClass(mapperClass);
  if (null != modelClass) {
    //Initialize SQL parsing
    if (globalCache.isSqlParserCache()) {
      PluginUtils.initSqlParserInfoCache(mapperClass);
    }
    TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
    injectSql(builderAssistant, mapperClass, modelClass, table);
  }
}

Before injection, extract the generic model from mapper class, because inheriting basemapper requires adding mapper’s corresponding model to the generic model. At this time, we need to extract it and initialize it into a tableinfo object after extraction. Tableinfo stores all the information of the model corresponding to the data base, including table primary key ID type, table name, table field information list And so on, which are acquired by reflection.

com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectSql:

protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
  if (StringUtils.isNotEmpty(table.getKeyProperty())) {
    /**Delete*/
    this.injectDeleteByIdSql(false, mapperClass, modelClass, table);
    /**Modification*/
    this.injectUpdateByIdSql(true, mapperClass, modelClass, table);
    /**Query*/
    this.injectSelectByIdSql(false, mapperClass, modelClass, table);
  } 
  /**Custom method*/
  this.inject(configuration, builderAssistant, mapperClass, modelClass, table);
}

All the SQL to be injected is called through this method. Autosqlinjector also provides an inject method. When you customize the SQL injector, you can inherit autosqlinjector and implement this method.

com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectDeleteByIdSql:

protected void injectSelectByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
  SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
  SqlSource sqlSource;
  if (batch) {
    sqlMethod = SqlMethod.SELECT_BATCH_BY_IDS;
    StringBuilder ids = new StringBuilder();
    ids.append("\n<foreach item=\"item\" index=\"index\" collection=\"coll\" separator=\",\">");
    ids.append("#{item}");
    ids.append("\n</foreach>");
    sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(),
                                                                            sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), ids.toString()), modelClass);
  } else {
    sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false),
                                                              table.getTableName(), table.getKeyColumn(), table.getKeyProperty()), Object.class);
  }
  this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource, modelClass, table);
}

I randomly select an injection to delete SQL. Other SQL injections are similar to this. Sqlmethod is an enumeration class, which stores all the automatically injected SQL and method names. If it is a batch operation, the SQL statements defined by sqlmethod are adding a batch operation statement. Then create a sqlsource object based on table and SQL information.

com.baomidou.mybatisplus.mapper.AutoSqlInjector#addMappedStatement:

public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
                                          SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType,
                                          KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
  //Mappedstatement exists
  String statementName = mapperClass.getName() + "." + id;
  if (hasMappedStatement(statementName)) {
    System.err.println("{" + statementName
                       + "} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.");
    return null;
  }
  /**Cache logical processing*/
  boolean isSelect = false;
  if (sqlCommandType == SqlCommandType.SELECT) {
    isSelect = true;
  }
  return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null,
                                             parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
                                             configuration.getDatabaseId(), languageDriver, null);
}

The final operation of SQL injector will determine whether mappedstatement exists. There is a reason for this judgment. It will prevent repeated injection. If your mapper method has been registered in the logic of mybatis, MP will not inject again. Finally, the addMappedStatement method of the MapperBuilderAssistant helper class is invoked to perform the registration operation.

Here, the source code of an SQL auto injector is analyzed. In fact, its implementation is very simple, because it uses the mechanism of mybatis to stand on the shoulders of giants to innovate.

I hope that in your future career, we will not only be a crud programmer who can only call API, but also have a thorough spirit. Reading the source code is very boring, but reading the source code will not only let you know the implementation principle of the bottom layer of the API, let you know what it is and why it is, but also broaden your thinking and improve your architecture design ability. Through reading the source code, you can see how big guys design a framework and why it is so designed.

SQL injector for source code analysis of mybatis plus