mybatis源码分析(一) 配置文件的解析过程

mybatis的源码有人已经做过一个中文的注释,代码github上有mybatis中文注释源码

mybatis框架有两个非常重要的xml文件,一个是mybatis的config文件,一个就是mapper文件,mybatis会根据config的xml文件去生成一个Configuration类,在这个过程中也会根据配置的mapper文件生成MappedStatement,这篇博客探究的就是这样一个过程,往下看

如果单单使用mybatis,我们的做法是导包,配置,然后如下

String resource = "org/mybatis/example/mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);try (SqlSession session = sqlSessionFactory.openSession()) {  BlogMapper mapper = session.getMapper(BlogMapper.class);  Blog blog = mapper.selectBlog(101);}

所以从SqlSessionFactoryBuilder().build说起,点击进入build方法,新建了一个XMLConfigBuilder,然后build(parser.parse()),

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);return build(parser.parse());

先看parser.parse()方法,这方法中将之前的mybatis的xml文件进行解析,生成了Configration类返回,

//解析配置  private void parseConfiguration(XNode root) {    try {      //分步骤解析      //issue #117 read properties first      //1.properties      propertiesElement(root.evalNode("properties"));      //2.类型别名      typeAliasesElement(root.evalNode("typeAliases"));      //3.插件      pluginElement(root.evalNode("plugins"));      //4.对象工厂      objectFactoryElement(root.evalNode("objectFactory"));      //5.对象包装工厂      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));      //6.设置      settingsElement(root.evalNode("settings"));      // read it after objectFactory and objectWrapperFactory issue #631      //7.环境      environmentsElement(root.evalNode("environments"));      //8.databaseIdProvider      databaseIdProviderElement(root.evalNode("databaseIdProvider"));      //9.类型处理器      typeHandlerElement(root.evalNode("typeHandlers"));      //10.映射器      mapperElement(root.evalNode("mappers"));    } catch (Exception e) {      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);    }  }

仔细分析这几行代码,首先看第一个properties解析

//1.properties  //<properties resource="org/mybatis/example/config.properties">  //    <property name="username" value="dev_user"/>  //    <property name="password" value="F2Fa3!33TYyg"/>  //</properties>  private void propertiesElement(XNode context) throws Exception {    if (context != null) {      //如果在这些地方,属性多于一个的话,MyBatis 按照如下的顺序加载它们:      //1.在 properties 元素体内指定的属性首先被读取。      //2.从类路径下资源或 properties 元素的 url 属性中加载的属性第二被读取,它会覆盖已经存在的完全一样的属性。      //3.作为方法参数传递的属性最后被读取, 它也会覆盖任一已经存在的完全一样的属性,这些属性可能是从 properties 元素体内和资源/url 属性中加载的。      //传入方式是调用构造函数时传入,public XMLConfigBuilder(Reader reader, String environment, Properties props)      //1.XNode.getChildrenAsProperties函数方便得到孩子所有Properties      Properties defaults = context.getChildrenAsProperties();      //2.然后查找resource或者url,加入前面的Properties      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.");      }      if (resource != null) {        defaults.putAll(Resources.getResourceAsProperties(resource));      } else if (url != null) {        defaults.putAll(Resources.getUrlAsProperties(url));      }      //3.Variables也全部加入Properties      Properties vars = configuration.getVariables();      if (vars != null) {        defaults.putAll(vars);      }      parser.setVariables(defaults);      configuration.setVariables(defaults);    }  }

具体的xml解析过程就没必要详细看了,最后可以看到所有的properties都被存入了Configuration的variables变量中,

然后往下看类型别名的解析,关于别名,首先Configuration类中定义了一个TypeAliasRegistry

//类型别名注册机  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

这个TypeAliasRegistry中有一个Map存放了别名和别名的类

private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();

所以typeAliasesElement(root.evalNode("typeAliases"))这个方法中的操作就是解析出别名放入这个map中,定义别名的两种方式具体可以看官网。

再往下看,插件的解析

//3.插件  //MyBatis 允许你在某一点拦截已映射语句执行的调用。默认情况下,MyBatis 允许使用插件来拦截方法调用//<plugins>//  <plugin interceptor="org.mybatis.example.ExamplePlugin">//    <property name="someProperty" value="100"/>//  </plugin>//</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);        //调用InterceptorChain.addInterceptor        configuration.addInterceptor(interceptorInstance);      }    }  }

插件虽然比较复杂,但是解析的部分却很简单,主要是resolveClass方法

//根据别名解析Class,其实是去查看 类型别名注册/事务管理器别名  protected Class<?> resolveClass(String alias) {    if (alias == null) {      return null;    }    try {      return resolveAlias(alias);    } catch (Exception e) {      throw new BuilderException("Error resolving class. Cause: " + e, e);    }  }

这个别名的解析过程其实就是去之前说的那个别名的map中查询,有的话就返回,没的话就直接转成Class,所以mybatis里面很多配置属性type="xxx"的,例如datasource的type="POOLED",这个POOLED其实就是类型的别名。最后获取到Class之后newInstance创建一个对象,放入Interceptor拦截器链中,这个拦截器链和SpringMvc类似,其实就是一个拦截器链对象InterceptorChain里面放了一个List集合,调用的时候for循环依次调用,去看看代码

protected final InterceptorChain interceptorChain = new InterceptorChain();

Configuration类中定义了这样一个过滤器链,后面某个地方肯定会执行pluginAll方法

public Object pluginAll(Object target) {    //循环调用每个Interceptor.plugin方法    for (Interceptor interceptor : interceptors) {      target = interceptor.plugin(target);    }    return target;  }

这地方用过插件就很熟悉了,plugin方法中我们基本都这样写,而这个方法就是创建了一个代理对象

return Plugin.wrap(target, this);
public static Object wrap(Object target, Interceptor interceptor) {    //取得签名Map    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);    //取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor)    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这个方法

//取得签名Map  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {    //取Intercepts注解,例子可参见ExamplePlugin.java    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);    // issue #251    //必须得有Intercepts注解,没有报错    if (interceptsAnnotation == null) {      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());          }    //value是数组型,Signature的数组    Signature[] sigs = interceptsAnnotation.value();    //每个class里有多个Method需要被拦截,所以这么定义    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();    for (Signature sig : sigs) {      Set<Method> methods = signatureMap.get(sig.type());      if (methods == null) {        methods = new HashSet<Method>();        signatureMap.put(sig.type(), methods);      }      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;  }

这里从我们注释在拦截器插件的类注解Intercepts 上获取Signature数组,循环数组,解析结果放入signatureMap中,signatureMap是一个Class为键,Method的Set列表为Value的Map,说白了这个解析结果就是一个对象中需要拦截的哪几个方法。

再回头往下看,

很熟悉的动态代理方法,因为传入的InvocationHandler也是Plugin这个类,所以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.intercept,也即插入了我们自己的逻辑        return interceptor.intercept(new Invocation(target, method, args));      }      //最后还是执行原来逻辑      return method.invoke(target, args);    } catch (Exception e) {      throw ExceptionUtil.unwrapThrowable(e);    }  }

分析一下这段代码,这就是从刚才解析的需要拦截的方法的Map中取出该类的拦截列表方法,看看是不是包括当前的方法,是的话就执行intercept也就是我们写的那些拦截方法。再最后执行方法本身的逻辑。标准老套娃!

再回到XMLConfigBuilder中,接着往下

//4.对象工厂  objectFactoryElement(root.evalNode("objectFactory"));

这个就是解析出一个类方法放到Configuration的objectFactory中,覆盖它默认的对象工厂

然后是解析对象包装工厂,反射器工厂,settings,environments等等原理和之前都差不多,所以跳过,

看重点最后一个mapperElement方法

//10.映射器//10.1使用类路径//<mappers>//  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>//  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>//  <mapper resource="org/mybatis/builder/PostMapper.xml"/>//</mappers>////10.2使用绝对url路径//<mappers>//  <mapper url="file:///var/mappers/AuthorMapper.xml"/>//  <mapper url="file:///var/mappers/BlogMapper.xml"/>//  <mapper url="file:///var/mappers/PostMapper.xml"/>//</mappers>////10.3使用java类名//<mappers>//  <mapper class="org.mybatis.builder.AuthorMapper"/>//  <mapper class="org.mybatis.builder.BlogMapper"/>//  <mapper class="org.mybatis.builder.PostMapper"/>//</mappers>////10.4自动扫描包下所有映射器//<mappers>//  <package name="org.mybatis.builder"/>//</mappers>  private void mapperElement(XNode parent) throws Exception {    if (parent != null) {      for (XNode child : parent.getChildren()) {        if ("package".equals(child.getName())) {          //10.4自动扫描包下所有映射器          String mapperPackage = child.getStringAttribute("name");          configuration.addMappers(mapperPackage);        } else {          String resource = child.getStringAttribute("resource");          String url = child.getStringAttribute("url");          String mapperClass = child.getStringAttribute("class");          if (resource != null && url == null && mapperClass == null) {            //10.1使用类路径            ErrorContext.instance().resource(resource);            InputStream inputStream = Resources.getResourceAsStream(resource);            //映射器比较复杂,调用XMLMapperBuilder            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());            mapperParser.parse();          } else if (resource == null && url != null && mapperClass == null) {            //10.2使用绝对url路径            ErrorContext.instance().resource(url);            InputStream inputStream = Resources.getUrlAsStream(url);            //映射器比较复杂,调用XMLMapperBuilder            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());            mapperParser.parse();          } else if (resource == null && url == null && mapperClass != null) {            //10.3使用java类名            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.");          }        }      }    }  }

直接看package的解析,其实这种形式解析过程也类似,关键都是调用了configuration.addMapper这个方法,所以直接看这个方法,这个方法在Configuration类的mapperRegistry中

//看一下如何添加一个映射  public <T> void addMapper(Class<T> type) {    //mapper必须是接口!才会添加    if (type.isInterface()) {      if (hasMapper(type)) {        //如果重复添加了,报错        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");      }      boolean loadCompleted = false;      try {        knownMappers.put(type, new MapperProxyFactory<T>(type));        // It's important that the type is added before the parser is run        // otherwise the binding may automatically be attempted by the        // mapper parser. If the type is already known, it won't try.        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);        parser.parse();        loadCompleted = true;      } finally {        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?        if (!loadCompleted) {          knownMappers.remove(type);        }      }    }  }

重点就是new MapperProxyFactory(type),这里将存入一个Mapper的代理工厂类。

再往下看,创建了一个MapperAnnotationBuilder,然后再看parse方法。

public void parse() {    String resource = type.toString();    if (!configuration.isResourceLoaded(resource)) {      loadXmlResource();      configuration.addLoadedResource(resource);      assistant.setCurrentNamespace(type.getName());      parseCache();      parseCacheRef();      Method[] methods = type.getMethods();      for (Method method : methods) {        try {          // issue #237          if (!method.isBridge()) {            parseStatement(method);          }        } catch (IncompleteElementException e) {          configuration.addIncompleteMethod(new MethodResolver(this, method));        }      }    }    parsePendingMethods();  }

首先configuration.isResourceLoaded会判断是否加载了mapper的xml,很显然,如果用package方式的,走到这一步,就只是找到了接口,将代理工厂存入map中,并没有去加载xml,所以会loadXmlResource()

private void loadXmlResource() {    // Spring may not know the real resource name so we check a flag    // to prevent loading again a resource twice    // this flag is set at XMLMapperBuilder#bindMapperForNamespace    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {      String xmlResource = type.getName().replace('.', '/') + ".xml";      InputStream inputStream = null;      try {        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);      } catch (IOException e) {        // ignore, resource is not required      }      if (inputStream != null) {        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());        xmlParser.parse();      }    }  }

这里将接口全面的.替换成了/,所以假如接口是a.test,那xml就一定得是a/test.xml,然后会新建一个XMLMapperBuilder,这里可以回去mapperElement方法中看的解析,也是通过XMLMapperBuilder,所以这些解析方式其实大同小异,然后再看XMLMapperBuilder的parse方法

//解析  public void parse() {    //如果没有加载过再加载,防止重复加载    if (!configuration.isResourceLoaded(resource)) {      //配置mapper      configurationElement(parser.evalNode("/mapper"));      //标记一下,已经加载过了      configuration.addLoadedResource(resource);      //绑定映射器到namespace      bindMapperForNamespace();    }    //还有没解析完的东东这里接着解析?      parsePendingResultMaps();    parsePendingChacheRefs();    parsePendingStatements();  }

先看configurationElement方法

//配置mapper元素//<mapper namespace="org.mybatis.example.BlogMapper">//  <select id="selectBlog" parameterType="int" resultType="Blog">//    select * from Blog where id = #{id}//  </select>//</mapper>  private void configurationElement(XNode context) {    try {      //1.配置namespace      String namespace = context.getStringAttribute("namespace");      if (namespace.equals("")) {        throw new BuilderException("Mapper's namespace cannot be empty");      }      builderAssistant.setCurrentNamespace(namespace);      //2.配置cache-ref      cacheRefElement(context.evalNode("cache-ref"));      //3.配置cache      cacheElement(context.evalNode("cache"));      //4.配置parameterMap(已经废弃,老式风格的参数映射)      parameterMapElement(context.evalNodes("/mapper/parameterMap"));      //5.配置resultMap(高级功能)      resultMapElements(context.evalNodes("/mapper/resultMap"));      //6.配置sql(定义可重用的 SQL 代码段)      sqlElement(context.evalNodes("/mapper/sql"));      //7.配置select|insert|update|delete TODO      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));    } catch (Exception e) {      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);    }  }

首先看cache-ref的解析

//2.配置cache-ref,在这样的 情况下你可以使用 cache-ref 元素来引用另外一个缓存。 //<cache-ref namespace="com.someone.application.data.SomeMapper"/>  private void cacheRefElement(XNode context) {    if (context != null) {      //增加cache-ref      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));      try {        cacheRefResolver.resolveCacheRef();      } catch (IncompleteElementException e) {        configuration.addIncompleteCacheRef(cacheRefResolver);      }    }  }

先往configuration中存放cache-ref的map中添加当前解析的cache-ref的namespace,然后创建一个cache-ref解析器解析,

public Cache resolveCacheRef() {      //反调MapperBuilderAssistant解析    return assistant.useCacheRef(cacheRefNamespace);  }
public Cache useCacheRef(String namespace) {    if (namespace == null) {      throw new BuilderException("cache-ref element requires a namespace attribute.");    }    try {      unresolvedCacheRef = true;      Cache cache = configuration.getCache(namespace);      if (cache == null) {        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");      }      currentCache = cache;      unresolvedCacheRef = false;      return cache;    } catch (IllegalArgumentException e) {      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);    }  }

这里调用的是MapperBuilderAssistant这个助手的方法,而在这个助手类中,逻辑是这样的,去configuration的cache的map中获取cache,如果cache已经创建了,就返回。如果还没有创建,那么就抛出一个IncompleteElementException异常,异常被外部捕获,将当前cache-ref的解析器放入一个用来存放未完成cache-ref解析的列表中。

然后接下来解析cache,

//3.配置cache  cacheElement(context.evalNode("cache"));

方法中依旧是调用助手类的方法

//调用builderAssistant.useNewCache      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);

接下来的几个resultmap,sql等解析的过程基本类似。

当前解析完成之后,再往下看,会去解析之前未完全解析的各类对象,进入第一个方法

private void parsePendingResultMaps() {    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();    synchronized (incompleteResultMaps) {      Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();      while (iter.hasNext()) {        try {          iter.next().resolve();          iter.remove();        } catch (IncompleteElementException e) {          // ResultMap is still missing a resource...        }      }    }  }

之前存入map中的未完全解析的解析器取出循环调用之前同样的方法,而在此刻,之前需要等待创建的对象现在都已经创建完成,所以可以完成创建(我想了一下,这里面好像没有a需要b,b需要c的这种,被依赖的好像都是没有需要依赖的)。

再回到MapperAnnotationBuilder中,接下去是方法的注解解析,和之前xml的区别就是解析的方法,跳过。

最终SqlSessionFactoryBuilder会执行到这行代码,生成一个DefaultSqlSessionFactory

public SqlSessionFactory build(Configuration config) {    return new DefaultSqlSessionFactory(config);  }

到此解析结束。

(0)

相关推荐

  • MyBatis插件原理分析,看完感觉自己better了

    回复"面试"获取全套面试资料 本文主要内容: 大多数框架都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外. 在Mybatis中最出名的就是PageHelper ...

  • SSM MyBatis二级缓存和第三方Ehchache配置

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 二级缓存 Mybatis中,默认二级缓存是开启的.可以关闭. 一级缓存开启的.可以被卸载吗?不可以的.一级缓存 ...

  • 答了Mybatis这个问题后,面试官叫我回去等通知……

    背景 前段时间在我的技术群里,大家讨论起了为什么UserMapper.java是个接口,没有具体实现类,而我们可以直接调用其方法? 关于这个问题,我之前面试过一些人,很多人是这么回答的: 1.我领导叫 ...

  • Mybatis的CRUD

    Mybatis的CRUD

  • temp1

    补充:多对多关联关系 1.如果不使用中间表 在某一个表中,使用一个字段保存多个"外键"值,这将导致无法使用SQL语句进行关联查询. 2.使用中间表 这样就可以使用SQL进行关联查询 ...

  • mybatis源码分析(二) 执行过程

    这边博客衔接上一篇mybatis的xml解析的博客,在xml解析完成之后,首先会解析成一个Configuration对象,然后创建一个DefaultSqlSessionFactory的session工 ...

  • Spring源码分析之AOP从解析到调用

    正文: 在上一篇,我们对IOC核心部分流程已经分析完毕,相信小伙伴们有所收获,从这一篇开始,我们将会踏上新的旅程,即Spring的另一核心:AOP! 首先,为了让大家能更有效的理解AOP,先带大家过一 ...

  • 《一本小小的MyBatis源码分析书》.pdf

    回复"面试"获取全套面试资料 什么是MyBatis? MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作. MyBatis有什么优点? 与JDBC相比,减少了 ...

  • 精尽Spring MVC源码分析 - 一个请求的旅行过程

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...

  • 怒肝一夜 | Mybatis源码深度解析

    回复"面试"获取全套面试资料 本文:12006字,阅读时长:10分15秒 前面已经发过Mybatis源码解析的文章了,本文是对前面文章进行精简以及部分调整优化,总结出来的一篇万字M ...

  • 设计模式(一)——Java单例模式(代码+源码分析)

    设计模式(一)——Java单例模式(代码+源码分析)

  • 设计模式(十五)——命令模式(Spring框架的JdbcTemplate源码分析)

    设计模式(十五)——命令模式(Spring框架的JdbcTemplate源码分析)

  • Qt update刷新之源码分析(一)

    在做GUI开发时,要让控件刷新,会调用update函数:那么在调用了update函数后,Qt究竟基于什么原理.执行了什么代码使得屏幕上有变化?本文就带大家来探究探究其内部源码. Qt手册中关于QWid ...

  • 【老孟Flutter】源码分析系列之InheritedWidget

    老孟导读:这是2021年源码系列的第一篇文章,其实源码系列的文章不是特别受欢迎,一个原因是原理性的知识非常枯燥,我自己看源码的时候特别有感触,二是想把源码分析讲的通俗易懂非常困难,自己明白 和 让别人 ...