mybatis源码配置文件解析

素颜马尾好姑娘i 2023-01-01 01:46 381阅读 0赞

mybatis源码配置文件解析


一、解析properties标签

mybatis作为日常开发的常用ORM框架,在开发中起着很重要的作用,了解其源码对日常的开发有很大的帮助。源码版本为:3-3.4.x,可自行到github进行下载。

从这篇文章开始逐一分析mybatis的核心配置文件(mybatis-config.xml),今天先来看properties标签的解析过程。

概述

在单独使用mybatis的时候,mybatis的核心配置文件(mybatis-config.xml)就显的特别重要,是整个mybatis运行的基础,只有把配置文件中的各个标签正确解析后才可以正确使用mybatis,下面看properties标签的配置,properties标签的作用就是加载properties文件或者property标签,下面看其具体配置,实例如下

  1. <properties resource="org/mybatis/example/config.properties">
  2. <property name="username" value="tuser"/>
  3. <property name="password" value="ABCD1234"/>
  4. </properties>

上面是配置的properties标签的配置,在标签中配置了resource属性和property子标签。下面看具体的解析流程,这里分析properties标签的解析过程,启动流程暂不说,直接看解析的代码。

详述

上面,看到了properties标签的配置,下面看其解析方法,这里只粘贴部分代码,下面是parseConfiguration方法的代码,

  1. private void parseConfiguration(XNode root) {
  2. try {
  3. //issue #117 read properties first
  4. //解析properties标签
  5. propertiesElement(root.evalNode("properties"));
  6. //解析settings标签
  7. Properties settings = settingsAsProperties(root.evalNode("settings"));
  8. loadCustomVfs(settings);
  9. //解析别名标签,例<typeAlias alias="user" type="cn.com.bean.User"/>
  10. typeAliasesElement(root.evalNode("typeAliases"));
  11. //解析插件标签
  12. pluginElement(root.evalNode("plugins"));
  13. //解析objectFactory标签,此标签的作用是mybatis每次创建结果对象的新实例时都会使用ObjectFactory,如果不设置
  14. //则默认使用DefaultObjectFactory来创建,设置之后使用设置的
  15. objectFactoryElement(root.evalNode("objectFactory"));
  16. //解析objectWrapperFactory标签
  17. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  18. //解析reflectorFactory标签
  19. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  20. settingsElement(settings);
  21. // read it after objectFactory and objectWrapperFactory issue #631
  22. //解析environments标签
  23. environmentsElement(root.evalNode("environments"));
  24. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  25. typeHandlerElement(root.evalNode("typeHandlers"));
  26. //解析<mappers>标签
  27. mapperElement(root.evalNode("mappers"));
  28. } catch (Exception e) {
  29. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  30. }
  31. }

从上面的代码中可以找到下面的代码,即为解析的代码,

  1. propertiesElement(root.evalNode("properties"));

这个方法就是解析properties标签,下面看具体的解析过程。

1、解析子标签和属性

  1. /**
  2. * 解析mybatis-config.xml文件中的properties标签
  3. *<properties resource="org/mybatis/example/config.properties">
  4. *<property name="username" value="dev_user"/>
  5. *<property name="password" value="F2Fa3!33TYyg"/>
  6. *</properties>
  7. *解析步骤:
  8. *1、解析配置的property标签,放到defaults中;
  9. *2、解析resource或url属性,放到defaults中;
  10. *3、获取configuration中的variables变量值,放到defaults中
  11. * @param context
  12. * @throws Exception
  13. */
  14. private void propertiesElement(XNode context) throws Exception {
  15. if (context != null) {
  16. //1、读取properties标签中的property标签<property name="" value=""/>
  17. Properties defaults = context.getChildrenAsProperties();
  18. //2、读取properties标签中的resource、url属性
  19. String resource = context.getStringAttribute("resource");
  20. String url = context.getStringAttribute("url");
  21. //resource和url属性不能同时出现在properties标签中
  22. if (resource != null && url != null) {
  23. throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
  24. }
  25. //如果resource不为空,则解析为properties,放到defaults中,由于defaults是key-value结构,所以会覆盖相同key的值
  26. if (resource != null) {
  27. defaults.putAll(Resources.getResourceAsProperties(resource));
  28. } else if (url != null) {//如果url不为空,则解析为properties,放到defaults中,由于defaults是key-value结构,所以会覆盖相同key的值
  29. defaults.putAll(Resources.getUrlAsProperties(url));
  30. }
  31. //3、获得configuration中的variables变量的值,此变量可以通过SqlSessionFactoryBuilder.build()传入properties属性值
  32. Properties vars = configuration.getVariables();
  33. //如果调用build的时候传入了properties属性,放到defaults中
  34. if (vars != null) {
  35. defaults.putAll(vars);
  36. }
  37. //放到parser和configuration对象中
  38. parser.setVariables(defaults);
  39. configuration.setVariables(defaults);
  40. }
  41. }

从上面的解析过程可以看到,首先解析properties标签的子标签,也就是property标签,通过下面的方法获得,

  1. //1、读取properties标签中的property标签<property name="" value=""/>
  2. Properties defaults = context.getChildrenAsProperties();

解析property标签,并放到Properties对象中。那么是如何放到Properties对象中的那,在getChildrenAsProperties方法中,

  1. public Properties getChildrenAsProperties() {
  2. Properties properties = new Properties();
  3. for (XNode child : getChildren()) {
  4. String name = child.getStringAttribute("name");
  5. String value = child.getStringAttribute("value");
  6. if (name != null && value != null) {
  7. properties.setProperty(name, value);
  8. }
  9. }
  10. return properties;
  11. }

可以看出是循环property标签,获得其name和value属性,并放入properties对象中。

接着解析properties的resource和url属性,如下

  1. //2、读取properties标签中的resource、url属性
  2. String resource = context.getStringAttribute("resource");
  3. String url = context.getStringAttribute("url");

分别获得resource和url属性,这里这两个属性都是一个路径。

2、处理属性

下面看这个判断,

  1. //resource和url属性不能同时出现在properties标签中
  2. if (resource != null && url != null) {
  3. throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
  4. }

这个判断表明在properties标签中,resource和url属性不能同时出现。

2.1、处理resource和url属性

下面看resource和url属性的处理,这里resource和url两个属性都是代表的一个路径,所以这里肯定是需要读取相应路径下的文件。

  1. //如果resource不为空,则解析为properties,放到defaults中,由于defaults是key-value结构,所以会覆盖相同key的值
  2. if (resource != null) {
  3. defaults.putAll(Resources.getResourceAsProperties(resource));
  4. } else if (url != null) {//如果url不为空,则解析为properties,放到defaults中,由于defaults是key-value结构,所以会覆盖相同key的值
  5. defaults.putAll(Resources.getUrlAsProperties(url));
  6. }

下面看对resource的处理,调用的Resources.getResourceAsProperties(resource))方法,对resource进行处理,

  1. public static Properties getResourceAsProperties(String resource) throws IOException {
  2. Properties props = new Properties();
  3. InputStream in = getResourceAsStream(resource);
  4. props.load(in);
  5. in.close();
  6. return props;
  7. }

从上面的代码可以看出是要转化为InputStream,最后放到Properties对象中,这里加载文件的详细过程,后面再详细分析。

下面看对url的处理,调用Resources.getUrlAsProperties(url)方法,对url进行处理,

  1. public static Properties getUrlAsProperties(String urlString) throws IOException {
  2. Properties props = new Properties();
  3. InputStream in = getUrlAsStream(urlString);
  4. props.load(in);
  5. in.close();
  6. return props;
  7. }

上面的代码依然是把url代表的文件处理成Properties对象。

2.3、处理已添加的Properties

在上面处理完property子标签、resource和url属性后,还进行了下面的处理,即从configuration中获得properties,

  1. //3、获得configuration中的variables变量的值,此变量可以通过SqlSessionFactoryBuilder.build()传入properties属性值
  2. Properties vars = configuration.getVariables();
  3. //如果调用build的时候传入了properties属性,放到defaults中
  4. if (vars != null) {
  5. defaults.putAll(vars);
  6. }

如果configuration中已经存在properties信息,则取出来,放到defaults中。

2.4、放入configuration对象中

经过上面的处理,最后把所有的properties信息放到configuration中,

  1. //放到parser和configuration对象中
  2. parser.setVariables(defaults);
  3. configuration.setVariables(defaults);

把defaults放到了configuration的variables属性中,代表的是整个mybatis环境中所有的properties信息。这个信息可以在mybatis的配置文件中使用${key}使用,比如,${username},则会从configuration的variables中寻找key为username的属性值,并完成自动属性值替换。

小结

上面分析了properties标签的解析过程,先解析property标签,然后是resource、url属性,最后是生成SqlSessionFactory的使用调用SqlSessionFactoryBuilder的build方法,传入的properties,从上面的解析过程,可以知道如果存在重复的键,那么最先解析的会被后面解析的覆盖掉,也就是解析过程是:property子标签—>resource—>url—>开发者设置的,那么覆盖过程为:开发者设置的—>url—>resource—>property子标签,优先级最高的为开发者自己设置的properties属性。


二、解析settings标签

概述

在mybatis的核心配置文件(mybatis-config.xml)文件中,有关于settings标签的配置,如下

  1. <settings>
  2. <!-- 设置日志输出为LOG4J -->
  3. <setting name="logImpl" value="STDOUT_LOGGING" />
  4. <!--将以下画线方式命名的数据库列映射到 Java 对象的驼峰式命名属性中-->
  5. <setting name= "mapUnderscoreToCamelCase" value="true" />
  6. </settings>

上面只简单的给出settings标签的配置,settings标签配置在标签中,是标签的子标签。在settings标签中可以配置setting子标签,上面是我的一个配置,是以name-value键值对的放式进行配置。这里有个问题setting标签中的name怎么配置,共有多少配置?

详述

上面,看到了settings标签的配置方式,下面看其解析过程,在XMLConfigBuilder类中的parseConfiguration方法中有关于该标签的解析,

  1. private void parseConfiguration(XNode root) {
  2. try {
  3. //issue #117 read properties first
  4. //解析properties标签
  5. propertiesElement(root.evalNode("properties"));
  6. //解析settings标签,1、把<setting>标签解析为Properties对象
  7. Properties settings = settingsAsProperties(root.evalNode("settings"));
  8. /*2、对<settings>标签中的<setting>标签中的内容进行解析,这里解析的是<setting name="vfsImpl" value=",">
  9. * VFS是mybatis中用来表示虚拟文件系统的一个抽象类,用来查找指定路径下的资源。上面的key为vfsImpl的value可以是VFS的具体实现,必须
  10. * 是权限类名,多个使用逗号隔开,如果存在则设置到configuration中的vfsImpl属性中,如果存在多个,则设置到configuration中的仅是最后一个
  11. * */
  12. loadCustomVfs(settings);
  13. //解析别名标签,例<typeAlias alias="user" type="cn.com.bean.User"/>
  14. typeAliasesElement(root.evalNode("typeAliases"));
  15. //解析插件标签
  16. pluginElement(root.evalNode("plugins"));
  17. //解析objectFactory标签,此标签的作用是mybatis每次创建结果对象的新实例时都会使用ObjectFactory,如果不设置
  18. //则默认使用DefaultObjectFactory来创建,设置之后使用设置的
  19. objectFactoryElement(root.evalNode("objectFactory"));
  20. //解析objectWrapperFactory标签
  21. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  22. //解析reflectorFactory标签
  23. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  24. settingsElement(settings);
  25. // read it after objectFactory and objectWrapperFactory issue #631
  26. //解析environments标签
  27. environmentsElement(root.evalNode("environments"));
  28. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  29. typeHandlerElement(root.evalNode("typeHandlers"));
  30. //解析<mappers>标签
  31. mapperElement(root.evalNode("mappers"));
  32. } catch (Exception e) {
  33. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  34. }
  35. }

上面便是parseConfiguration方法,在此方法中下面的方法对settings进行了解析,

  1. //解析settings标签,1、把<setting>标签解析为Properties对象
  2. Properties settings = settingsAsProperties(root.evalNode("settings"));

调用settingsAsProperties方法,从方法名中可以看出要把settings标签中的内容解析到Proerties对象中,因为settings标签中是name-value的配置,刚好解析到Properties中以键值对的形式存储。下面是settingsAsProperties方法,

  1. private Properties settingsAsProperties(XNode context) {
  2. if (context == null) {
  3. return new Properties();
  4. }
  5. //把<setting name="" value="">标签解析为Properties对象
  6. Properties props = context.getChildrenAsProperties();
  7. // Check that all settings are known to the configuration class
  8. MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
  9. //如果获取的配置的<setting name="" value="">信息,name不在metaConfig中,则会抛出异常
  10. //这里metaConfig中的信息是从Configuration类中解析出来的,包含set方法的属性
  11. //所以在配置<setting>标签的时候,其name值可以参考configuration类中的属性,配置为小写
  12. for (Object key : props.keySet()) {
  13. //从metaConfig的relector中的setMethods中判断是否存在该属性,setMethods中存储的是可写的属性,
  14. //所以这里要到setMethods中进行判断
  15. if (!metaConfig.hasSetter(String.valueOf(key))) {
  16. throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
  17. }
  18. }
  19. return props;
  20. }

1、解析子标签

解析子标签也就是settings标签中的setting标签,使用下面的方法进行解析,

  1. //把<setting name="" value="">标签解析为Properties对象
  2. Properties props = context.getChildrenAsProperties();
  3. 调用了getChildrenAsProperties方法,
  4. public Properties getChildrenAsProperties() {
  5. Properties properties = new Properties();
  6. for (XNode child : getChildren()) {
  7. String name = child.getStringAttribute("name");
  8. String value = child.getStringAttribute("value");
  9. if (name != null && value != null) {
  10. properties.setProperty(name, value);
  11. }
  12. }
  13. return properties;
  14. }

该方法就是解析标签中的标签,取出标签中的name和value属性,存储到Properties对象中且返回。

我们再看上面的settingsAsProperties方法,调用上述getChildrenAsProperties方法获得Properties对象后又进行了其他操作。

2、校验setting标签中的name值是否存在

2.1、获得setting标签中的所有name值

在本文开篇提到一个问题,setting标签中的name值怎么配置,答案是可以参考mybatis的官方文档,在官方文档中有详细的解释,再有就是分析源码,继续往下看。

在settingsAsProperties方法中看下面一行代码,

  1. // Check that all settings are known to the configuration class
  2. MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);

上面这行代码就解析了setting标签中的name可以配置的所有值。再看代码上的注释,是不是豁然开朗。该方法有两个参数,一个是Configuration.class,一个是localReflectorFactory,看localReflectorFactory,

  1. private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

使用了DefaultReflectorFactory,看其默认构造方法

dcd28c917676a27746748b4dc12719c2.png

默认构造方法仅初始化了classCacheEnabled和relectorMap两个属性。后过来继续看MetaClass.forClass方法,

  1. public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
  2. return new MetaClass(type, reflectorFactory);
  3. }

该方法返回的是一个MetaClass的对象,

  1. private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
  2. this.reflectorFactory = reflectorFactory;
  3. this.reflector = reflectorFactory.findForClass(type);
  4. }

重点看reflectorFactory.findForClass方法,这里reflectorFactory是DefaultReflectorFactory的一个实例。下面是DefaultReflectorFactory的findForClass方法,

  1. @Override
  2. public Reflector findForClass(Class<?> type) {
  3. if (classCacheEnabled) {
  4. // synchronized (type) removed see issue #461
  5. Reflector cached = reflectorMap.get(type);
  6. if (cached == null) {
  7. cached = new Reflector(type);
  8. reflectorMap.put(type, cached);
  9. }
  10. return cached;
  11. } else {
  12. return new Reflector(type);
  13. }
  14. }

上面方法中,重点看new Reflector(type)这句方法,

  1. public Reflector(Class<?> clazz) {
  2. type = clazz;
  3. //解析默认的构造方法,及无参构造方法
  4. addDefaultConstructor(clazz);
  5. //解析clazz中的get方法,这里的clazz指的是Configuration.class
  6. addGetMethods(clazz);
  7. //解析clazz中的set方法,这里的clazz指的是Configuration.class
  8. addSetMethods(clazz);
  9. addFields(clazz);
  10. readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
  11. writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
  12. for (String propName : readablePropertyNames) {
  13. caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
  14. }
  15. for (String propName : writeablePropertyNames) {
  16. caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
  17. }
  18. }

此方法完成的功能是解析clazz(包含其父类)的构造方法、getXX方法、setXX方法、字段,通过一个类的Class对象获取。

addDefaultConstructor(clazz)如下,

  1. private void addDefaultConstructor(Class<?> clazz) {
  2. //获得该类的声明的构造方法
  3. Constructor<?>[] consts = clazz.getDeclaredConstructors();
  4. //对构造方法进行循环
  5. for (Constructor<?> constructor : consts) {
  6. //判断构造方法的参数是否为0,为0代表为默认的无参构造方法
  7. if (constructor.getParameterTypes().length == 0) {
  8. //如果是私有的(修饰符为private),这里需要设置可见。
  9. if (canAccessPrivateMethods()) {
  10. try {
  11. constructor.setAccessible(true);
  12. } catch (Exception e) {
  13. // Ignored. This is only a final precaution, nothing we can do.
  14. }
  15. }
  16. if (constructor.isAccessible()) {
  17. this.defaultConstructor = constructor;
  18. }
  19. }
  20. }
  21. }

上面方法获得传入的Class对象所以构造方法,把默认的无参构造方法赋给defaultConstructor。

addGetMethods(clazz)如下,

  1. private void addGetMethods(Class<?> cls) {
  2. Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
  3. //使用反射的放上获得cls的所有方法
  4. Method[] methods = getClassMethods(cls);
  5. //把所有的方法放入conflictingGetters中,key为属性名,value为List<Method>
  6. for (Method method : methods) {
  7. //方法的参数大于0,则结束本次循环,因为这里解析的是get方法,get方法默认不应该有参数
  8. if (method.getParameterTypes().length > 0) {
  9. continue;
  10. }
  11. String name = method.getName();
  12. //如果以get或is开头,且方法名称分别大于3和2,则说明是get方法
  13. if ((name.startsWith("get") && name.length() > 3)
  14. || (name.startsWith("is") && name.length() > 2)) {
  15. //通过方法名转化为属性名,如,getUserName--userName
  16. name = PropertyNamer.methodToProperty(name);
  17. addMethodConflict(conflictingGetters, name, method);
  18. }
  19. }
  20. /**处理一个属性多个get方法的情况,即conflictingGetter方法中一个key对应的value的长度大于1的情况,如下
  21. *key propertyName
  22. *value list<Method> 其长度大于1
  23. */
  24. resolveGetterConflicts(conflictingGetters);
  25. }

获取所有以get和is开头的方法,调用addMethodConflict方法,这里的方法名直译过来是添加冲突的方法,这里冲突怎么理解,我们看addMethodConflict方法,

  1. private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
  2. //根据字段名取方法
  3. List<Method> list = conflictingMethods.get(name);
  4. if (list == null) {
  5. list = new ArrayList<Method>();
  6. conflictingMethods.put(name, list);
  7. }
  8. list.add(method);
  9. }

这里是根据get和is开头的方法获取属性名作为键值,并且使用list作为value进行存储,为什么使用list那,我们看下面的方法

  1. public void getUser(){}
  2. public User getuser(){}
  3. public List<User> getUser(){}
  4. public void getUser(String id){}

上面三个方法都会以user为键进行存储,但是其方法名是一样的,所以这里要存储为list,即存储多个Method对象。

我们知道一个字段的属性的get或set方法,不可能出现上面的情况,所以针对上面的情况需要做处理,这里调用resolveGetterConflicts(conflicttingGetters),

  1. private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
  2. //遍历conflictingGetters
  3. for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
  4. Method winner = null;
  5. String propName = entry.getKey();
  6. //循环value这里value是一个List<Method>类型
  7. for (Method candidate : entry.getValue()) {
  8. if (winner == null) {
  9. winner = candidate;
  10. continue;
  11. }
  12. //获得get方法的返回值类型
  13. Class<?> winnerType = winner.getReturnType();
  14. Class<?> candidateType = candidate.getReturnType();
  15. //如果winnerType和candidateType相等,
  16. if (candidateType.equals(winnerType)) {
  17. if (!boolean.class.equals(candidateType)) {
  18. throw new ReflectionException(
  19. "Illegal overloaded getter method with ambiguous type for property "
  20. + propName + " in class " + winner.getDeclaringClass()
  21. + ". This breaks the JavaBeans specification and can cause unpredictable results.");
  22. } else if (candidate.getName().startsWith("is")) {
  23. winner = candidate;
  24. }
  25. } else if (candidateType.isAssignableFrom(winnerType)) {
  26. // OK getter type is descendant
  27. } else if (winnerType.isAssignableFrom(candidateType)) {
  28. winner = candidate;
  29. } else {
  30. throw new ReflectionException(
  31. "Illegal overloaded getter method with ambiguous type for property "
  32. + propName + " in class " + winner.getDeclaringClass()
  33. + ". This breaks the JavaBeans specification and can cause unpredictable results.");
  34. }
  35. }
  36. addGetMethod(propName, winner);
  37. }
  38. }

上面的方法处理了上面提到的一个属性存在多个get方法的情况,最后调用addGetMethod方法,

  1. private void addGetMethod(String name, Method method) {
  2. if (isValidPropertyName(name)) {
  3. getMethods.put(name, new MethodInvoker(method));
  4. Type returnType = TypeParameterResolver.resolveReturnType(method, type);
  5. getTypes.put(name, typeToClass(returnType));
  6. }
  7. }

上面的方法把信息放到了getMethods和getTyps中,分别存储了get方法和返回值。

上面分析了Reflector中的addGetMethods方法,addSetMethods方法和其处理过程类似,最终把set方法和返回值放到了setMethods和setTypes中。

addFileds(clazz)方法即是处理clazz中的属性,

  1. private void addFields(Class<?> clazz) {
  2. Field[] fields = clazz.getDeclaredFields();
  3. for (Field field : fields) {
  4. if (canAccessPrivateMethods()) {
  5. try {
  6. field.setAccessible(true);
  7. } catch (Exception e) {
  8. // Ignored. This is only a final precaution, nothing we can do.
  9. }
  10. }
  11. if (field.isAccessible()) {
  12. //检查是否存在set方法,如果不存在添加该field
  13. if (!setMethods.containsKey(field.getName())) {
  14. // issue #379 - removed the check for final because JDK 1.5 allows
  15. // modification of final fields through reflection (JSR-133). (JGB)
  16. // pr #16 - final static can only be set by the classloader
  17. int modifiers = field.getModifiers();
  18. if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
  19. addSetField(field);
  20. }
  21. }
  22. //检查是否存在get方法,如果不存在添加该field
  23. if (!getMethods.containsKey(field.getName())) {
  24. addGetField(field);
  25. }
  26. }
  27. }
  28. //添加父类的field
  29. if (clazz.getSuperclass() != null) {
  30. addFields(clazz.getSuperclass());
  31. }
  32. }

获得field之后,判断是否在getMethods和setMethods中,如果不在则进行添加,只看addSetField方法,

  1. private void addSetField(Field field) {
  2. if (isValidPropertyName(field.getName())) {
  3. setMethods.put(field.getName(), new SetFieldInvoker(field));
  4. Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
  5. setTypes.put(field.getName(), typeToClass(fieldType));
  6. }
  7. }

从上面看到如果一个field不存在set方法,则生成一个SetFieldInvoker把该对象放入setMethods,从这里可以看出一个setting配置的name值在configuration中可以没有set方法。同理也可以没有get方法。

上面分析完了settingsAsProperties方法中的下面这行代码,

  1. MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);

把Configuration中的构造方法、get方法、set方法、field放入了metaConfig中的reflector对象中的下列属性

  1. private final String[] readablePropertyNames;
  2. private final String[] writeablePropertyNames;
  3. private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
  4. private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
  5. private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
  6. private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
  7. private Constructor<?> defaultConstructor;

2.2、校验配置的setting标签中的name是否存在

上面分析完了MetaClass.forClass方法,下面看如何对setting标签配置的name进行校验

  1. for (Object key : props.keySet()) {
  2. //从metaConfig的relector中的setMethods中判断是否存在该属性,setMethods中存储的是可写的属性,
  3. //所以这里要到setMethods中进行判断
  4. if (!metaConfig.hasSetter(String.valueOf(key))) {
  5. throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
  6. }
  7. }

遍历从setting标签解析出来的Properties对象,调用metaConfig.hasSetter方法,

  1. public boolean hasSetter(String name) {
  2. PropertyTokenizer prop = new PropertyTokenizer(name);
  3. if (prop.hasNext()) {
  4. if (reflector.hasSetter(prop.getName())) {
  5. MetaClass metaProp = metaClassForProperty(prop.getName());
  6. return metaProp.hasSetter(prop.getChildren());
  7. } else {
  8. return false;
  9. }
  10. } else {
  11. return reflector.hasSetter(prop.getName());
  12. }
  13. }

看hasSetter的定义

  1. public boolean hasSetter(String propertyName) {
  2. return setMethods.keySet().contains(propertyName);
  3. }

可以看到是判断setMethods是否存在该key,也就是已set方法为表标准,只要在setMethods中,便可以在标签的name中配置,具体配置值还需要看其类型。

小结

上面分析了mybatis的核心配置文件中标签的解析及子标签中name属性的配置值是怎么取的。如果要扩展核心文件配置中的setting标签的name属性值,需要在configuration中进行配置,及其他操作。


三、解析typeAliases标签

概述

在mybatis核心配置文件(mybatis-config.xml)中有关typeAliases的配置如下,

  1. <typeAliases>
  2. <package name="cn.com.mybatis.bean"></package>
  3. <typeAlias name="user" type="cn.com.mybatis.bean.User"></typeAlias>
  4. </typeAliases>

上面给出了两种配置typeAlias的放式,一种是配置package标签,一种是typeAlias表。

我上面的配置是有问题的,在测试的时候一直报下面的错误,

21f8f235fc200ddd8f2f9848aec170a7.png

上面的问题困扰了笔者好久,没找到原因,因为解析typeAliases标签的源码中找不到任何的原因,最后排查日志,原来是在加载核心配置文件的时候要把配置和mybatis的dtd文件进行验证,这里是验证出错了,具体的错误是typeAlias标签必须在package标签的前边,也就是标签是有顺序的。把配置改为下面的顺序,程序正常,

  1. <typeAliases>
  2. <typeAlias alias="user" type="cn.com.mybatis.bean.User"></typeAlias>
  3. <package name="cn.com.mybatis.bean"/>
  4. </typeAliases>

1、配置标签

标签配置的是一个包名,mybatis会扫描该包下的所有类,并注册一个别名,这里在标签中无法为某个类指定一个自定义的别名,mybatis提供了另外一种方式可以使用自定义的别名,即@Alias注解,在类上标记该注解,如下,

  1. package cn.com.mybatis.bean;
  2. import org.apache.ibatis.type.Alias;
  3. //配置别名为myMenu
  4. @Alias(value="myMenu")
  5. public class Menu {
  6. private String menuId;
  7. private String menuName;
  8. private String url;
  9. }

上面为Menu类配置了别名,在扫描该包的时候会使用自定义的别名,不会使用mybatis默认的别名规则(Class.getSimpleName())

2、配置标签

这种配置是单独为某个类配置别名,其中alias属性可以不配置,不配置则使用mybatis默认的别名规则,如下

  1. <typeAlias alias="MyUser" type="cn.com.mybatis.bean.User"></typeAlias>

上面看了typeAlias的两种配置方式,那么何为typeAlias,意思就是给一个类配置一个别名,如这里有一个cn.com.mybatis.bean.User类,可以为其配置别名为MyUser,

那么在配置文件中便可以使用别名代替类的全限类名,目的是简便。这里需要注意的是配置的别名的使用范围仅限于mybatis的配置文件中(包含核心配置文件和Mpper映射文件)

详述

上面,了解了typeAlias的配置及作用,下面看mybatis是如何解析的。

在XMLConfigBuilder类中的parseConfiguration方法,

  1. private void parseConfiguration(XNode root) {
  2. try {
  3. //issue #117 read properties first
  4. //解析properties标签
  5. propertiesElement(root.evalNode("properties"));
  6. //解析settings标签,1、把<setting>标签解析为Properties对象
  7. Properties settings = settingsAsProperties(root.evalNode("settings"));
  8. /*2、对<settings>标签中的<setting>标签中的内容进行解析,这里解析的是<setting name="vfsImpl" value=",">
  9. * VFS是mybatis中用来表示虚拟文件系统的一个抽象类,用来查找指定路径下的资源。上面的key为vfsImpl的value可以是VFS的具体实现,必须
  10. * 是权限类名,多个使用逗号隔开,如果存在则设置到configuration中的vfsImpl属性中,如果存在多个,则设置到configuration中的仅是最后一个
  11. * */
  12. loadCustomVfs(settings);
  13. //解析别名标签,例<typeAlias alias="user" type="cn.com.bean.User"/>
  14. typeAliasesElement(root.evalNode("typeAliases"));
  15. //解析插件标签
  16. pluginElement(root.evalNode("plugins"));
  17. //解析objectFactory标签,此标签的作用是mybatis每次创建结果对象的新实例时都会使用ObjectFactory,如果不设置
  18. //则默认使用DefaultObjectFactory来创建,设置之后使用设置的
  19. objectFactoryElement(root.evalNode("objectFactory"));
  20. //解析objectWrapperFactory标签
  21. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  22. //解析reflectorFactory标签
  23. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  24. settingsElement(settings);
  25. // read it after objectFactory and objectWrapperFactory issue #631
  26. //解析environments标签
  27. environmentsElement(root.evalNode("environments"));
  28. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  29. typeHandlerElement(root.evalNode("typeHandlers"));
  30. //解析<mappers>标签
  31. mapperElement(root.evalNode("mappers"));
  32. } catch (Exception e) {
  33. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  34. }
  35. }

从上面可以看出typeAliasesElement方法,此方法用来解析typeAliases标签及其子标签,

  1. private void typeAliasesElement(XNode parent) {
  2. if (parent != null) {
  3. for (XNode child : parent.getChildren()) {
  4. //1、解析package标签
  5. if ("package".equals(child.getName())) {
  6. String typeAliasPackage = child.getStringAttribute("name");
  7. configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
  8. } else {
  9. //2、解析typeAlias标签
  10. String alias = child.getStringAttribute("alias");
  11. String type = child.getStringAttribute("type");
  12. try {
  13. Class<?> clazz = Resources.classForName(type);
  14. if (alias == null) {
  15. typeAliasRegistry.registerAlias(clazz);
  16. } else {
  17. typeAliasRegistry.registerAlias(alias, clazz);
  18. }
  19. } catch (ClassNotFoundException e) {
  20. throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
  21. }
  22. }
  23. }
  24. }
  25. }

typeAliasesElement方法会分别解析typeAliases标签的package和typeAlias子标签。通过上面的分析知道在配置的时候标签要在标签前边,但这里按照源码的顺序先分析标签的解析。

1、解析标签

下面看typeAliasesElement方法中对package标签的解析,

  1. if ("package".equals(child.getName())) {
  2. String typeAliasPackage = child.getStringAttribute("name");
  3. configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
  4. }

从上面可以看到获取标签的name属性,也就配置的包名,然后调用下面的方法,

  1. configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);

可以看到从Configuration中获得TypeAliasRegistry,然后调用其registerAliases方法,

  1. public void registerAliases(String packageName){
  2. registerAliases(packageName, Object.class);
  3. }

又调用另外一个方法,如下,

  1. /**
  2. *
  3. * 为包下的所有java bean注册别名
  4. * @param packageName
  5. * @param superType
  6. */
  7. public void registerAliases(String packageName, Class<?> superType){
  8. ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  9. //把该包下的所有类进行加载,把其Class对象放到resolverUtil的matches中
  10. resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  11. Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
  12. for(Class<?> type : typeSet){
  13. // Ignore inner classes and interfaces (including package-info.java)
  14. // Skip also inner classes. See issue #6
  15. if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
  16. registerAlias(type);
  17. }
  18. }
  19. }

上面方法的作用是遍历给的的包名,把该包下的所有的类进行加载,并放到resolverUtil中的matches中,这里具体的遍历方法暂且不看。遍历完成后取出resolverUtil中的所有Class对象,只要不是匿名类、接口则执行registerAlias方法,

  1. public void registerAlias(Class<?> type) {
  2. //获得类的简单类名,如cn.com.mybatis.bean.User 则其简单名称为User
  3. String alias = type.getSimpleName();
  4. //判断类上是否存在@Alias注解
  5. Alias aliasAnnotation = type.getAnnotation(Alias.class);
  6. //如果存在@Alias注解,则使用注解上配置的value属性作为别名
  7. if (aliasAnnotation != null) {
  8. alias = aliasAnnotation.value();
  9. }
  10. registerAlias(alias, type);
  11. }

看上面的方法,上面的方法先获得Class的简单类名,

  1. //获得类的简单类名,如cn.com.mybatis.bean.User 则其简单名称为User
  2. String alias = type.getSimpleName();

然后会判断类上是否有@Alias注解,如果有则取其value值作为类的别名,

  1. //判断类上是否存在@Alias注解
  2. Alias aliasAnnotation = type.getAnnotation(Alias.class);
  3. //如果存在@Alias注解,则使用注解上配置的value属性作为别名
  4. if (aliasAnnotation != null) {
  5. alias = aliasAnnotation.value();
  6. }

进行上面的判断,存在@Alias注解,使用其value值作为别名,否则使用类的简单类名(Class.getSimpleName()),然后执行registerAlias方法,

  1. public void registerAlias(String alias, Class<?> value) {
  2. if (alias == null) {
  3. throw new TypeException("The parameter alias cannot be null");
  4. }
  5. // issue #748
  6. String key = alias.toLowerCase(Locale.ENGLISH);
  7. //如果已经注册了改别名则会抛异常
  8. if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
  9. throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
  10. }
  11. TYPE_ALIASES.put(key, value);
  12. }

上面的代码会把别名转化为英文的小写作为存入的key,使用对应的Class存入TYPE_ALIASES中。如果已经注册过该key则会抛出异常,也就是不允许重复注册或者相同的key是无法覆盖的。这里还有一个问题,如果我们配置的是别名中含有大写,那么注册的时候是小写的,在使用的时候是用配置的还是用注册的,例,上面的例子,

  1. package cn.com.mybatis.bean;
  2. import org.apache.ibatis.type.Alias;
  3. //配置别名为myMenu
  4. @Alias(value="myMenu")
  5. public class Menu {
  6. private String menuId;
  7. private String menuName;
  8. private String url;
  9. }

这里配置的是myMenu,注册的确实下面的

19220e4968b70e069ddf441b37ea7f85.png

可以看到注册之后的是mymenu。其实在使用的时候是大小写不敏感的,在匹配的时候会统一转化为小写,这样就可以对应TYPE_ALIASES中已注册的别名。

2、解析标签

上面分析了标签的解析过程,下面看有关标签的解析,

20a464c6857713587f836846629a571e.png

解析标签即是获取alias和type两个属性,可以看到对alias进行了判断,也就说可以不配置alias属性,那么会使用下面的方法处理

  1. public void registerAlias(Class<?> type) {
  2. //获得类的简单类名,如cn.com.mybatis.bean.User 则其简单名称为User
  3. String alias = type.getSimpleName();
  4. //判断类上是否存在@Alias注解
  5. Alias aliasAnnotation = type.getAnnotation(Alias.class);
  6. //如果存在@Alias注解,则使用注解上配置的value属性作为别名
  7. if (aliasAnnotation != null) {
  8. alias = aliasAnnotation.value();
  9. }
  10. registerAlias(alias, type);
  11. }

该方法前面已分析,会判断配置的类是否含有@Alias注解,如果有则使用注解上的value值。这里存在一个问题,如果在标签中配置了alias,在类上也有@Alias注解,且不一样,以哪个为准,通过上面的分析,得出下面的结论,

在使用标签的时候,配置了alias属性,在类上也有@Alias(value=”myAlias2”),已配置的为准(最终别名为myAlias)

下面看registerAlias(alias,type)方法,

  1. public void registerAlias(String alias, Class<?> value) {
  2. if (alias == null) {
  3. throw new TypeException("The parameter alias cannot be null");
  4. }
  5. // issue #748
  6. String key = alias.toLowerCase(Locale.ENGLISH);
  7. //如果已经注册了改别名则会抛异常
  8. if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
  9. throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
  10. }
  11. TYPE_ALIASES.put(key, value);
  12. }

此方法上面分析过,如果存在相同的key会抛异常,最终存入TYPE_ALIASES中。

小结

本章分析了mybatis核心配置文件(mybatis-config.xml)的标签的配置及源码解析。

另在写Mapper映射文件和核心配置文件的时候会使用一些自定义的别名,这些别名是怎么注册的那,在Configuration、TypeAliasRegistry类中进行了注册,如下Configuration,

  1. public Configuration() {
  2. typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
  3. typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
  4. typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
  5. typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
  6. typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
  7. typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
  8. typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
  9. typeAliasRegistry.registerAlias("LRU", LruCache.class);
  10. typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
  11. typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
  12. typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
  13. typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
  14. typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
  15. typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
  16. typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
  17. typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
  18. typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
  19. typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
  20. typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
  21. typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
  22. typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
  23. typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
  24. languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
  25. languageRegistry.register(RawLanguageDriver.class);
  26. }

在TypeAliasRegistry中注册了下面的别名,

  1. //默认的构造方法,初始化系统内置的别名
  2. public TypeAliasRegistry() {
  3. registerAlias("string", String.class);
  4. registerAlias("byte", Byte.class);
  5. registerAlias("long", Long.class);
  6. registerAlias("short", Short.class);
  7. registerAlias("int", Integer.class);
  8. registerAlias("integer", Integer.class);
  9. registerAlias("double", Double.class);
  10. registerAlias("float", Float.class);
  11. registerAlias("boolean", Boolean.class);
  12. registerAlias("byte[]", Byte[].class);
  13. registerAlias("long[]", Long[].class);
  14. registerAlias("short[]", Short[].class);
  15. registerAlias("int[]", Integer[].class);
  16. registerAlias("integer[]", Integer[].class);
  17. registerAlias("double[]", Double[].class);
  18. registerAlias("float[]", Float[].class);
  19. registerAlias("boolean[]", Boolean[].class);
  20. registerAlias("_byte", byte.class);
  21. registerAlias("_long", long.class);
  22. registerAlias("_short", short.class);
  23. registerAlias("_int", int.class);
  24. registerAlias("_integer", int.class);
  25. registerAlias("_double", double.class);
  26. registerAlias("_float", float.class);
  27. registerAlias("_boolean", boolean.class);
  28. registerAlias("_byte[]", byte[].class);
  29. registerAlias("_long[]", long[].class);
  30. registerAlias("_short[]", short[].class);
  31. registerAlias("_int[]", int[].class);
  32. registerAlias("_integer[]", int[].class);
  33. registerAlias("_double[]", double[].class);
  34. registerAlias("_float[]", float[].class);
  35. registerAlias("_boolean[]", boolean[].class);
  36. registerAlias("date", Date.class);
  37. registerAlias("decimal", BigDecimal.class);
  38. registerAlias("bigdecimal", BigDecimal.class);
  39. registerAlias("biginteger", BigInteger.class);
  40. registerAlias("object", Object.class);
  41. registerAlias("date[]", Date[].class);
  42. registerAlias("decimal[]", BigDecimal[].class);
  43. registerAlias("bigdecimal[]", BigDecimal[].class);
  44. registerAlias("biginteger[]", BigInteger[].class);
  45. registerAlias("object[]", Object[].class);
  46. registerAlias("map", Map.class);
  47. registerAlias("hashmap", HashMap.class);
  48. registerAlias("list", List.class);
  49. registerAlias("arraylist", ArrayList.class);
  50. registerAlias("collection", Collection.class);
  51. registerAlias("iterator", Iterator.class);
  52. registerAlias("ResultSet", ResultSet.class);
  53. }

上面两个类注册了系统内置的别名,在核心配置文件和Mapper映射文件中可使用,mybatis会自动映射其注册类型,且大小写不区分。


四、解析plugins标签

概述

在mybatis的核心配置文件(mybatis-config.xml)文件中,有关plugins的配置如下,

  1. <!-- 拦截器 -->
  2. <plugins>
  3. <plugin interceptor="cn.com.mybatis.plugins.MyInterceptor" />
  4. </plugins>

在mybatis的plugins叫做插件,其实也可以理解为拦截器。在plugins标签中配置plugin子标签,plugin子标签可以配置多个,多个子标签是有顺序的。除了上面的配置方式在每个plugin下还可以配置property子标签,

  1. <!-- 拦截器 -->
  2. <plugins>
  3. <plugin interceptor="cn.com.mybatis.plugins.MyInterceptor" />
  4. <plugin interceptor="cn.com.mybatis.plugins.MyInterceptor2">
  5. <property name="name" value="mybatis"/>
  6. <property name="age" value="10"/>
  7. </plugin>
  8. </plugins>

上面的配置在第二个拦截器中新增了两个属性name和age,这两个属性会被解析为Properties对象。

在上面可以看到一个重要的配置是plugin标签中的interceptor属性,该属性配置的是一个拦截器类,对应该类有什么要求那,一个普通的类就可以吗,答案是就是一个普通的类,但必须满足下面的几个条件,才可以被mybatis作为插件使用,

  • 实现mybatis中的Interceptor接口,并重写其中的三个方法;
  • 在类上声明@Intercepts注解,该注解标明该拦截器拦截的方法,一个例子如下,

    @Intercepts({@Signature(type=Executor.class,method=”query”,args= {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})

拦截的是实现了Executor接口的query方法,后面的args配置的该方法的入参。在mybatis中默认拦截以下接口的方法,

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

Executor(执行器)、ParameterHandler(参数处理器)、ResultSetHandler(结果集处理器)、StatementHandler(SQL语法构建器)在mybatis中是四个顶级接口,贯穿了mybatis执行sql的全过程,从这里可以看出mybatis的拦截器的强大。

下面看下我的拦截器MyInterceptor的源码,

  1. package cn.com.mybatis.plugins;
  2. import java.util.Properties;
  3. import org.apache.ibatis.executor.Executor;
  4. import org.apache.ibatis.mapping.MappedStatement;
  5. import org.apache.ibatis.plugin.Interceptor;
  6. import org.apache.ibatis.plugin.Intercepts;
  7. import org.apache.ibatis.plugin.Invocation;
  8. import org.apache.ibatis.plugin.Plugin;
  9. import org.apache.ibatis.plugin.Signature;
  10. import org.apache.ibatis.session.ResultHandler;
  11. import org.apache.ibatis.session.RowBounds;
  12. @Intercepts({@Signature(type=Executor.class,method="query",args= {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
  13. public class MyInterceptor implements Interceptor{
  14. /**
  15. * incation 封装了拦截的类,方法,方法参数
  16. */
  17. @Override
  18. public Object intercept(Invocation invocation) throws Throwable {
  19. // TODO Auto-generated method stub
  20. //执行被拦截的类中的方法
  21. Object o=invocation.proceed();
  22. return o;
  23. }
  24. /**
  25. * 返回一个JDK代理对象
  26. */
  27. @Override
  28. public Object plugin(Object target) {
  29. // TODO Auto-generated method stub
  30. return Plugin.wrap(target, this);
  31. }
  32. /**
  33. * 允许在配置文件中配置一些属性即
  34. * <plugin interceptor="">
  35. * <property name ="" value =""/>
  36. * </plugin>
  37. */
  38. @Override
  39. public void setProperties(Properties properties) {
  40. // TODO Auto-generated method stub
  41. }
  42. }

详述

上面,了解了plugis标签的基本配置及使用,下面看mybatis是如何解析的。

在XMLConfigBuilder类中的parseConfiguration方法

  1. private void parseConfiguration(XNode root) {
  2. try {
  3. //issue #117 read properties first
  4. //解析properties标签
  5. propertiesElement(root.evalNode("properties"));
  6. //解析settings标签,1、把<setting>标签解析为Properties对象
  7. Properties settings = settingsAsProperties(root.evalNode("settings"));
  8. /*2、对<settings>标签中的<setting>标签中的内容进行解析,这里解析的是<setting name="vfsImpl" value=",">
  9. * VFS是mybatis中用来表示虚拟文件系统的一个抽象类,用来查找指定路径下的资源。上面的key为vfsImpl的value可以是VFS的具体实现,必须
  10. * 是权限类名,多个使用逗号隔开,如果存在则设置到configuration中的vfsImpl属性中,如果存在多个,则设置到configuration中的仅是最后一个
  11. * */
  12. loadCustomVfs(settings);
  13. //解析别名标签,例<typeAlias alias="user" type="cn.com.bean.User"/>
  14. typeAliasesElement(root.evalNode("typeAliases"));
  15. //解析插件标签
  16. pluginElement(root.evalNode("plugins"));
  17. //解析objectFactory标签,此标签的作用是mybatis每次创建结果对象的新实例时都会使用ObjectFactory,如果不设置
  18. //则默认使用DefaultObjectFactory来创建,设置之后使用设置的
  19. objectFactoryElement(root.evalNode("objectFactory"));
  20. //解析objectWrapperFactory标签
  21. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  22. //解析reflectorFactory标签
  23. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  24. settingsElement(settings);
  25. // read it after objectFactory and objectWrapperFactory issue #631
  26. //解析environments标签
  27. environmentsElement(root.evalNode("environments"));
  28. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  29. typeHandlerElement(root.evalNode("typeHandlers"));
  30. //解析<mappers>标签
  31. mapperElement(root.evalNode("mappers"));
  32. } catch (Exception e) {
  33. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  34. }
  35. }

从上面的代码中,我们找到下面这行解析plugins标签的代码,

  1. //解析插件标签
  2. pluginElement(root.evalNode("plugins"));

上面折行代码即在解析配置文件中的plugis标签,其定义如下

  1. /**
  2. * 解析<plugin>标签
  3. * @param parent
  4. * @throws Exception
  5. */
  6. private void pluginElement(XNode parent) throws Exception {
  7. if (parent != null) {
  8. for (XNode child : parent.getChildren()) {
  9. //1、获得interceptor属性
  10. String interceptor = child.getStringAttribute("interceptor");
  11. //2、把plugin标签下的property标签的内容解析为Properties对象
  12. Properties properties = child.getChildrenAsProperties();
  13. //3、使用配置的interceptor生成一个实例,配置的interceptor需要实现interceptor接口
  14. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
  15. //4、执行interceptor的setProperties方法,把配置的properties标签中的值设置到setProperties中
  16. interceptorInstance.setProperties(properties);
  17. //5、添加interceptor
  18. configuration.addInterceptor(interceptorInstance);
  19. }
  20. }
  21. }

1、解析标签

从上面的代码中可以看到在循环plugin标签,取得interceptor配置的类的全限类名,然后获得plugin标签下的property标签的内容,调用getChildrenAsProperties方法,

  1. public Properties getChildrenAsProperties() {
  2. Properties properties = new Properties();
  3. for (XNode child : getChildren()) {
  4. String name = child.getStringAttribute("name");
  5. String value = child.getStringAttribute("value");
  6. if (name != null && value != null) {
  7. properties.setProperty(name, value);
  8. }
  9. }
  10. return properties;
  11. }

解析完property标签后,下面会生成interceptor实例。

2、生成interceptor实例

上面解析完plugin标签的内容后会生成interceptor实例,且调用其setProperty方法,

  1. //使用配置的interceptor生成一个实例,配置的interceptor需要实现interceptor接口
  2. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
  3. //执行interceptor的setProperties方法,把配置的properties标签中的值设置到setProperties中
  4. interceptorInstance.setProperties(properties);

从上面的代码可以看到把配置的interceptor全限类名解析为一个Class对象,然后调用其默认的构造方法获得一个实例,然后调用了其setProperties方法,即把在标签中配置的property属性值注入到interceptor实例中,在我的例子中即调用下面的方法,

  1. /**
  2. * 允许在配置文件中配置一些属性即
  3. * <plugin interceptor="">
  4. * <property name ="" value =""/>
  5. * </plugin>
  6. */
  7. @Override
  8. public void setProperties(Properties properties) {
  9. // TODO Auto-generated method stub
  10. this.properties=properties;
  11. }

这样就可以把标签中的内容转化到interceptor实例中的属性。

3、添加到configuration中

经过上面的两个步骤的分析,拦截器已经从配置文件中的配置转化为了一个实例,下面就是要放入核心配置类configuration中,

  1. configuration.addInterceptor(interceptorInstance);

看addInterceptor方法,该方法是configuration类中的方法,

  1. public void addInterceptor(Interceptor interceptor) {
  2. interceptorChain.addInterceptor(interceptor);
  3. }

调用了interceptorChain的addInterceptor方法,interceptorChain使用下面的方式进行初始化,

  1. //拦截器链
  2. protected final InterceptorChain interceptorChain = new InterceptorChain();

其addInterceptor方法如下,

436b2f503e54a5a136d6cd87ee86cf49.png

可以看到最终放到了interceptors中,这是一个ArrayList。

最终完成了解析plugins标签并把拦截器实例放到configuration中的过程。

小结

本章分析了mybatis中plugins标签的解析过程,该标签的解析过程相对简单,重点是拦截器背后的原理,是如何起到拦截作用,待下一章分析。


五、解析mappers标签

概述

在mybatis的核心配置文件(mybatis-config.xml)中,有关mappers的配置如下,

  1. <mappers>
  2. <!-- <mapper resource="cn/com/mybatis/dao/UserMapper.xml"/>
  3. <mapper resource="cn/com/mybatis/dao/MenuMapper.xml"/> -->
  4. <!--第二种做法 -->
  5. <package name="cn.com.mybatis.dao" />
  6. </mappers>

从上面的配置文件,可以看到配置mappers文件有两种方式,一种是配置mapper标签,另一种是配置package标签。从配置的内容上来看,其配置的方式也是存在差别,配置mapper标签配置的是一个xml文件,该文件中存在相关的sql语句;配置package标签配置的是一个包的权限路径(在spring和mybatis结合的时候使用了此种方式),该包表示的是mapper的接口文件。

最终上面的两种方式都会被解析到mybatis的configuration类中,供用户使用。如果存在重复配置mybatis会如何处理,下面在分析过程中会解答该问题。

详述

上面了解了标签的使用方式,下面看mybatis是如何解析该标签的。

在XMLConfigBuilder类中的parseConfiguration方法

  1. private void parseConfiguration(XNode root) {
  2. try {
  3. //issue #117 read properties first
  4. //解析properties标签
  5. propertiesElement(root.evalNode("properties"));
  6. //解析settings标签,1、把<setting>标签解析为Properties对象
  7. Properties settings = settingsAsProperties(root.evalNode("settings"));
  8. /*2、对<settings>标签中的<setting>标签中的内容进行解析,这里解析的是<setting name="vfsImpl" value=",">
  9. * VFS是mybatis中用来表示虚拟文件系统的一个抽象类,用来查找指定路径下的资源。上面的key为vfsImpl的value可以是VFS的具体实现,必须
  10. * 是权限类名,多个使用逗号隔开,如果存在则设置到configuration中的vfsImpl属性中,如果存在多个,则设置到configuration中的仅是最后一个
  11. * */
  12. loadCustomVfs(settings);
  13. //解析别名标签,例<typeAlias alias="user" type="cn.com.bean.User"/>
  14. typeAliasesElement(root.evalNode("typeAliases"));
  15. //解析插件标签
  16. pluginElement(root.evalNode("plugins"));
  17. //解析objectFactory标签,此标签的作用是mybatis每次创建结果对象的新实例时都会使用ObjectFactory,如果不设置
  18. //则默认使用DefaultObjectFactory来创建,设置之后使用设置的
  19. objectFactoryElement(root.evalNode("objectFactory"));
  20. //解析objectWrapperFactory标签
  21. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  22. //解析reflectorFactory标签
  23. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  24. settingsElement(settings);
  25. // read it after objectFactory and objectWrapperFactory issue #631
  26. //解析environments标签
  27. environmentsElement(root.evalNode("environments"));
  28. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  29. typeHandlerElement(root.evalNode("typeHandlers"));
  30. //解析<mappers>标签
  31. mapperElement(root.evalNode("mappers"));
  32. } catch (Exception e) {
  33. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  34. }
  35. }

在该方法的最下方,看下面这行代码

  1. //解析<mappers>标签
  2. mapperElement(root.evalNode("mappers"));

上面这行代码便是解析mappers标签的方法的调用。看其方法定义,

  1. /**
  2. * 解析<mappers>标签,在此标签中可以配置<mapper>和<package>两种标签,其中<mapper>标签可以配置resource、url、class三种属性,
  3. * 这里的三种属性,仅可以同时出现一个;<package>标签只需要配置包名即可。
  4. * @param parent
  5. * @throws Exception
  6. */
  7. private void mapperElement(XNode parent) throws Exception {
  8. if (parent != null) {
  9. for (XNode child : parent.getChildren()) {
  10. //1、解析package标签,获得name属性即包名
  11. if ("package".equals(child.getName())) {
  12. String mapperPackage = child.getStringAttribute("name");
  13. //扫描包名,把
  14. configuration.addMappers(mapperPackage);
  15. } else {//2、解析<mapper>标签,标签中可以配置resource、url、class三个属性,但只能配置其中一个。
  16. String resource = child.getStringAttribute("resource");
  17. String url = child.getStringAttribute("url");
  18. String mapperClass = child.getStringAttribute("class");
  19. if (resource != null && url == null && mapperClass == null) {
  20. ErrorContext.instance().resource(resource);
  21. InputStream inputStream = Resources.getResourceAsStream(resource);
  22. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
  23. /**
  24. * 处理mapper文件和对应的接口
  25. */
  26. mapperParser.parse();
  27. } else if (resource == null && url != null && mapperClass == null) {
  28. ErrorContext.instance().resource(url);
  29. InputStream inputStream = Resources.getUrlAsStream(url);
  30. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
  31. mapperParser.parse();
  32. } else if (resource == null && url == null && mapperClass != null) {
  33. Class<?> mapperInterface = Resources.classForName(mapperClass);
  34. configuration.addMapper(mapperInterface);
  35. } else {
  36. throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
  37. }
  38. }
  39. }
  40. }
  41. }

通过上面代码的分析及在配置文件中的配置,解析标签分为两部分,分别解析package和mapper子标签。且是循环解析,也就是在含义多个包的时候需要配置多个package子标签。

1、解析package子标签

从上面的方法也就是mapperElement方法中,可以知道在解析标签时首先解析的是package子标签,也就是说在同时配置package和mapper子标签时,先解析的是package子标签,解析标签是有顺序的。下面解析package子标签的过程,仅给出和解析package有关的代码,

  1. //1、解析package标签,获得name属性即包名
  2. if ("package".equals(child.getName())) {
  3. String mapperPackage = child.getStringAttribute("name");
  4. //扫描包名,把
  5. configuration.addMappers(mapperPackage);
  6. }

上面的代码,解析出package子标签中的包名,调用了configuration.addMappers方法,

  1. public void addMappers(String packageName) {
  2. mapperRegistry.addMappers(packageName);
  3. }

调用了mapperRegistry.addMappers方法,

  1. /**
  2. * @since 3.2.2
  3. */
  4. public void addMappers(String packageName) {
  5. addMappers(packageName, Object.class);
  6. }

下面看addMappers方法,

  1. public void addMappers(String packageName, Class<?> superType) {
  2. //解析packageName下的class文件
  3. ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  4. resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  5. Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  6. //处理解析好的mapper接口文件
  7. for (Class<?> mapperClass : mapperSet) {
  8. addMapper(mapperClass);
  9. }
  10. }

上面的方法首先会解析指定包下的class文件,看下面的解析过程,

  1. resolverUtil.find(new ResolverUtil.IsA(superType), packageName);

看find方法,

  1. public ResolverUtil<T> find(Test test, String packageName) {
  2. //把包名中的“.”替换成“/”
  3. String path = getPackagePath(packageName);
  4. try {
  5. //获得包路径下的所有文件名称
  6. List<String> children = VFS.getInstance().list(path);
  7. for (String child : children) {
  8. if (child.endsWith(".class")) {
  9. addIfMatching(test, child);
  10. }
  11. }
  12. } catch (IOException ioe) {
  13. log.error("Could not read package: " + packageName, ioe);
  14. }
  15. return this;
  16. }

遍历包下的所有class文件,调用addIfMatching方法,

  1. @SuppressWarnings("unchecked")
  2. protected void addIfMatching(Test test, String fqn) {
  3. try {
  4. String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
  5. ClassLoader loader = getClassLoader();
  6. if (log.isDebugEnabled()) {
  7. log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
  8. }
  9. Class<?> type = loader.loadClass(externalName);
  10. if (test.matches(type)) {
  11. matches.add((Class<T>) type);
  12. }
  13. } catch (Throwable t) {
  14. log.warn("Could not examine class '" + fqn + "'" + " due to a " +
  15. t.getClass().getName() + " with message: " + t.getMessage());
  16. }
  17. }

加载class文件,判断是否符合test.matches,该方法如下,

  1. /** Returns true if type is assignable to the parent type supplied in the constructor. */
  2. @Override
  3. public boolean matches(Class<?> type) {
  4. return type != null && parent.isAssignableFrom(type);
  5. }

如果符合条件则放入matches中,matches定义在ResolverUtil中。回到addMappers方法中,find方法结束后调用下面的方法,获取matches中的值,

  1. Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();

然后循环解析mapperSet,

  1. //处理解析好的mapper接口文件
  2. for (Class<?> mapperClass : mapperSet) {
  3. addMapper(mapperClass);
  4. }

解析过程如下,

  1. public <T> void addMapper(Class<T> type) {
  2. if (type.isInterface()) {//判断是否为接口
  3. if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
  4. throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
  5. }
  6. boolean loadCompleted = false;
  7. try {
  8. //把type放入knownMappers中,其value为一个MapperProxyFactory对象
  9. knownMappers.put(type, new MapperProxyFactory<T>(type));
  10. // It's important that the type is added before the parser is run
  11. // otherwise the binding may automatically be attempted by the
  12. // mapper parser. If the type is already known, it won't try.
  13. //对mapper文件进行解析,
  14. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  15. //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
  16. parser.parse();
  17. loadCompleted = true;
  18. } finally {
  19. if (!loadCompleted) {//如果解析失败,则删除knowMapper中的信息
  20. knownMappers.remove(type);
  21. }
  22. }
  23. }
  24. }

把mapper接口类封装为MapperProxyFactory对象,并放入knownMappers中,接着对接口类进行解析,如果解析失败会把刚才放入knownMappers中的值从knownMappers中移除。下面看如何解析接口类(解析对应的XML文件),

  1. public void parse() {
  2. String resource = type.toString();
  3. if (!configuration.isResourceLoaded(resource)) {
  4. //解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
  5. /**
  6. * 解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
  7. * 并放入mappedStatements中
  8. *
  9. */
  10. loadXmlResource();
  11. configuration.addLoadedResource(resource);
  12. assistant.setCurrentNamespace(type.getName());
  13. //解析接口上的@CacheNamespace注解
  14. parseCache();
  15. parseCacheRef();
  16. //获得接口中的所有方法,并解析方法上的注解
  17. Method[] methods = type.getMethods();
  18. for (Method method : methods) {
  19. try {
  20. // issue #237
  21. if (!method.isBridge()) {
  22. //解析方法上的注解
  23. parseStatement(method);
  24. }
  25. } catch (IncompleteElementException e) {
  26. configuration.addIncompleteMethod(new MethodResolver(this, method));
  27. }
  28. }
  29. }
  30. parsePendingMethods();
  31. }

上面的解析分为两个过程,首先解析对应的XML映射文件,再解析方法上的注解。

1.1、解析xml文件

下面看如何继续对应的XML文件,

  1. loadXmlResource();

看如何解析xml文件,

  1. private void loadXmlResource() {
  2. // Spring may not know the real resource name so we check a flag
  3. // to prevent loading again a resource twice
  4. // this flag is set at XMLMapperBuilder#bindMapperForNamespace
  5. if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
  6. //解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
  7. String xmlResource = type.getName().replace('.', '/') + ".xml";
  8. InputStream inputStream = null;
  9. try {
  10. inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
  11. } catch (IOException e) {
  12. // ignore, resource is not required
  13. }
  14. if (inputStream != null) {
  15. XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
  16. //解析xml映射文件
  17. xmlParser.parse();
  18. }
  19. }
  20. }

首先确定XML映射文件的位置,和接口类同名且在同一个包下。如下的例子,

c6eea9b3d226cce7a711f1587b28de3b.png

确定好对应的映射文件位置,接着便是解析该xml文件,

  1. if (inputStream != null) {
  2. XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
  3. //解析xml映射文件
  4. xmlParser.parse();
  5. }

解析过程如下,

  1. public void parse() {
  2. if (!configuration.isResourceLoaded(resource)) {
  3. //解析mapper文件中的<mapper>标签及其子标签
  4. configurationElement(parser.evalNode("/mapper"));
  5. configuration.addLoadedResource(resource);
  6. bindMapperForNamespace();
  7. }
  8. parsePendingResultMaps();
  9. parsePendingCacheRefs();
  10. parsePendingStatements();
  11. }

解析的过程在解析标签的时候再详细分析。解析的最终结果是把XML中的select|update|delete|insert标签转化为MappedStatement对象,放入configuration中。

1.2、解析接口中方法上的注解

上面解析了接口对于的XML文件,下面看如何解析接口中的方法,

  1. //获得接口中的所有方法,并解析方法上的注解
  2. Method[] methods = type.getMethods();
  3. for (Method method : methods) {
  4. try {
  5. // issue #237
  6. if (!method.isBridge()) {
  7. //解析方法上的注解
  8. parseStatement(method);
  9. }
  10. } catch (IncompleteElementException e) {
  11. configuration.addIncompleteMethod(new MethodResolver(this, method));
  12. }

看parseStatement方法,

  1. void parseStatement(Method method) {
  2. Class<?> parameterTypeClass = getParameterType(method);
  3. LanguageDriver languageDriver = getLanguageDriver(method);
  4. //获得方法上的注解,并生成SqlSource
  5. SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
  6. if (sqlSource != null) {
  7. Options options = method.getAnnotation(Options.class);
  8. //生成mappedStatementId,为接口的权限类名+方法名。从这里可以得出同一个接口或namespace中不允许有同名的方法名或id
  9. final String mappedStatementId = type.getName() + "." + method.getName();
  10. Integer fetchSize = null;
  11. Integer timeout = null;
  12. StatementType statementType = StatementType.PREPARED;
  13. ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
  14. SqlCommandType sqlCommandType = getSqlCommandType(method);
  15. boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  16. boolean flushCache = !isSelect;
  17. boolean useCache = isSelect;
  18. KeyGenerator keyGenerator;
  19. String keyProperty = "id";
  20. String keyColumn = null;
  21. if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
  22. // first check for SelectKey annotation - that overrides everything else
  23. SelectKey selectKey = method.getAnnotation(SelectKey.class);
  24. if (selectKey != null) {
  25. keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
  26. keyProperty = selectKey.keyProperty();
  27. } else if (options == null) {
  28. keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  29. } else {
  30. keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  31. keyProperty = options.keyProperty();
  32. keyColumn = options.keyColumn();
  33. }
  34. } else {
  35. keyGenerator = NoKeyGenerator.INSTANCE;
  36. }
  37. if (options != null) {
  38. if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
  39. flushCache = true;
  40. } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
  41. flushCache = false;
  42. }
  43. useCache = options.useCache();
  44. fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
  45. timeout = options.timeout() > -1 ? options.timeout() : null;
  46. statementType = options.statementType();
  47. resultSetType = options.resultSetType();
  48. }
  49. String resultMapId = null;
  50. ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
  51. if (resultMapAnnotation != null) {
  52. String[] resultMaps = resultMapAnnotation.value();
  53. StringBuilder sb = new StringBuilder();
  54. for (String resultMap : resultMaps) {
  55. if (sb.length() > 0) {
  56. sb.append(",");
  57. }
  58. sb.append(resultMap);
  59. }
  60. resultMapId = sb.toString();
  61. } else if (isSelect) {
  62. resultMapId = parseResultMap(method);
  63. }
  64. assistant.addMappedStatement(
  65. mappedStatementId,
  66. sqlSource,
  67. statementType,
  68. sqlCommandType,
  69. fetchSize,
  70. timeout,
  71. // ParameterMapID
  72. null,
  73. parameterTypeClass,
  74. resultMapId,
  75. getReturnType(method),
  76. resultSetType,
  77. flushCache,
  78. useCache,
  79. // TODO gcode issue #577
  80. false,
  81. keyGenerator,
  82. keyProperty,
  83. keyColumn,
  84. // DatabaseID
  85. null,
  86. languageDriver,
  87. // ResultSets
  88. options != null ? nullOrEmpty(options.resultSets()) : null);
  89. }
  90. }

从上面的代码,可以看出最终调用了assistant.addMappedStatement方法,该方法会把注解信息封装为MappedStatement对象,放入configuration中。详细过程,后面分析。

2、解析mapper子标签

上面分析了mybatis解析标签的过程,下面看直接解析子标签。代码为部分代码

  1. //2、解析<mapper>标签,标签中可以配置resource、url、class三个属性,但只能配置其中一个。
  2. String resource = child.getStringAttribute("resource");
  3. String url = child.getStringAttribute("url");
  4. String mapperClass = child.getStringAttribute("class");
  5. if (resource != null && url == null && mapperClass == null) {
  6. ErrorContext.instance().resource(resource);
  7. InputStream inputStream = Resources.getResourceAsStream(resource);
  8. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
  9. /**
  10. * 处理mapper文件和对应的接口
  11. */
  12. mapperParser.parse();
  13. } else if (resource == null && url != null && mapperClass == null) {
  14. ErrorContext.instance().resource(url);
  15. InputStream inputStream = Resources.getUrlAsStream(url);
  16. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
  17. mapperParser.parse();
  18. } else if (resource == null && url == null && mapperClass != null) {
  19. Class<?> mapperInterface = Resources.classForName(mapperClass);
  20. configuration.addMapper(mapperInterface);
  21. } else {
  22. throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
  23. }

前边说过,在子标签中可以配置resource、url、class三个属性,但是只能配置其中一个,上面分别对其进行了解析,其解析过程和上面解析中的过程类似,解析resource和url属性的时候都是把XML映射文件解析为inputSream,然后对文件进行解析;解析class属性的时候和解析的过程一样。

小结

本章分析了mybatis解析标签的过程,分为解析子标签,其解析过程主要为解析Mapper接口和XML映射文件,其详细过程后面详细分析。


六、解析mappers标签(解析resource和url属性)

概述

在上篇文章中分析了标签,重点分析了子标签,除了可以配置子标签外,在标签中还可以配置子标签,该子标签可以配置的熟悉有resource、url、class三个属性,解析resource和url的过程大致相同,看解析resource属性的过程。

下面看部分代码,

  1. if (resource != null && url == null && mapperClass == null) {
  2. ErrorContext.instance().resource(resource);
  3. InputStream inputStream = Resources.getResourceAsStream(resource);
  4. XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
  5. /**
  6. * 处理mapper文件和对应的接口
  7. */
  8. mapperParser.parse();
  9. }

可以看到调用了XMLMapperBuilder的parse方法,

  1. public void parse() {
  2. if (!configuration.isResourceLoaded(resource)) {
  3. //1、解析mapper文件中的<mapper>标签及其子标签,并设置CurrentNamespace的值,供下面第2步使用
  4. configurationElement(parser.evalNode("/mapper"));
  5. //这里想loadResoures中设置的是XML映射文件的路径
  6. configuration.addLoadedResource(resource);
  7. //2、绑定Mapper接口,并解析对应的XML映射文件
  8. bindMapperForNamespace();
  9. }
  10. parsePendingResultMaps();
  11. parsePendingCacheRefs();
  12. parsePendingStatements();
  13. }

从上面的代码中可以看出首先解析resource资源说代表的XML映射文件,然后解析XML;映射文件中的namespace配置的接口。

详述

通过上面的分析知道,解析resource配置的XML映射文件,分为两步,第一步就是解析XML映射文件的内容;第二步是解析XML映射文件中配置的namespace属性,也就是对于的Mapper接口。

1、解析XML映射文件

看如何解析XML映射文件内容,也就是下面的这行代码,

  1. configurationElement(parser.evalNode("/mapper"));

看具体的实现,

  1. /**
  2. * 解析XML映射文件的内容
  3. * @param context
  4. */
  5. private void configurationElement(XNode context) {
  6. try {
  7. //获得namespace属性
  8. String namespace = context.getStringAttribute("namespace");
  9. if (namespace == null || namespace.equals("")) {
  10. throw new BuilderException("Mapper's namespace cannot be empty");
  11. }
  12. //设置到currentNamespace中
  13. builderAssistant.setCurrentNamespace(namespace);
  14. //解析<cache-ref>标签
  15. cacheRefElement(context.evalNode("cache-ref"));
  16. //二级缓存标签
  17. cacheElement(context.evalNode("cache"));
  18. parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  19. resultMapElements(context.evalNodes("/mapper/resultMap"));
  20. sqlElement(context.evalNodes("/mapper/sql"));
  21. //解析select、insert、update、delete子标签
  22. buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  23. } catch (Exception e) {
  24. throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  25. }
  26. }

上面方法解析XML映射文件的内容,其中有个和二级缓存相关的配置,即标签。那么xml映射文件可以配置哪些标签那,看下面,

80731269cc74dee078f3bb69aebfef4a.png

在XML映射文件中可以配置上面的这些标签,也就是上面方法中解析的内容。重点看解析select、update、delete、select。也就是下面这行代码

  1. //解析select、insert、update、delete子标签
  2. buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

其方法定义如下,

  1. private void buildStatementFromContext(List<XNode> list) {
  2. if (configuration.getDatabaseId() != null) {
  3. buildStatementFromContext(list, configuration.getDatabaseId());
  4. }
  5. buildStatementFromContext(list, null);
  6. }

这里会校验databaseId,如果自定义配置了,则使用自定义的,否则使用默认的,看方法buildStatementFromContext方法

  1. private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  2. for (XNode context : list) {
  3. final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
  4. try {
  5. statementParser.parseStatementNode();
  6. } catch (IncompleteElementException e) {
  7. configuration.addIncompleteStatement(statementParser);
  8. }
  9. }
  10. }

调用XMLStatementBuilder的parseStatementNode方法

  1. /**
  2. * 解析select、update、delete、insert标签
  3. */
  4. public void parseStatementNode() {
  5. String id = context.getStringAttribute("id");
  6. String databaseId = context.getStringAttribute("databaseId");
  7. if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
  8. return;
  9. }
  10. Integer fetchSize = context.getIntAttribute("fetchSize");
  11. Integer timeout = context.getIntAttribute("timeout");
  12. String parameterMap = context.getStringAttribute("parameterMap");
  13. String parameterType = context.getStringAttribute("parameterType");
  14. Class<?> parameterTypeClass = resolveClass(parameterType);
  15. String resultMap = context.getStringAttribute("resultMap");
  16. String resultType = context.getStringAttribute("resultType");
  17. String lang = context.getStringAttribute("lang");
  18. LanguageDriver langDriver = getLanguageDriver(lang);
  19. Class<?> resultTypeClass = resolveClass(resultType);
  20. String resultSetType = context.getStringAttribute("resultSetType");
  21. StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  22. ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  23. String nodeName = context.getNode().getNodeName();
  24. SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  25. boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  26. boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  27. //查询语句默认开启一级缓存,这里默认是true
  28. boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  29. boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
  30. // Include Fragments before parsing
  31. XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  32. includeParser.applyIncludes(context.getNode());
  33. // Parse selectKey after includes and remove them.
  34. processSelectKeyNodes(id, parameterTypeClass, langDriver);
  35. // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  36. //生成SqlSource,这里分两种,DynamicSqlSource和RawSqlSource
  37. SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  38. String resultSets = context.getStringAttribute("resultSets");
  39. String keyProperty = context.getStringAttribute("keyProperty");
  40. String keyColumn = context.getStringAttribute("keyColumn");
  41. KeyGenerator keyGenerator;
  42. //例,id="selectUser"
  43. //这里的keyStatementId=selectUser!selectKey
  44. String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  45. //keyStatementId=cn.com.dao.userMapper.selectUser!selectKey
  46. keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  47. if (configuration.hasKeyGenerator(keyStatementId)) {
  48. keyGenerator = configuration.getKeyGenerator(keyStatementId);
  49. } else {
  50. keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
  51. configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
  52. ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  53. }
  54. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  55. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  56. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  57. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  58. }

上面的代码主要是解析标签中的各种属性,那么标签中可以配置哪些属性那,下面看select标签的属性,详情可参见https://www.w3cschool.cn/mybatis/f4uw1ilx.html

045bef78c2cbe02e8dea389c12c37035.png

0e24e8165feb9249c4fd9a7712b4aadf.png

f9997eba3f48cb9d2f653c9ea1ee93b3.png

上面是select标签中可以配置的属性列表。

上面的代码重点看以下重点

二级缓存

下面看和缓存相关的

  1. boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  2. boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  3. //查询语句默认开启一级缓存,这里默认是true
  4. boolean useCache = context.getBooleanAttribute("useCache", isSelect);

这里仅针对select查询语句使用缓存,这里的默认不会刷新缓存flushCache为false,默认开启缓存useCache为ture,这里的缓存指的是一级缓存,经常说的mybatis一级缓存,一级缓存是sqlSession级别的。

看完了一级缓存,下面看SqlSource的内容

SqlSource

下面是SqlSource相关的,

  1. //生成SqlSource,这里分两种,DynamicSqlSource和RawSqlSource
  2. SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

上面是生成SqlSource的过程,

  1. @Override
  2. public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  3. XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  4. return builder.parseScriptNode();
  5. }

看parseScriptNode方法

  1. public SqlSource parseScriptNode() {
  2. MixedSqlNode rootSqlNode = parseDynamicTags(context);
  3. SqlSource sqlSource = null;
  4. if (isDynamic) {
  5. //含义${}符合的为DynamicSqlSource
  6. sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  7. } else {
  8. //不含有${}的为rawSqlSource
  9. sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  10. }
  11. return sqlSource;
  12. }

从上面的代码可以看到在映射文件中根据参数占位符的标识符(${}、#{})分为DynamicSqlSource和RawSqlSource。具体如何判断,后面详细分析。

addMappedStatement

最后看builderAssistant.addMappedStatement方法,

  1. public MappedStatement addMappedStatement(
  2. String id,
  3. SqlSource sqlSource,
  4. StatementType statementType,
  5. SqlCommandType sqlCommandType,
  6. Integer fetchSize,
  7. Integer timeout,
  8. String parameterMap,
  9. Class<?> parameterType,
  10. String resultMap,
  11. Class<?> resultType,
  12. ResultSetType resultSetType,
  13. boolean flushCache,
  14. boolean useCache,
  15. boolean resultOrdered,
  16. KeyGenerator keyGenerator,
  17. String keyProperty,
  18. String keyColumn,
  19. String databaseId,
  20. LanguageDriver lang,
  21. String resultSets) {
  22. if (unresolvedCacheRef) {
  23. throw new IncompleteElementException("Cache-ref not yet resolved");
  24. }
  25. //cn.com.dao.UserMapper.selectUser
  26. id = applyCurrentNamespace(id, false);
  27. boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  28. MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
  29. .resource(resource)
  30. .fetchSize(fetchSize)
  31. .timeout(timeout)
  32. .statementType(statementType)
  33. .keyGenerator(keyGenerator)
  34. .keyProperty(keyProperty)
  35. .keyColumn(keyColumn)
  36. .databaseId(databaseId)
  37. .lang(lang)
  38. .resultOrdered(resultOrdered)
  39. .resultSets(resultSets)
  40. .resultMaps(getStatementResultMaps(resultMap, resultType, id))
  41. .resultSetType(resultSetType)
  42. .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
  43. .useCache(valueOrDefault(useCache, isSelect))
  44. .cache(currentCache);
  45. ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  46. if (statementParameterMap != null) {
  47. statementBuilder.parameterMap(statementParameterMap);
  48. }
  49. MappedStatement statement = statementBuilder.build();
  50. /*向mappedStatements字段中加入MappedStatement,这里会加入两个key
  51. * cn.com.dao.UserMapper.selectUser statement
  52. * selectUser statement
  53. * 每次都会插入上面的两种key,两种key对应的value都是同一个statement
  54. *
  55. */
  56. configuration.addMappedStatement(statement);
  57. return statement;
  58. }

该方法主要完成的功能是生成MappedStatement,且放入configuration中。

2、解析namespace属性

上面分析了解析XML映射文件的内容的过程,最后的结果是把XML映射文件中的select、update、insert、delete标签的内容解析为MappedStatement。下面看解析XML映射文件中的namespace属性,

  1. //2、绑定Mapper接口,并解析对应的XML映射文件
  2. bindMapperForNamespace();

上面我给的注释是绑定接口并解析对应的XML映射文件,这个方法没有参数,怎么绑定具体的接口并解析对应的映射文件那,

  1. private void bindMapperForNamespace() {
  2. String namespace = builderAssistant.getCurrentNamespace();
  3. if (namespace != null) {
  4. Class<?> boundType = null;
  5. try {
  6. //加载类,这里加载的是mapper文件中配置的namespace配置的接口
  7. boundType = Resources.classForName(namespace);
  8. } catch (ClassNotFoundException e) {
  9. //ignore, bound type is not required
  10. }
  11. if (boundType != null) {
  12. if (!configuration.hasMapper(boundType)) {//判断该接口是否被加载过,在mapperRegistry中的knowsMapper中判断
  13. // Spring may not know the real resource name so we set a flag
  14. // to prevent loading again this resource from the mapper interface
  15. // look at MapperAnnotationBuilder#loadXmlResource
  16. //把该Mapper接口作为已加载的资源存放到loadedResources中,loadedResources存放的是已加载的mapper接口的路径,和第一步设置的XML映射文件路径不是同一个值。
  17. configuration.addLoadedResource("namespace:" + namespace);
  18. //把该接口放到mapperRegistry中的knowsMapper中,并解析该接口,根据loadedResources判定是否需要解析该Mapper接口
  19. configuration.addMapper(boundType);
  20. }
  21. }
  22. }
  23. }

获得builderAssistant.getCurrentNamespace(),在解析XML映射文件时,第一步便是设置该属性,这里用到的便是上一步中设置的那个XML映射文件中的namespace属性值。获得该接口的名称,判断是否生成过MapperProxyFactory,即放入过knownMappers中,看configuration.hasMapper方法,

  1. public boolean hasMapper(Class<?> type) {
  2. return mapperRegistry.hasMapper(type);
  3. }
  4. public <T> boolean hasMapper(Class<T> type) {
  5. return knownMappers.containsKey(type);
  6. }

如果在knownMappers中,则不进行解析,如果不在才进行下面的逻辑处理,调用configuration.addLoadedResource方法,放入loadedResources中,标识在第一步已经解析过对应的XML映射文件;调用configuration.addMapper方法,解析该接口,这个过程和在标签中配置class属性的过程是一样的,后面详细分析。

小结

本章分析了mappers标签中mapper子标签中resource和url属性的解析过程,首先解析对应的XML映射文件,解析的结果为MappedStatement对象,然后解析其namespace对应的接口,解析的结果为MapperProxyFactory对象。


七、解析mappers标签(解析class属性)

概述

在mybatis的核心配置文件中配置mappers标签有以下方式,

  1. <mappers>
  2. <mapper class="cn.com.mybatis.dao.UserMapper"/>
  3. </mappers>

上面这种方式便是mapper标签的class属性配置方式,其解析部分过程如下,

  1. else if (resource == null && url == null && mapperClass != null) {
  2. Class<?> mapperInterface = Resources.classForName(mapperClass);
  3. configuration.addMapper(mapperInterface);
  4. } else {
  5. throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
  6. }

可以看到主要是调用了configuration.addMapper方法,和上篇文章中解析namespace调用的方法是一致的。看其具体实现

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

下方分析mapperRegistry.addMapper方法。

详述

mapperRegistry.addMapper方法的定义如下,

  1. public <T> void addMapper(Class<T> type) {
  2. if (type.isInterface()) {//判断是否为接口
  3. if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
  4. throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
  5. }
  6. boolean loadCompleted = false;
  7. try {
  8. //1、把type放入knownMappers中,其value为一个MapperProxyFactory对象
  9. knownMappers.put(type, new MapperProxyFactory<T>(type));
  10. // It's important that the type is added before the parser is run
  11. // otherwise the binding may automatically be attempted by the
  12. // mapper parser. If the type is already known, it won't try.
  13. //2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
  14. //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
  15. /**sqlAnnotationTypes.add(Select.class);
  16. sqlAnnotationTypes.add(Insert.class);
  17. sqlAnnotationTypes.add(Update.class);
  18. sqlAnnotationTypes.add(Delete.class);
  19. sqlProviderAnnotationTypes.add(SelectProvider.class);
  20. sqlProviderAnnotationTypes.add(InsertProvider.class);
  21. sqlProviderAnnotationTypes.add(UpdateProvider.class);
  22. sqlProviderAnnotationTypes.add(DeleteProvider.class);
  23. *
  24. */
  25. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  26. parser.parse();
  27. loadCompleted = true;
  28. } finally {
  29. if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息
  30. knownMappers.remove(type);
  31. }
  32. }
  33. }
  34. }

该方法主要分为下面几个步骤。

1、检查是否解析过接口

首先会判断knowMappers中是否已经存在该接口,如果存在则会抛出异常

  1. if (hasMapper(type)) {//如果knownMappers中已经存在该type,则抛出异常
  2. throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
  3. }

如果不存在则放入knownMappers中,

  1. //1、把type放入knownMappers中,其value为一个MapperProxyFactory对象
  2. knownMappers.put(type, new MapperProxyFactory<T>(type));

继续解析对应的映射文件及接口方法注解

2、解析接口对应的映射文件及接口方法注解

上面把mapper接口放入了knownMappers中,接着需要解析映射文件及注解,

  1. //2、对mapper文件及注解进行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes两个变量
  2. //具体的解析过程,1、先解析对应的XML映射文件,2、再解析接口方法中的注解信息
  3. /**sqlAnnotationTypes.add(Select.class);
  4. sqlAnnotationTypes.add(Insert.class);
  5. sqlAnnotationTypes.add(Update.class);
  6. sqlAnnotationTypes.add(Delete.class);
  7. sqlProviderAnnotationTypes.add(SelectProvider.class);
  8. sqlProviderAnnotationTypes.add(InsertProvider.class);
  9. sqlProviderAnnotationTypes.add(UpdateProvider.class);
  10. sqlProviderAnnotationTypes.add(DeleteProvider.class);
  11. *
  12. */
  13. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  14. parser.parse();
  15. loadCompleted = true;

上面的代码,生成了一个MapperAnnotationBuilder实例,

  1. public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
  2. String resource = type.getName().replace('.', '/') + ".java (best guess)";
  3. this.assistant = new MapperBuilderAssistant(configuration, resource);
  4. this.configuration = configuration;
  5. this.type = type;
  6. sqlAnnotationTypes.add(Select.class);
  7. sqlAnnotationTypes.add(Insert.class);
  8. sqlAnnotationTypes.add(Update.class);
  9. sqlAnnotationTypes.add(Delete.class);
  10. sqlProviderAnnotationTypes.add(SelectProvider.class);
  11. sqlProviderAnnotationTypes.add(InsertProvider.class);
  12. sqlProviderAnnotationTypes.add(UpdateProvider.class);
  13. sqlProviderAnnotationTypes.add(DeleteProvider.class);
  14. }

给sqlAnnotationTypes和sqlProviderAnnotationTypes进行了赋值。

下面看具体的解析过程,

  1. parser.parse();

MapperAnnotationBuilder的parse方法如下,

  1. public void parse() {
  2. String resource = type.toString();
  3. if (!configuration.isResourceLoaded(resource)) {//判断是否加载过该Mapper接口
  4. //解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
  5. /**
  6. * 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
  7. * 并放入mappedStatements中
  8. *
  9. */
  10. loadXmlResource();
  11. configuration.addLoadedResource(resource);
  12. assistant.setCurrentNamespace(type.getName());
  13. //解析接口上的@CacheNamespace注解
  14. parseCache();
  15. parseCacheRef();
  16. //2、获得接口中的所有方法,并解析方法上的注解
  17. Method[] methods = type.getMethods();
  18. for (Method method : methods) {
  19. try {
  20. // issue #237
  21. if (!method.isBridge()) {
  22. //解析方法上的注解
  23. parseStatement(method);
  24. }
  25. } catch (IncompleteElementException e) {
  26. configuration.addIncompleteMethod(new MethodResolver(this, method));
  27. }
  28. }
  29. }
  30. parsePendingMethods();
  31. }

首先判断是否加载过该资源,

  1. if (!configuration.isResourceLoaded(resource)) {
  2. }

只有未加载过,才会执行该方法的逻辑,否则该方法执行完毕。

  1. public boolean isResourceLoaded(String resource) {
  2. return loadedResources.contains(resource);
  3. }

从loadResources中进行判断,判断是否解析过该Mapper接口,答案是没有解析过,则会继续解析。

1.1、解析对应的XML文件

首先会解析XML文件,调用下面的方法,

  1. //解析和接口同名的xml文件,前提是存在该文件,如果不存在该文件要怎么解析那?答案是解析接口中方法上的注解
  2. /**
  3. * 1、解析和接口同名的xml配置文件,最终要做的是把xml文件中的标签,转化为mapperStatement,
  4. * 并放入mappedStatements中
  5. *
  6. */
  7. loadXmlResource();

看loadXmlResource方法

  1. /**
  2. * 解析mapper配置文件
  3. */
  4. private void loadXmlResource() {
  5. // Spring may not know the real resource name so we check a flag
  6. // to prevent loading again a resource twice
  7. // this flag is set at XMLMapperBuilder#bindMapperForNamespace
  8. if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
  9. //解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
  10. String xmlResource = type.getName().replace('.', '/') + ".xml";
  11. InputStream inputStream = null;
  12. try {
  13. inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
  14. } catch (IOException e) {
  15. // ignore, resource is not required
  16. }
  17. if (inputStream != null) {
  18. XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
  19. //解析xml映射文件
  20. xmlParser.parse();
  21. }
  22. }
  23. }

首先进行了判断,进入if判断,看判断上的注解

  1. // Spring may not know the real resource name so we check a flag
  2. // to prevent loading again a resource twice
  3. // this flag is set at XMLMapperBuilder#bindMapperForNamespace

第一句注解没理解什么意思,第二句的意思是方式两次加载资源,第三句是说明了该标识是在XMLMapperBuilder类中的bindMapperForNamespace中进行的设置,如下

4a08377c72d177b5694750f1804d3b7a.png

为什么这样设置,后面会总结mapper的加载流程详细说明该问题。

判断之后寻找相应的XML映射文件,映射文件的文件路径如下,

  1. //解析对应的XML映射文件,其名称为接口类+"."+xml,即和接口类同名且在同一个包下。
  2. String xmlResource = type.getName().replace('.', '/') + ".xml";

从上面可以看出Mapper接口文件和XML映射文件在同一个包下,且文件名称相同(扩展名不同)。接着便是解析XML映射文件的逻辑。

  1. if (inputStream != null) {
  2. XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
  3. //解析xml映射文件
  4. xmlParser.parse();
  5. }

该逻辑和《mybatis源码配置文件解析之五:解析mappers标签(解析XML映射文件)》的过程是一样的,调用XMLMapperBuilder的parse方法进行解析,解析的结果为MapperStatement对象。

1.2、解析接口方法上的注解

上面是解析接口对应的XML映射文件,解析完成之后,还要解析接口方法上的注解,因为mybatis的sql配置有两种方式,一种是通过XML映射文件,另一种便是注解(当SQL比较复杂建议使用映射文件的方式),下面看解析注解的过程,

  1. //2、获得接口中的所有方法,并解析方法上的注解
  2. Method[] methods = type.getMethods();
  3. for (Method method : methods) {
  4. try {
  5. // issue #237
  6. if (!method.isBridge()) {
  7. //解析方法上的注解
  8. parseStatement(method);
  9. }
  10. } catch (IncompleteElementException e) {
  11. configuration.addIncompleteMethod(new MethodResolver(this, method));
  12. }
  13. }

通过反射的方式获得接口中的所有方法,遍历方法执行parseStatement方法

  1. void parseStatement(Method method) {
  2. Class<?> parameterTypeClass = getParameterType(method);
  3. LanguageDriver languageDriver = getLanguageDriver(method);
  4. //获得方法上的注解,并生成SqlSource
  5. SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
  6. if (sqlSource != null) {
  7. Options options = method.getAnnotation(Options.class);
  8. //生成mappedStatementId,为接口的权限类名+方法名。从这里可以得出同一个接口或namespace中不允许有同名的方法名或id
  9. final String mappedStatementId = type.getName() + "." + method.getName();
  10. Integer fetchSize = null;
  11. Integer timeout = null;
  12. StatementType statementType = StatementType.PREPARED;
  13. ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
  14. SqlCommandType sqlCommandType = getSqlCommandType(method);
  15. boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  16. boolean flushCache = !isSelect;
  17. boolean useCache = isSelect;
  18. KeyGenerator keyGenerator;
  19. String keyProperty = "id";
  20. String keyColumn = null;
  21. if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
  22. // first check for SelectKey annotation - that overrides everything else
  23. SelectKey selectKey = method.getAnnotation(SelectKey.class);
  24. if (selectKey != null) {
  25. keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
  26. keyProperty = selectKey.keyProperty();
  27. } else if (options == null) {
  28. keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  29. } else {
  30. keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  31. keyProperty = options.keyProperty();
  32. keyColumn = options.keyColumn();
  33. }
  34. } else {
  35. keyGenerator = NoKeyGenerator.INSTANCE;
  36. }
  37. if (options != null) {
  38. if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
  39. flushCache = true;
  40. } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
  41. flushCache = false;
  42. }
  43. useCache = options.useCache();
  44. fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
  45. timeout = options.timeout() > -1 ? options.timeout() : null;
  46. statementType = options.statementType();
  47. resultSetType = options.resultSetType();
  48. }
  49. String resultMapId = null;
  50. ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
  51. if (resultMapAnnotation != null) {
  52. String[] resultMaps = resultMapAnnotation.value();
  53. StringBuilder sb = new StringBuilder();
  54. for (String resultMap : resultMaps) {
  55. if (sb.length() > 0) {
  56. sb.append(",");
  57. }
  58. sb.append(resultMap);
  59. }
  60. resultMapId = sb.toString();
  61. } else if (isSelect) {
  62. resultMapId = parseResultMap(method);
  63. }
  64. assistant.addMappedStatement(
  65. mappedStatementId,
  66. sqlSource,
  67. statementType,
  68. sqlCommandType,
  69. fetchSize,
  70. timeout,
  71. // ParameterMapID
  72. null,
  73. parameterTypeClass,
  74. resultMapId,
  75. getReturnType(method),
  76. resultSetType,
  77. flushCache,
  78. useCache,
  79. // TODO gcode issue #577
  80. false,
  81. keyGenerator,
  82. keyProperty,
  83. keyColumn,
  84. // DatabaseID
  85. null,
  86. languageDriver,
  87. // ResultSets
  88. options != null ? nullOrEmpty(options.resultSets()) : null);
  89. }
  90. }

注解的解析和解析XML映射文件的方式是一样的,解析的属性是一致的。需要注意下面的注解

  1. @SelectProvider(type=BaseUserProvider.class,method="getUser")

该注解的意思是定义select语句的提供者,需要配置type和method,即提供类的Class对象和相应的方法(返回一个字符串)

3、解析失败回退

如果在继续过程中失败或抛出异常,则进行回退,回退的意思是从knownMappers中删除该类型。

  1. finally {
  2. if (!loadCompleted) {//3、如果解析失败,则删除knowMapper中的信息
  3. knownMappers.remove(type);
  4. }
  5. }

因为Mapper解析的过程有两个结果一个是放入到configuration.knownMappers中的MapperProxyFactory对象,一个是放入到configuration.mappedStatements中MappedStatement对象,由于生产MappedStatement对象失败,所以要回退生成MapperProxyFactory对象过程。

小结

本章分析了mybatis解析的过程,依旧是包含MapperProxyFactory和MappedStatement两个过程。


解析mappers标签流程图

" class="reference-link">6d58496144a575917a1527cf4deb0a48.png


发表评论

表情:
评论列表 (有 0 条评论,381人围观)

还没有评论,来说两句吧...

相关阅读

    相关 MyBatis框架

    MyBatis是一个优秀的持久层框架,通过简洁的XML配置方式就能消除以前传统 使用JDBC出现大量重复的代码,以及参数的设置和结果集的映射. 如果能够懂的底层代码和原理,