mybatis-plus源码分析之sql注入器

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: mybatis-plus是完全基于mybatis开发的一个增强工具,它的设计理念是在mybatis的基础上只做增强不做改变,为简化开发、提高效率而生,它在mybatis的基础上增加了很多实用性的功能,比如增加了乐观锁插件、字段自动填充功能、分页插件、条件构造器、sql注入器等等,这些在开发过程中都是非常实用的功能,mybatis-plus可谓是站在巨人的肩膀上进行了一系列的创新,我个人极力推荐。下面我会详细地从源码的角度分析mybatis-plus(下文简写成mp)是如何实现sql自动注入的原理。

mybatis-plus是完全基于mybatis开发的一个增强工具,它的设计理念是在mybatis的基础上只做增强不做改变,为简化开发、提高效率而生,它在mybatis的基础上增加了很多实用性的功能,比如增加了乐观锁插件、字段自动填充功能、分页插件、条件构造器、sql注入器等等,这些在开发过程中都是非常实用的功能,mybatis-plus可谓是站在巨人的肩膀上进行了一系列的创新,我个人极力推荐。下面我会详细地从源码的角度分析mybatis-plus(下文简写成mp)是如何实现sql自动注入的原理。


温故知新



我们回顾一下mybatis的Mapper的注册与绑定过程,我之前也写过一篇「Mybatis源码分析之Mapper注册与绑定」,在这篇文章中,我详细地讲解了Mapper绑定的最终目的是将xml或者注解上的sql信息与其对应Mapper类注册到MappedStatement中,既然mybatis-plus的设计理念是在mybatis的基础上只做增强不做改变,那么sql注入器必然也是在将我们预先定义好的sql和预先定义好的Mapper注册到MappedStatement中。


现在我将Mapper的注册与绑定过程用时序图再梳理一遍:


640.jpg


解析一下这几个类的作用:


  • SqlSessionFactoryBean:继承了FactoryBean和InitializingBean,符合spring loc容器bean的基本规范,可在获取该bean时调用getObject()方法到SqlSessionFactory。
  • XMLMapperBuilder:xml文件解析器,解析Mapper对应的xml文件信息,并将xml文件信息注册到Configuration中。
  • XMLStatementBuilder:xml节点解析器,用于构建select/insert/update/delete节点信息。
  • MapperBuilderAssistant:Mapper构建助手,将Mapper节点信息封装成statement添加到MappedStatement中。
  • MapperRegistry:Mapper注册与绑定类,将Mapper的类信息与MapperProxyFactory绑定。
  • MapperAnnotationBuilder:Mapper注解解析构建器,这也是为什么mybatis可以直接在Mapper方法添加注解信息就可以不用在xml写sql信息的原因,这个构建器专门用于解析Mapper方法注解信息,并将这些信息封装成statement添加到MappedStatement中。


从时序图可知,Configuration配置类存储了所有Mapper注册与绑定的信息,然后创建SqlSessionFactory时再将Configuration注入进去,最后经过SqlSessionFactory创建出来的SqlSession会话,就可以根据Configuration信息进行数据库交互,而MapperProxyFactory会为每个Mapper创建一个MapperProxy代理类,MapperProxy包含了Mapper操作SqlSession所有的细节,因此我们就可以直接使用Mapper的方法就可以跟SqlSession进行交互。


饶了一圈,发现我现在还没讲sql注入器的源码分析,你不用慌,你得体现出老司机的成熟稳定,之前我也跟你说了sql注入器的原理了,只剩下源码分析,这时候我们应该在源码分析之前做足前戏,前戏做足就剩下撕、拉、扯、剥开源码的外衣了,来不及解释了快上车!


640.jpg


源码分析



从Mapper的注册与绑定过程的时序图看,要想将sql注入器无缝链接地添加到mybatis里面,那就得从Mapper注册步骤添加,果然,mp很鸡贼地继承了MapperRegistry这个类然后重写了addMapper方法:


com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (hasMapper(type)) {
            // TODO 如果之前注入 直接返回
            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 自定义无 XML 注入
            MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}


方法中将MapperAnnotationBuilder替换成了自家的MybatisMapperAnnotationBuilder,在这里特别说明一下,mp为了不更改mybatis原有的逻辑,会用继承或者直接粗暴地将其复制过来,然后在原有的类名上加上前缀“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 注入 CURD 动态 SQL (应该在注解之前注入)
        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注入器就是从这个方法里面添加上去的,首先判断Mapper是否是BaseMapper的超类或者超接口,BaseMapper是mp的基础Mapper,里面定义了很多默认的基础方法,意味着我们一旦使用上mp,通过sql注入器,很多基础的数据库操作都可以直接继承BaseMapper实现了,开发效率爆棚有木有!


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是mp的全局缓存类,用于存放mp自带的一些功能,很明显,sql注入器就存放在GlobalConfiguration中。


这个方法是先从全局缓存类中获取自定义的sql注入器,如果在GlobalConfiguration中没有找到自定义sql注入器,就会设置一个mp默认的sql注入器AutoSqlInjector。


sql注入器接口:

// SQL 自动注入器接口
public interface ISqlInjector {
  // 根据mapperClass注入SQL
  void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
  // 检查SQL是否注入(已经注入过不再注入)
  void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
  // 注入SqlRunner相关
  void injectSqlRunner(Configuration configuration);
}


所有自定义的sql注入器都需要实现ISqlInjector接口,mp已经为我们默认实现了一些基础的注入器:


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


其中AutoSqlInjector提供了最基本的sql注入,以及一些通用的sql注入与拼装的逻辑,LogicSqlInjector在AutoSqlInjector的基础上复写了删除逻辑,因为我们的数据库的数据删除实质上是软删除,并不是真正的删除。


640.png


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


该方法是sql注入器的入口,在入口处添加了注入过后不再注入的判断功能。


// 注入单点 crudSql
@Override
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
  this.configuration = builderAssistant.getConfiguration();
  this.builderAssistant = builderAssistant;
  this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
  // 驼峰设置 PLUS 配置 > 原始配置
  GlobalConfiguration globalCache = this.getGlobalConfig();
  if (!globalCache.isDbColumnUnderline()) {
    globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase());
  }
  Class<?> modelClass = extractModelClass(mapperClass);
  if (null != modelClass) {
    // 初始化 SQL 解析
    if (globalCache.isSqlParserCache()) {
      PluginUtils.initSqlParserInfoCache(mapperClass);
    }
    TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
    injectSql(builderAssistant, mapperClass, modelClass, table);
  }
}


注入之前先将Mapper类提取泛型模型,因为继承BaseMapper需要将Mapper对应的model添加到泛型里面,这时候我们需要将其提取出来,提取出来后还需要将其初始化成一个TableInfo对象,TableInfo存储了数据库对应的model所有的信息,包括表主键ID类型、表名称、表字段信息列表等等信息,这些信息通过反射获取。


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

protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
  if (StringUtils.isNotEmpty(table.getKeyProperty())) {
    /** 删除 */
    this.injectDeleteByIdSql(false, mapperClass, modelClass, table);
    /** 修改 */
    this.injectUpdateByIdSql(true, mapperClass, modelClass, table);
    /** 查询 */
    this.injectSelectByIdSql(false, mapperClass, modelClass, table);
  } 
  /** 自定义方法 */
  this.inject(configuration, builderAssistant, mapperClass, modelClass, table);
}


所有需要注入的sql都是通过该方法进行调用,AutoSqlInjector还提供了一个inject方法,自定义sql注入器时,继承AutoSqlInjector,实现该方法就行了。


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


我随机选择一个删除sql的注入,其它sql注入都是类似这么写,SqlMethod是一个枚举类,里面存储了所有自动注入的sql与方法名,如果是批量操作,SqlMethod的定义的sql语句在添加批量操作的语句。再根据table和sql信息创建一个SqlSource对象。


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是否存在
  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;
  }
  /** 缓存逻辑处理 */
  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);
}


sql注入器的最终操作,这里会判断MappedStatement是否存在,这个判断是有原因的,它会防止重复注入,如果你的Mapper方法已经在Mybatis的逻辑里面注册了,mp不会再次注入。最后调用MapperBuilderAssistant助手类的addMappedStatement方法执行注册操作。


写在最后



到这里,一个sql自动注入器的源码就分析完了,其实实现起来很简单,因为它利用了Mybatis的机制,站在巨人的肩膀上进行创新。


我希望在你们今后的职业生涯里,不要只做一个只会调用API的CRUD程序员,我们要有一种刨根问底的精神。阅读源码很枯燥,但阅读源码不仅会让你知道API底层的实现原理,让你知其然也知其所以然,还可以开阔你的思维,提升你的架构设计能力,通过阅读源码,可以看到大佬们是如何设计一个框架的,为什么会这么设计。


相关文章
|
12天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
1月前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
30 10
|
2月前
|
SQL XML Java
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
文章介绍了MyBatis中动态SQL的用法,包括if、choose、where、set和trim标签,以及foreach标签的详细使用。通过实际代码示例,展示了如何根据条件动态构建查询、更新和批量插入操作的SQL语句。
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
|
1月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
125 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
2月前
|
SQL 安全 数据库
惊!Python Web安全黑洞大曝光:SQL注入、XSS、CSRF,你中招了吗?
在数字化时代,Web应用的安全性至关重要。许多Python开发者在追求功能时,常忽视SQL注入、XSS和CSRF等安全威胁。本文将深入剖析这些风险并提供最佳实践:使用参数化查询预防SQL注入;通过HTML转义阻止XSS攻击;在表单中加入CSRF令牌增强安全性。遵循这些方法,可有效提升Web应用的安全防护水平,保护用户数据与隐私。安全需持续关注与改进,每个细节都至关重要。
128 5
|
2月前
|
SQL 安全 数据库
深度揭秘:Python Web安全攻防战,SQL注入、XSS、CSRF一网打尽!
在Web开发领域,Python虽强大灵活,却也面临着SQL注入、XSS与CSRF等安全威胁。本文将剖析这些常见攻击手段,并提供示例代码,展示如何利用参数化查询、HTML转义及CSRF令牌等技术构建坚固防线,确保Python Web应用的安全性。安全之路永无止境,唯有不断改进方能应对挑战。
67 5
|
2月前
|
SQL 安全 数据安全/隐私保护
Python Web安全大挑战:面对SQL注入、XSS、CSRF,你准备好了吗?
在构建Python Web应用时,安全性至关重要。本文通过三个真实案例,探讨了如何防范SQL注入、XSS和CSRF攻击。首先,通过参数化查询替代字符串拼接,防止SQL注入;其次,利用HTML转义机制,避免XSS攻击;最后,采用CSRF令牌验证,保护用户免受CSRF攻击。这些策略能显著增强应用的安全性,帮助开发者应对复杂的网络威胁。安全是一个持续的过程,需不断学习新知识以抵御不断变化的威胁。
115 1
|
2月前
|
SQL 安全 数据库
Python Web开发者必看!SQL注入、XSS、CSRF全面解析,守护你的网站安全!
在Python Web开发中,构建安全应用至关重要。本文通过问答形式,详细解析了三种常见Web安全威胁——SQL注入、XSS和CSRF,并提供了实用的防御策略及示例代码。针对SQL注入,建议使用参数化查询;对于XSS,需对输出进行HTML编码;而防范CSRF,则应利用CSRF令牌。通过这些措施,帮助开发者有效提升应用安全性,确保网站稳定运行。
47 1
|
2月前
|
SQL 安全 数据库
深度揭秘:Python Web安全攻防战,SQL注入、XSS、CSRF一网打尽!
在Web开发领域,Python虽强大灵活,但安全挑战不容小觑。本文剖析Python Web应用中的三大安全威胁:SQL注入、XSS及CSRF,并提供防御策略。通过示例代码展示如何利用参数化查询、HTML转义与CSRF令牌构建安全防线,助您打造更安全的应用。安全是一场持久战,需不断改进优化。
46 3
|
2月前
|
SQL 安全 数据库
从入门到精通:Python Web安全守护指南,SQL注入、XSS、CSRF全防御!
【9月更文挑战第13天】在开发Python Web应用时,安全性至关重要。本文通过问答形式,详细介绍如何防范SQL注入、XSS及CSRF等常见威胁。通过使用参数化查询、HTML转义和CSRF令牌等技术,确保应用安全。附带示例代码,帮助读者从入门到精通Python Web安全。
86 6