浅谈mybatis执行原理

前言

什么是mybatis?

官网是这么说的:MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录

为什么要研究原理?

我认为第一个理由就是为了更好的使用mybatis,我们不仅要知其然,更要知其所以然,第二个理由就是为了面试,现在很多面试官都喜欢问框架的源码,例如spring源码、mybatis源码等等

如何学习原理?

要想了解mybatis原理首先得先阅读文档,其中最重要的就是主配置文件中的配置项,可以配置的如下

当然常用的配置有Properties(属性)、setting(设置)、typeHandlers(类型处理器)、plugins(插件 也是最重要的)、environments(环境配置),接下来我将从这几个方面为大家解读mybatis源码

Properties源码解读

这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。

准备

例如:

   <properties>
        <property name="myDriver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="myUrl" value="jdbc:mysql://localhost:3306/mytest"/>
        <property name="myUsername" value="root"/>
        <property name="myPassword" value="123456"/>
    </properties>

设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。比如:

<dataSource type="POOLED">
                <property name="driver" value="${myDriver}"/>
                <property name="url" value="${myUrl}"/>
                <property name="username" value="${myUsername}"/>
                <property name="password" value="${myPassword}"/>
</dataSource>

源码分析 

项目启动

 public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
       
    }

从上面代码不难看出解析标签的代码就是

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

进入这个方法

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //构造解析器
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //解析xml,并创建sessionFactory
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        if (inputStream != null) {
          inputStream.close();
        }
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

从上面的代码可以看出解析xml的代码是praser.parse(),代码如下

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //解析mybatis的配置文件
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

上面一段代码中最重要代码肯定是parseConfiguration(parser.evalNode("/configuration"));,这个是解析xml的configuration标签,也就是根标签

分析如何解析标签中的表达式

我们先进入parser.evalNode("/configuration")中看看,后面的每个标签都是这样解析的

  public XNode evalNode(String expression) {
    return evalNode(document, expression);
  }
  public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
      return null;
    }
    return new XNode(this, node, variables);
  }

这边由于我们使用的是build(inputStream),所以这边的variables是空,这边在创建XNode对象时其实已经在解析xml了,看看XNode的构造方法

  public XNode(XPathParser xpathParser, Node node, Properties variables) {
    this.xpathParser = xpathParser;
    this.node = node;
    this.name = node.getNodeName();
    this.variables = variables;
    this.attributes = parseAttributes(node);
    this.body = parseBody(node);
  }

这边会把传进来的参数存储起来,然后调用parseAttributes(node)解析节点,看看方法如下

private Properties parseAttributes(Node n) {
    Properties attributes = new Properties();
    NamedNodeMap attributeNodes = n.getAttributes();
    if (attributeNodes != null) {
      for (int i = 0; i < attributeNodes.getLength(); i++) {
        Node attribute = attributeNodes.item(i);
        String value = PropertyParser.parse(attribute.getNodeValue(), variables);
        attributes.put(attribute.getNodeName(), value);
      }
    }
    return attributes;
  }

上面的方法会将标签的值进行解析,解析的方法就PropertyParser.parse(attribute.getNodeValue(), variables); 点进去看看源码

  public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

这边创建了一个参数处理器和参数解析器,构造方法如下

    private VariableTokenHandler(Properties variables) {
      this.variables = variables;
      this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
      this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
    }

设置参数是否可以有默认值,默认是false,设置默认的分隔符是:

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

这边设置解析的字符串开头是${ 结尾是 } 参数处理器是上面传入的解析器

上面的parser.parse(string)方法就开始解析字符串了,代码如下

public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    //获取${的位置 如果不存在就直接返回原字符串
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    //将字符串变成字符数组
    char[] src = text.toCharArray();
    //开始截取的位置
    int offset = 0;
    //返回的字符串
    final StringBuilder builder = new StringBuilder();
    //定义表达式
    StringBuilder expression = null;
    do {
      //如果${不是开始位置并且第start位置的字符是\
      // 这边第start位置肯定是$,所以这边的if条件走不到
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // 如果找到了${就让我们找}
        //如果表达式是空就创建,不是空就把值清掉
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        //把 ${ 之前的字符串存储起来
        builder.append(src, offset, start - offset);
        //计算目前${之后第一个字符串的位置,并重新赋值给开始截取的位置
        offset = start + openToken.length();
        //获取${之后的第一个 } 的位置
        int end = text.indexOf(closeToken, offset);
        //如果存在}的话,就要把表达式找出来
        //当找不到下一个}时就会结束当前循环
        while (end > -1) {
          //当开始截取的位置和}的位置一样 或者 第end的位置不是反斜杠
          if ((end <= offset) || (src[end - 1] != '\\')) {
            //把${到}之间的字符串存到表达式中,跳出循环开始解析
            expression.append(src, offset, end - offset);
            break;
          }
          //能走到这边就证明字符串中存在 \} 这就是转义},这就不算结束标签 
          //这边加上${到}之间的字符串并去除反斜杠  ,并加上 }
          expression.append(src, offset, end - offset - 1).append(closeToken);
          //新的起点变成了结束位置加上}
          offset = end + closeToken.length();
          //开始找新的}的索引
          end = text.indexOf(closeToken, offset);
        }
        //如果字符串后续不包含 } 就把后面的直接拼到字符串上
        if (end == -1) {
          // 如果没有找到} 就直接把后面的字符串拼接到字符串上 结束循环
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //如果字符串中包含${和}就要走这边解析
          builder.append(handler.handleToken(expression.toString()));
          //新的起点变成了结束位置加上}
          offset = end + closeToken.length();
        }
      }

      //获取下一个${位置  等没有下一个${就结束循环
      start = text.indexOf(openToken, offset);
    } while (start > -1);

    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

上面代码做的事情就是解析字符串,这边创建的解析器开始和结束标签分别是${},这边解析的逻辑就是先看字符串中是否有${,如果没有就直接返回,如果有先把${前面的字符串存储下来,然后就查看字符串后面是否有},如果这个}前一位要是\,相当于是转义这个字符了,就不算结束标签,得重新找,如果找不到就直接返回原来字符串,如果找到了},且前面没有\,就把上一个${到这个}之间的字符串存储为表达式,然后到跳出循环,开始调用解析方法,解析后继续查看剩余的字符串里面是否包含${,如果包含的话就重复上面的步骤,不包含的话就加上剩余的字符串然后返回

下面来看看解析的方法

 public String handleToken(String content) {
      //如果没有变量值就给字符串加上"${" + content + "}" 返回
      if (variables != null) {
        String key = content;
        //如果容许有默认值
        //如果值中包含:就从这边走,然后返回
        if (enableDefaultValue) {
          //将字符串分割,寻找:位置
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          //将字符串分割为key:value
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          //如果参数中有对应的值的变量就替换,没有就是null
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        //如果不容许有默认值就走下面这从参数中取值
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }

上面的这段代码主要意思就是先看变量参数是否为空,如果是空就直接返回"${" + content + "}"

要是不为空就看是否开启默认值(是否开启看启动项目是否传入了org.apache.ibatis.parsing.PropertyParser.enable-default-value的值为true的变量,这边默认是false

如果开启了默认值,就要根据分隔符(默认值是:,如果传入了org.apache.ibatis.parsing.PropertyParser.default-value-separator参数,就以这个为分隔符)对表达式字符串进行分割,如果字符串中不包含分隔符,那么就以当前字符串为key,值为null,如果包含分隔符,就以分割出来的第一个值为key,第二个值去参数中找,找到就是对应的那个值,找不到的话值就是null

如果未开启默认值,就以当前字符串为key去参数中找,找到的话,值就替换为那个,找不到值就是null

总结一下哪些表达式是可以被mybatis解析的

${xxxxx}、xxx${xxx}xxx、xxx${xxx\}}xxxx、xxx${xxxx\}}xxxx${xxxxx}xxxx${xxxxx}xxxx

分析解析Properties

解析完configuration根标签后就会进入如下方法

  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      //解析properties标签,将标签里面的内容存到环境变量中,在接下来的文件中可以动态替换
      propertiesElement(root.evalNode("properties"));
      //这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各项设置的含义、默认值等。
      //校验配置是否合法,如果全部合法,就返回一个Properties
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //设置Vfs
      loadCustomVfs(settings);
      //设置日志类
      loadCustomLogImpl(settings);
      //设置别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //设置插件
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //查看是否配置了对应的属性,如果没有配置就使用默认值
      settingsElement(settings);
      // 设置数据源,支持多数据源
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //类型转换器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

上面第一个解析的就是Properties,这也就说明在Properties中配置的变量在下面解析中都能使用

看一下propertiesElement(root.evalNode("properties"));源码

  private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      //解析子标签,并把值分装成Properties对象
      Properties defaults = context.getChildrenAsProperties();
      //如果引入了外包的Properties,就要把它加载到环境中,并在下面存储到Properties对象中
      String resource = context.getStringAttribute("resource");
      //如果引入了外包的Properties,就要把它加载到环境中,并在下面存储到Properties对象中
      String url = context.getStringAttribute("url");
      //不能同时使用url和resource
      if (resource != null && url != null) {
        throw new BuilderException(
            "The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      //获取环境变量
      Properties vars = configuration.getVariables();
      if (vars != null) {
        //存到Properties对象中
        defaults.putAll(vars);
      }
      //给解析器中设置变量
      parser.setVariables(defaults);
      //给配置类中设置变量
      configuration.setVariables(defaults);
    }
  }

总结:上面代码主要做的事情就是把Properties中配置的和引入外部的变量存到环境中方便后面解析使用

setting源码解读

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各项设置的含义、默认值等

使用只需要在主配置文件中引入如下标签即可,例如

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

解析setting标签

从源码上看Properties settings = settingsAsProperties(root.evalNode("settings"));是用于解析setting标签并校验合法性的,查看方法
private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    //获取标签信息,存储到Properties对中
    Properties props = context.getChildrenAsProperties();
    // 检查配置类是否知道所有设置 下面的方法是解析configuration类,根据setter和getter方法获取有哪些属性是可以修改或可读
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
      if (!metaConfig.hasSetter(String.valueOf(key))) {
        throw new BuilderException(
            "The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
      }
    }
    return props;
  }

上面的方法做的事情就是解析setting的子标签,并校验这些值的合法性,如果不合法就报错,把解析出来的值存到Properties对象中

设置Vfc

方法名称loadCustomVfs(settings);我也不知道Vfc是干什么用的,我没使用

官网的解释:指定 VFS 的实现,自定义 VFS 的实现的类全限定名,以逗号分隔

private void loadCustomVfs(Properties props) throws ClassNotFoundException {
    //获取属性值
    String value = props.getProperty("vfsImpl");
    if (value != null) {
      //按照逗号分割
      String[] clazzes = value.split(",");
      for (String clazz : clazzes) {
        if (!clazz.isEmpty()) {
          @SuppressWarnings("unchecked")
            //加载类
          Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
          //存储Vfc
          configuration.setVfsImpl(vfsImpl);
        }
      }
    }
  }

设置日志

方法名loadCustomLogImpl(settings);指定 MyBatis 所用日志的具体实现,未指定时将自动查找

  private void loadCustomLogImpl(Properties props) {
    //获取日志类的类型
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    //设置日志实现类
    configuration.setLogImpl(logImpl);
  }

值可以写SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING中的任何一种,或者使用全限定类名,为什么可以这么写,请看resolveClass源码

为什么可以下那几个值是因为项目启动的时候会把一些可用的日志实现类的全限定类名写到别名里,所以这边可以通过别名来取值,如果不是这里面的就要写全限定类名,这样才会执行加载方法

设置setting

方法名 settingsElement(settings);这边可以设置全局配置,查看是否配置了对应的属性,如果没有配置就使用默认值

 private void settingsElement(Properties props) {
    configuration
        .setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(
        AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
    configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
    configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
    configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
    configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
    configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
    configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
    configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
    configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
    configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
    configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
    configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
    configuration.setLazyLoadTriggerMethods(
        stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
    configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
    configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
    configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
    configuration.setLogPrefix(props.getProperty("logPrefix"));
    configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
    configuration.setArgNameBasedConstructorAutoMapping(
        booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false));
    configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
    configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
  }

mybatis中可以配置的配置都在上面了,其他的配置都不生效

plugins源码解读

官网介绍

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心

添加插件流程

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

原理解析

添加插件

设置插件的方法pluginElement(root.evalNode("plugins"));方法源码如下

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //获取所有的interceptor标签
        String interceptor = child.getStringAttribute("interceptor");
        //获取interceptor标签下面的Property标签,并把值取出来
        Properties properties = child.getChildrenAsProperties();
        //实例化插件,并调用setProperties方法
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor()
            .newInstance();
        interceptorInstance.setProperties(properties);
        //把插件存储到configuration中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

上面的方法做的事情就是解析interceptor标签,并解析改标签下面的Property标签,实例化该插件,调用该对象的setProperties方法,把插件存储到configuration中

解析原理

首先找到哪些地方会用到Interceptor,找到Interceptor接口,按住ctrl鼠标点击plugin方法

 

可以看出是InterceptorChain.pluginAll在使用,再看看哪里在使用这个方法

我相信大家现在应该知道为什么官网介绍插件可以拦截这四个接口了吧

查看源码可以发现,每个方法里面都使用了interceptorChain.pluginAll方法,那我们来看看这个方法是干什么的

  public Object pluginAll(Object target) {
    //执行每个插件的plugin方法
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

由上面的循环可以看出,一个对象可以被多个插件拦截)再来个看看Interceptor.plugin方法

  default Object plugin(Object target) {
    //使用包装方法包装对象
    return Plugin.wrap(target, this);
  }

再来看看Plugin.wrap方法

  public static Object wrap(Object target, Interceptor interceptor) {
    //这边的target是要包装的对象,interceptor是插件
    //这边就是解析注解,并把注解中的信息解析成一个map
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //获取该对象的类型
    Class<?> type = target.getClass();
    //获取当前对象是符合哪个插件的,即是否是注解中标识的接口的实现类或实现类的子类
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //如果是就创建代理对象
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
    }
    //如果不是就返回原对象
    return target;
  }

上面重要到方法有getSignatureMap和getAllInterfaces,分别看一下源码

 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //获取插件上面的Intercepts注解
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    //如果没有注解就报错
    if (interceptsAnnotation == null) {
      throw new PluginException(
          "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    //获取注解中的每个Signature信息
    Signature[] sigs = interceptsAnnotation.value();
    //定义一个map用于存储每个类中的方法
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      //查看当前map是否包含该类名的key,如果不包含就创建,并且赋值为hashset
      Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
      try {
        //通过方法名和方法参数获取方法对象,然后存储
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e,
            e);
      }
    }
    return signatureMap;
  }

这个方法做的事情就是解析该插件的注解信息,并把注解信息解析到一个map中

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      //获取当前类型的接口
      for (Class<?> c : type.getInterfaces()) {
        //如果注解中包含该接口,就把该接口加入的结果中
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      //获取当前类型的父类
      type = type.getSuperclass();
    }
    //返回第一个满足的类型
    return interfaces.toArray(new Class<?>[0]);
  }

上面这个方法做的事情就是查看当前对象是否是插件注解中定义的接口的实现类或者实现类的子类

总结一下这个wrap方法:首先是解析插件注解信息,并把结果存到一个map中,然后判断当前对象是否是注解中定义接口的实现类或实现类的子类,如果是就通过Jdk创建代理对象,如果不是就返回原对象

要是创建代理对象执行方法如下

 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //获取当前类的拦截方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //看当前方法是否是被拦截的对象,如果是拦截方法,就执行这个插件方法
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //如果不是拦截方法,直接执行
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

上面方法做的事情是获取当前类的方法,看该方法是否在被拦截的方法中,如果在就执行插件方法,如果不在就执行原方法

自定义插件并使用

我定义了五个插件

分别如下

package com.cyz.config;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Method;
import java.util.Properties;

/**
 * @author cyz
 * @date 2023/9/1 10:27
 */
@Intercepts({@Signature(
  type = Executor.class,
  method = "query",
  args = {MappedStatement.class, Object.class,
    RowBounds.class, ResultHandler.class, CacheKey.class,
    BoundSql.class}),
  @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class,
      RowBounds.class, ResultHandler.class})})
public class MyExecutorInterceptor implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    System.out.println("我是Executor拦截器啊");
    System.out.println(method.getName());
    return invocation.proceed();
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.printf(properties.getProperty("someProperty"));
    Interceptor.super.setProperties(properties);
  }
}
package com.cyz.config;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Method;
import java.util.Properties;

/**
 * @author cyz
 * @date 2023/9/1 10:27
 */
@Intercepts({@Signature(
  type = Executor.class,
  method = "query",
  args = {MappedStatement.class, Object.class,
    RowBounds.class, ResultHandler.class, CacheKey.class,
    BoundSql.class}),
  @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class,
      RowBounds.class, ResultHandler.class})})
public class MyExecutorInterceptor2 implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    System.out.println("Executor我是拦截器啊2");
    Object[] args = invocation.getArgs();
    MappedStatement statements = (MappedStatement) args[0];
    SqlSource sqlSource = statements.getSqlSource();
    String sql = sqlSource.getBoundSql(statements).getSql();
    System.out.println(sql);
    sql +="limit 2";

    System.out.println(method.getName());
    return invocation.proceed();
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.printf(properties.getProperty("someProperty"));
    Interceptor.super.setProperties(properties);
  }
}

package com.cyz.config;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.Properties;

/**
 * @author cyz
 * @date 2023/9/6 20:06
 */
@Intercepts({@Signature(
  type = ParameterHandler.class,
  method = "getParameterObject",
  args = {}),
  @Signature(
    type = ParameterHandler.class,
    method = "setParameters",
    args = {PreparedStatement.class})})
public class MyParameterHandlerInterceptor  implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    System.out.println("我是ParameterHandler拦截器啊");
    System.out.println(method.getName());
    return invocation.proceed();
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.printf(properties.getProperty("someProperty"));
    Interceptor.super.setProperties(properties);
  }
}


package com.cyz.config;

import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;

/**
 * @author cyz
 * @date 2023/9/6 20:03
 */
@Intercepts({@Signature(
  type = ResultSetHandler.class,
  method = "handleResultSets",
  args = {Statement.class}),
  @Signature(
    type = ResultSetHandler.class,
    method = "handleCursorResultSets",
    args = {Statement.class})})
public class MyResultSetHandlerInterceptor implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    System.out.println("我是ResultSetHandler拦截器啊");
    System.out.println(method.getName());
    return invocation.proceed();
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.printf(properties.getProperty("someProperty"));
    Interceptor.super.setProperties(properties);
  }
}


package com.cyz.config;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;

/**
 * @author cyz
 * @date 2023/9/6 19:54
 */
@Intercepts({@Signature(
  type = StatementHandler.class,
  method = "query",
  args = {Statement.class, ResultHandler.class}),
  @Signature(
    type = StatementHandler.class,
    method = "prepare",
    args = {Connection.class, Integer.class})})
public class MyStatementHandlerInterceptor implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    System.out.println("我是StatementHandler拦截器啊");
    System.out.println(method.getName());
    return invocation.proceed();
  }

  @Override
  public void setProperties(Properties properties) {
    System.out.printf(properties.getProperty("someProperty"));
    Interceptor.super.setProperties(properties);
  }
}


加入配置文件

启动项目,结果如下

typeHandlers源码解读

官网描述

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器

源码解析

添加类型转换器

你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型

添加方法typeHandlerElement(root.evalNode("typeHandlers"));

 private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //注册包中的类型转换器
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          //注册包中的类型转换器
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }

上面方法做的事就是解析标签,并把值都存储到类型转换器map中

如何使用

首先查看在哪边使用该类型转换器,先按照上面的方法注册好类型转换器

在转化器中打断点

启动项目

从断点可以看出在DefaultResultSetHandler中使用,从该类的第一个方法开始分析

DefaultResultSetHandler.handleResultSets

重新在这个方法上打上断点,重启项目

1、进入

2、如果结果处理器是空就创建一个默认的处理器

3、进入方法

4、进入方法

5、进入方法

6、进入方法

7、进入方法

8、进入方法

9、进入方法

10、获取到了处理器

11、获取到处理,然后调用方法

如果获取不到处理器呢?有返回值,且结果处理器是空 回到上面6方法

12、进入方法 自动映射

13、进入方法 注意这边传的配置是是否开启驼峰命名

14、进入方法

15、进入方法

16、进入方法  从从下面方法可以知道,如果表中字段是带下划线的,这边如果开启驼峰命名的话是可以去掉下划线的

这边把所有的字母都变成的大写,所以在mybatis中属性的大小写不影响赋值

在回到13,下面判断该字段是否有setter方法

如果有setter方法,如果有setter方法就在看看该字段的类型是否有类型处理器,如果有就直接使用类型处理器处理,正常情况想,java中的常用类型都被加入到类型处理器了

如果没有对应的setter方法呢,这边会怎么处理呢

从代码可知他并没有做处理,只是记录了日志

具体使用

从上面11处可以知道会调用处理器的typeHandler.getResult

所以要实现getNullableResult(ResultSet rs, String columnName)方法

其他的类型转换器都是这样的

environments源码解读

官网说明

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。

不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:

  • 每个数据库对应一个 SqlSessionFactory 实例

为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。可以接受环境配置的两个方法签名是:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

如果忽略了环境参数,那么将会加载默认环境,如下所示:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);

为什么说这边默认支持两种事务类型JDBC和MANAGED,因为创建configuration时会自动加入如下事务管理器

数据源类型默认支持三种,也是在上图体现

源码解读

解析方法是environmentsElement(root.evalNode("environments"));

  private void environmentsElement(XNode context) throws Exception {
    //如果配置了environment标签就执行下面解析过程
    if (context != null) {
      //如果在构建sqlSession对象时没有传入环境参数,就去默认的环境变量
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        //判断当前加载的是不是当前的环境变量的环境
        //如果当前环境是空就报错
        if (isSpecifiedEnvironment(id)) {
          //解析事务标签,创建事务工厂并返回
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          //解析数据源标签,创建数据源工厂并返回
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          //获取数据源对象
          DataSource dataSource = dsFactory.getDataSource();
          //构建environment对象
          Environment.Builder environmentBuilder = new Environment.Builder(id).transactionFactory(txFactory)
              .dataSource(dataSource);
          //存到configuration对象中
          configuration.setEnvironment(environmentBuilder.build());
          break;
        }
      }
    }
  }

解析事务处理器的方法

private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    //如果事务标签不为空就执行下面代码
    if (context != null) {
      //获取事务类型
      String type = context.getStringAttribute("type");
      //获取其他配置属性值
      Properties props = context.getChildrenAsProperties();
      //解析事务是否是规定的两种之一,要不是就实体化传入的类型,要是实例化失败就报错
      //创建事务工厂
      TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      //给工厂设置值,从源码看
      // jdbc类型的只有skipSetAutoCommitOnClose参数有效,true或false
      // Managed类型的只有closeConnectione参数有效,true或false
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }

解析数据源代码如下

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      //获取数据源类型
      String type = context.getStringAttribute("type");
      //获取其他属性并存储到Properties中
      Properties props = context.getChildrenAsProperties();
      //解析数据源类型是否是规定的三种之一,要不是就实体化传入的类型,要是实例化失败就报错
      //创建数据源工厂
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      //给数据源工厂设置变量
      factory.setProperties(props);
      //返回数据源工厂
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

总结:解析environment标签,只解析当前环境的配置,每个环境配置中有事务处理器和数据源配置,最后会把环境Id,数据源,事务处理器构建为一个environment对象存到configuration对象中,因为configuration对象中的environment是一个对象,所以每个配置中只能有一个数据源配置

在哪边使用

找到configuration类型中的getEnvironment方法,按住ctrl鼠标点击

上面两个看源码可以知道是用于缓存的,按照环境Id来进行缓存,而他值也来自defaultSqlSessionFactory,中间那个是用于构建执行器,他的值也是来自defaultSqlSessionFactory,所以这些值都是来defaultSqlSessionFactory,而构造方法有两个,如下

上面两个方法大部分都是一样的,只有一点不一样,方法的主要目的是为了构建DefaultSqlSession对象

把配置文件传给他还有执行器,以及是否自动开启事务

如果要想实现多环境,只需要传入对应的参数构建多个SqlSessionFactory

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
MyBatis是一个流行的Java持久化框架,它提供了一种简单且灵活的方式来访问数据库。MyBatis的配置文件对整个框架的使用产生深远的影响,因此我们需要认真学习它。[1]配置文件中包含了数据库连接信息、映射器配置、SQL语句等重要内容,通过配置文件可以实现对数据库的增删改查操作。 除了配置文件,MyBatis最强大的工具之一是映射器。映射器是用于定义SQL语句和Java方法之间映射关系的工具,我们在使用MyBatis时会经常使用到它。[1]映射器可以将数据库表的字段映射到Java对象的属性上,使得我们可以方便地进行对象与数据库之间的转换。 在实际工作中,我们经常会遇到一些特殊的场景,需要灵活运用MyBatis来解决问题。比如处理数据库的BLOB字段的读写、批量更新、调用存储过程、分页、使用参数作为列名、分表等等。[2]这些场景都是通过实战总结出来的,具有较强的实用价值,可以帮助我们更好地应对实际开发中的需求。 此外,MyBatis和Spring框架的结合也是非常常见的。Spring框架是Java世界最流行的IOC和AOP框架之一,而MyBatis和Spring的结合可以构建高性能的大型网站。[3]通过使用Spring MVC和MyBatis,我们可以充分发挥它们的优势,实现灵活可配置的SQL操作,从而构建高性能的Java互联网应用。 总结起来,深入浅出地学习MyBatis的技术原理和实战经验,可以帮助我们更好地理解和应用这个持久化框架,提高开发效率和代码质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值