基础功能包用来为其他包提供一些基础功能。这些包与MyBatis核心逻辑的耦合度很低,甚至有很多包可以在创建其他项目时直接复用。
1. exceptions
exceptions包为 MyBatis定义了绝大多数异常类的父类,同时也提供了异常类的生产工厂。
1.1 Java异常
“异常”代表程序运行中遇到了意料之外的事情,为了表征异常,Java标准库中内建了一些通用的异常,这些类以 Throwable为父类。而 Throwable又派生出 Error类和 Exception类两大子类。
- Error及其子类,代表了 JVM自身的异常。这一类异常发生时,无法通过程序来修正。最可靠的方式就是尽快停止 JVM的运行。
- Exception 及其子类,代表程序运行中发生了意料之外的事情。这些意外的事情可以被 Java异常处理机制处理。
而 Exception类及其子类又可以划分为两大类:
- RuntimeException及其子类:这一类异常其实是程序设计的错误,通过修正程序设计是可以避免的,如数组越界异常、数值异常等。
- 非RuntimeException及其子类:这一类异常的发生通常由外部因素导致,是不可预知和避免的,如 IO异常、类型寻找异常等。
在所有异常中,Error及其子类代表JVM出现异常,且无法通过软件修复;RuntimeException 及其子类是程序设计的错误,可以在编写程序时避免。以上这两大类异常称为免检异常,即不需要对这两类异常进行强制检查。而除上述两类异常外的其他异常,它们的发生与外部环境有关,称为必检异常。在编写程序时必须用 try、catch 语句将其包围起来。对于 Throwable 对象,其主要的成员变量有 detailMessage和 cause。
detailMessage 为一个字符串,用来存储异常的详细信息。
cause 为另一个 Throwable 对象,用来存储引发异常的原因。这是因为一个异常发生时,通常引发异常的上级程序也发生异常,从而导致一连串的异常产生,叫作异常链。一个异常的 cause属性可以指向引发它的下级异常,从而将整个异常链保存下来。
1.2 序列化和反序列化
序列化是把对象转换为字节序列的过程;反序列化是把字节序列恢复为对象的过程。对象的序列化主要有两个目的:一是将对象转化成字节后保存在存储介质中,即为了持久化对象;二是将对象转化成字节后在网络上传输,即为了传输对象。
在 Java中,要表明一个类的对象是可序列化的,则必须继承 Serializable接口或其子接口 Externalizable接口。Serializable 接口的使用非常简单,只要一个类实现了该接口,便表明该类的对象是可序列化的,而不需要增加任何方法。
序列化与反序列化过程中,要面临版本问题。例如,将一个 User类的对象 user1持久化到了硬盘中,然后增删了 User类的属性,那么此时还能将持久化在硬盘中的 user1对象的序列还原成一个新的 User类的对象吗?该问题的回答需要涉及 Serializable接口的 serialVersionUID字段,serialVersionUID字段叫作序列化版本控制字段。在反序列化过程中,如果对象字节序列中的 serialVersionUID与当前类的该值不同,则反序列化失败,否则成功。
如果没有显式地为一个类定义 serialVersionUID属性,系统就会自动生成一个。自动生成的序列化版本控制字段与类的类名、类及其属性修饰符、接口及接口顺序、属性、构造函数等相关,其中任何一项的改变都会导致 serialVersionUID发生变化。因此,对于上面所述的 user1对象的序列能否还原成一个新的 User类的对象,需要分情况进行讨论。
- 如果旧 User类和新 User类中均有 serialVersionUID字段,且其值一样,则持久化在硬盘中的 user1对象的序列可以还原成一个新的 User类的对象。还原的过程中,新User类对象中新增的属性值为 null。
- 如果旧 User 类和新 User 类中一方不含 serialVersionUID 字段,或两方都含有serialVersionUID字段但其值不同,则无法反序列化,反序列化过程中会报出序列号版本不一致异常(InvalidClassException)。
在使用时,一般都会为实现Serializable接口的类显式声明一个serialVersionUID。这样便可以:
- 在希望类的版本间实现序列化和反序列化的兼容时,保持 serialVersionUID值不变。
- 在希望类的版本间序列化和反序列化不兼容时,确保 serialVersionUID值发生变化。
1.3 MyBatis的异常类
exceptions包中有三个与Exception相关的类,分别是IbatisException类、PersistenceException类和 TooManyResultsException类。在 MyBatis的其他包中,还有许多异常类。这些异常类中除 RuntimeSqlException类外,均为 PersistenceException的子类。
IbatisException类仅仅作为PersistenceException类的父类存在,所以IbatisException类是被架空的,IbatisException类上有@Deprecated注解,表明该类在未来可能会被废弃。
通过 MyBatis异常类的类图还可以看出,众多的异常类并没有放在 exceptions包中,而是散落在其他各个包中。这涉及项目规划时的分包问题。exceptions包就是按照类型划分出来的,但也有许多异常类按照功能划分到了其他包中。MyBatis 中的包也是按照上述两种方式划分的,一类是按照类型划分出来的包,如exceptions包、annotations包;一类是按照功能划分出来的包,如 logging包、plugin包。
通常,在规划一个项目的包结构时,可以按照以下两种方式进行包的划分。
- 按照类型方式划分,例如将所有的接口类放入一个包,将所有的 Controller类放入一个包。这种分类方式从类型上看更为清晰,但是会将完成同一功能的多个类分散在不同的包中,不便于模块化开发。
- 按照功能方式划分,例如将所有与加/解密有关的类放入一个包,将所有与 HTTP请求有关的类放入一个包。这种分类方式下,同一功能的类内聚性高,便于模块化开发,但会导致同一包内类的类型混乱。
在项目设计和开发中,我们推荐优先将功能耦合度高的类放入按照功能划分的包中,而将功能耦合度低或供多个功能使用的类放入按照类型划分的包中。这种划分思想不仅可以用在包的划分上,类、方法、代码片段的组合与拆分等都可以参照这种思想。
下面继续分析 PersistenceException类和 TooManyResultsException类。与 MyBatis中的众多其他 Exception类一样,这两个类都有一个设置了值的 serialVersionUID字段,并且每个类都有四种构造方法:
- 无参构造方法;
- 传入错误信息字符串的构造方法;
- 传入上级 Throwable实例的构造方法;
- 传入上级 Throwable实例和错误信息字符串的构造方法。
为 Throwable 类及其子类创建上述四种构造方法几乎是惯例。这样一来,无论已知几个输入参数信息,都可以方便地调用合适的构造方法创建实例。
1.4 ExceptionFactory类
ExceptionFactory类只有两个方法:
- 构造方法由 private修饰,确保该方法无法在类的外部被调用,也就永远无法生成该类的实例;
- wrapException方法就是 ExceptionFactory类提供的静态方法,它用来生成并返回一个RuntimeException对象;
通常,会对一些工具类、工厂类等仅提供静态方法的类进行这样的设置,因为这些类不需要实例化就可以使用。
package org.apache.ibatis.exceptions;
import org.apache.ibatis.executor.ErrorContext;
/**
* @author Clinton Begin
*/
public class ExceptionFactory {
// 不允许实例化该类
private ExceptionFactory() {
// Prevent Instantiation
}
// 静态方法,直接调用
/**
* 生成一个RuntimeException异常
* @param message 异常信息
* @param e 异常
* @return 新的RuntimeException异常
*/
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
}
2. reflection
2.1 装饰器模式
这种设计模式是指能够在一个类的基础上增加一个装饰类(或叫包装类),并在装饰类中增加一些新的特性和功能。通过对原有类的包装,就可以在不改变原有类的情况下,为原有类增加更多的功能。
装饰器模式在编程开发中经常使用。
- 通常的使用场景是在一个核心基本类的基础上,提供大量的装饰类,从而使核心基本类经过不同的装饰类修饰后获得不同的功能。
- 装饰类还有一个优点,就是可以叠加使用,即一个核心基本类可以被多个装饰类修饰,从而同时具有多个装饰类的功能。
2.2 反射
通过 Java反射,能够在类的运行过程中知道这个类有哪些属性和方法,还可以修改属性、调用方法、建立类的实例。Java反射机制主要提供了以下功能。
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时修改任意一个对象的成员变量;
- 在运行时调用任意一个对象的方法。
反射极大地提升了 Java 的灵活性,降低了 diffObj 方法和输入参数的耦合,使功能更为通用。
参考 ObjectLogger
2.3 Type接口及其子类
在反射中,Type接口代表一个类型,此接口中只定义了一个方法。
Type接口的子类:
- Class类:它代表运行的 Java程序中的类和接口,枚举类型(属于类)、注解(属于接口)也都是 Class类的子类。
- WildcardType 接口:它代表通配符表达式。例如,“?”“?extends Number”“?super Integer”都是通配符表达式。
- TypeVariable 接口:它是类型变量的父接口。例如,“Map<K,V>”中的“K”“V”就是类型变量。ParameterizedType 接口:它代表参数化的类型。例如,“Collection <String>”就是参数化的类型。
- GenericArrayType接口:它代表包含 ParameterizedType或者 TypeVariable元素的列表。
2.4 factory子包
factory子包中的类用来基于反射生产出各种对象,首先看ObjectFactory接口,它有以下几个方法:
package org.apache.ibatis.reflection.factory;
import java.util.List;
import java.util.Properties;
/**
* MyBatis uses an ObjectFactory to create all needed new Objects.
*
* @author Clinton Begin
* 对象工厂
*/
public interface ObjectFactory {
/**
* Sets configuration properties. 设置工厂的配置属性
* @param properties configuration properties
*/
default void setProperties(Properties properties) {
// NOP
}
/**
* Creates a new object with default constructor. 采用无参构造方法生成这个类型的对象
* @param type Object type 对象类型
* @return
*/
<T> T create(Class<T> type);
/**
* Creates a new object with the specified constructor and params. 根据参数列表找到相应的有参构造方法生成这个类型的实例
* @param type Object type 对象类型
* @param constructorArgTypes Constructor argument types 有参构造的参数类型
* @param constructorArgs Constructor argument values 有参构造的参数值
* @return
*/
<T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
/**
* Returns true if this object can have a set of other objects.
* It's main purpose is to support non-java.util.Collection objects like Scala collections.
*
* @param type Object type 对象类型
* @return whether it is a collection or not 判断传入的类型是否是集合类型
* @since 3.1.0
*/
<T> boolean isCollection(Class<T> type);
}
DefaultObjectFactory 继承了 ObjectFactory 接口,是默认的对象工厂实现。DefaultObjectFactory的 create方法用来生产对象,而两个 create方法最终都用到了instantiateClass方法。
instantiateClass 方法能够通过反射找到与参数匹配的构造方法,然后基于反射调用该构造方法生成一个对象。
/**
* 创建类的实例
* @param type 要创建实例的类
* @param constructorArgTypes 构造方法入参类型
* @param constructorArgs 构造方法入参
* @param <T> 实例类型
* @return 创建的实例
*/
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
// 构造方法
Constructor<T> constructor;
if (constructorArgTypes == null || constructorArgs == null) { // 参数类型列表为null或者参数列表为null
// 因此获取无参构造函数
constructor = type.getDeclaredConstructor();
try {
// 使用无参构造函数创建对象
return constructor.newInstance();
} catch (IllegalAccessException e) {
// 如果发生异常,则修改构造函数的访问属性后再次尝试
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance();
} else {
throw e;
}
}
}
// 根据入参类型查找对应的构造器
constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[constructorArgTypes.size()]));
try {
// 采用有参构造函数创建实例
return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()]));
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
// 如果发生异常,则修改构造函数的访问属性后再次尝试
constructor.setAccessible(true);
return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()]));
} else {
throw e;
}
}
} catch (Exception e) {
// 收集所有的参数类型
String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList)
.stream().map(Class::getSimpleName).collect(Collectors.joining(","));
// 收集所有的参数
String argValues = Optional.ofNullable(constructorArgs).orElseGet(Collections::emptyList)
.stream().map(String::valueOf).collect(Collectors.joining(","));
throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e);
}
}
DefaultObjectFactory中还有一个 resolveInterface方法。当传入的目标类型是一个接口时,该方法可给出一个符合该接口的实现。例如,当创建一个 Map对象时,最终会创建一个 HashMap对象。
// 判断要创建的目标对象的类型,即如果传入的是接口则给出它的一种实现
protected Class<?> resolveInterface(Class<?> type) {
Class<?> classToCreate;
if (type == List.class || type == Collection.class || type == Iterable.class) {
classToCreate = ArrayList.class;
} else if (type == Map.class) {
classToCreate = HashMap.class;
} else if (type == SortedSet.class) { // issue #510 Collections Support
classToCreate = TreeSet.class;
} else if (type == Set.class) {
classToCreate = HashSet.class;
} else {
classToCreate = type;
}
return classToCreate;
}
2.5 invoker子包
reflection 包下的 invoker 子包是执行器子包,该子包中的类能够基于反射实现对象方法的调用和对象属性的读写。通过反射可以很方便地调用对象的方法和读写方法的属性。而 invoker子包则进一步封装和简化了这些操作。
invoker 子包有一个 Invoker 接口和三个实现类,Invoker接口的三个实现类分别用来处理三种不同情况:
- GetFieldInvoker:负责对象属性的读操作;
- SetFieldInvoker:负责对象属性的写操作;
- MethodInvoker:负责对象其他方法的操作。
阅读 Invoker接口的源码,它只定义了以下两个抽象方法。
- invoke方法,即执行方法。该方法负责完成对象方法的调用和对象属性的读写。在三个实现类中,分别是属性读取操作、属性赋值操作、方法触发操作。
- getType方法,用来获取类型。它对于 GetFieldInvoker和 SetFieldInvoker的含义也是明确的,即获得目标属性的类型。可 MethodInvoker对应的是一个方法,getType方法对于 MethodInvoker类型而言的意义:如果一个方法有且只有一个输入参数,则 type为输入参数的类型;否则,type为方法返回值的类型。
2.6 property子包
reflection包下的 property子包是属性子包,该子包中的类用来完成与对象属性相关的操作。
2.6.1 PropertyCopier
如果想让 user2的属性和 user1的属性完全一致,则需要对属性一一进行复制,这样的过程是繁杂的。PropertyCopier 作为属性复制器,就是用来解决上述问题的。借助于属性复制器PropertyCopier,我们可以方便地将一个对象的属性复制到另一个对象中:
User user1 = new User(1,"Shawn",18);
User user2 = new User();
user2.setId(user1.getId());
user2.setName(user1.getName());
user2.setAge(user1.getAge());
System.out.println("======");
User user3 = new User();
PropertyCopier.copyBeanProperties(user1.getClass(),user1,user2);
属性复制器 PropertyCopier的属性复制工作在 copyBeanProperties方法中完成:
/**
* 完成对象的输出拷贝
* @param type 对象的类型
* @param sourceBean 提供属性值的对象
* @param destinationBean 要被写入新属性值的对象
*/
public static void copyBeanProperties(Class<?> type, Object sourceBean, Object destinationBean) {
// 这两个对象同属的类
Class<?> parent = type;
while (parent != null) {
// 获取该类的所有属性
final Field[] fields = parent.getDeclaredFields();
// 循环遍历属性进行拷贝
for (Field field : fields) {
try {
try {
field.set(destinationBean, field.get(sourceBean));
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
field.setAccessible(true);
field.set(destinationBean, field.get(sourceBean));
} else {
throw e;
}
}
} catch (Exception e) {
// Nothing useful to do, will only fail on final fields, which will be ignored.
}
}
parent = parent.getSuperclass();
}
}
copyBeanProperties方法的工作原理非常简单:通过反射获取类的所有属性,然后依次将这些属性值从源对象复制出来并赋给目标对象。但是要注意一点,该属性复制器无法完成继承得来的属性的复制,因为 getDeclaredFields方法返回的属性中不包含继承属性。
2.6.2 PropertyNamer
PropertyNamer提供属性名称相关的操作功能,例如,通过 get、set方法的方法名找出对应的属性等。要想让 PropertyNamer 正常地发挥作用,需保证对象属性、方法的命名遵循 Java Bean的命名规范,即:
- 如果类的成员变量的名字是 abc,那么该属性对应的读写方法分别为 getAbc()和 setAbc()。
- 如果类的属性是 boolean 类型,则允许使用“is”代替上面的“get”,读方法命名为 isAbc()。
2.6.3 PropertyTokenizer
PropertyTokenizer 是一个属性分词器。传入一个形如“student[sId].name”的字符串后,该标记器会将其拆分开,放入各个属性中:
/**
* 假设传入的为student[sId].name
* 则各个属性得到以下结果
*
* 该属性标记器只能处理一级,即点后面的都作为children
*/
public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
// student
private String name;
// student[sId]
private final String indexedName;
// sId
private String index;
// name
private final String children;
public PropertyTokenizer(String fullname) {
int delim = fullname.indexOf('.');
if (delim > -1) {
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
name = fullname;
children = null;
}
indexedName = name;
delim = name.indexOf('[');
if (delim > -1) {
index = name.substring(delim + 1, name.length() - 1);
name = name.substring(0, delim);
}
}
...
}
2.7 wrapper子包
reflection包下的 wrapper子包是对象包装器子包,该子包中的类使用装饰器模式对各种类型的对象(包括基本 Bean对象、集合对象、Map对象)进行进一步的封装,为其增加一些功能,使它们更易于使用。wrapper子包类图如图:
2.7.1 ObjectWrapperFactory 和 DefaultObjectWrapperFactory
ObjectWrapperFactory 是对象包装器工厂的接口,DefaultObjectWrapperFactory 是它的默认实现。不过该默认实现中并没有实现任何功能。MyBatis 也允许用户通过配置文件中的 objectWrapperFactory节点来注入新的 ObjectWrapperFactory。
2.7.2 ObjectWrapper
ObjectWrapper接口是所有对象包装器的总接口。以 BeanWrapper为例,我们介绍一下包装器的实现。
在介绍之前我们先了解 reflection包中的两个类:MetaObject类和 MetaClass类。
meta 在中文中常译为“元”,在英文单词中作为词头有“涵盖”“超越”“变换”等多种含义。在这里,这三种含义都是存在的。例如,MetaObject类中涵盖了对应 Object类中的全部信息,并经过变化和拆解得到了一些更为细节的信息。因此,可以将 MetaObject类理解为一个涵盖对象(Object)中更多细节信息和功能的类,称为“元对象”。同理,MetaClass就是一个涵盖了类型(Class)中更多细节信息和功能的类,称为“元类”。
BeanWrapper属性中的 metaObject 属性从其父类 BaseWrapper 继承而来。通过对 BeanWrapper属性的了解,加上对 MetaObject类和 MetaClass类的简单介绍,可以得出结论:BeanWrapper中包含了一个 Bean的对象信息、类型信息,并提供了更多的一些功能,存在的方法有:
- get:获得被包装对象某个属性的值;
- set:设置被包装对象某个属性的值;
- findProperty:找到对应的属性名称;
- getGetterNames:获得所有的属性 get方法名称;
- getSetterNames:获得所有的属性 set方法名称;
- getSetterType:获得指定属性的 set方法的类型;
- getGetterType:获得指定属性的 get方法的类型;
- hasSetter:判断某个属性是否有对应的 set方法;
- hasGetter:判断某个属性是否有对应的 get方法;
- instantiatePropertyValue:实例化某个属性的值。
因此,一个 Bean经过 BeanWrapper封装后,就可以暴露出大量的易用方法,从而可以简单地实现对其属性、方法的操作。同理,wrapper子包下的 CollectionWrapper、MapWrapper与 BeanWrapper一样,它们分别负责包装 Collection和 Map类型,从而使它们暴露出更多的易用方法。BaseWrapper作为 BeanWrapper和 MapWrapper的父类,为这两个类提供一些共用的基础方法。源码阅读时,遇到同类型的类(一般具有类似的名称、功能),可以重点阅读其中的一个类。当这个类的源码阅读清楚时,同类型类的源码也就清晰了。
2.8 反射核心类 Reflector
reflection包中最为核心的类就是 Reflector类。Reflector 类负责对一个类进行反射解析,并将解析后的结果在属性中存储起来。
/**
* This class represents a cached set of class definition information that
* allows for easy mapping between property names and getter/setter methods.
*
* @author Clinton Begin
*/
public class Reflector {
// 要被反射解析的类
private final Class<?> type;
// 能够读的属性列表,即有get方法的属性列表
private final String[] readablePropertyNames;
// 能够写的属性列表,即有set方法的属性列表
private final String[] writablePropertyNames;
// set方法映射表。键为属性名,值为对应的set方法
private final Map<String, Invoker> setMethods = new HashMap<>();
// get方法映射表。键为属性名,值为对应的get方法
private final Map<String, Invoker> getMethods = new HashMap<>();
// set方法输入类型。键为属性名,值为对应的该属性的set方法的类型(实际为set方法的第一个参数的类型)
private final Map<String, Class<?>> setTypes = new HashMap<>();
// get方法输出类型。键为属性名,值为对应的该属性的set方法的类型(实际为set方法的返回值类型)
private final Map<String, Class<?>> getTypes = new HashMap<>();
// 默认构造函数
private Constructor<?> defaultConstructor;
// 大小写无关的属性映射表。键为属性名全大写值,值为属性名
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
/**
* Reflector的构造方法
* @param clazz 需要被反射处理的目标类
*/
public Reflector(Class<?> clazz) {
// 要被反射解析的类
type = clazz;
// 设置默认构造器属性
addDefaultConstructor(clazz);
// 解析所有的getter
addGetMethods(clazz);
// 解析所有有setter
addSetMethods(clazz);
// 解析所有属性
addFields(clazz);
// 设定可读属性
readablePropertyNames = getMethods.keySet().toArray(new String[0]);
// 设定可写属性
writablePropertyNames = setMethods.keySet().toArray(new String[0]);
// 将可读或者可写的属性放入大小写无关的属性映射表
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
....
}
Reflector 类将一个类反射解析后,会将该类的属性、方法等一一归类放到以上的各个属性中。因此 Reflector类完成了主要的反射解析工作,这也是我们将其称为反射核心类的原因。reflection包中的其他类则多是在其反射结果的基础上进一步包装的,使整个反射功能更易用。Reflector类反射解析一个类的过程是由构造函数触发的,逻辑非常清晰。具体到每个子方法,其逻辑比较简单。下面以其中的 addGetMethods 方法为例进行介绍。addGetMethods方法的功能是分析参数中传入的类,将类中的 get方法添加到 getMethods方法中。
/**
* 找出类中的get方法
* @param clazz 需要被反射处理的目标类
*/
private void addGetMethods(Class<?> clazz) {
// 存储属性的get方法。Map的键为属性名,值为get方法列表。某个属性的get方法用列表存储是因为前期可能会为某一个属性找到多个可能get方法。
Map<String, List<Method>> conflictingGetters = new HashMap<>();
// 找出该类中所有的方法
Method[] methods = getClassMethods(clazz);
// 过滤出get方法,过滤条件有:无入参、符合Java Bean的命名规则;然后取出方法对应的属性名、方法,放入conflictingGetters
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
// 如果一个属性有多个疑似get方法,resolveGetterConflicts用来找出合适的那个
resolveGetterConflicts(conflictingGetters);
}
其中的 conflictingGetters变量是一个 Map,它的 key是属性名称,value是该属性可能的get方法的列表。但是,最终每个属性真正的 get方法应该只有一个。resolveGetterConflicts方法负责尝试找出该属性真正的 get方法。
ReflectorFactory是 Reflector的工厂接口,而 DefaultReflectorFactory是该工厂接口的默认实现。下面直接以 DefaultReflectorFactory为例,介绍 Reflector工厂。DefaultReflectorFactory 中最核心的方法就是用来生成一个类的 Reflector 对象的findForClass方法:
/**
* 工厂接口
*/
public interface ReflectorFactory {
boolean isClassCacheEnabled();
void setClassCacheEnabled(boolean classCacheEnabled);
Reflector findForClass(Class<?> type);
}
/**
* 工厂接口的默认实现
*/
public class DefaultReflectorFactory implements ReflectorFactory {
private boolean classCacheEnabled = true;
private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>();
public DefaultReflectorFactory() {
}
@Override
public boolean isClassCacheEnabled() {
return classCacheEnabled;
}
@Override
public void setClassCacheEnabled(boolean classCacheEnabled) {
this.classCacheEnabled = classCacheEnabled;
}
/**
* 生产Reflector对象
* @param type 目标类型
* @return 目标类型的Reflector对象
*/
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) { // 允许缓存
// 生产入参type的反射器对象,并放入缓存
return reflectorMap.computeIfAbsent(type, Reflector::new);
} else {
return new Reflector(type);
}
}
}
2.9 反射包装类 MetaObject 和 MetaClass
reflection包中存在许多的包装类,它们使用装饰器模式(又称包装模式)将许多反射相关的类包装得更为易用。wrapper子包中的包装类依赖两个更为基础的包装类:MetaClass类和MetaObject类。
MetaObject 被称为元对象,是一个针对普通 Object 对象的反射包装类,整个包装类中除了原始对象本身外,还包装了对象包装器、对象工厂、对象包装器工厂、反射工厂等。因此,只要使用 MetaObject对一个对象进行包装,包装类中就具有大量的辅助类,便于进行各种反射操作。
public class MetaObject {
// 原始对象
private final Object originalObject;
// 对象包装器
private final ObjectWrapper objectWrapper;
// 对象工厂
private final ObjectFactory objectFactory;
// 对象包装器工厂
private final ObjectWrapperFactory objectWrapperFactory;
// 反射工厂
private final ReflectorFactory reflectorFactory;
...
}
SystemMetaObject中限定了一些默认值,其中的 forObject方法可以使用默认值输出一个 MetaObject对象。因此,SystemMetaObject是一个只能使用默认值的 MetaObject工厂。
public final class SystemMetaObject {
public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
public static final MetaObject NULL_META_OBJECT = MetaObject.forObject(NullObject.class, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
private SystemMetaObject() {
// Prevent Instantiation of Static Class
}
private static class NullObject {
}
public static MetaObject forObject(Object object) {
return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
}
}
MetaClass 被称为元类,它对类做了进一步封装,其内部集成了类可能使用的反射器和反射器工厂。
2.10 异常拆包工具 ExceptionUtil
ExceptionUtil中的upwrapThrowable()方法,可以拆解InvocationTargetException和UndeclaredThrowableException异常的包装,从而得到被包装的真正异常。
public class ExceptionUtil {
private ExceptionUtil() {
// Prevent Instantiation
}
/**
* 拆解InvocationTargetException和UndeclaredThrowableException异常的包装,从而得到被包装的真正异常
* @param wrapped 包装后的异常
* @return 拆解出的被包装异常
*/
public static Throwable unwrapThrowable(Throwable wrapped) {
// 该变量用以存放拆包得到的异常
Throwable unwrapped = wrapped;
while (true) {
if (unwrapped instanceof InvocationTargetException) {
// 拆包获得内部异常
unwrapped = ((InvocationTargetException) unwrapped).getTargetException();
} else if (unwrapped instanceof UndeclaredThrowableException) {
// 拆包获得内部异常
unwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable();
} else {
// 该异常无需拆包
return unwrapped;
}
}
}
}
unwrapThrowable 方法的结构非常简单,但是我们需要思考它存在的意义:为什么需要给 InvocationTargetException和 UndeclaredThrowableException这两个类拆包?这两个类为什么要把其他异常包装起来?很多时候读懂源码的实现并不难,但是一定要多思考源码为什么这么写。只有这样,才能在源码阅读的过程中有更多的收获。InvocationTargetException为必检异常,UndeclaredThrowableException为免检的运行时异常。它们都不属于 MyBatis,而是来自 java.lang.reflect包。
反射操作中,代理类通过反射调用目标类的方法时,目标类的方法可能抛出异常。反射可以调用各种目标方法,因此目标方法抛出的异常是多种多样无法确定的。这意味着反射操作可能抛出一个任意类型的异常。可以用 Throwable 去接收这个异常,但这无疑太过宽泛。
InvocationTargetException就是为解决这个问题而设计的,当反射操作的目标方法中出现异常时,都统一包装成一个必检异常 InvocationTargetException。InvocationTargetException内部的 target 属性则保存了原始的异常。这样一来,便使得反射操作中的异常更易管理。
public class InvocationTargetException extends ReflectiveOperationException {
/**
* Use serialVersionUID from JDK 1.1.X for interoperability
*/
private static final long serialVersionUID = 4085088731926701167L;
/**
* This field holds the target if the
* InvocationTargetException(Throwable target) constructor was
* used to instantiate the object 用来保存被包装的异常
*
* @serial
*
*/
private Throwable target;
/**
* Constructs a InvocationTargetException with a target exception
* and a detail message. 构造方法
*
* @param target the target exception 被包装的异常
* @param s the detail message 异常的详细信息
*/
public InvocationTargetException(Throwable target, String s) {
super(s, null); // Disallow initCause
this.target = target;
}
}
下面再讲讲 UndeclaredThrowableException。根据 Java的继承原则,我们知道:如果子类中要重写父类中的方法,那么子类方法中抛出的必检异常必须是父类方法中声明过的类型。
在建立目标类的代理类时,通常是建立了目标类接口的子类或者目标类的子类。因此,将 Java的继承原则放在代理类和被代理类上可以演化为:
- 如果代理类和被代理类实现了共同的接口,则代理类方法中抛出的必检异常必须是在共同接口中声明过的;
- 如果代理类是被代理类的子类,则代理类方法中抛出的必检异常必须是在被代理类的方法中声明过的。
可是在代理类中难免会在执行某些方法时抛出一些共同接口或者父类方法中没有声明的必检异常,那这个问题该怎么解决呢?如果不抛出,则它是必检异常,必须抛出;如果抛出,则父接口或父类中没有声明该必检异常,不能抛出。答案就是这些必检异常会被包装为免检异常 UndeclaredThrowableException 后抛出。所以说 UndeclaredThrowableException 也是一个包装了其他异常的异常:
public class UndeclaredThrowableException extends RuntimeException {
static final long serialVersionUID = 330127114055056639L;
/**
* the undeclared checked exception that was thrown 被包装的必检异常
* @serial
*/
private Throwable undeclaredThrowable;
/**
* Constructs an {@code UndeclaredThrowableException} with the
* specified {@code Throwable} and a detail message. 构造方法
*
* @param undeclaredThrowable the undeclared checked exception
* that was thrown 被包装的必检异常
* @param s the detail message 异常的详细信息
*/
public UndeclaredThrowableException(Throwable undeclaredThrowable,
String s)
{
super(s, null); // Disallow initCause
this.undeclaredThrowable = undeclaredThrowable;
}
}
有一个简单的例子可以恰好同时涉及 InvocationTargetException 和 UndeclaredThrowablexception 这两个异常。就是代理类在进行反射操作时发生异常,于是异常被包装成 InvocationTargetException。InvocationTargetException显然没有在共同接口或者父类方法中声明过,于是又被包装成了UndeclaredThrowableException。这样,真正的异常就被包装了两层。这也是为什么在ExceptionUtil的unwrapThrowable方法中存在一个“while (true)”死循环,用来持续拆包。
总之,InvocationTargetException 和 UndeclaredThrowableException 这两个类都是异常包装类,需要拆包后才能得到真正的异常类。而 ExceptionUtil的 unwrapThrowable方法就可以完成该拆包工作。
2.11 参数名解析器 ParamNameResolver
ParamNameResolver 是一个参数名解析器,用来按顺序列出方法中的虚参,并对实参进行名称标注。
// 方法入参的参数次序表。键为参数次序,值为参数名称或者参数@Param注解的值
private final SortedMap<Integer, String> names;
// 该方法入参中是否含有@Param注解
private boolean hasParamAnnotation;
ParamNameResolver 类主要的方法有两个:构造方法 ParamNameResolver 和getNamedParams方法。构造方法 ParamNameResolver 能够将目标方法的参数名称依次列举出来。在列举的过程中,如果某个参数存在@Param注解,则会用注解的 value值替换参数名。经过构造方法 ParamNameResolver 解析后,可通过调试工具看到属性 names 和hasParamAnnotation中的值。
/**
* 参数名解析器的构造方法
* @param config 配置信息
* @param method 要被分析的方法
*/
public ParamNameResolver(Configuration config, Method method) {
// 获取参数类型列表
final Class<?>[] paramTypes = method.getParameterTypes();
// 准备存取所有参数的注解,是二维数组
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// 循环处理各个参数
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// 跳过特别的参数
continue;
}
// 参数名称
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
// 找出参数的注解
if (annotation instanceof Param) {
// 如果注解是Param
hasParamAnnotation = true;
// 那就以Param中值作为参数名
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// 否则,保留参数的原有名称
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// 参数名称取不到,则按照参数index命名
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
而 getNamedParams方法是在构造方法确定的 names属性和 hasParamAnnotation属性值的基础上,给出实参的参数名:
/**
* <p>
* A single non-special parameter is returned without a name.
* Multiple parameters are named using the naming rule.
* In addition to the default names, this method also adds the generic names (param1, param2,
* ...).
* </p>
*
* 将被解析的方法中的参数名称列表与传入的`Object[] args`进行对应,返回对应关系。
*
*
* 如果只有一个参数,直接返回参数
* 如果有多个参数,则进行与之前解析出的参数名称进行对应,返回对应关系
*
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 首先按照类注释中提供的key,存入一遍 【参数的@Param名称 或者 参数排序:实参值】
// 注意,key和value交换了位置
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
// 再按照param1, param2, ...的命名方式存入一遍
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
2.12 泛型解析器 TypeParameterResolver
TypeParameterResolver是泛型参数解析器。它的功能是什么?很多情况下,弄清一个类的功能对阅读其源码十分必要。假设有 User和 Student两个类:
public class User<T> {
public List<T> getInfo(){
return null;
}
}
public class Student extends User<Number> {
}
请问:Student类中的 getInfo方法(继承自父类 User)的输出参数类型是什么?
答案:是“List<Number>”。但是得出这个答案的过程却涉及 User 和 Student两个类。首先通过 User 类确定 getInfo 方法的输出结果是“List<T>”,然后通过 Student类得知“T”被设置为“Number”。因此,Student 类中的 getInfo 方法的输出参数是“List<Number>”。
TypeParameterResolver 类的功能就是完成上述分析过程,帮助 MyBatis 推断出属性、返回值、输入参数中泛型的具体类型。TypeParameterResolver便分析出 User类中的 getInfo方法的输出参数是“List<Object>”,Student类中的 getInfo方法的输出参数是“List<Number>”。
了解了 TypeParameterResolver类的功能后,下面来查看它的源码。它对外提供以下三个方法。
- resolveFieldType:解析属性的泛型;
- resolveReturnType:解析方法返回值的泛型;
- resolveParamTypes:解析方法输入参数的泛型。
上述这三个方法都只是将要解析的变量从属性、方法返回值、方法输入参数中找出来。变量的泛型解析才是最核心的工作。以resolveParamTypes方法为例,该方法将变量从方法输入参数中找出后,对每个变量都调用了 resolveType 方法。因此,resolveType是最重要的方法,resolveType方法根据目标类型的不同调用不同的子方法进行处理。
/**
* @return The parameter types of the method as an array of {@link Type}s. If they have type parameters in the declaration,<br>
* they will be resolved to the actual runtime {@link Type}s.
*/
/**
* 解析方法入参
* @param method 目标方法
* @param srcType 目标方法所属的类
* @return 解析结果
*/
public static Type[] resolveParamTypes(Method method, Type srcType) {
// 取出方法的所有入参
Type[] paramTypes = method.getGenericParameterTypes();
// 定义目标方法的类或接口
Class<?> declaringClass = method.getDeclaringClass();
// 解析结果
Type[] result = new Type[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
// 对每个入参依次调用resolveType方法
result[i] = resolveType(paramTypes[i], srcType, declaringClass);
}
return result;
}
/**
* 解析变量的实际类型
* @param type 变量的类型
* @param srcType 变量所属于的类
* @param declaringClass 定义变量的类
* @return 解析结果
*/
private static Type resolveType(Type type, Type srcType, Class<?> declaringClass) {
if (type instanceof TypeVariable) { // 如果是类型变量,例如“Map<K,V>”中的“K”、“V”就是类型变量。
return resolveTypeVar((TypeVariable<?>) type, srcType, declaringClass);
} else if (type instanceof ParameterizedType) { // 如果是参数化类型,例如“Collection<String>”就是参数化的类型。
return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass);
} else if (type instanceof GenericArrayType) { // 如果是包含ParameterizedType或者TypeVariable元素的列表
return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass);
} else {
return type;
}
}
在分析 resolveType方法的源码之前,有必要再强调一下 resolveType的输入参数,以防大家混淆。以上文中提到的“Student类中的 getInfo方法(继承自父类 User)的输出参数类型是什么?”这一问题为例,则:
- type:指要分析的字段或者参数的类型。这里我们要分析的是 getInfo的输出参数,即“List<T>”的类型。
- srcType:指要分析的字段或者参数所属的类。我们这里要分析的是 Student类中的getInfo方法,故所属的类是 Student类。
- declaringClass:指定义要分析的字段或者参数的类。getInfo 方法在 User 类中被定义,故这里是 User类。
resolveType 根据不同的参数类型调用了不同的子方法进行处理。我们直接以“List<T>”对应的 resolveParameterizedType子方法为例进行分析,而该子方法也是所有子方法中最为复杂的一个。“List<T>”作为参数化类型会触发 resolveParameterizedType 方法进行处理。
/**
* 解析参数化类型的实际结果
* @param parameterizedType 参数化类型的变量
* @param srcType 该变量所属于的类
* @param declaringClass 定义该变量的类
* @return 参数化类型的实际结果
*/
private static ParameterizedType resolveParameterizedType(ParameterizedType parameterizedType, Type srcType, Class<?> declaringClass) {
// 变量的原始类型。本示例中为List
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
// 获取类型参数。本示例中只有一个类型参数T
Type[] typeArgs = parameterizedType.getActualTypeArguments();
// 类型参数的实际类型
Type[] args = new Type[typeArgs.length];
for (int i = 0; i < typeArgs.length; i++) { // 依次处理每一个类型参数
if (typeArgs[i] instanceof TypeVariable) { // 类型参数是类型变量。例如parameterizedType为List<T>则属于这种情况
args[i] = resolveTypeVar((TypeVariable<?>) typeArgs[i], srcType, declaringClass);
} else if (typeArgs[i] instanceof ParameterizedType) { // 类型参数是参数化类型。例如parameterizedType为List<List<T>>则属于这种情况
args[i] = resolveParameterizedType((ParameterizedType) typeArgs[i], srcType, declaringClass);
} else if (typeArgs[i] instanceof WildcardType) { // 类型参数是通配符泛型。例如parameterizedType为List<? extends Number>则属于这种情况
args[i] = resolveWildcardType((WildcardType) typeArgs[i], srcType, declaringClass);
} else { // 类型参数是确定的类型。例如parameterizedType为List<String>则会进入这里
args[i] = typeArgs[i];
}
}
return new ParameterizedTypeImpl(rawType, null, args);
}
对于 parameterizedType 为“List<T>”,因此会继续调用resolveTypeVar方法对泛型变量“T”进行进一步的解析。resolveTypeVar方法会尝试通过继承关系等确定泛型变量的具体结果。
/**
* 解析泛型变量的实际结果
* @param typeVar 泛型变量
* @param srcType 该变量所属于的类
* @param declaringClass 定义该变量的类
* @return 泛型变量的实际结果
*/
private static Type resolveTypeVar(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass) {
// 解析出的泛型变量的结果
Type result;
Class<?> clazz;
if (srcType instanceof Class) { // 该变量属于确定的类。该示例中,变量T属于Student类,Student类是一个确定的类
clazz = (Class<?>) srcType;
} else if (srcType instanceof ParameterizedType) { // 该变量属于参数化类型
ParameterizedType parameterizedType = (ParameterizedType) srcType;
// 获取参数化类型的原始类型
clazz = (Class<?>) parameterizedType.getRawType();
} else {
throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass());
}
if (clazz == declaringClass) { // 变量属于的类和定义变量的类一致。该示例中,变量T属于Student,定义于User
// 确定泛型变量的上届
Type[] bounds = typeVar.getBounds();
if (bounds.length > 0) {
return bounds[0];
}
// 泛型变量无上届,则上届为Object
return Object.class;
}
// 获取变量属于的类的父类。在该示例中,变量属于Student类,其父类为User<Number>类
Type superclass = clazz.getGenericSuperclass();
// 扫描父类,查看能否确定边界。该示例中,能确定出边界为Number
result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass);
if (result != null) {
return result;
}
// 获取变量属于的类的接口
Type[] superInterfaces = clazz.getGenericInterfaces();
// 依次扫描各个父接口,查看能否确定边界。该示例中,Student类无父接口
for (Type superInterface : superInterfaces) {
result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface);
if (result != null) {
return result;
}
}
// 如果始终找不到结果,则未定义。即为Object
return Object.class;
}
在 resolveType 方法中,会根据变量的类型调用 resolveTypeVar、resolveParameterizedType、resolveGenericArrayType三个方法进行解析。resolveGenericArrayType方法并不复杂,只是根据元素类型又调用了其他几个方法。
这样,我们以断点调试法为基础,以“List<T>”类型的泛型变量为用例,通过以点带面的方式完成了 TypeParameterResolver类的源码阅读。这种以用例为主线的源码阅读方法能帮助我们排除很多干扰从而专注于一条逻辑主线。而等这条逻辑主线的源码被阅读清楚时,其他逻辑主线往往也会迎刃而解。
3. annotations 和 lang
注解类
4. type
4.1 分类
把 type包内的类分为以下六组。
- 类型处理器:1个接口、1个基础实现类、1个辅助类、43个实现类。
- TypeHandler:类型处理器接口;
- BaseTypeHandler:类型处理器的基础实现;
- TypeReference:类型参考器;
- *TypeHandler:43个类型处理器。
- 类型注册表:3个。
- SimpleTypeRegistry:基本类型注册表,内部使用 Set 维护了所有 Java 基本数据类型的集合;
- TypeAliasRegistry:类型别名注册表,内部使用 HashMap维护了所有类型的别名和类型的映射关系;
- TypeHandlerRegistry:类型处理器注册表,内部维护了所有类型与对应类型处理器的映射关系。
- 注解类:3个。
- Alias:使用该注解可以给类设置别名,设置后,别名和类型的映射关系便存入TypeAliasRegistry中;
- MappedJdbcTypes:有时我们想使用自己的处理器来处理某些 JDBC 类型,只需创建 BaseTypeHandler 的子类,然后在上面加上该注解,声明它要处理的JDBC类型即可;
- MappedTypes:有时我们想使用自己的处理器来处理某些Java类型,只需创建BaseTypeHandler的子类,然后在上面加上该注解,声明它要处理的 Java类型即可。
- 异常类:1个。
TypeException:表示与类型处理相关的异常。
- 工具类:1个。
ByteArrayUtils:提供数组转化的工具方法。
- 枚举类:1个。
JdbcType:在 Enum中定义了所有的 JDBC类型,类型来源于 java.sql.Types。
以上类中,注解类、异常类、工具类、枚举类都非常简单,不再单独介绍。下面将重点介绍类型处理器和类型注册表。
4.2 模板模式
模板中规定了大体的框架,只留下一些细节供使用者来修改和完善。使用同一模板做出的不同产品都具有一致的框架。在模板模式中,需要使用一个抽象类定义一套操作的整体步骤(即模板),而抽象类的子类则完成每个步骤的具体实现。这样,抽象类的不同子类遵循了同样的一套模板。例如,定义一套打扫卫生的模板,且定义了4个大的步骤:准备prepare,实施implement,善后windup,汇报report。可以基于该模板完成一些具体的打扫卫生工作,如擦玻璃和擦黑板的实现。
/**
* @author Shawn
* @date 2022/3/21 18:05
* @title 模板模式案例
*/
public abstract class Cleaning {
public void clean(){
prepare();
implement();
windup();
report();
}
abstract void prepare();
abstract void implement();
abstract void windup();
void report(){
System.out.println("cleaning is over");
}
}
class WipeGlass extends Cleaning {
@Override
void prepare() {
System.out.println("找到抹布");
System.out.println("清洗抹布");
}
@Override
void implement() {
System.out.println("擦洗玻璃");
}
@Override
void windup() {
System.out.println("清理窗台");
}
}
class WipeBlackboard extends Cleaning {
@Override
void prepare() {
System.out.println("找到黑板擦");
}
@Override
void implement() {
System.out.println("用力擦黑板");
}
@Override
void windup() {
System.out.println("清理粉笔屑");
}
}
4.3 类型处理器
4.3.1 TypeHandler 和 BaseTypeHandler
作为一个 ORM框架,处理 Java对象和数据库关系之间的映射是 MyBatis工作中的重要部分。然而在 Java中存在 Integer、String、Data等各种类型的数据;在数据库中存在varchar、longvarchar、tinyint等各种类型的数据。不同类型的字段所需的读、写方法各不相同,因此需要对不同类型的字段采取相应的处理方式。例如,在对 User对象的属性进行读取和赋值时,需要用 Integer的相关处理方式来操作 id、sex属性,而要用 String的相关处理方式来操作 name、schoolName属性。
在 type包中,将每种类型对应的处理方式封装在了对应的类型处理器 TypeHandler 中。例如,IntegerTypeHandler负责完成对 Integer类型的处理。type 包共有 43 个类型处理器,这些类型处理器的名称也均以“TypeHandler”结尾。而 TypeHandler 和 BaseTypeHandler 则分别是类型处理器接口和类型处理器基类。
TypeHandler<T>是一个接口,其中定义了进行数据处理操作的几个抽象方法。而BaseTypeHandler<T> 继承了 TypeHandler<T>接口,并实现了 TypeHandler<T>中的接口。
在类型处理器相关类的设计中采用了模板模式,BaseTypeHandler<T>作为所有类型处理器的基类,定义了模板的框架。而在各个具体的实现类中,则实现了具体的细节。
/**
* BaseTypeHandler 从结果集中读出一个结果
* @param rs 结果集
* @param columnName 要读取的结果的列名称
* @return 结果值
* @throws SQLException
*/
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
try {
return getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
}
}
/**
* IntegerTypeHandler 从结果集中读出一个可能为null的结果
* @param rs 结果集
* @param columnName 要读取结果的列名称
* @return 结果值
* @throws SQLException
*/
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
int result = rs.getInt(columnName);
return result == 0 && rs.wasNull() ? null : result;
}
以 BaseTypeHandler 中 getResult(ResultSet,String)方法为例,该方法完成了异常处理等统一的工作,而与具体类型相关的 getNullableResult(ResultSet,String)操作则通过抽象方法交给具体的类型处理器实现。这就是典型的模板模式。
BaseTypeHandler<T>交给具体的类型处理器实现的抽象方法一共只有四个。在每个类型处理器都需要实现这四个方法:
- void setNonNullParameter(PreparedStatement,int,T,JdbcType):向 PreparedStatement 对象中的指定变量位置写入一个不为 null的值;
- T getNullableResult(ResultSet,String):从 ResultSet 中按照字段名读出一个可能为null的数据;
- T getNullableResult(ResultSet,int):从 ResultSet 中按照字段编号读出一个可能为null的数据;
- T getNullableResult(CallableStatement,int):从 CallableStatement 中按照字段编号读出一个可能为 null的数据。
因为上面的抽象方法跟具体的类型相关,因此存在泛型参数 T。在每种类型处理器中,都给出了泛型参数的值。以 IntegerTypeHandler 为例,它设置泛型参数值为 Integer。IntegerTypeHandler处理的类型是 Integer,这表明其 getNullableResult 方法给出的就是一个 Integer结果。
4.3.2 TypeReference
43个类型处理器可以处理不同 Java 类型的数据,而这些类型处理器都是 TypeHandler 接口的子类,因此可以都作为 TypeHandler来使用。那会不会遇到一个问题,当 MyBatis取到某一个 TypeHandler 时,却不知道它到底是用来处理哪一种 Java类型的处理器?
为了解决这一问题,MyBatis 定义了一个 TypeReference 类。它能够判断出一个TypeHandler用来处理的目标类型。而它判断的方法也很简单:取出 TypeHandler实现类中的泛型参数 T的类型,这个值的类型也便是该 TypeHandler能处理的目标类型。该功能由getSuperclassTypeParameter方法实现,该方法能将找出的目标类型存入类中的 rawType属性。
/**
* 用来处理泛型
* @param <T>
*/
public abstract class TypeReference<T> {
// 泛型类中的实际类型
private final Type rawType;
protected TypeReference() {
rawType = getSuperclassTypeParameter(getClass());
}
/**
* 解析出当前TypeHandler实现类能够处理的目标类型
* @param clazz TypeHandler实现类
* @return 该TypeHandler实现类能够处理的目标类型
*/
Type getSuperclassTypeParameter(Class<?> clazz) {
// 获取clazz类的带有泛型的直接父类
Type genericSuperclass = clazz.getGenericSuperclass();
if (genericSuperclass instanceof Class) {
// 进入这里说明genericSuperclass是class的实例
if (TypeReference.class != genericSuperclass) { // genericSuperclass不是TypeReference类本身
// 说明没有解析到足够上层,将clazz类的父类作为入参递归调用
return getSuperclassTypeParameter(clazz.getSuperclass());
}
// 说明clazz实现了TypeReference类,但是却没有使用泛型
throw new TypeException("'" + getClass() + "' extends TypeReference but misses the type parameter. "
+ "Remove the extension or add a type parameter to it.");
}
// 运行到这里说明genericSuperclass是泛型类。获取泛型的第一个参数,即T
Type rawType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
if (rawType instanceof ParameterizedType) { // 如果时参数化类型
// 获取参数化类型的实际类型
rawType = ((ParameterizedType) rawType).getRawType();
}
return rawType;
}
public final Type getRawType() {
return rawType;
}
@Override
public String toString() {
return rawType.toString();
}
}
TypeReference 类是 BaseTypeHandler 的父类,因此所有的类型处理器都继承了TypeReference 的功能。这意味着对任何一个类型处理器调用 getSuperclassTypeParameter方法,都可以得到该处理器用来处理的目标类型。
4.4 类型注册表
定义了大量的类型处理器之后,MyBatis 还需要在遇到某种类型的数据时,快速找到与数据的类型对应的类型处理器。这个过程就需要各种类型注册表的帮助。type 包中的类型注册表有三个:SimpleTypeRegistry、TypeAliasRegistry 和TypeHandlerRegistry。
- SimpleTypeRegistry 是一个非常简单的注册表,其内部使用一个 SIMPLE_TYPE_SET 变量维护所有 Java基本类型。SIMPLE_TYPE_SET中的赋值是在 static代码块中进行的。这说明在 SimpleTypeRegistry初始化后,就已经将所有的 Java基本类型维护到了 SIMPLE_TYPE_SET中。
static {
SIMPLE_TYPE_SET.add(String.class);
SIMPLE_TYPE_SET.add(Byte.class);
SIMPLE_TYPE_SET.add(Short.class);
SIMPLE_TYPE_SET.add(Character.class);
SIMPLE_TYPE_SET.add(Integer.class);
SIMPLE_TYPE_SET.add(Long.class);
SIMPLE_TYPE_SET.add(Float.class);
SIMPLE_TYPE_SET.add(Double.class);
SIMPLE_TYPE_SET.add(Boolean.class);
SIMPLE_TYPE_SET.add(Date.class);
SIMPLE_TYPE_SET.add(Class.class);
SIMPLE_TYPE_SET.add(BigInteger.class);
SIMPLE_TYPE_SET.add(BigDecimal.class);
}
- TypeAliasRegistry是一个类型别名注册表,其内部使用 typeAliases变量维护类型的别名与类型的对应关系。有了这个注册表,我们就可以在很多场合使用类型的别名来指代具体的类型。
- TypeHandlerRegistry 是这三个注册表中最为核心的一个,数据类型和相关处理器的对应关系就是由它维护的。
在介绍TypeHandlerRegistry 之前,我们先介绍 Java数据类型和 JDBC数据类型。假设某个对象中存在一个“String name”属性,则 name属性在 Java中的数据类型是String。然而它在数据库中可能是 char、varchar,也可能是 tinytext、text 等多种类型。因此,Java数据类型和 JDBC数据类型并不是一对一的关系,而是一对多的关系。
// JDBC类型与对应类型处理器的映射
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
// Java类型与Map<JdbcType, TypeHandler<?>>的映射
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
// 未知类型的处理器
private final TypeHandler<Object> unknownTypeHandler = new UnknownTypeHandler(this);
// 键为typeHandler.getClass() ,值为typeHandler。里面存储了所有的类型处理器
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
// 空的Map<JdbcType, TypeHandler<?>>,表示该Javal类型没有对应的Map<JdbcType, TypeHandler<?>>
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
// 默认的枚举类型处理器
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
了解一下TypeHandlerRegistry的属性后,也可以猜测出如何才能拿到一个类型的类型处理器,实际就是一个两次映射的过程。
- 根据传入的 Java 类型,调用 getJdbcHandlerMap 子方法找寻对应的jdbcTypeHandlerMap后返回。
- 基于 jdbcTypeHandlerMap,根据 JDBC类型找到对应的 TypeHandler。
例如,在给定 Java类型是 String,而 JDBC类型是 varchar后,就能唯一确定一个类型处理器。getTypeHandler 方法完成的就是这一过程,该方法带注释的源码如:
/**
* 找出一个类型处理器
* @param type Java类型
* @param jdbcType JDBC类型
* @param <T> 类型处理器的目标类型
* @return 类型处理器
*/
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) { // 是ParamMap,因此不是单一的Java类型
return null;
}
// 先根据Java类型找到对应的jdbcHandlerMap
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) { // 存在jdbcHandlerMap
// 根据JDBC类型找寻对应的处理器
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
// 使用null作为键进行一次找寻,通过本类源码可知当前jdbcHandlerMap可能是EnumMap也可能是HashMap
// EnumMap不允许键为null,因此总是返回null。HashMap允许键为null。这是这并不是一次无用功
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// 如果jdbcHandlerMap只有一个类型处理器,就取出他
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// 返回找到的类型处理器
return (TypeHandler<T>) handler;
}
SimpleTypeRegistry、TypeAliasRegistry、TypeHandlerRegistry这三个类型注册表的存在,使 MyBatis 不仅可以根据类型找寻其类型处理器,而且还可以根据类型别名找寻对应的类型处理器。
5. io
说起IO(输入Input/输出Output),首先想到的就是对磁盘文件的读写。在Mybatis的工作中,与磁盘文件的交互主要就是对xml配置文件的读操作,此外IO包还提供了对内存中类文件(class文件)的操作。
5.1 单例模式
单例模式的类提供一个方法得到该类的对象,并且总是保证这个对象是唯一的。
5.2 代理模式
代理模式是指建立某一个对象的代理对象,并且由代理对象控制对原对象的引用。例如,我们不能直接访问对象 A,则可以建立对象 A的代理对象 A Proxy。这样,就可以通过访问 A Proxy来间接地使用对象 A的功能。A Proxy就像 A的对外联络人一般。代理模式类图:
代理模式能够实现很多功能:
- 隔离功能:通过建立一个目标对象的代理对象,可以防止外部对目标对象的直接访问,这样就使得目标对象与外部隔离。我们可以在代理对象中增加身份验证、权限验证等功能,从而实现对目标对象的安全防护。
- 扩展功能:对一个目标对象建立代理对象后,可以在代理对象中增加更多的扩展功能。例如,可以在代理对象中增加日志记录功能,这样对目标对象的访问都会被代理对象计入日志。
- 直接替换:对一个目标对象建立代理对象后,可以直接使用代理对象完全替换目标对象,由代理对象来实现全部的功能。例如,MyBatis 中数据库操作只是一个抽象方法,但实际运行中会建立代理对象来完成数据库的读写操作。
5.2.1 静态代理
静态代理就是代理模式最简单的实现。所谓“静态”,是指被代理对象和代理对象在程序中是确定的,不会在程序运行过程中发生变化。但是静态代理也有一些局限性,最明显的就是代理对象和被代理对象是在程序中写死的,显然不够灵活。动态代理则没有此弊端。
5.2.2 VFS
磁盘文件系统分为很多种,如 FAT、VFAT、NFS、NTFS等。不同文件系统的读写操作各不相同。VFS(Virtual File System)作为一个虚拟的文件系统将各个磁盘文件系统的差异屏蔽了起来,提供了统一的操作接口。这使得上层的软件能够用单一的方式来跟底层不同的文件系统沟通,在操作磁盘文件时,软件程序不需要和实体的文件系统打交道,只需要和 VFS沟通即可。这使得软件系统的磁盘操作变得更为简单。
MyBatis的 io包中 VFS的作用是从应用服务器中找寻和读取资源文件,这些资源文件可能是配置文件、类文件等。io包中 VFS相关类主要有三个:DefaultVFS类和 JBoss6VFS类是 VFS类的两个实现类。
在确定了具体的实现类之后,外部只需通过 VFS中的方法即可完成外部文件的读取。VFS类中的主要两个属性中保存了内置和用户自定义的 VFS实现类。
/** The built-in implementations. */
public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };
/** The list to which implementations are added by {@link #addImplClass(Class)}. */
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<>();
VFS 类中含有一个内部类 VFSHolder,该类使用了单例模式。其中的 createVFS 方法能够对外给出唯一的 VFS实现类。在 VFSHolder类的 createVFS方法中,先组建一个 VFS实现类的列表,然后依次对列表中的实现类进行校验。第一个通过校验的实现类即被选中。在组建列表时,用户自定义的实现类放在了列表的前部,这保证了用户自定义的实现类具有更高的优先级。
/**
* 给出一个VFS实现。单例模式
* @return VFS实现
*/
static VFS createVFS() {
// 所有VFS实现类的列表。
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++) {
Class<? extends VFS> impl = impls.get(i);
try {
// 生成一个实现类的对象
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 | IllegalAccessException e) {
log.error("Failed to instantiate " + impl, e);
return null;
}
}
if (log.isDebugEnabled()) {
log.debug("Using VFS adapter " + vfs.getClass().getName());
}
return vfs;
}
}
内部类 VFSHolder中最终确定的 VFS实现类会被放入 INSTANCE变量中。这样,当外部调用 VFS类的 getInstance方法时就可以拿到该 VFS实现类的对象:
/**
* Get the singleton {@link VFS} instance. If no {@link VFS} implementation can be found for the
* current environment, then this method returns null.
*/
public static VFS getInstance() {
return VFSHolder.INSTANCE;
}
MyBatis中为 VFS提供了两个内部实现类,分别是 DefaultVFS类和 JBoss6VFS类,下面分别进行介绍。
5.2.3 DefaultVFS类
DefaultVFS 作为默认的 VFS 实现类,其 isValid 函数恒返回 true。因此,只要加载DefaultVFS类,它一定能通过 VFS类中 VFSHolder单例中的校验,并且在进行实现类的校验时 DefaultVFS排在整个校验列表的最后。因此,DefaultVFS成了所有 VFS实现类的保底方案,即最后一个验证,但只要验证一定能通过。除了 isValid方法外,DefaultVFS中还有以下几个方法。
- list(URL,String):列出指定 url下符合条件的资源名称;
- listResources(JarInputStream,String):列出给定 jar包中符合条件的资源名称;
- findJarForResource(URL):找出指定路径上的 jar包,返回 jar包的准确路径;
- getPackagePath(String):将 jar包名称转为路径;
- isJar:判断指定路径上是否是 jar包。
以上方法均采用直接读取文件的方式来实现,结构并不复杂。
5.2.4 JBoss6VFS类
JBoss是一个基于 J2EE的开放源代码的应用服务器,JBoss6是 JBoss中的一个版本。JBoss6VFS即为借鉴 JBoss6设计的一套 VFS实现类。在 JBoss6VFS中主要存在两个内部类。
- VirtualFile:仿照 JBoss中的 VirtualFile类设计的一个功能子集;
- VFS:仿照 JBoss中的 VFS类设计的一个功能子集。
阅读 VirtualFile和 VFS中的方法便可以发现,这些方法中都没有实现具体的操作,而是调用 JBoss中的相关方法。VirtualFile中的 getPathNameRelativeTo方法中直接使用invoke语句,将操作转给了 org.jboss.vfs.VirtualFile类中的 getPathNameRelativeTo方法。因此,这里使用了代理模式,此处的 VirtualFile内部类是 JBoss中 VirtualFile的静态代理类。
/**
* 获取相关的路径名
* @param parent 父级路径名
* @return 相关路径名
*/
String getPathNameRelativeTo(VirtualFile parent) {
try {
return invoke(getPathNameRelativeTo, virtualFile, parent.virtualFile);
} catch (IOException e) {
// This exception is not thrown by the called method
log.error("This should not be possible. VirtualFile.getPathNameRelativeTo() threw IOException.");
return null;
}
}
同理,VFS内部类是 JBoss中 VFS的静态代理类。在 JBoss6VFS类中,两个内部类 VirtualFile和 VFS都是代理类,只负责完成将相关操作转给被代理类的工作。那么,要想使 JBoss6VFS类正常工作,必须确保被代理类存在。确定被代理类是否存在的过程在 JBoss6VFS类的 initialize方法中完成。该方法由静态代码块触发,因此会在类的加载阶段执行:
/**
* JBoss6VFS中的静态代码块
*/
static {
initialize();
}
/** Find all the classes and methods that are required to access the JBoss 6 VFS. */
/**
* 初始化JBoss6VFS类。主要是根据被代理类是否存在来判断自身是否可用
*/
protected static synchronized void initialize() {
if (valid == null) {
// 首先假设是可用的
valid = Boolean.TRUE;
// 校验所需要的类是否存在。如果不存在,则valid设置为false
VFS.VFS = checkNotNull(getClass("org.jboss.vfs.VFS"));
VirtualFile.VirtualFile = checkNotNull(getClass("org.jboss.vfs.VirtualFile"));
// 校验所需要的方法是否存在。如果不存在,则valid设置为false
VFS.getChild = checkNotNull(getMethod(VFS.VFS, "getChild", URL.class));
VirtualFile.getChildrenRecursively = checkNotNull(getMethod(VirtualFile.VirtualFile,
"getChildrenRecursively"));
VirtualFile.getPathNameRelativeTo = checkNotNull(getMethod(VirtualFile.VirtualFile,
"getPathNameRelativeTo", VirtualFile.VirtualFile));
// 判断以上所需方法的返回值是否和预期一致。如果不一致,则valid设置为false
checkReturnType(VFS.getChild, VirtualFile.VirtualFile);
checkReturnType(VirtualFile.getChildrenRecursively, List.class);
checkReturnType(VirtualFile.getPathNameRelativeTo, String.class);
}
}
在初始化方法中,会尝试从 JBoss 的包中加载和校验所需要的类和方法。最后,还通过返回值对加载的方法进行了进一步的校验。而在以上的各个过程中,只要发现加载的类、方法不存在或者返回值发生了变化,则认为 JBoss 中的类不可用。在这种情况下,checkNotNull方法和 checkReturnType方法中会调用 setInvalid 方法将 JBoss6VFS的 valid字段设置为 false,表示 JBoss6VFS类不可用。
5.3 类文件的加载
除了从磁盘中读取普通文件外,从磁盘中获取类文件(Class文件)并加载成一个类也是一种常用的功能。要把类文件加载成类,需要类加载器的支持。ClassLoaderWrapper 类中封装了五种类加载器,而 Resources 类又对 ClassLoaderWrapper 类进行了一些封装。下面我们重点关注ClassLoaderWrapper类。
/**
* 获取所有的类加载器
* @param classLoader 传入的一种类加载器
* @return 所有类加载器的列表
*/
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader,
defaultClassLoader,
Thread.currentThread().getContextClassLoader(),
getClass().getClassLoader(),
systemClassLoader};
}
ClassLoaderWrapper的五种类加载器由 getClassLoaders方法给出,这五种类加载器依次是:
- 作为参数传入的类加载器,可能为 null;
- 系统默认的类加载器,如未设置则为 null;
- 当前线程的线程上下文中的类加载器;
- 当前对象的类加载器;
- 系统类加载器,在 ClassLoaderWrapper的构造方法中设置。
以上五种类加载器的优先级由高到低。在读取类文件时,依次到上述五种类加载器中进行寻找,只要某一次寻找成功即返回结果。classForName 方法是一个根据类名找出指定类的方法,下面以该方法为例,查看五种类加载器是如何轮番上阵发挥作用的。
/**
* 轮番使用各个加载器尝试加载一个类
* @param name 类名
* @param classLoader 类加载列表
* @return 加载出的类
* @throws ClassNotFoundException
*/
Class<?> classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException {
// 对五个classLoader依次尝试
for (ClassLoader cl : classLoader) {
if (null != cl) {
try {
// 使用当前类加载器尝试是否能成功
Class<?> c = Class.forName(name, true, cl);
if (null != c) {
// 只要找到目标类,则返回结果
return c;
}
} catch (ClassNotFoundException e) {
// 故意忽略该异常,因为这只是在某一个classLoader中没找到目标类。下面会在所有classLoader均寻找失败后重新抛出该异常
}
}
}
// 所有classLoader均寻找失败,抛出异常
throw new ClassNotFoundException("Cannot find class: " + name);
}
5.4 ResolverUtil 类
ResolverUtil是一个工具类,主要功能是完成类的筛选。这些筛选条件可以是:
- 类是否是某个接口或类的子类;
- 类是否具有某个注解。
为了能够基于这些条件进行筛选,ResolverUtil中设置有一个内部接口 Test。Test是一个筛选器,内部类中有一个抽象方法 matches来判断指定类是否满足筛选条件。est接口及其实现类:
Test内部类有两个实现类,都重写了 matches方法。
- IsA类中的 matches方法可以判断目标类是否实现了某个接口或者继承了某各类;
- AnnotatedWith类中的 matches方法可以判断目标类是否具有某个注解。
最终通过校验的类会放到 ResolverUtil类的 matches属性中。这样一来,在读取某个路径上的类文件时,还可以借助 ResolverUtil对类文件进行一些筛选。ResolverUtil中的 find方法即支持筛选出指定路径下的符合指定条件的类文件:
/**
* 筛选出指定路径下符合一定条件的类
* @param test 测试条件
* @param packageName 路径
* @return ResolverUtil本身
*/
public ResolverUtil<T> find(Test test, String packageName) {
// 获取起始包路径
String path = getPackagePath(packageName);
try {
// 找出包中的各个文件
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
// 对类文件进行测试
if (child.endsWith(".class")) { // 必须是类文件
// 测试是否满足测试条件。如果满足,则将该类文件记录下来
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
上述方法中传入的 Test参数可以是 IsA对象,也可以是 AnnotatedWith对象,这样就可以将 packageName 路径下所有符合条件的类文件找出来。真正触发测试的是addIfMatching子方法:
/**
* 判断一个类文件是否满足条件。如果满足则记录下来
* @param test 测试条件
* @param fqn 类文件全名
*/
@SuppressWarnings("unchecked")
protected void addIfMatching(Test test, String fqn) {
try {
// 转化为外部名称
String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
// 类加载器
ClassLoader loader = getClassLoader();
if (log.isDebugEnabled()) {
log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
}
// 加载类文件
Class<?> type = loader.loadClass(externalName);
if (test.matches(type)) { // 执行测试
// 测试通过则记录到matches属性中
matches.add((Class<T>) type);
}
} catch (Throwable t) {
log.warn("Could not examine class '" + fqn + "'" + " due to a " +
t.getClass().getName() + " with message: " + t.getMessage());
}
}
6. logging
logging包负责完成Mybatis操作中的日常记录工作。
6.1 适配器模式
适配器模式,基于该模式设计的类能够在两个或多个不兼容的类之间起到沟通桥梁的作用。转换插头就是一个适配器的典型例子。不同的转换插头能够适配不同国家的插座标准,从而使得一个电器能在多个国家使用。
上述代码中,方法三是核心方法,它需要四个输入参数。在有些场景下,调用方只能提供2个或3个参数,方法一和方法二就充当了方法适配器的作用。这两个适配器通过为未知参数设置默认值的方式,搭建起了调用方和核心方法之间的桥梁。
不过,通常我们说起适配器模式是指类适配器或者对象适配器。
Target 接口是 Client想调用的标准接口,而 Adaptee是提供服务但不符合标准接口的目标类。Adapter便是为了 Client能顺利调用 Adaptee而创建的适配器类。Adapter既实现了 Target接口又继承了 Adaptee类,从而使 Client能够与Adaptee适配。类适配器类图:
而对象适配器 Adaptee不再继承目标类,而是直接持有一个目标类的对象。这样,Adapter可以直接将 Client要求的操作委托给目标类对象处理,也实现了 Client和 Adaptee 之间的适配。而且这种适配器更为灵活一些,因为要适配的目标对象是作为初始化参数传给 Adapter的。对象适配器类图:
适配器模式能够使得原本不兼容的类可以一起工作。通常情况下,如果目标类是可以修改的,则不需要使用适配器模式,直接修改目标类即可。但如果目标类是不可以修改的(例如目标类由外部提供,或者目标类被众多其他类依赖必须保持不变),那么适配器模式则会非常有用。
6.2 基于反射的动态代理
静态代理中代理对象和被代理对象是在程序中写死的,不够灵活。具体来说,要想建立某个对象的静态代理,必须为其建立一个代理类,而且所有被代理的方法均需在代理类中直接调用。这就使得代理类高度依赖被代理类,被代理类的任何变动都可能引发代理类的变动。
而动态代理则灵活很多,它能在代码运行时动态地为某个对象增加代理,并且能为代理对象动态地增加方法。动态代理的实现方式【TODO调研】有很多种,这一节我们介绍较为常用的一种:基于反射的动态代理。
- 在 Java中 java.lang.reflect包下提供了一个 Proxy类和一个 InvocationHandler接口,使用它们就可以实现动态代理。
- ProxyHandler类继承 java.lang.reflect.InvocationHandler接口,并实现其中的 invoke方法。invoke方法中需要传入被代理对象、被代理方法及调用被代理方法所需的参数。动态代理类可以代理多个其他类。例如,在“ProxyHandler proxyHandler=new ProxyHandler(user)”中给 ProxyHandler 传入不同的被代理对象,然后就可以使用Proxy.newProxyInstance生成不同的代理对象。
- “Proxy.newProxyInstance”方法通过反射创建一个实现了“UserInterface”接口的对象,这个对象就是代理对象 userProxy。因此,对于基于反射的动态代理而言,有一个必需的条件:被代理的对象必须有一个父接口。
/**
* @author Shawn
* @date 2022/3/22 16:30
* @title 动态代理
*/
public interface UserInterface {
String sayHello(String name);
}
public class User implements UserInterface{
@Override
public String sayHello(String name) {
System.out.println("hello " + name);
return "OK";
}
}
public class ProxyHandler<T> implements InvocationHandler {
private final T target;
public ProxyHandler(T target){
this.target = target;
}
/**
* 代理方法
* @param proxy 代理对象
* @param method 代理方法
* @param args 代理方法的参数
* @return 方法执行结果
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("pre words");
Object ans = method.invoke(target, args);
System.out.println("post words");
return ans;
}
}
public class Test {
public static void main(String[] args) {
// 创建被代理对象
User user = new User();
// 初始化一个ProxyHandler对象
ProxyHandler<User> proxyHandler = new ProxyHandler<>(user);
// 使用Proxy类的一个静态方法生成代理对象userProxy
UserInterface userProxy = (UserInterface) Proxy.newProxyInstance(User.class.getClassLoader(), new Class[]{UserInterface.class}, proxyHandler);
// 通过接口调用相应的方法,实际由Proxy执行
userProxy.sayHello("shawn");
}
}
6.3 日志框架与日志级别
Java 领域的日志框架已经非常丰富,有 log4j、Logging、commons-logging、slf4j、logback等,它们为 Java的日志打印工作提供了极大的便利。
为了方便日志管理,日志框架大都对日志等级进行了划分。常见的日志等级划分方式如下。
- Fatal:致命等级的日志,指发生了严重的会导致应用程序退出的事件;
- Error:错误等级的日志,指发生了错误,但是不影响系统运行;
- Warn:警告等级的日志,指发生了异常,可能是潜在的错误;
- Info:信息等级的日志,指一些在粗粒度级别上需要强调的应用程序运行信息;
- Debug:调试等级的日志,指一些细粒度的对于程序调试有帮助的信息;
- Trace:跟踪等级的日志,指一些包含程序运行详细过程的信息。
有了以上的日志划分后,在打印日志时我们就可以定义日志的级别。也可以根据日志等级进行输出,防止大量的日志信息混杂在一起。目前,在很多集成开发环境中可以调节日志的显示级别,使只有一定级别以上的日志才会显示出来,这样能够根据不同的使用情形进行日志的筛选。
6.4 Log接口
logging包中最重要的就是Log接口,它有11个实现类,分布在logging包的不同子包中。Log接口及其实现类的类图:
下面先详细了解 Log 接口中的方法。Log 接口中定义了日志框架要实现的几个基本方法:
- error:打印 Error级别日志;
- warn:打印 Warn级别日志;
- debug:打印 Debug级别日志;
- trace:打印 Trace级别日志;
- isDebugEnabled:判断打印 Debug级别日志的功能是否开启;
- isTraceEnabled:判断打印 Trace级别日志的功能是否开启。
上述各个方法主要是实现不同级别日志的打印功能。isDebugEnabled方法和 isTraceEnabled方法是从效率角度考虑而设计的。首先,Debug和 Trace是两个级别比较低的日志,越是级别低的日志越有这样的特点:
- 很少开启:因为它们级别很低,大多时候该级别的信息不需要展示;
- 输出频次高:低级别日志的触发门槛很低,这意味着一旦它们开启,往往会以非常高的频率输出日志信息;
- 内容冗长:它们中通常包含非常丰富和细致的信息,因此信息内容往往十分冗长。
在日志打印过程中调用了 trace 方法。以“org.apache.commons.logging.impl.SimpleLog”下的 trace 方法(可以通过 JakartaCommons LoggingImpl实现类中的 trace方法追踪到该方法)低级别的日志很少开启,这意味着 this.isLevelEnabled(1)的返回值大概率是 false。因此代码中的字符串拼接结果是无用的,会被直接丢弃。并且低级别日志输出频次高且内容冗长,这意味着这种无用的字符串拼接是频发的且资源消耗很大。要想避免上述无用的字符串操作导致的大量系统资源消耗,就需要使用 isDebugEnabled方法或 isTraceEnabled方法对低级别的日志输出进行前置判断。
这样,借助 isTraceEnabled方法就避免了资源的浪费。在阅读源码的过程中,读懂源码只是完成了浅层知识的学习。在读懂源码的同时思考源码为何这么设计将会使我们有更大的收获,也会使我们更容易读懂源码。
6.5 Log接口的实现类
在 Log接口的 11个实现类中:
- 最简单的实现类就是 NoLoggingImpl类,因为它是一种不打印日志的实现类,内部几乎没有任何的操作逻辑。
- StdOutImpl 实现类也非常简单,对于 Error 级别的日志调用 System.err.println 方法进行打印,而对于其他级别的日志则调用System.out.println方法进行打印。
- 其他的 9 个实现类中,Slf4jLocationAwareLoggerImpl 类和 Slf4jLoggerImpl 类是Slf4jImpl 类的装饰器,Log4j2AbstractLoggerImpl 类和 Log4j2LoggerImpl 类是 Log4j2Impl类的装饰器。
- 接下来重点分析剩下的 5 个实现类,它们是 JakartaCommonsLoggingImpl、Jdk14LoggingImpl、Log4jImpl、Log4j2Impl 和 Slf4jImpl。
我们以 commons 子包中的JakartaCommonsLoggingImpl为例,查看其具体实现。
public class JakartaCommonsLoggingImpl implements org.apache.ibatis.logging.Log {
private final Log log;
public JakartaCommonsLoggingImpl(String clazz) {
// 下面引用的LogFactory是rg.apache.commons.logging.LogFactory
// 因此获得了CommonsLogging的log
log = LogFactory.getLog(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.error(s, e);
}
@Override
public void error(String s) {
log.error(s);
}
@Override
public void debug(String s) {
log.debug(s);
}
@Override
public void trace(String s) {
log.trace(s);
}
@Override
public void warn(String s) {
log.warn(s);
}
}
可以看出,JakartaCommonsLoggingImpl 是一个典型的对象适配器。它的内部持有一个“org.apache.commons.logging.Log”对象,然后所有方法都将操作委托给了该对象。
6.6 LogFactory
我们已经知道 Log接口有着众多的实现类,而 LogFactory就是制造实现类的工厂。最终,该工厂会给出一个可用的 Log实现类,由它来完成 MyBatis的日志打印工作。Log 接口的实现类都是对象适配器(装饰器类除外),最终的实际工作要委托给被适配的目标对象来完成。因此是否存在一个可用的目标对象成了适配器能否正常工作的关键所在。于是 LogFactory的主要工作就是尝试生成各个目标对象。如果一个目标对象能够被生成,那该目标对象对应的适配器就是可用的。
LogFactory生成目标对象的工作在静态代码块中被触发。 LogFactory的静态代码块:
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
/**
* 尝试实现一个日志实例
* @param runnable 用来尝试实现日志实例的操作
*/
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
tryImplementation方法会在 logConstructor为 null的情况下调用 Runnable对象的 run方法。要注意一点,直接调用 Runnable对象的 run方法并不会触发多线程,因此静态代码块中的多个 tryImplementation 方法是依次执行的。这也意味着 useNoLogging 方法中引用的NoLoggingImpl 实现类是最后的保底实现类,而且 NoLoggingImpl 不需要被适配对象的支持,一定能够成功。因此,最终的保底日志方案就是不输出日志。
以“tryImplementation(LogFactory::useCommonsLogging)”为例继续追踪源码,该方法通过 useCommonsLogging方法调用 setImplementation方法。
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
/**
* 设置日志实现
* @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);
}
}
setImplementation方法会尝试获取参数中类的构造函数,并用这个构造函数创建一个日志记录器。如果这次创建是成功的,则意味着以后的创建也是成功的,即当前参数中的类是可用的。因此,把参数中类的构造方法赋给了 logConstructor属性。这样,当外部调用 getLog方法时,便可以由 logConstructor创建一个 Log类的实例。
在静态代码块中,我们发现 StdOutImpl类并没有参与设置 logConstructor属性的过程,这是因为它不在默认日志输出方式的备选列表中。不过这并不代表着它毫无用处,因为MyBatis允许我们自行指定日志实现类。例如,在配置文件的 settings节点下配置如下信息,则可以自定义 StdOutImpl类作为日志输出方式,使 MyBatis的日志输出到控制台上。自行指定日志实现类是在 XML解析阶段通过调用 LogFactory中的 useCustomLogging方法实现的。它相比于静态代码块中的方法执行得更晚,会覆盖前面的操作,因此具有更高的优先级。
6.7 JDBC日志打印
MyBatis是 ORM框架,它负责数据库信息和 Java对象的互相映射操作,而不负责具体的数据库读写操作,具体的数据库读写操作是由 JDBC进行的。既然 MyBatis不进行数据库的查询,那 MyBatis的日志中便不会包含 JDBC的操作日志。然而,很多时候 MyBatis的映射错误是由于 JDBC的错误引发的,例如 JDBC无法正确执行查询操作或者查询得到的结果类型与预期的不一致等。因此,JDBC 的运行日志是分析 MyBatis 框架报错的重要依据。然而,JDBC日志有自身的一套输出体系,JDBC日志和 MyBatis日志是分开的,这会给我们的调试工作带来很多的困难。
jdbc子包就是用来解决这个问题的。jdbc子包基于代理模式,让 MyBatis能够将 JDBC的操作日志打印出来,极大地方便了我们的调试工作。接下来就介绍 jdbc子包是如何实现这个操作的。
BaseJdbcLogger作为基类提供了一些子类会用到的基本功能,而其他几个实现类则为相应类提供日志打印能力。例如,ConnectionLogger 为“java.sql.Connection”类提供日志打印能力。
BaseJdbcLogger各个子类使用动态代理来实现日志的打印。以 ConnectionLogger为例,介绍 BaseJdbcLogger实现类的实现逻辑。ConnectionLogger继承了InvocationHandler接口,从而成为一个代理类。在BaseExecutor的 getConnection方法中,当 statementLog的 Debug功能开启时,getConnection 方法返回的不是一个原始的 Connection 对象,而是由“ConnectionLogger.newInstance”方法生成的一个代理对象。所有“java.sql.Connection”对象的方法调用都会进入 ConnectionLogger 中的invoke方法中。
/**
* 获取一个Connection对象
* @param statementLog 日志对象
* @return Connection对象
* @throws SQLException
*/
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) { // 启用调试日志
// 生成Connection对象的具有日志记录功能的代理对象ConnectionLogger对象
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
// 返回原始的Connection对象
return connection;
}
}
/**
* 代理方法
* @param proxy 代理对象
* @param method 代理方法
* @param params 代理方法的参数
* @return 方法执行结果
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
// 获得方法来源,如果方法继承自Object类则直接交由目标对象执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if ("prepareStatement".equals(method.getName())) { // Connection中的prepareStatement方法
if (isDebugEnabled()) { // 启用Debug
// 输出方法中的参数信息
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
// 交由目标对象执行
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
// 返回一个PreparedStatement的代理,该代理中加入了对PreparedStatement的日志打印操作
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("prepareCall".equals(method.getName())) { // Connection中的prepareCall方法
if (isDebugEnabled()) { // 启用Debug
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
// 交由目标对象执行
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
// 返回一个PreparedStatement的代理,该代理中加入了对PreparedStatement的日志打印操作
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) { // Connection中的createStatement方法
// 交由目标对象执行
Statement stmt = (Statement) method.invoke(connection, params);
// 返回一个Statement的代理,该代理中加入了对Statement的日志打印操作
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else { // 其它方法
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
上述 invoke方法主要完成了以下两个附加的操作。
- 在 prepareStatement、prepareCall这两个方法执行之前增加了日志打印操作。
- 在需要返回 PreparedStatement 对象、StatementLogger 对象的方法中,返回的是这些对象的具有日志打印功能的代理对象。这样,PreparedStatement 对象、StatementLogger对象中的方法也可以打印日志。
7. parsing
7.1 XML文件
可扩展标记语言(eXtensible Markup Language,XML)是一种标记语言。所谓的标记是指计算机所能理解的信息符号,通过标记可以实现软件开发者与计算机之间的信息沟通。常见的 HTML便是一种标记语言,不过 HTML语言中的标签(如“<h1> </h1>”“<img\>”等)都是固定的,是不可扩展的。XML则可以由开发人员自由扩展定义。
XML可扩展的一个重要表现就是 XML文档的结构是可以自由定义的。定义 XML文档可以使用 DTD(Document Type Definition,文档类型定义),也可以使用 XML Schema。不过在介绍 DTD和 XML Schema之前,我们先来了解 XML文档的结构。
7.1.1 XML文档的结构
XML文档中包含众多的节点。节点分为以下几类:元素节点、属性节点、文本节点、文档节点等。在实际指代中,可以省略“节点”二字,也可以将以上各类统称为“节点”。
在一个 XML文档中,可以存在什么元素及每个元素是怎样的,这些是由 XML文档的定义文件来进行描述的,如 DTD(此类文件的后缀名为 dtd)或者 XML Schema(此类文件的后缀名为 xsd)。
以 XML Schema文档为例,我们可以使用代码来定义上图展示的 XML片段。
而使用 DTD,则可以使用代码来定义前图展示的 XML片段。
DOCTYPE声明中,members是根节点名称,“[]”中为节点的限制条件。而且,DTD也支持使用外部 DTD文档来定义 XML文档,例如,在 MyBatis的配置文档开头可以看到引用了外部的 DTD文档。
在上图所示的 DOCTYPE声明中,各个项目的含义如下。
- configuration:表示当前 XML文档的根节点为 configuration。
- PUBLIC:表示当前 XML文档采用的是公共的 DTD。
- -//mybatis.org//DTD Config 3.0//EN:表示 DTD文档的信息。
(--:表示是非 ISO组织;mybatis.org:表示组织名称 mybatis.org;DTD Config 3.0:表示文本描述,包括版本号;EN:表示 DTD文档是英文。
- http://mybatis.org/dtd/mybatis-3-config.dtd:表示文档的下载地址。
7.1.2 XPath
XPath(XML Path Language,XML路径语言)作为一种小型的查询语言能够根据 XML结构树在树中寻找节点。XPath定义了一组语法,能够从结构树中筛选出满足要求的节点。如果对 CSS选择器或 jQuery选择器比较熟悉的话,那掌握 XPath的语法还是非常简单的,因为这些选择器的语法思路是相通的。XPath语法示例:
javax.xml.xpath包提供了强大的 XPath解析功能,可以基于它实现 XML的解析。
7.2 XML解析
MyBatis的配置文件与映射文件均是 XML文件,因此解析并读取 XML文档中的内容是 MyBatis展开后续工作的基础。MyBatis中的 parsing包就是用来进行 XML文件解析的包。在解析 XML文件的过程中,XPathParser类与 XNode类是两个最为关键的类,XPathParser类与XNode类主要关系的类图:
XPathParser 类中封装了“javax.xml.xpath.XPath”类的对象,因此 XPathParser类便具有了 XML解析的能力。
// 代表要解析的整个XML文档
private final Document document;
// 是否开启验证
private boolean validation;
// EntityResolver,通过它可以声明寻找DTD文件的方法,例如通过本地寻找,而不是只能通过网络下载dtd文件
private EntityResolver entityResolver;
// MyBatis配置文件中的properties信息
private Properties variables;
// javax.xml.xpath.XPath工具
private XPath xpath;
public XPathParser(String xml) {
// 初始化属性
commonConstructor(false, null, null);
// 从输入中获取整个xml文档
this.document = createDocument(new InputSource(new StringReader(xml)));
}
有必要说明一下,上述“private Properties variables”属性存储的内容就是 MyBatis配置文件中 properties 节点的信息。properties 节点会在解析配置文件的最开始就被解析,然后相关信息会被放入“private Properties variables”属性并在解析后续节点时发挥作用。XPathParser存在多个重载的构造方法,它们均根据传入的参数完成属性的初始化并构造出 XML文档对应的 Document对象。除去构造方法外,便是大量提供 XML文档中节点解析功能的“eval*”方法,这些方法最后都调用了 evaluate方法。
/**
* 进行XML节点的解析
* @param expression 解析的语句
* @param root 解析根
* @param returnType 返回值类型
* @return 解析结果
*/
private Object evaluate(String expression, Object root, QName returnType) {
try {
// 对指定节点root运行解析语法expression,获得returnType类型的解析结果
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
在 evaluate方法中,使用“javax.xml.xpath.XPath”对象进行了节点解析。因此,整个XPathParser类本质就是对“javax.xml.xpath.XPath”的封装和调用,可以把 XPathParser类看作 javax.xml.xpath.XPath类的包装类。
同样,可以将 parsing 包中的 XNode 类看作 org.w3c.dom.Node 类的包装类。org.w3c.dom.Node类是用来表示 DOM中节点的类,而 XNode类只是在 org.w3c.dom.Node类的基础上提取和补充了几个属性。XNode对象的属性:
// org.w3c.dom.Node 表示是xml中的一个节点
private final Node node;
// 节点名
private final String name;
// 节点体
private final String body;
// 节点的属性
private final Properties attributes;
// MyBatis配置文件中的properties信息
private final Properties variables;
// XML解析器XPathParser
private final XPathParser xpathParser;
XNode对象的上述属性中,name、body、attributes这三个属性是从“org.w3c.dom.Node”对象中提取出来的,而 variables、xpathParser这两个属性则是额外补充进来的。我们知道 XPathParser类具有解析 XML节点的能力,也就是说,XNode类中封装了自身的解析器。在一个类中封装自己的解析器,这是一种非常常见的做法,如此一来这个类不需要外界的帮助便可以解析自身,即获得了自解析能力。
大家可能有过这样的经历:由于新安装的计算机上没有解压软件,于是从网络或者朋友那里得到了一份解压软件。可是,拿到手的解压软件安装包却是一个压缩文件。尚未安装解压软件的你必然没法打开压缩文件获得安装包。而自解压文件(SelF-eXtracting,SFX)能够帮助你摆脱这个困境。自解析类也有类似的优点,它减少了对外部类的依赖,具有更高的内聚性,也更为易用。
正是得益于 XNode类的自解析特性,它本身提供了一些“eval*”方法,从而能够解析自身节点内的信息。
7.3 文档解析中的变量替换
MyBatis配置文件中 properties节点会在解析配置文件的最开始就被解析,并在解析后续节点时发挥作用。可是如何才能让这些信息在 XML 文件的解析中发挥作用呢?回到 XPathParser类,其“evalString(Object,String)”方法如代码所示
/**
* 解析XML文件中的字符串
* @param root 解析根
* @param expression 解析的语句
* @return 解析出的字符串
*/
public String evalString(Object root, String expression) {
// 解析出字符串结果
String result = (String) evaluate(expression, root, XPathConstants.STRING);
// 对字符串中的属性进行处理
result = PropertyParser.parse(result, variables);
return result;
}
我们发现在解析字符串时,通过“PropertyParser.parse”方法对解析出来的结果进行了进一步处理。而这一步处理中,properties节点的信息便发挥了作用。
PropertyParser类是属性解析器,与之关系密切的几个类的类图如图所示:
下面以 GenericTokenParser类为入口阅读这几个类。GenericTokenParser类是通用的占位符解析器,共有三个属性,相关注释如代码所示:
// 占位符的起始标志
private final String openToken;
// 占位符的结束标志
private final String closeToken;
// 占位符处理器
private final TokenHandler handler;
GenericTokenParser 类中有唯一的一个 parse 方法,该方法主要完成占位符的定位工作,然后把占位符的替换工作交给与其关联的 TokenHandler 处理。我们通过一个例子对parse方法的功能进行介绍。
假设“openToken=#{”“closeToken=}”,向 GenericTokenParser中的 parse方法传入的参数为“jdbc:mysql://127.0.0.1:3306/${dbname}?serverTimezone=UTC”,则 parse方法会将被“#{”和“}”包围的 dbname 字符串解析出来,作为输入参数传入 handler 中的handleToken方法,然后用 handleToken方法的返回值替换“${dbname}”字符串。
GenericTokenParser提供的占位符定位功能应用非常广泛,而不仅仅局限在 XML解析中,毕竟它的名称是“通用的”占位符解析器。SQL 语句的解析也离不开它的帮助。SQL语句中使用“#{}”或“${}”来设置的占位符也是依靠 GenericTokenParser 来完成解析的,流程与本节介绍的一样。
TokenHandler 是一个接口,它只定义了一个抽象方法handleToken。handleToken 方法要求输入一个字符串,然后返回一个字符串。例如,可以输入一个变量的名称,然后返回该变量的值。PropertyParser类的内部类 VariableTokenHandler便继承了该接口。VariableTokenHandler类:
private static class VariableTokenHandler implements TokenHandler {
// 输入的属性变量,是HashTable的子类
private final Properties variables;
// 是否启用默认值
private final boolean enableDefaultValue;
// 如果启用默认值的化,键和默认值之间的分割符
private final String defaultValueSeparator;
private VariableTokenHandler(Properties variables) {
this.variables = variables;
// 如果传入的属性中有就以属性中为准,否则设为默认值
// 默认不支持默认值,除非在属性中用org.apache.ibatis.parsing.PropertyParser.enable-default-value修改
this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
// 默认分隔符为“:”,除非在属性中用org.apache.ibatis.parsing.PropertyParser.default-value-separator修改
this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
}
// 取出variables中指定key的value,如果variables为null,则给出defaultValue
private String getPropertyValue(String key, String defaultValue) {
return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
}
/**
* 根据一个字符串,给出另一个字符串。多用在字符串替换等处
* 具体实现中,会以content作为键,从variables找出并返回对应的值
* 由键寻值的过程中支持设置默认值
* 如果启用默认值,则content形如"key:defaultValue"
* 如果没有启用默认值,则content形如"key"
* @param content 输入的字符串
* @return 输出的字符串
*/
@Override
public String handleToken(String content) {
if (variables != null) { // variables不为null
String key = content;
if (enableDefaultValue) { // 如果启用默认值,则设置默认值
// 找出键与默认值分割符的位置
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) {
// 分隔符以前是键
key = content.substring(0, separatorIndex);
// 分隔符以后是默认值
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
return variables.getProperty(key, defaultValue);
}
}
if (variables.containsKey(key)) {
// 尝试寻找非默认的值
return variables.getProperty(key);
}
}
// 如果variables为null。不发生任何替换,直接原样还回
return "${" + content + "}";
}
}
了解了 VariableTokenHandler类的属性后,再阅读其 handleToken方法,向 handleToken 方法中传入输入参数后,该方法会以输入参数为键尝试从 variables属性中寻找对应的值返回。在这个由键寻值的过程中还可以支持默认值。
/**
* 进行字符串中属性变量的替换
* @param string 输入的字符串,可能包含属性变量
* @param variables 属性映射信息
* @return 经过属性变量替换的字符串
*/
public static String parse(String string, Properties variables) {
// 创建负责字符串替换的类
VariableTokenHandler handler = new VariableTokenHandler(variables);
// 创建通用的占位符解析器
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
// 开展解析,即替换占位符中的值
return parser.parse(string);
}
最后再看 PropertyParser中的静态方法 parse,它做了以下几个工作将 GenericTokenParser提供的占位符定位功能和 TokenHandler提供的字符串替换功能串接在了一起。
- 创建一个 VariableTokenHandler对象(TokenHandler接口子类的对象)。该对象能够从一个 Properties对象(这里传入的是 properties节点信息)中根据键索引一个值。
- 创建了一个属性解析器。只要设置了该属性解析器要匹配的模式,它就能将指定模式的属性值定位出来,然后将其替换为 TokenHandler接口中 handleToken方法的返回值。
这样一来,只要在 XML文件中使用“${”和“}”包围一个变量名,则该变量名就会被替换成 properties节点中对应的值。