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

    到此解析结束。

    关注公众号:java宝典
    a

  • 相关阅读:
    phpstudy apache 服务无法启动
    Nginx+keepalived实现负载均衡高可用配置
    Linux系统下zookeeper客户端命令使用
    JVM探究之 —— 类文件结构(脑图)
    JVM探究之 —— 类加载器-双亲委派模型
    Centos7 配置静态IP并使用xshell远程连接
    JVM探究之 —— 类加载过程
    JVM探究之 —— 垃圾回收(二)
    避免git clone和push时每次都需要输入用户名和密码
    jsch配置sftp服务器ssh免密登录
  • 原文地址:https://www.cnblogs.com/java-bible/p/14064088.html
Copyright © 2020-2023  润新知