Mybatis-Sql解析执行

这是我参加更文挑战的第22天

Mybatis-Sql解析执行

前面提到直接调用mybatis的selectOne方法和调用我们自己的mapper方法最终得到的结果是一样的,那么这是怎么实现的呢?而且我们的sql语句并不是一条完整的sql语句,那是怎么执行的呢?

源码杀起来

前面提到过在MapperAnnotationBuilder中有实现了获取sql语句的方法,getSqlSourceFromAnnotations方法获取sql语句并注入

在这里插入图片描述

现在看到了,拿到了我们注解添加的sql语句,那么是怎么执行的呢?在上一章介绍了为什么不调用selectOne方法,调用我们自己的mapper方法也可以获取正确的结果,是因为最终都会到XMLConfigBuilder执行。那具体细节是怎么实现的呢?杀源码去

上一章最终通过addMapper找到XMLConfigBuilder,对于mapperRegistry.getMapper(type, sqlSession);并没有分析:

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession); //点进去查看
}
复制代码

到了MapperRegister中的getMapper

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  //mapperProxyFactory,看这名字就知道是代理了实现了
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    return mapperProxyFactory.newInstance(sqlSession);  //点击进入实例化
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}
复制代码

那么细节是怎么实现呢?mapperProxyFactory.newInstance(sqlSession)

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
复制代码
public class MapperProxyFactory<T> {
........
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //点击查看Proxy最终是由jdk的jar包实现
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}
复制代码
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)  //被代理对象要实现的InvocationHandler方法
    throws IllegalArgumentException
{
	..........
}
复制代码

return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);最终InvocationHandler传的参数为:MapperProxy mapperProxy,点击MapperProxy,该类实现了InvocationHandler接口

public class MapperProxy<T> implements InvocationHandler, Serializable {
    ......
        //实现了invoke方法
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else {
                return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

    ......
}
复制代码

打断点验证是否会执行到这里:

在这里插入图片描述

继续向下执行:cachedInvoker(method).invoke(proxy, method, args, sqlSession); 内部由多态实现invoke方法

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  return mapperMethod.execute(sqlSession, args); //点击进入execute方法
}
复制代码
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { //command是一个类,内部静态类SqlCommand是枚举类型,参数包含了数据库的基本操作
            //判断命令类型
            switch (command.getType()) {
                case INSERT: {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = rowCountResult(sqlSession.insert(command.getName(), param));
                    break;
                }
                case UPDATE: {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = rowCountResult(sqlSession.update(command.getName(), param));
                    break;
                }
                case DELETE: {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = rowCountResult(sqlSession.delete(command.getName(), param));
                    break;
                }
                case SELECT:
                    if (method.returnsVoid() && method.hasResultHandler()) {
                        executeWithResultHandler(sqlSession, args);
                        result = null;
                    } else if (method.returnsMany()) {
                        result = executeForMany(sqlSession, args);
                    } else if (method.returnsMap()) {
                        result = executeForMap(sqlSession, args);
                    } else if (method.returnsCursor()) {
                        result = executeForCursor(sqlSession, args);
                    } else {
                        Object param = method.convertArgsToSqlCommandParam(args);
                        //最终selectBook的动态代理在这里实现
                        result = sqlSession.selectOne(command.getName(), param);
                        if (method.returnsOptional()
                            && (result == null || !method.getReturnType().equals(result.getClass()))) {
                            result = Optional.ofNullable(result);
                        }
                    }
                    break;
                case FLUSH:
                    result = sqlSession.flushStatements();
                    break;
                default:
                    throw new BindingException("Unknown execution method for: " + command.getName());
            }    
            ....
    }
复制代码
public enum SqlCommandType { //参数如下:
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
}
复制代码

最终了解为什么我们的selectBook可以和selectOne执行结果一样

在这里插入图片描述

通过上面整个流程,我们可以试试自己模拟一个getMapper

1. 模拟sql语句的getMapper

  1. 实现SqlSession

    Sqlsession.java

    public class SqlSession {
      public  Object getMapper(Class clazz){
        MapperProxyFactory mapperProxyFactory = new MapperProxyFactory();
        //SqlSession.class.getClassLoader()获取SqlSession类
        return Proxy.newProxyInstance(SqlSession.class.getClassLoader(), new Class[]{clazz},mapperProxyFactory);
      }
    }
    复制代码
  2. 实现代理

    MapperProxyFactory

    //实现InvocationHandler接口
    public class MapperProxyFactory implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //获取Sql语句,然后执行,具体业务代码mybatis:selectOne;
            Select annotation = method.getAnnotation(Select.class);
            String sql = annotation.value()[0];   //获取数组第一个参数
            System.out.println("sql::::::"+sql+":::::::::args:"+args[0]);
            //这里就可以带调用JDBC执行
            return null;
        }
    }
    复制代码
  3. 测试TestMysql.java

    public class TestMysql {
      public static void main(String[] args) {
        SqlSession sqlSession = new SqlSession();
        MyBookMapper mapper = (MyBookMapper) sqlSession.getMapper(MyBookMapper.class);
        mapper.selectBook(200);
      }
    }
    复制代码

2. 全局配置源码分析

  1. 梳理mybatis的标签

    1//分步骤解析
    2//issue#117readpropertiesfirst
    3//1.properties
    4propertiesElement(root.evalNode("properties"));
    5//2.类型别名
    6typeAliasesElement(root.evalNode("typeAliases"));
    7//3.插件
    8pluginElement(root.evalNode("plugins"));
    9//4.对象工厂
    10objectFactoryElement(root.evalNode("objectFactory"));
    11//5.对象包装工厂
    12objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    13//6.设置
    14settingsElement(root.evalNode("settings"));
    15//readitafterobjectFactoryandobjectWrapperFactoryissue#631
    16//7.环境@monkey老师
    17environmentsElement(root.evalNode("environments"));
    18//8.databaseIdProvider
    19databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    20//9.类型处理器
    21typeHandlerElement(root.evalNode("typeHandlers"));
    22//10.映射器
    23mapperElement(root.evalNode("mappers"));
    复制代码

1. properties

xml解析,可以知道是XMLConfigBuilder类

private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    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);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}
复制代码

连接数据库可以有两种方式,一种是在全局文件中直接指定,另一种是通过加载properties文件实现

<dataSource type="POOLED">
    <property name="driver" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</dataSource>
复制代码

properties和property有什么区别呢?

<!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
>

<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
>
复制代码

properties

  <properties resource="jdbc.properties">
    <property name="password" value="123"></property>
  </properties>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"></transactionManager>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
复制代码

jdbc.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/demo
username=root
password=root
复制代码

properties配置文件要优于xml

1.properties 源码实现(主要是数据库的配置)

  1. 在XMLConfigBuilder中打断点

     propertiesElement(root.evalNode("properties"));
    复制代码
    private void propertiesElement(XNode context) throws Exception {
    ..........
        //先解析源文件,再获取属性值
        if (resource != null) {
          defaults.putAll(Resources.getResourceAsProperties(resource));  //加载源文件,并解析属性文件
        } else if (url != null) {
          defaults.putAll(Resources.getUrlAsProperties(url));  //通过url加载并解析属性文件
        }
        //获取属性值
        Properties vars = configuration.getVariables();
        if (vars != null) {
          defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
      }
    }
    复制代码

2. settings源码实现

作用:mybatis运行基本属性设置。
使用:
此方法并没有做核心解析

  1. 解析settings子节点的内容,并将解析结果转成Properties对象
  2. 为Configuration创建元信息对象
  3. 通过MetaClass检测Configuration中是否存在某个属性的setter方法,不存在则抛异常
  4. 若通过MetaClass的检测,则返回Properties对象,方法逻辑结束

settings参数详解

<!-- settings是 MyBatis 中全局的调整设置,它们会改变 MyBatis 的运行时行为,应谨慎设置 -->  
<settings>  
    <!--一般公司最多配置下面前2项-->
    <!-- 该配置影响的所有映射器中配置的缓存的全局开关。默认值true -->  
    <setting name="cacheEnabled" value="true"/>  
    
    <!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。默认值false  -->  
    <setting name="lazyLoadingEnabled" value="true"/>  
    
    <!-- 是否允许单一语句返回多结果集(需要兼容驱动)。 默认值true -->  
    <setting name="multipleResultSetsEnabled" value="true"/>  
    
    <!-- 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。默认值true -->  
    <setting name="useColumnLabel" value="true"/>  
    
    <!-- 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 默认值false  -->  
    <setting name="useGeneratedKeys" value="false"/>  
    
    <!--  指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 -->   
    <!-- 默认值PARTIAL -->  
    <setting name="autoMappingBehavior" value="PARTIAL"/>  
    <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/> 
    
    <!--  配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。默认SIMPLE  -->  
    <setting name="defaultExecutorType" value="SIMPLE"/>  
    
    <!-- 设置超时时间,它决定驱动等待数据库响应的秒数。 -->  
    <setting name="defaultStatementTimeout" value="25"/>  
    <setting name="defaultFetchSize" value="100"/>  
    
    <!-- 允许在嵌套语句中使用分页(RowBounds)默认值False -->  
    <setting name="safeRowBoundsEnabled" value="false"/> 
    
    <!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。  默认false -->  
    <setting name="mapUnderscoreToCamelCase" value="false"/>  
    
    <!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。  
默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。  若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。  -->  
    <setting name="localCacheScope" value="SESSION"/>  
    
    <!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。  -->  
    <setting name="jdbcTypeForNull" value="OTHER"/>  
    
    <!--   指定哪个对象的方法触发一次延迟加载。  -->  
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>  
</settings>  
复制代码

1. 源码分析

Properties settings = settingsAsProperties(root.evalNode("settings"));
复制代码
//settings配置解析结果
private Properties settingsAsProperties(XNode context) {
  if (context == null) {
    return new Properties();
  }
  Properties props = context.getChildrenAsProperties();
  // Check that all settings are known to the configuration class
  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;
}
复制代码

最终解析结果如图:

在这里插入图片描述

将结果返回给以下两个方法

loadCustomLogImpl(settings);
....
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")));
}
复制代码

3. typeAliases属性源码解析(了解即可)

  1. 别名设置。

  2. 使用:
    在MyBatis中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输
    入别名即可,无需再把全限定的类名写出来。在MyBatis中,我们有两种方式进行别名配
    置。第一种是仅配置包名,让MyBatis去扫描包中的类型,并根据类型得到相应的别名。
    这种方式可配合Alias注解使用,即通过注解为某个类配置别名,而不是让MyBatis按照
    默认规则生成别名。这种方式的配置如下:

    <typeAliases>
        <packagename="domain.blog"/>
    </typeAliases>
    @Alias("author")
    publicclassAuthor{
    ...
    }
    复制代码

    第二种方式是通过手动的方式,明确为某个类型配置别名。这种方式的配置如下:

    <typeAliases>
        <typeAliasalias="Author"type="domain.blog.Author"/>
        <typeAliasalias="Blog"type="domain.blog.Blog"/>
        <typeAliasalias="Comment"type="domain.blog.Comment"/>
        <typeAliasalias="Post"type="domain.blog.Post"/>
        <typeAliasalias="Section"type="domain.blog.Section"/>
        <typeAliasalias="Tag"type="domain.blog.Tag"/>
    </typeAliases>
    复制代码

    源码实现

    typeAliasesElement(root.evalNode("typeAliases"));
    复制代码
    private void typeAliasesElement(XNode parent) {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                if ("package".equals(child.getName())) {
                    String typeAliasPackage = child.getStringAttribute("name");
                    configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
                } else {
                    String alias = child.getStringAttribute("alias");
                    String type = child.getStringAttribute("type");
                    try {
                        Class<?> clazz = Resources.classForName(type);
                        if (alias == null) {
                            typeAliasRegistry.registerAlias(clazz);
                        } else {
                            typeAliasRegistry.registerAlias(alias, clazz);
                        }
                    } catch (ClassNotFoundException e) {
                        throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                    }
                }
            }
        }
    }
    复制代码

4. plugins源码解析:

作用:插件是MyBatis提供的一个拓展机制,通过插件机制我们可在SQL执行过程中的某些
点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现Interceptor接口。
然后在插件类上添加@Intercepts和@Signature注解,用于指定想要拦截的目标方法。
MyBatis允许拦截下面接口中的一些方法:
使用:

网上找一个测试类:可以拦截sql 执行时间,优化sql。并打印sql 以及参数

第一步:创建类:

SqlPrintInterceptor 并实现 Interceptor

package com.ra.common.plugin;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
/*
 *  MyBatis 将mybatis要执行的sql拦截打印出来
 */
@Intercepts
({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlPrintInterceptor implements Interceptor{

    private static final Logger log = LoggerFactory.getLogger(SqlPrintInterceptor.class);

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameterObject = null;
        if (invocation.getArgs().length > 1) {
            parameterObject = invocation.getArgs()[1];
        }

        long start = System.currentTimeMillis();

        Object result = invocation.proceed();

        String statementId = mappedStatement.getId();
        BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
        Configuration configuration = mappedStatement.getConfiguration();
        String sql = getSql(boundSql, parameterObject, configuration);

        long end = System.currentTimeMillis();
        long timing = end - start;
        //根据个人喜好看需要打印怎么sql,本人是打印打印  1s的
        if(log.isInfoEnabled() && timing>1000){
            log.info("执行sql耗时:" + timing + " ms" + " - id:" + statementId + " - Sql:" );
            log.info("   "+sql);
        }

        return result;
    }

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

    @Override
    public void setProperties(Properties properties) {
    }

    private String getSql(BoundSql boundSql, Object parameterObject, Configuration configuration) {
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    sql = replacePlaceholder(sql, value);
                }
            }
        }
        return sql;
    }

    private String replacePlaceholder(String sql, Object propertyValue) {
        String result;
        if (propertyValue != null) {
            if (propertyValue instanceof String) {
                result = "'" + propertyValue + "'";
            } else if (propertyValue instanceof Date) {
                result = "'" + DATE_FORMAT.format(propertyValue) + "'";
            } else {
                result = propertyValue.toString();
            }
        } else {
            result = "null";
        }
        return sql.replaceFirst("\\?", Matcher.quoteReplacement(result));
    }
}
复制代码

第二步:在mybatis-config.xml 文件加上

<plugins>
   <!--监控 sql 埋点 分页-->
   <plugin interceptor="com.ra.common.plugin.SqlPrintInterceptor"></plugin>
</plugins>
复制代码

第三步:调整输出日志级别

### Uncomment for MyBatis logging
log4j.logger.org.apache.ibatis=DEBUG
复制代码

执行demo

输出如下:

DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
org.apache.ibatis.session.defaults.DefaultSqlSession@55a561cf
INFO [main] - 执行sql耗时:203 ms - id:org.apache.ibatis.anzhitestmybatis.mapper.MyBookMapper.selectBook - Sql:
INFO [main] -    SELECT * FROM account WHERE balance = 200.0
MyBook{username='huahua', balance=200.0}
复制代码

1. 源码分析:

pluginElement(root.evalNode("plugins"));
复制代码
private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);  //将解析的属性放入Interceptor中
    }
  }
}
复制代码
public void addInterceptor(Interceptor interceptor) { //添加interceptor
  interceptorChain.addInterceptor(interceptor);
}
复制代码

与上述执行sql语句流程一样,通过executor执行,这里使用了责任链模式

executor = (Executor) interceptorChain.pluginAll(executor);
复制代码
private final List<Interceptor> interceptors = new ArrayList<>();

public Object pluginAll(Object target) {        //遍历获取interceptor
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);  //调用plugin方法
  }
  return target;
}
复制代码
//Interceptor接口
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}
复制代码

最终调用实现Interceptor的SqlPrintInterceptor类的plugin方法

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

进一步查看Plugin.wrap(target, this);

public static Object wrap(Object target, Interceptor interceptor) {
  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));  // 调用Plugin, Plugin实现了InvocationHandler
  }
  return target;
}
复制代码

所以我们看一下invoke方法:

@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)) {
      //interceptor此时就是SqlPrintInterceptor
      return interceptor.intercept(new Invocation(target, method, args)); 
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}
复制代码

而SqlPrintInterceptor实现了interceptor接口,所以最终调用的是SqlPrintInterceptor的intercept方法,即业务逻辑的实现

@Override
public Object intercept(Invocation invocation) throws Throwable {
  MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
  Object parameterObject = null;
  if (invocation.getArgs().length > 1) {
    parameterObject = invocation.getArgs()[1];
  }

  long start = System.currentTimeMillis();

  Object result = invocation.proceed();

  String statementId = mappedStatement.getId();
  BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
  Configuration configuration = mappedStatement.getConfiguration();
  String sql = getSql(boundSql, parameterObject, configuration);

  long end = System.currentTimeMillis();
  long timing = end - start;
  //根据个人喜好看需要打印怎么sql,本人是打印打印  1s的
  //if(log.isInfoEnabled() && timing>1000){
  if(log.isInfoEnabled()){
    log.info("执行sql耗时:" + timing + " ms" + " - id:" + statementId + " - Sql:" );
    log.info("   "+sql);
  }

  return result;
}
复制代码

5 environments源码解析:

事务管理器和数据源(只能是一个)

<environmentsdefault="development">
    <environmentid="development">   <!--<environmentid="development,development2 ">不能这样写-->
        <transactionManagertype="JDBC"/>
        <dataSourcetype="POOLED">
            <propertyname="driver"value="${jdbc.driver}"/>
            <propertyname="url"value="${jdbc.url}"/>
            <propertyname="username"value="${jdbc.username}"/>
            <propertyname="password"value="${jdbc.password}"/>
            </dataSource>
        </environment>
    <!--development2不会生效-->
        <environmentid="development2">
        <transactionManagertype="JDBC"/>
        <dataSourcetype="POOLED">
            <propertyname="driver"value="${jdbc.driver}"/>
            <propertyname="url"value="${jdbc.url}"/>
            <propertyname="username"value="${jdbc.username}"/>
            <propertyname="password"value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
复制代码

从源码分析为什么只取默认的:

environmentsElement(root.evalNode("environments"));
复制代码
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");  //如果为空,默认default的value
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //将数据源对象赋值给dsFactory
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}
复制代码

3. Mybatis源码分析之BoundSql&SqlNode

mybatis怎么将不完整的sql语句正常执行?而且

在这里插入图片描述

我们的sql语句是#,这里为什么是?呢?

在执行SQL之前,需要将SQL语句完整的解析出来。我们都知道SQL是配置在映射文件中的,但由于映射
文件中的SQL可能会包含占位符#{},以及动态SQL标签,比如、
。因此,我们并不能直接使用映射文件中配置的SQL。MyBatis会将映射文件中的SQL
解析成一组SQL片段。如果某个片段中也包含动态SQL相关的标签,那么,MyBatis会对该片段
再次进行分片。最终,一个SQL配置将会被解析成一个SQL片段树

在这里插入图片描述

在这里插入图片描述

怎么改呢??(这里没有仔细分析,感觉听得模糊,后面听懂了再补上)

源码:
切入点:org.apache.ibatis.executor.BaseExecutor#query
》org.apache.ibatis.mapping.MappedStatement#getBoundSql
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
》org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.WhereHandler#handleNode
例子:
org.apache.ibatis.scripting.xmltags.StaticTextSqlNode#apply解析静态的sql
org.apache.ibatis.scripting.xmltags.TrimSqlNode.FilteredDynamicContext#applyAll
替换where后面出现antor之类的
org.apache.ibatis.parsing.GenericTokenParser#parse》#改成?

补充:又回去重看了一遍

取消原来插件加载,不然代码拦截sql,不方便找源码

selectOne打断点:

//4、指定mapper中的映射方法,执行数据库操作,并处理结果集,基于xml配置
MyBook goods = sqlSession.selectOne("org.apache.ibatis.anzhitestmybatis.mapper.MyBookMapper.selectBook", 200);
复制代码

进入:DefaultSqlSession.java中的

@Override
public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);   //点击进入
    ......
}
复制代码
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    .......
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);//点击进入
    ........
}
复制代码

在CachingExecutor.java类中,

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  //查看此时boundSql变量:select * from account where balance = ?,有#变为?
  //做转换是因为数据库无法识别#,所以需要JDBC转换
  BoundSql boundSql = ms.getBoundSql(parameterObject);  //点击查看
  //创建缓存
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码

底层杀一波怎么转换

//拿到sql语句
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
复制代码
@Override
public BoundSql getBoundSql(Object parameterObject) {
  return new BoundSql(configuration, sql, parameterMappings, parameterObject); //最终返回一个BoundSql
}
复制代码

那么上述sqlSource是什么时候创建的呢?

在解析XMLStatementBuilder类中这一句创建createSqlSource

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
复制代码

那还是没说怎么改写啊?莫急莫急,下面开始介绍,在sqlNode接口中,apply方法

public interface SqlNode {
  boolean apply(DynamicContext context);
}
复制代码

在这里插入图片描述

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
   //循环改写sql语句,(在后面的源码中将sql拆分成两段)最后拼接成完整的sql语句
    contents.forEach(node -> node.apply(context)); 
    return true;
  }
}
复制代码

在这里插入图片描述

在这里插入图片描述

查看调用栈,调用getSql获取sql

在这里插入图片描述

如何获取解析,parseScriptNode,判断当前是动态sql还是静态sql

在这里插入图片描述

获取和解析sql了解了,再来看执行contents.forEach(node -> node.apply(context));

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    //select * from account where balance = #{balance},这里还是原本的sql
    context.appendSql(text); 
    return true;
  }

}
复制代码
public void appendSql(String sql) {
  sqlBuilder.add(sql);   //放入sqlBuilder
}
复制代码

一次循环完毕,经过多次循环,执行完毕之后,在调用RawSqlSource时

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    //此时查看sqlSource:select * from account where balance = ?转换完成
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}
复制代码

那是怎么做到的呢?

实例测试

在原有的GenericTokenParserTest类中添加main测试

//添加sql语句替换测试实例
public static void main(String[] args) {
  GenericTokenParser parser = new GenericTokenParser("#{", "}", new VariableTokenHandler(new HashMap<String, String>() {
    {
      put("balance", "200");
    }
  }));
  System.out.println(parser.parse("select * from account where balance = #{balance}"));
}
复制代码

执行结果如下

select * from account where balance = 200

也就是说new GenericTokenParser()会完成我们sql的改写。

当我们查看parse,进入

public String parse(String text) {
    if (text == null || text.isEmpty()) {
        return "";
    }
    ............
}
复制代码

查看parse方法:

在这里插入图片描述

点击SqlSourceBuilder.java

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
  GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  String sql = parser.parse(originalSql);
  return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
复制代码

点击查看SqlSource parse()方法:

在这里插入图片描述

至此闭环,找到了如何替换的底层实现GenericTokenParser,一大串的解析

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

  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;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        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) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          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);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}
复制代码

至此改完了,那改完怎么附值呢?

先写一个测试实例,验证:

BookMapper.java

public interface MyBookMapper {
  //@Select("SELECT * FROM account WHERE balance = #{balance}")
  MyBook selectBook(double balance);
  MyBook sqlBook(double balance);
}
复制代码

SqlTestMain

package org.apache.ibatis.anzhitestmybatis;

import org.apache.ibatis.anzhitestmybatis.pojo.MyBook;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;

public class SqlTestMain {
    public static void main(String[] args) throws IOException {
        String resource = "org/apache/ibatis/anzhitestmybatis/mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactoryBuilder.openSession();

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("balance",200);
        hashMap.put("username","huahua");
        List<MyBook> all = sqlSession
            .selectList("org.apache.ibatis.anzhitestmybatis.mapper.MyBookMapper.sqlBook", hashMap);
        for (MyBook book:all){
            System.out.println(book);
        }
    }
}
复制代码

MyBookMapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.anzhitestmybatis.mapper.MyBookMapper">

    <select id="selectBook" resultType="org.apache.ibatis.anzhitestmybatis.pojo.MyBook">
        select * from account where balance = #{balance}
    </select>

    <select id="sqlBook" resultType="org.apache.ibatis.anzhitestmybatis.pojo.MyBook">
        select * from account where
        <if test="balance!=null">
            balance=#{balance}
        </if>
        <if test="username!=null">
            and username=#{username}
        </if>
    </select>

</mapper>
复制代码

执行sql语句并没有and username,输出如下:

select * from account WHERE balance=200

那怎么改写的呢?

TrimSqlNode类的apply方法

@Override
public boolean apply(DynamicContext context) {
  FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  boolean result = contents.apply(filteredDynamicContext);
  filteredDynamicContext.applyAll();
  return result;
}
复制代码
public void applyAll() {
  //将sql截成两段,这里是后半段balance=#{balance}
  sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());  
  String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  if (trimmedUppercaseSql.length() > 0) {
    //对sql语句作处理,去除and等字符
    applyPrefix(sqlBuffer, trimmedUppercaseSql);
    applySuffix(sqlBuffer, trimmedUppercaseSql);
  }
  delegate.appendSql(sqlBuffer.toString());
}
复制代码
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) {
    prefixApplied = true;
    if (prefixesToOverride != null) {
      //prefixesToOverride
      for (String toRemove : prefixesToOverride) {
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}
复制代码

prefixesToOverride

在这里插入图片描述

经过处理之后:

and username=#{username}变为:where username=#{username}

getBoundSql是所有请求的入口

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享