目录

2.5.加载自定义的文件访问系统和日志系统

优质
小牛编辑
149浏览
2023-12-01

parseConfiguration方法中我们通过propertiesElementsettingsAsProperties两个方法已经完成了对mybatis属性配置的解析和准备工作。

//issue #117 read properties first
 // 加载资源配置文件,并覆盖对应的属性[properties节点]
 propertiesElement(root.evalNode("properties"));
 // 将settings标签内的内容转换为Properties,并进行校验。
 Properties settings = settingsAsProperties(root.evalNode("settings"));
 // 根据settings的配置确定访问资源文件的方式
 loadCustomVfs(settings);

 // ...

接下来的需要做的就是使用前面获取到的属性来对mybatis的运行环境进行简单的配置,主要包括初始化访问虚拟文件系统的VFS实例,以及日志Log实例。

// 根据settings的配置确定访问资源文件的方式
loadCustomVfs(settings);
// 根据settings的配置确定日志处理的方式
loadCustomLogImpl(settings);

VFS

配置虚拟文件访问系统VFS的方法loadCustomVfs实现比较简单:

/**
  * 加载访问系统虚拟文件系统的实现类
  *
  * @param props 系统全局配置
  * @throws ClassNotFoundException 未找到实现类
  */
 private void loadCustomVfs(Properties props) throws ClassNotFoundException {
     // 获取配置的vfsImpl属性,如果存在的话,将会覆盖默认的虚拟文件系统访问实现类
     String value = props.getProperty("vfsImpl");
     if (value != null) {
         // 多个实现类以","分隔
         String[] clazzes = value.split(",");
         for (String clazz : clazzes) {
             if (!clazz.isEmpty()) {
                 // 通过类名称获取类定义
                 @SuppressWarnings("unchecked")
                 Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
                 // 更新Mybatis访问虚拟文件系统的实现类
                 // 这就意味着当时用逗号作为分隔符定义了多个实现类时,最后一个实现类生效
                 configuration.setVfsImpl(vfsImpl);
             }
         }
     }
 }

用户可以通过全局配置文件来配置VFS的实现类,如果有多个VFS实现,可以使用英文的逗号进行分隔:

<properties>
    <property name="vfsImpl" value="实现1,实现2"/>
</properties>

VFS实现类的具体注册工作是由Configuration对象的setVfsImpl方法来完成的:

/**
 * 新增一个VFS实例
 *
 * @param vfsImpl VFS实例
 */
public void setVfsImpl(Class<? extends VFS> vfsImpl) {
    if (vfsImpl != null) {
        this.vfsImpl = vfsImpl;
        // 添加一个新的VFS实例
        VFS.addImplClass(this.vfsImpl);
    }
}

setVfsImpl方法实现也比较简单,首先刷新了Configuration对象的vfsImpl属性的值,之后调用VFSaddImplClass方法完成自定义VFS实例的注册工作。

Configuration对象的vfsImpl属性用于简单的记录当前使用的VFS实例类型:

/**
 * 虚拟文件系统,提供了一个访问系统文件资源的简单API,
 * 对当前使用的VFS实例类型进行记录
 */
protected Class<? extends VFS> vfsImpl;

VFSaddImplClass方法则用来缓存用户自定义的VFS实现类。

说了这么久,这个VFS具体是做什么的呢?

VFS全称是Virtual File System,他是一个虚拟的文件系统,为读取不同存储介质中的文件和数据提供一个统一的API方法。

在mybatis中,VFS定义了访问程序宿主机资源的API接口,这些资源包括:jar包,class文件,配置文件等。

mybatis内置了两种VFS实现:JBoss6VFSDefaultVFS

/**
 * 内置的VFS实现类
 */
public static final Class<?>[] IMPLEMENTATIONS = {JBoss6VFS.class, DefaultVFS.class};

其中JBoss6VFS的实现依赖于JBoss 6VFSAPI,DefaultVFS则是适用于大多数应用程序服务器的VFS的默认实现。

VFS中定义了两个常量,其中IMPLEMENTATIONS我们已经看过了,他用于记录mybatis内置的VFS实现类。

另一个常量USER_IMPLEMENTATIONS属性用来保存用户自定义VFS实现集合:

/**
 * 用户自定义VFS实现集合。
 * <p>
 * 通过{@link #addImplClass(Class)}方法添加的用户自定义实现。
 */
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<>();

我们刚才提到的VFSaddImplClass方法的具体作用就是把用户自定义的VFS实现类保存到USER_IMPLEMENTATIONS集合中:

/**
 * 添加用户自定义的VFS实现,自定义实现优先级高于内置的实现
 *
 * @param clazz 被添加的{@link VFS}实现
 */
public static void addImplClass(Class<? extends VFS> clazz) {
    if (clazz != null) {
        USER_IMPLEMENTATIONS.add(clazz);
    }
}

其实在VFS中还定义了一个名为VFSHolder的单例对象,该对象的作用是获取VFS实例。

/**
  * VFS 单例实例持有者
  */
 private static class VFSHolder {

     static final VFS INSTANCE = createVFS();

     @SuppressWarnings("unchecked")
     static VFS createVFS() {
         // Try the user implementations first, then the built-ins
         List<Class<? extends VFS>> impls = new ArrayList<>();
         // 优先使用用户自己加载的
         impls.addAll(USER_IMPLEMENTATIONS);
         // 使用系统默认的
         impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));

         // 遍历所有实现类,获取一个有效的
         VFS vfs = null;
         for (int i = 0; vfs == null || !vfs.isValid(); i++) {
             // 当获取不到vfs对象时或者找到有效的vfs对象时结束.

             // 获取vfs实例类型
             Class<? extends VFS> impl = impls.get(i);
             try {
                 // 实例化vfs
                 vfs = impl.newInstance();
                 if (vfs == null || !vfs.isValid()) {
                     if (log.isDebugEnabled()) {
                         log.debug("VFS implementation " + impl.getName() +
                                 " is not valid in this environment.");
                     }
                 }
             } catch (InstantiationException e) {
                 log.error("Failed to instantiate " + impl, e);
                 return null;
             } catch (IllegalAccessException e) {
                 log.error("Failed to instantiate " + impl, e);
                 return null;
             }
         }

         if (log.isDebugEnabled()) {
             log.debug("Using VFS adapter " + vfs.getClass().getName());
         }

         return vfs;
     }
 }

方法很长,但是逻辑很简单,就是通过反射机制获取VFS实例对象,其中用户自定义实现优先级要高于系统默认实现。

单例也是常用的设计模式之一,我们通常称之为单例模式,他是一种创建型模式,他的作用是确保指定的对象在一定的范围内是独一无二的,这个范围通常就是整个系统内。

在这里VFSHolder是一个饿汉型的单例模式的应用。

VFS对外暴露了四个方法,其中:

  • getInstance方法用于获取VFS的单例对象:
    /**
      * 获取VFS实现
      */
     public static VFS getInstance() {
         return VFSHolder.INSTANCE;
     }
    
  • addImplClass用于添加用户自定义的VFS实现类
    /**
    * 添加用户自定义的VFS实现,自定义实现优先级高于内置的实现
    *
    * @param clazz 被添加的{@link VFS}实现
    */
    public static void addImplClass(Class<? extends VFS> clazz) {
      if (clazz != null) {
          USER_IMPLEMENTATIONS.add(clazz);
      }
    }
    
  • isValid方法用于判断当前VFS实例是否有效,比如JBoss6VFS在没有引入jboss-vfs依赖的前提下,他就是无效的。
    /**
    * 在当前环境内,该VFS实现是否有效
    */
    public abstract boolean isValid();
    
  • list方法用于递归获取指定路径下的所有资源列表
    /**
    * 递归获取指定路径下的所有资源列表
    *
    * @param path 资源路径
    * @return 所有资源
    * @throws IOException If I/O errors occur
    */
    public List<String> list(String path) throws IOException {
      List<String> names = new ArrayList<>();
      for (URL url : getResources(path)) {
          names.addAll(list(url, path));
      }
      return names;
    }
    
    关于VFS的实现类的解析,我们会在后面进行补充。

Log

我们继续看mybatis初始化Log所做的工作。

用户可以通过mybatis的全局配置文件来指定mybatis系统内部使用的日志实现类:

<properties>
    <property name="logImpl" value="日志实现"/>
</properties>

对于logImpl属性的解析工作是由方法loadCustomLogImpl来完成的:

/**
  * 加载自定义日志实现类
  *
  * @param props 全局配置
  */
 private void loadCustomLogImpl(Properties props) {
     // 获取日志实现类
     Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
     // 配置日志实现
     configuration.setLogImpl(logImpl);
 }

该方法的实现也比较简单,调用resolveClass方法获取日志实现类,并把后续的日志配置操作委托给了configurationsetLogImpl方法来完成。

resolveClass方法是在XMLConfigBuilder的父类BaseBuilder中定义的一个方法,它用于将指定的类名转换为对应的JAVA类型,他提供了对mybatis别名机制的支持.

/**
  * 读取指定别名的实际类型
  *
  * @param alias 别名
  * @param <T>   实际类型
  * @return 指定别名的实际类型
  */
 protected <T> Class<? extends T> resolveClass(String alias) {
     if (alias == null) {
         return null;
     }
     try {
         // 解析别名
         return resolveAlias(alias);
     } catch (Exception e) {
         throw new BuilderException("Error resolving class. Cause: " + e, e);
     }
 }

具体实现是由借助resolveAlias方法委托给TypeAliasRegistryresolveAlias方法来完成的:

/**
    * 从别名注册表中取出指定的类
    *
    * @param alias 别名
    * @param <T>   指定的类型
    */
   protected <T> Class<? extends T> resolveAlias(String alias) {
       return typeAliasRegistry.resolveAlias(alias);
   }

TypeAliasRegistry这个类我们在前面稍有提及,他提供了注册类型别名和解析类型别名的能力,在mybatis中担任类型别名注册表的职责。

他的无参构造方法,默认完成了一些常用类型的别名注册工作:

public TypeAliasRegistry() {
   registerAlias("string", String.class);

   registerAlias("byte", Byte.class);
   registerAlias("long", Long.class);
   registerAlias("short", Short.class);
   registerAlias("int", Integer.class);
   registerAlias("integer", Integer.class);
   registerAlias("double", Double.class);
   registerAlias("float", Float.class);
   registerAlias("boolean", Boolean.class);

   registerAlias("byte[]", Byte[].class);
   registerAlias("long[]", Long[].class);
   registerAlias("short[]", Short[].class);
   registerAlias("int[]", Integer[].class);
   registerAlias("integer[]", Integer[].class);
   registerAlias("double[]", Double[].class);
   registerAlias("float[]", Float[].class);
   registerAlias("boolean[]", Boolean[].class);

   registerAlias("_byte", byte.class);
   registerAlias("_long", long.class);
   registerAlias("_short", short.class);
   registerAlias("_int", int.class);
   registerAlias("_integer", int.class);
   registerAlias("_double", double.class);
   registerAlias("_float", float.class);
   registerAlias("_boolean", boolean.class);

   registerAlias("_byte[]", byte[].class);
   registerAlias("_long[]", long[].class);
   registerAlias("_short[]", short[].class);
   registerAlias("_int[]", int[].class);
   registerAlias("_integer[]", int[].class);
   registerAlias("_double[]", double[].class);
   registerAlias("_float[]", float[].class);
   registerAlias("_boolean[]", boolean[].class);

   registerAlias("date", Date.class);
   registerAlias("decimal", BigDecimal.class);
   registerAlias("bigdecimal", BigDecimal.class);
   registerAlias("biginteger", BigInteger.class);
   registerAlias("object", Object.class);

   registerAlias("date[]", Date[].class);
   registerAlias("decimal[]", BigDecimal[].class);
   registerAlias("bigdecimal[]", BigDecimal[].class);
   registerAlias("biginteger[]", BigInteger[].class);
   registerAlias("object[]", Object[].class);

   registerAlias("map", Map.class);
   registerAlias("hashmap", HashMap.class);
   registerAlias("list", List.class);
   registerAlias("arraylist", ArrayList.class);
   registerAlias("collection", Collection.class);
   registerAlias("iterator", Iterator.class);

   registerAlias("ResultSet", ResultSet.class);
}

registerAlias方法负责完成类型别名和类型映射关系的具体注册工作:

/**
 * 将指定类型和别名注册到别名注册表
 *
 * @param alias 别名
 * @param value 指定类型
 */
public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    // 小写化
    String key = alias.toLowerCase(Locale.ENGLISH);
    // 不可重复注册,校验是否已经注册过
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
        throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    // 添加到别名映射表
    TYPE_ALIASES.put(key, value);
}

他的实现也比较简单,只有三步操作,将别名小写,校验别名是否被重复注册,保存到TYPE_ALIASES集合内。

TYPE_ALIASESTypeAliasRegistry中唯一的一个属性,他被定义为Map<String, Class<?>>类型,负责维护类型别名和具体的类型的关系。

/**
 * 负责维护类型别名和具体的类型的关系
 */
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>();

在简单的了解TypeAliasRegistry对象之后,我们回到XMLConfigBuilderloadCustomLogImpl方法上来,看一下他被BaseBuilder调用的resolveAlias(String)方法:

/**
 * 将类型别名解析为具体的类型
 *
 * @param string 类型别名或者完全限定名
 */
@SuppressWarnings("unchecked")
// throws class cast exception as well if types cannot be assigned
public <T> Class<T> resolveAlias(String string) {
    try {
        if (string == null) {
            return null;
        }
        // issue #748
        String key = string.toLowerCase(Locale.ENGLISH);
        Class<T> value;
        if (TYPE_ALIASES.containsKey(key)) {
            // 优先从别名注册表加载
            value = (Class<T>) TYPE_ALIASES.get(key);
        } else {
            // 假设string是一个全限定类名称,通过反射获取
            value = (Class<T>) Resources.classForName(string);
        }
        return value;
    } catch (ClassNotFoundException e) {
        throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
}

该方法的实现也比较简单,在将类型别名小写化后,会优先尝试从现有类型别名映射关系中获取其对应的java类型,如果没有的话,则将该别名作为一个全限定名称通过反射获取其对应的java类型。

在获取到具体的日志实现类型之后,XMLConfigBuilder会将该日志实现类型作为参数传递给ConfigurationsetLogImpl方法,完成后续日志实现的配置.

ConfigurationsetLogImpl方法中,先简单的记录了当前日志实现类,之后就将具体更新日志实现的操作交给了LogFactoryuseCustomLogging方法:

public void setLogImpl(Class<? extends Log> logImpl) {
    if (logImpl != null) {
        // 缓存记录当前日志实现类
        this.logImpl = logImpl;
        // 更新具体的日志实现
        LogFactory.useCustomLogging(this.logImpl);
    }
}

在看LogFactoryuseCustomLogging方法之前,我们先简单了解一下LogFactory

LogFactory是一个工厂类,他的作用是获取Log接口的实例对象,Log接口提供了mybatis中操作日志的统一接口定义。

LogFactory内部定义了一个静态代码块用于加载默认的Log接口实现类:

static {
    // 尝试加载一个有效的日志实现
    // SL4J
    tryImplementation(LogFactory::useSlf4jLogging);
    // COMMONS-LOGGING
    tryImplementation(LogFactory::useCommonsLogging);
    // LOG4J2
    tryImplementation(LogFactory::useLog4J2Logging);
    // LOG4J
    tryImplementation(LogFactory::useLog4JLogging);
    // JDK-LOGGING
    tryImplementation(LogFactory::useJdkLogging);
    // NO-LOGGING
    tryImplementation(LogFactory::useNoLogging);
}

加载的顺序以及采用日志实现的优先级依次是:SL4J,COMMONS-LOGGING,LOG4J2,LOG4J,JDK-LOGGING,NO-LOGGING。 上面涉及到的以use开头的各个方法实际上只是对setImplementation方法的一层简单封装:

public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
}

public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}

public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}

public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}

public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}

public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}

public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}

public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}

因此实现加载日志实现的方法tryImplementation实际上也是对setImplementation方法的一层包装,他忽略掉了setImplementation可能抛出的异常:

/**
 * 尝试加载日志实现类
 */
private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
        try {
            runnable.run();
        } catch (Throwable t) {
            // ignore
        }
    }
}

logConstructor属性记录了当前使用的Log实现类的构造方法,调用tryImplementation方法的时候,只有在logConstructor的值没有被指定时,才会尝试加载新的日志实现类。

setImplementation方法中,LogFactory会尝试获取Log实现类中入参为String的构造方法并赋值给logConstructor属性,如果Log实现没有入参为String的构造方法 ,将会导致异常的发生。

/**
 * 配置日志实现类
 *
 * @param implClass 日志实现
 */
private static void setImplementation(Class<? extends Log> implClass) {
    try {
        // 获取构造
        Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
        // 生成实例
        Log log = candidate.newInstance(LogFactory.class.getName());
        if (log.isDebugEnabled()) {
            log.debug("Logging initialized using '" + implClass + "' adapter.");
        }
        logConstructor = candidate;
    } catch (Throwable t) {
        throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
}

对于mybatis的日志解析,到这里就暂时告一段落,关于更具体的日志实现,我们会在后面进行补充。

XMLConfigBuilderparseConfiguration方法完成VFSLOG的配置工作之后,我们就迎来了typeAliases元素的解析工作。