ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
Configuration存储着mybatis运行时所需要的全部配置信息,那么它是如何从mybatis-config.xml转换过来的呢?实际运行中它又起到什么作用呢?今天我通过一个小例子,结合源码一步一步探索一下Configuration的解析流程,以便更加深入的了解其运行机制。 # 从Demo开始 下面是一个小小的示例。指定了配置文件mybatis-config.xml,通过SqlSessionFactoryBuilder创建SqlSessionFactory,打开SqlSession获取Mapper,执行Mapper方法。 ~~~java public static void main(String[] args) throws IOException { // mybatis配置文件 String path = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(path); // 获取 SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建 SqlSession try (SqlSession sqlSession = sqlSessionFactory.openSession()) { CompanyDao dao = sqlSession.getMapper(CompanyDao.class); CompanyDO companyDO = dao.selectById(1, "test"); System.out.println(companyDO); } } 复制代码 ~~~ 从main方法中我们并没有看到Configuration的影子,是因为在实际运行中,操作数据库是使用SqlSession,而SqlSession是由SqlSessionFactory按需创建的,Configuration对象保存在SqlSessionFactory中,当创建SqlSession时会把Configuration传递到SqlSession。 # 配置解析流程 示例代码3-6行就完成了Configuration的解析工作,这个过程发生在方法SqlSessionFactoryBuilder#build()中,进入方法内部会发现实际执行xml解析的是类XMLConfigBuilder。 ~~~java public SqlSessionFactory build(InputStream inputStream) { // 调用重载方法 return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 创建XMLConfigBuilder对象,这个是Configuration解析类 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 首先把xml解析为Configuration对象,然后创建SqlSessionFactory对象。 // 我们下来主要关心parse()方法 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } 复制代码 ~~~ 在mybatis中XMLConfigBuilder负责解析mybatis-config.xml,实现配置信息从文件到内存对象的转换与映射。首先看下构造方法。 ~~~java // 接收配置文件流对象、environment以及通过代码传入的属性配置。 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } 复制代码 ~~~ XMLConfigBuilder构造方法创建了xml的解析器parser,parser负责处理复杂的xml读取工作。另外,构造方法还接收了props对象,它会覆盖从配置文件中解析的同名属性信息。 parse()方法及parseConfiguration()是解析流程的主干流程,其中,parseConfiguration中逐个完成了xml中各部分的解析,每种配置封装为了独立的方法,理解起来比较容易。每个XMLConfigBuilder只能解析一次,重复解析会导致异常。 ~~~java public Configuration parse() { //只能解析一次,否则异常 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 标记为已解析 parsed = true; //解析configuration节点下的内容 parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { //issue #117 read properties first //解析属性 propertiesElement(root.evalNode("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); // read it after objectFactory and objectWrapperFactory issue #631 //加载环境配置 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); } } 复制代码 ~~~ 以上每部分配置的加载方法,其内部都会为configuration赋值,最终解析完成所有配置,再由parse方法返回给调用方。下面逐个了解各部分mybatis配置的解析流程。 ## 属性(properties) 用于定义mybatis运行所需的属性信息,如数据库连接相关配置。属性的定义有三种方式:在mybatis-config.xml#properties中定义属性、通过外部properties文件定义,然后使用resource方式引入mybatis-config.xml、通过 SqlSessionFactoryBuilder.build() 方法中传入属性值。 ~~~java // 入参为root.evalNode("properties"),即properties节点下所有子节点 private void propertiesElement(XNode context) throws Exception { if (context != null) { //读取所有子节点中配置的属性信息,所有信息存储defaults Properties defaults = context.getChildrenAsProperties(); //若以resource或url方式导入外部配置,则加载resource或url中配置信息 String resource = context.getStringAttribute("resource"); String url = context.getStringAttribute("url"); 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."); } //外部配置写入defaults,这里需要注意:外部配置会覆盖上面从properties中读取的配置信息 if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); } //从configuration读取以代码方式传入的配置信息,如果有也写入defaults, //此时也会覆盖前两步加载的同名配置 Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } //属性解析完成,存储。 parser.setVariables(defaults); configuration.setVariables(defaults); } } 复制代码 ~~~ 从这个方法的执行流程可知,如果同一个属性在不同的位置进行重复配置,那么MyBatis 将按照下面的顺序来加载: * 首先读取在 properties 元素体内指定的属性。 * 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。 * 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。 ## 设置(settings) ### 设置项 这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。通过以下代码+注释解释各个setting及其用途。其中,大部分setting都拥有默认值,实际可按需更改。 ~~~java public class Configuration { //环境配置 protected Environment environment; //是否允许在嵌套语句中使用分页(RowBounds),默认false protected boolean safeRowBoundsEnabled; //是否允许在嵌套语句中使用结果处理器(ResultHandler),默认true protected boolean safeResultHandlerEnabled = true; //是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 protected boolean mapUnderscoreToCamelCase; //开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载。 protected boolean aggressiveLazyLoading; //是否允许单个语句返回多结果集(需要数据库驱动支持),默认值true protected boolean multipleResultSetsEnabled = true; //允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。 // 尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 protected boolean useGeneratedKeys; //使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 protected boolean useColumnLabel = true; //全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,默认true protected boolean cacheEnabled = true; //指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。 // 注意基本类型(int、boolean 等)是不能设置成 null 的。 // 默认false protected boolean callSettersOnNulls; //允许使用方法签名中的名称作为语句参数名称。 //为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。 //默认值true protected boolean useActualParamName = true; //当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 // 请注意,它也适用于嵌套的结果集(如集合或关联),默认false protected boolean returnInstanceForEmptyRow; //指定 MyBatis 增加到日志名称的前缀。默认无 protected String logPrefix; //指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 //SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING //默认无 protected Class <? extends Log> logImpl; //指定 VFS 的实现 protected Class <? extends VFS> vfsImpl; //MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 // 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 // 可选值:SESSION | STATEMENT protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION; //当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 //某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 //默认值:OTHER protected JdbcType jdbcTypeForNull = JdbcType.OTHER; //指定对象的哪些方法触发一次延迟加载。用逗号分隔的方法列表。 //例如:equals,clone,hashCode,toString protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString")); //设置超时时间,它决定数据库驱动等待数据库响应的秒数。默认未设置。 protected Integer defaultStatementTimeout; //为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。默认未设置 protected Integer defaultFetchSize; //配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 //可选值:SIMPLE,REUSE,BATCH,默认值:SIMPLE protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE; //指定 MyBatis 应如何自动映射列到字段或属性。 // NONE 表示关闭自动映射; // PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 // FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 //默认值:PARTIAL protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL; //指定发现自动映射目标未知列(或未知属性类型)的行为。 //NONE: 不做任何反应 //WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN) //FAILING: 映射失败 (抛出 SqlSessionException) //默认值:NONE protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE; // 属性列表 protected Properties variables = new Properties(); protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); //每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 //默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 //如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。 protected ObjectFactory objectFactory = new DefaultObjectFactory(); protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); //延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 //默认值:false protected boolean lazyLoadingEnabled = false; protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL protected String databaseId; /** * Configuration factory class. * Used to create Configuration for loading deserialized unread properties. * * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a> */ protected Class<?> configurationFactory; //语言驱动注册中心 protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry(); //存储所有Mapper中的映射声明,k-v结构 protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection") .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource()); //数据缓存,k-v结构 protected final Map<String, Cache> caches = new StrictMap<>("Caches collection"); //存储所有结果映射,来自Mapper protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection"); //存储所有参数迎神,来自Mapper protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection"); protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection"); //存储已加载的资源文件,如mapper.xml protected final Set<String> loadedResources = new HashSet<>(); //存储sql片段 protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); //存储未解析完成的声明 protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>(); //存储未解析完成的缓存解析 protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>(); //存储未解析完成的结果映射 protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>(); //存储未解析完成的方法 protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>(); //…… } 复制代码 ~~~ ### 加载流程 mybatis提供的配置项很多,为了方便使用,都提供了默认值。多数情况下,我们只需要按需更改即可。settingsAsProperties用于完成setting的配置解析工作: ~~~java private Properties settingsAsProperties(XNode context) { //节点为空,返回空的属性对象 if (context == null) { return new Properties(); } //读取所有设置信息,存入props Properties props = context.getChildrenAsProperties(); // Check that all settings are known to the configuration class // 通过反射方式检查props加载的配置信息是否为已知的配置项,如果存在未知配置项,抛出异常。 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; } 复制代码 ~~~ 设置加载过程中采用了反射方式检查配置文件中配置项的合法性,存在未知配置项会导致异常。另外,这个方法把解析到的配置项返回,并没有直接为configuration设置,而是通过后面的settingsElement方法进行初始化。因为还要继续加载vfs、logImpl的配置信息。 ## 类型别名 类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如: ~~~java <typeAliases> <typeAlias type="com.raysonxin.dataobject.CompanyDO" alias="CompanyDO"/> </typeAliases> 复制代码 ~~~ 别名降低了使用的复杂度,它在类型查找、映射方面起到很大的作用,typeAliasesElement负责完成别名的解析加载工作: ~~~java 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); //别名为空,默认类名首字母小写;不是空,按照alias注册 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); } } } } } 复制代码 ~~~ 从代码可知,mybatis支持两种别名的注册方式: * 按照包名注册:按照指定的包扫描类,若存在别名注解,则使用注解指定的名称;否则使用类的首字母小写默认; * 按照类全限定名称注册:指定了别名则使用其作为别名,否则使用类的首字母小写默认; **另外需要注意的是,这是mybatis提供的自定义别名的方式,其实mybatis已经提供了绝大多数常用的别名,具体在Configuration的构造方法中,大家可以自行查看。** ## 插件(plugins) MyBatis 允许我们在映射语句执行过程中的某一点进行拦截调用,这可以很方便的让我们扩展mybatis的能力。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括: * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) * ParameterHandler (getParameterObject, setParameters) * ResultSetHandler (handleResultSets, handleOutputParameters) * StatementHandler (prepare, parameterize, batch, update, query) 这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。先看下接口Interceptor的定义: ~~~java public interface Interceptor { //拦截接口,这里面实现拦截处理逻辑 Object intercept(Invocation invocation) throws Throwable; //为上述目标增加拦截,增强 Object plugin(Object target); //设置插件属性 void setProperties(Properties properties); } 复制代码 ~~~ ### 自定义插件 自定义插件只需实现 Interceptor 接口,并指定想要拦截的方法签名即可,这里的接口方法和签名必须与Mybatis源码中的定义完全一致,下面通过一个例子进行说明。 ~~~java @Intercepts( @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) ) public class CustomInterceptor implements Interceptor { Properties properties = new Properties(); @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("before CustomInterceptor"); Object object = invocation.proceed(); System.out.println("after CustomInterceptor"); return object; } @Override public Object plugin(Object target) { return target instanceof Executor ? Plugin.wrap(target, this) : target; } @Override public void setProperties(Properties properties) { this.properties = properties; } } 复制代码 ~~~ CustomInterceptor对Executor#query方法进行了拦截,在query前后输出一些埋点信息(可以替换为自己的业务逻辑);plugin方法实现逻辑是:当目标组件为Executor时使用Plugin.wrap方法会为其增加插件功能。接下来在mybatis-config.xml中进行配置: ~~~xml <plugins> <plugin interceptor="com.raysonxin.plugin.CustomInterceptor"> <property name="name" value="abcd"/> </plugin> </plugins> 复制代码 ~~~ 看下运行效果: **![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e04ba08ff039414ab97508bd57949daf~tplv-k3u1fbpfcp-zoom-1.image)** ### 插件加载与执行流程 直接上代码:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement ~~~java //入参为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).newInstance(); //设置插件属性字段 interceptorInstance.setProperties(properties); //添加插件信息到configurtation configuration.addInterceptor(interceptorInstance); } } } 复制代码 ~~~ 插件加载流程比较简单:获取名称,获取属性,实例化插件,设置插件属性,最终添加到configuration。XMLConfigBuilder完成插件加载后把插件保存在Configuration#interceptorChain,那实际运行中是如何应用的呢? 如前文所说,mybatis支持在Executor、StatementHandler、ParameterHandler、ResultSetHandler四大组件的若干方法上通过插件增强,mybatis正是在四大组件的创建过程中采用动态代理的方式应用插件的。四大组件的创建方法在Configuration中,如下所示,以newExecutor为例对插件应用过程进行梳理。 ~~~java public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } //添加插件逻辑,采用动态代理进行包装 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } 复制代码 ~~~ 组件对象创建完成后通过interceptorChain.pluginAll()方法依次应用插件,四个方法的逻辑类似。interceptor.plugin()由插件实现,它会调用Plugin#wrap方法进行增强。看下这一段的源码逻辑: ~~~java public Object pluginAll(Object target) { //循环依次遍历插件 for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } 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) { //采用动态代理创建代理类实现,在我们的例子中相当于为Executor对象进行了增强。 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } 复制代码 ~~~ 所以,经过Plugin#wrap包装后的Executor对象其实是一个代理对象,其中包含了Executor原有的逻辑,按照动态代理的原理,我们看下Plugin#invoke的执行逻辑: ~~~java @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)) { //如果是则执行插件的intercept方法,内部会调用原有的method,入口是:Invocation#proceed return interceptor.intercept(new Invocation(target, method, args)); } //未被增强,直接调用原来的方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } 复制代码 ~~~ 编写插件需要注意对原有mybatis的影响,防止破坏 MyBatis 的核心模块。通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。 ## 类型处理器(typeHandler) MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。通过TypeHandler接口定义也可以清晰的看到TypeHandler的作用: ~~~java public interface TypeHandler<T> { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; } 复制代码 ~~~ ### 自定义类处理器 如果默认的类型处理器不能满足使用需求,可以按照要求自定义类型处理器。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。 以第二种方式举例,自定义类型处理器CustomTypeHandler,该处理器对应的java类型是String,jdbc类型是VARCHAR,includeNullJdbcType为true。 ~~~java @MappedJdbcTypes(value = JdbcType.VARCHAR,includeNullJdbcType = true) public class CustomTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } } 复制代码 ~~~ 然后在mybatis-config.xml中添加如下配置: ~~~xml <typeHandlers> <typeHandler handler="com.raysonxin.typehandler.CustomTypeHandler"/> </typeHandlers> 复制代码 ~~~ ### 加载流程 类型处理器的注册过程比较复杂,原因是代码中包含了许多个重载方法,看上去晕头转向,我们先来认识一下TypeHandlerRegistry是如何设计的,直接看代码。 ~~~java public final class TypeHandlerRegistry { //这个字段存储了jdbcType到类型处理器的映射,使用EnumMap private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class); //存储javaType对应的处理器映射,需要根据jdbcType选择处理器 private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>(); //未知类型处理器,内部会进一步解析处理器类型并路由 private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this); //存储所有的类型与类型处理器的映射,jdbcType或者javaType private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>(); //NULL处理器 private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap(); //默认枚举类型处理器 private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class; //构造方法内默认注册了常用的类型处理器 public TypeHandlerRegistry() { register(Boolean.class, new BooleanTypeHandler()); register(boolean.class, new BooleanTypeHandler()); register(JdbcType.BOOLEAN, new BooleanTypeHandler()); register(JdbcType.BIT, new BooleanTypeHandler()); //.... } //... } 复制代码 ~~~ TypeHandlerRegistry内部维护了java类型、jdbc类型与类型处理器之间的关系,分别从不同的角度进行映射: * JdbcType对应的处理器(JDBC\_TYPE\_HANDLER\_MAP):意思是当处理一个jdbc类型时,可以从这里获取处理器; * javaType对应的处理器(TYPE\_HANDLER\_MAP):这个字段使用了两层映射。第一层是javaType,代表了该javaType具有的处理器列表;第二层是jdbcType与处理器之间的对应关系。综合下来,确认一个处理器需要进行最终确认处理器。 * 全类型映射(ALL\_TYPE\_HANDLERS\_MAP):维护了jdbc类型和java类型所对应的所有映射器。 * 空类型处理器和默认枚举类型处理器。 ~~~java //入参为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 { //单个类型处理器注册的配置方式 //获取javaType,这个是类型的别名,应该注册在typeAliasRegistry String javaTypeName = child.getStringAttribute("javaType"); //获取jdbcType String jdbcTypeName = child.getStringAttribute("jdbcType"); //获取handler名称,这是类的全限定名称 String handlerTypeName = child.getStringAttribute("handler"); //通过javaType获取其对应的类型,可能为空;若指定了但是没有找到,会报异常 Class<?> javaTypeClass = resolveClass(javaTypeName); //获取JdbcType,枚举类型 JdbcType jdbcType = resolveJdbcType(jdbcTypeName); //获取处理器对应的类型 Class<?> typeHandlerClass = resolveClass(handlerTypeName); if (javaTypeClass != null) { if (jdbcType == null) { //Case-1:按照java类型、处理器类型注册 //a、根据typeHandlerClass创建处理器对象typeHandler; //b、获取typeHandler注解中的jdbcTypes,可能为空 //c、jdbcTypes!=null:注册javaType-jdbcType-handler,如果注解中includeNullJdbcType=true,注册空类型; // jdbcTypes==null:仅注册javaType-handler; typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { //Case-2:按照java类型、jdbc类型、处理器类型注册 //a、创建typeHandler对象; //b、注册关联javaType-jdbcType-handler typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { //Case-3:按照处理器类型注册 //a、获取typeHandlerClass的MappedTypes注解; //b、javaType不为空:按照javatype注册,内部会获取MappedJdbcTypes,关联三者。 //c、这种情况还没看明白。。。 typeHandlerRegistry.register(typeHandlerClass); } } } } } //Case-1~3,最终都会走到这里 private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { //从map中查询已有的javaType对应的处理器 Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType); //不存在或者为空,执行初始化与添加操作 if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); TYPE_HANDLER_MAP.put(javaType, map); } //关联javaType-jdbcType-handler map.put(jdbcType, handler); } //加入总的映射 ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); } 复制代码 ~~~ 通过代码可以发现,JDBC\_TYPE\_HANDLER\_MAP仅仅是在构造方法逻辑中进行了添加,相当于是mybatis默认的处理器。我们自定义的处理器会添加到TYPE\_HANDLER\_MAP、ALL\_TYPE\_HANDLERS\_MAP,虽然有不同的注册方式,但是殊途同归,都是通过上述方法实现了注册。 总结一下: * 通过配置文件自定义的类型处理器,会添加到TYPE\_HANDLER\_MAP、ALL\_TYPE\_HANDLERS\_MAP,而获取类型处理器是优先使用javaType来查询的。如果自定义的类型处理器了重复定义了javaType或jdbcType,会对系统默认的处理器产生覆盖。 * 如果在类型处理器的注解和xml配置中同时声明了javaType,那么xml中配置的javaType会生效。 ## 映射器(mapper) MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。——*摘自《XML 映射器》* 映射器是mybatis面向接口编程的利器,它使用动态代理技术,实现了接口与mapper中sql语句的动态绑定,免去了JDBC中手动实现执行Statement的繁杂过程。mybatis提供多种方式支持mapper资源查找:可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。 ### 映射器元素 SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出): * cache – 该命名空间的缓存配置。 * cache-ref – 引用其它命名空间的缓存配置。 * resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。 * ~parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。~ * sql – 可被其它语句引用的可重用语句块。 * insert – 映射插入语句。 * update – 映射更新语句。 * delete – 映射删除语句。 * select – 映射查询语句 以上顶级元素还有较多的属性用于控制其行为,大家可以自行查阅官方文档,这里不再贴出来了。 ### 加载流程 我们随着示例及源码来看下mybatis是如何加载解析mapper的,然后通过另外一篇文章来剖析其执行过程。示例配置信息,示例中定义了CompanyMapper.xml,以资源方式引入到mybatis-config.xml。 ~~~xml <!--mybatis-config.xml--> <mappers> <mapper resource="mapper/CompanyMapper.xml"/> </mappers> <!--CompanyMapper.xml--> <mapper namespace="com.raysonxin.dao.CompanyDao"> <resultMap id="baseResultMap" type="com.raysonxin.dataobject.CompanyDO"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="cpy_type" property="cpyType"/> </resultMap> <sql id="BaseColumns"> id,name,cpy_type </sql> <select id="selectById" resultMap="baseResultMap"> select <include refid="BaseColumns"></include> from company where id= #{id} and name = #{name} </select> </mapper> 复制代码 ~~~ mapper解析入口为XMLConfigBuilder#mapperElement,通过代码注释走下流程。 ~~~java //入参为root.evalNode("mappers"),即示例中的mappers节点,它可包含多个mapper子节点 private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 整包方式添加mapper if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { //这里包含了三种方式:资源导入、资源全限定路径、类名,分别获取mapper节点属性值 //三个值中只能配置一个,否则会报异常(最后的else) String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 资源方式 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); //使用XMLMapperBuilder类进行mapper解析,这里是重点。 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } //资源全限定路径方式 else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); //使用XMLMapperBuilder类进行mapper解析,同上。 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } //类名方式 else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } //非以上三种情况,异常 else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } } 复制代码 ~~~ 示例代码以resource方式引入mapper,按照这条线走下去,创建XMLMapperBuilder后调用其parse方法。这里与XMLConfigBuilder类似,同样使用XPathParser进行xml解析,另外使用MapperBuilderAssistant构建MappedStatement。 ~~~java private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { super(configuration); this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; } 复制代码 ~~~ 接下来进入解析流程,即XMLMapperBuilder#parse方法。大体流程是:若资源未被加载过,则解析mapper文件中的元素,把mapper添加到已加载资源,绑定mapper与namespace。无论是否加载过当前资源,都会处理那些之前未完成的ResultMap、CacheRef、Statement。 ~~~java public void parse() { //判断资源是否已经加载 if (!configuration.isResourceLoaded(resource)) { //这个是mapper解析的核心方法,入参是mapper对应的资源文件,示例中的CompanyMapper.xml configurationElement(parser.evalNode("/mapper")); //添加已加载资源到configuration configuration.addLoadedResource(resource); //这个方法是绑定mapper与namespace bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } 复制代码 ~~~ 核心解析方式是configurationElement,它依次解析mapper文件中的cache-ref、cache、parameterMap、resultMap、sql、select|insert|update|delete,本次重点分析后面三者的解析过程。 ~~~java private void configurationElement(XNode context) { try { //获取mapper中的namespace,示例中为:com.raysonxin.dao.CompanyDao String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } //设置builderAssistant的namespace builderAssistant.setCurrentNamespace(namespace); //解析cache-ref内容 cacheRefElement(context.evalNode("cache-ref")); //解析cache内容 cacheElement(context.evalNode("cache")); //解析parameterMap节点 parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析resultMap节点 resultMapElements(context.evalNodes("/mapper/resultMap")); //解析sql节点 sqlElement(context.evalNodes("/mapper/sql")); //构建statement buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } 复制代码 ~~~ #### resultMap 下可以定义多个resultMap,通常我们会把常用的返回字段定义为一个resultMap,方便在查询语句中引用。我们从resultMapElements开始看下源码: ~~~java private void resultMapElements(List<XNode> list) throws Exception { for (XNode resultMapNode : list) { try { resultMapElement(resultMapNode); } catch (IncompleteElementException e) { // ignore, it will be retried } } } private ResultMap resultMapElement(XNode resultMapNode) throws Exception { return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList(), null); } //真正的解析工作从这里开始 private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); //获取resultMap的id属性,示例中baseResultMap String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); //获取resultMap的type属性,后面是默认值设置顺序 String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); //获取extends属性 String extend = resultMapNode.getStringAttribute("extends"); //获取autoMapping属性 Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); //获取type对应的类型对象,首先从typeAliasRegistry获取,否则按照全路径限定名称反射创建 Class<?> typeClass = resolveClass(type); if (typeClass == null) { typeClass = inheritEnclosingType(resultMapNode, enclosingType); } Discriminator discriminator = null; List<ResultMapping> resultMappings = new ArrayList<>(); resultMappings.addAll(additionalResultMappings); List<XNode> resultChildren = resultMapNode.getChildren(); //依次解析子节点 for (XNode resultChild : resultChildren) { //是否为构造方法 if ("constructor".equals(resultChild.getName())) { //处理构造方法 processConstructorElement(resultChild, typeClass, resultMappings); } //discriminator鉴别器 else if ("discriminator".equals(resultChild.getName())) { discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); } //其他节点 else { List<ResultFlag> flags = new ArrayList<>(); // 是否为id节点 if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); } //构建resultMap:buildResultMappingFromContext resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } } 复制代码 ~~~ 不考虑复杂的resultMap,我们示例中的节点类型为id和result两种,我们直接进入方法buildResultMappingFromContext: ~~~java private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception { String property; if (flags.contains(ResultFlag.CONSTRUCTOR)) { property = context.getStringAttribute("name"); } else { //获取property值,对应javaType的字段 property = context.getStringAttribute("property"); } //数据表的字段 String column = context.getStringAttribute("column"); //获取javaType String javaType = context.getStringAttribute("javaType"); //获取jdbcType String jdbcType = context.getStringAttribute("jdbcType"); String nestedSelect = context.getStringAttribute("select"); String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.<ResultMapping> emptyList(), resultType)); String notNullColumn = context.getStringAttribute("notNullColumn"); String columnPrefix = context.getStringAttribute("columnPrefix"); //类型处理器 String typeHandler = context.getStringAttribute("typeHandler"); String resultSet = context.getStringAttribute("resultSet"); String foreignColumn = context.getStringAttribute("foreignColumn"); boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); //获取javaType类型对象 Class<?> javaTypeClass = resolveClass(javaType); //获取类型处理器 Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler); //jdbc类型枚举 JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); //创建ResultMapping对象并返回 return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); } 复制代码 ~~~ 这个解析过程包含了resultMap高级用法的逻辑,比如association、collections等,我们先从简单、常用的方式入手。buildResultMappingFromContext最终返回了resultMap中某个字段的映射信息,构建了java类型与数据库字段的映射关系。这样,通过遍历所有字段把resultMap下的所有字段一一映射。 我们再回到方法resultMapElement,遍历所有节点后得到resultMappings,创建了ResultMapResolver对象。从名称可以知道,这个类是ResultMap的解析器,它最终把resultMap配置信息转为mybaits的ResultMap对象并添加到configuration。 #### sql 实际开发中,我们可以使用sql标签定义那些可复用的sql片段,简化mapper文件的配置。在mybatis解析sql时,它也只是一个中间过程,它是解析select|update|insert|delete语句的基础。 ~~~java private void sqlElement(List<XNode> list) { if (configuration.getDatabaseId() != null) { sqlElement(list, configuration.getDatabaseId()); } sqlElement(list, null); } private void sqlElement(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { String databaseId = context.getStringAttribute("databaseId"); String id = context.getStringAttribute("id"); //拼接namespace,结果为:namespace+"."+id id = builderAssistant.applyCurrentNamespace(id, false); //是否符合datbaseId要求 if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { //sqlFragments保存所有sql片段 sqlFragments.put(id, context); } } } 复制代码 ~~~ #### select|insert|update|delete select|insert|update|delete对于sql语句中的同名命令,是我们使用最频繁的部分,最终mybatis会把这些标签转为MappedStatement,存储在configuration,同时与Mapper接口方法在运行时绑定。 ~~~java private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } } 复制代码 ~~~ 从代码可以看到XMLStatementBuilder,它是命令标签的处理器,核心方法是parseStatementNode。到这里,我们已经见过了XMLConfigBuilder、XMLMapperBuilder。看下处理过程: ~~~java public void parseStatementNode() { //获取标签中的id属性,如:selectById String id = context.getStringAttribute("id"); //databaseId,为空 String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } //属性fetchSize Integer fetchSize = context.getIntAttribute("fetchSize"); //属性timeout Integer timeout = context.getIntAttribute("timeout"); //属性parameterMap String parameterMap = context.getStringAttribute("parameterMap"); //属性parameterType String parameterType = context.getStringAttribute("parameterType"); //解析参数类型信息 Class<?> parameterTypeClass = resolveClass(parameterType); //获取属性resultMap String resultMap = context.getStringAttribute("resultMap"); //获取属性resultType String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); //获取属性statementType,未设置时使用默认值PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); //获取nodeName,就是标签的类型,select、insert之类 String nodeName = context.getNode().getNodeName(); //命令类型 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); //判断是否为select命令 boolean isSelect = sqlCommandType == SqlCommandType.SELECT; //是否刷新缓存 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); //是否使用缓存 boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing //这里解析sql语句中的include标签 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. //解析selectKey标签 processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) //创建SqlSource,必须在inclue和selectKey解析完成后执行, //内部会根据sql语句是否包含动态标签创建不同类型的sqlSource,解析sql所需的参数映射信息 //这个地方需要一篇单独的文章来分析 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); //处理主键生成器 KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } //创建MappedStatement,添加到configuration builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } 复制代码 ~~~ 源码的过程梳理一下: * 首先解析标签的基础属性,如id、fetchSize等, * 解析sql语句中inclue标签(把include的内容替换拼接进来), * 处理selcetKey标签(特定的databaseProvider需要), * 接下来创建SqlSource:这个是mybatis的核心内容,涉及到sql语句中参数提取、动态赋值等,以后在详细说明; * 处理KeyGenerator; * 使用builderAssistant创建MappedStatement,添加到configuration # 总结 Configuration是mybatis的全局配置类,mybatis运行时所需的一切都缓存在这里,它伴随mybatis的整个生命周期。理论上讲,Configuration是配置文件mybatis-config.xml的代码映射,mybatis提供了充分的灵活性、可扩展性,方便开发人员通过配置文件改变其运行行为。 本文通过示例及源码把mybatis-config.xml的解析过程、Configuration配置的大部分内容进行了梳理,通过梳理确实获益匪浅。由于水平优先,文中很多内容确实没有描述清楚,随着学习的不断深入,再做完善。 作者:码路印记 链接:https://juejin.cn/post/6878979290252361741 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。