本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是CSDN中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为CSDN博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/onlinedct/article/details/107306041
1.annotations包与lang包
annotations包与lang包存放的都是注解类。以@Param为例分析介绍注解的工作原理。
@Documented // 表明该注解会保留在API文档中
@Retention(RetentionPolicy.RUNTIME) // 表明注解会保留到运行阶段
@Target(ElementType.PARAMETER) // 表明注解可以应用在参数上
public @interface Param {
String value(); // 整个注解只有一个属性,名为value
}
使用@Param时只需在方法参数中使用改注解对参数命名就可以在映射文件中应用该参数。
User queryById(@Param("id")Integer userId);
在mapper.xml文件中引用变量名为id即可
<select id="queryById" resultType="User">
select * from user where id=#{id}
</select>
在ParamNameResolver类的构造函数中处理了@Param的参数名称替换
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);
}
循环遍历它的每个注解并使用“annotation instanceof Param”判断当前注解是否为“Param”注解。如果当前注解为“Param”注解,则会使用“((Param)annotation).value()”操作获取该注解的 value值作为参数的名称。 Integer userId参数使用了@Param(“id”)后,“id”便成了实参的名称,因此能够使用“id”索引到对应的实参。
2.type包
type包中的类有 55个之多,比较复杂但是通过分析梳理后
把 type包内的类分为以下六组。
- 类型处理器:1个接口、1个基础实现类、1个辅助类、43个实现类。
- TypeHandler:类型处理器接口;
- BaseTypeHandler:类型处理器的基础实现;
- TypeReference:类型参考器;
- TypeHandler:43个类型处理器。
- 类型注册表:3个。
归类总结是源码阅读中非常好的办法。往往越是大量的类,越是大量的方法,越有规律进行分类。这些原本繁杂的类和方法经过分类后,可能会变得很有条理。
2.1 类型注册表
- SimpleTypeRegistry:基本类型注册表,内部使用 Set 维护了所有 Java 基本数据类型的集合;
- TypeAliasRegistry:类型别名注册表,内部使用 HashMap维护了所有类型的别名和类型的映射关系;有了这个注册表,我们就可以在很多场合使用类型的别名来指代具体的类型。
- TypeHandlerRegistry:类型处理器注册表,内部维护了所有类型与对应类型处理器的映射关系。
三个类型注册表的存在,使 MyBatis 不仅可以根据类型找寻其类型处理器,而且还可以根据类型别名找寻对应的类型处理器。
2.2 注解类
- Alias:使用该注解可以给类设置别名,设置后,别名和类型的映射关系便存入TypeAliasRegistry中;
- MappedJdbcTypes:有时我们想使用自己的处理器来处理某些 JDBC 类型,只需创建 BaseTypeHandler 的子类,然后在上面加上该注解,声明它要处理的JDBC类型即可;
- MappedTypes:有时我们想使用自己的处理器来处理某些Java类型,只需创建BaseTypeHandler的子类,然后在上面加上该注解,声明它要处理的 Java类型即可。
2.3 异常类、工具类、枚举类
- 异常类:TypeException:表示与类型处理相关的异常。
- 工具类:ByteArrayUtils:提供数组转化的工具方法。
- 枚举类:JdbcType:在 Enum中定义了所有的 JDBC类型,类型来源于 java.sql.Types。
2.4 类型处理器
作为一个 ORM框架,处理 Java对象和数据库关系之间的映射是 MyBatis工作中的重要部分。然而在 Java中存在 Integer、String、Data等各种类型的数据,在数据库中也存在varchar、longvarchar、tinyint等各种类型的数据。不同类型的字段所需的读、写方法各不相同,因此需要对不同类型的字段采取相应的处理方式。
在 type包中,将每种类型对应的处理方式封装在了对应的类型处理器 TypeHandler中。例如,IntegerTypeHandler负责完成对 Integer类型的处理。type 包共有 43 个类型处理器,这些类型处理器的名称也均以“TypeHandler”结尾。而 TypeHandler和 BaseTypeHandler则分别是类型处理器接口和类型处理器基类。
在类型处理器相关类的设计中采用了模板模式,BaseTypeHandler<T>作为所有类型处理器的基类,定义了模板的框架。而在各个具体的实现类中,则实现了具体的细节。
在BaseTypeHandler中提供了四个抽象方法交由子类实现,getResult(ResultSet,String)方法为例,该方法完成了异常处理等统一的工作,而与具体类型相关的 getNullableResult(ResultSet,String)操作则通过抽象方法交给具体的类型处理器实现。这就是典型的模板模式。
/**
* 从结果集中读出一个结果
* @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);
}
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
2.5 TypeReference类
43个类型处理器可以处理不同 Java类型的数据,而这些类型处理器都是 TypeHandler接口的子类,因此可以都作为TypeHandler来使用。那会不会遇到一个问题,当 MyBatis取到某一个 TypeHandler 时,却不知道它到底是用来处理哪一种 Java类型的处理器?为了解决这一问题,MyBatis 定义了一个 TypeReference 类。它能够判断出一个TypeHandler用来处理的目标类型。而它判断的方法也很简单:取出 TypeHandler实现类中的泛型参数 T的类型,这个值的类型也便是该 TypeHandler能处理的目标类型。该功能由getSuperclassTypeParameter方法实现,该方法能将找出的目标类型存入类中的 rawType属性
/**
* 解析出当前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;
}
TypeReference 类是 BaseTypeHandler 的父类,因此所有的类型处理器都继承了TypeReference 的功能。这意味着对任何一个类型处理器调用 getSuperclassTypeParameter方法,都可以得到该处理器用来处理的目标类型。
3.io包
io包即输入/输出包,负责完成 MyBatis中与输入/输出相关的操作,对磁盘文件的读写。在MyBatis的工作中,与磁盘文件的交互主要是对 xml配置文件的读操作。因此,io包中提供对磁盘文件读操作的支持。除了读取磁盘文件的功能外,io包还提供对内存中类文件(class文件)的操作。
3.1VFS
磁盘文件系统分为很多种,如 FAT、VFAT、NFS、NTFS等。不同文件系统的读写操作各不相同。VFS(Virtual File System)作为一个虚拟的文件系统将各个磁盘文件系统的差异屏蔽了起来,提供了统一的操作接口。这使得上层的软件能够用单一的方式来跟底层不同的文件系统沟通。在操作磁盘文件时,软件程序不需要和实体的文件系统打交道,只需要和 VFS沟通即可。这使得软件系统的磁盘操作变得更为简单。
3.1.1 VFS实现类
DefaultVFS类和 JBoss6VFS类是 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实现类
private static class VFSHolder {
// 最终指定的实现类
static final VFS INSTANCE = createVFS();
/**
* 给出一个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类的 createVFS方法中,先组建一个 VFS实现类的列表,然后依次对列表中的实现类进行校验。第一个通过校验的实现类即被选中。在组建列表时,用户自定义的实现类放在了列表的前部,这保证了用户自定义的实现类具有更高的优先级。
3.1.2 DefaultVFS类
DefaultVFS 作为默认的 VFS 实现类,其 isValid 函数恒返回true。因此,只要加载DefaultVFS类,它一定能通过 VFS类中VFSHolder单例中的校验,并且在进行实现类的校验时DefaultVFS排在整个校验列表的最后。因此,DefaultVFS成了所有 VFS实现类的保底方案,即最后一个验证,但只要验证一定能通过。
- list(URL,String):列出指定 url下符合条件的资源名称;
- listResources(JarInputStream,String):列出给定 jar包中符合条件的资源名称;
- findJarForResource(URL):找出指定路径上的 jar包,返回 jar包的准确路径;
- getPackagePath(String):将 jar包名称转为路径;
- isJar:判断指定路径上是否是 jar包
3.1.3 JBoss6VFS类
JBoss是一个基于 J2EE的开放源代码的应用服务器,JBoss6是JBoss中的一个版本。JBoss6VFS即为借鉴 JBoss6设计的一套VFS实现类。在 JBoss6VFS中主要存在两个内部类。
- VirtualFile:仿照 JBoss中的 VirtualFile类设计的一个功能子集;
- VFS:仿照 JBoss中的 VFS类设计的一个功能子集。
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类。主要是根据被代理类是否存在来判断自身是否可用
*/
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);
}
}
3.2 类文件加载
要把类文件加载成类,需要类加载器的支持。ClassLoaderWrapper 类中封装了五种类加载器,而Resources 类又对 ClassLoaderWrapper 类进行了一些封装。
/**
* 获取所有的类加载器
* @param classLoader 传入的一种类加载器
* @return 所有类加载器的列表
*/
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader,
defaultClassLoader,
Thread.currentThread().getContextClassLoader(),
getClass().getClassLoader(),
systemClassLoader};
}
- 作为参数传入的类加载器,可能为 null;
- 系统默认的类加载器,如未设置则为 null;
- 当前线程的线程上下文中的类加载器;
- 当前对象的类加载器;
- 系统类加载器,在 ClassLoaderWrapper的构造方法中设置。
以上五种类加载器的优先级由高到低。在读取类文件时,依次到上述五种类加载器中进行寻找,只要某一次寻找成功即返回结果。
类加载给出了统一方法处理:
/**
* 轮番使用各个加载器尝试加载一个类
* @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);
}
3.3 ResolverUtil类
ResolverUtil类是一个工具类,用来完成类的筛选。可以筛选类是否是某个接口或类的子类、筛选类是否具有某个注解。
为了能够基于这些条件进行筛选,ResolverUtil中设置有一个内部接口 Test。Test是一个筛选器,内部类中有一个抽象方法matches来判断指定类是否满足筛选条件。
Test接口又有两个实现类:
- 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;
}
/**
* 判断一个类文件是否满足条件。如果满足则记录下来
* @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());
}
}
4.logging包
logging包负责完成 MyBatis操作中的日志记录工作。对于大多数系统而言,日志记录是必不可少的,它能够帮助我们追踪系统的状态或者定位问题所在。MyBatis作为一个 ORM框架,运行过程中可能会在配置解析、参数处理、数据查询、结果转化等各个环节中遇到错误,这时,MyBatis 输出的日志便成了定位错误的最好资料。
4.1日志框架与日志级别
日志框架是一种在目标对象发生变化时将相关信息记录进日志文件的框架。这样,当目标对象出现问题或需要核查目标对象变动历史时,日志框架记录的日志文件便可以提供翔实的资料。起初,Java的日志打印依靠软件开发者自行编辑输出语句将日志输出到文件流中。例如,通过“System.out.println”方法打印普通信息,或通过“System.err.println”方法打印错误信息。开发者自行编辑输出语句进行日志打印的方式非常烦琐,而且还会导致日志格式混乱,不利于日志分析软件的进一步处理。为了解决这些问题,产生了大量的日志框架。经过多年的发展,Java 领域的日志框架已经非常丰富,有 log4j、Logging、commons-logging、slf4j、logback等,它们为 Java的日志打印工作提供了极大的便利。
常见的日志等级划分方式如下。
- Fatal:致命等级的日志,指发生了严重的会导致应用程序退出的事件;
- Error:错误等级的日志,指发生了错误,但是不影响系统运行;
- Warn:警告等级的日志,指发生了异常,可能是潜在的错误;
- Info:信息等级的日志,指一些在粗粒度级别上需要强调的应用程序运行信息;
- Debug:调试等级的日志,指一些细粒度的对于程序调试有帮助的信息;
- Trace:跟踪等级的日志,指一些包含程序运行详细过程的信息。
在打印日志时我们可以定义日志的级别。也可以根据日志等级进行输出,防止大量的日志信息混杂在一起。目前,在很多集成开发环境中可以调节日志的显示级别,使只有一定级别以上的日志才会显示出来,这样能够根据不同的使用情形进行日志的筛选。
4.2 Log接口
logging包中的Log接口有11个实现类,分布在不同的子包中。
public interface Log {
boolean isDebugEnabled();//判断debug级别的日志功能是否开启
boolean isTraceEnabled();//判断trace级别的日志功能是否开启
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
isDebugEnabled方法和 isTraceEnabled方法是从效率角度考虑而设计的。首先,Debug和 Trace是两个级别比较低的日志,越是级别低的日志越有这样的特点:
- 很少开启:因为它们级别很低,大多时候该级别的信息不需要展示;
- 输出频次高:低级别日志的触发门槛很低,这意味着一旦它们开启,往往会以非常高的频率输出日志信息;
- 内容冗长:它们中通常包含非常丰富和细致的信息,因此信息内容往往十分冗长。
- 重要度低:这类日志往往不重要,程序正常的运行日志通常都只在开发阶段用来分析程序运行状态。
低级别的日志很少开启,这意味着 this.isLevelEnabled(1)的返回值大概率是 false。并且低级别日志输出频次高且内容冗长,这意味着这种无用的字符串拼接是频发的且资源消耗很大。要想避免上述无用的字符串操作导致的大量系统资源消耗,就需要使用 isDebugEnabled方法或isTraceEnabled方法对低级别的日志输出进行前置判断。
在阅读源码的过程中,读懂源码只是完成了浅层知识的学习。在读懂源码的同时思考源码为何这么设计将会使我们有更大的收获,也会使我们更容易读懂源码。
以JakartaCommonsLoggingImpl为例分析下实现:
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”对象,然后所有方法都将操作委托给了该对象。
4.3 LogFactory
Log接口有着众多的实现类,而 LogFactory就是制造实现类的工厂。最终,该工厂会给出一个可用的 Log实现类,由它来完成 MyBatis的日志打印工作。
Log 接口的实现类都是对象适配器(装饰器类除外),最终的实际工作要委托给被适配的目标对象来完成。因此是否存在一个可用的目标对象成了适配器能否正常工作的关键所在。于是 LogFactory的主要工作就是尝试生成各个目标对象。如果一个目标对象能够被生成,那该目标对象对应的适配器就是可用的。
// 这里的加载顺序就是MyBatis日志顺序
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
//调用setImplementation
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
//调用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);
}
}
4.4 JDBC日志打印
MyBatis是 ORM框架,它负责数据库信息和 Java对象的互相映射操作,而不负责具体的数据库读写操作。具体的数据库读写操作是由 JDBC进行的。既然 MyBatis不进行数据库的查询,那 MyBatis的日志中便不会包含 JDBC的操作日志。
然而,很多时候 MyBatis的映射错误是由于 JDBC的错误引发的,例如 JDBC无法正确执行查询操作或者查询得到的结果类型与预期的不一致等。因此,JDBC 的运行日志是分析 MyBatis框架报错的重要依据。然而,JDBC日志有自身的一套输出体系,JDBC日志和 MyBatis日志是分开的,这会给我们的调试工作带来很多的困难。jdbc子包就是用来解决这个问题的。jdbc子包基于代理模式,让 MyBatis能够将 JDBC的操作日志打印出来,极大地方便了我们的调试工作。接下来就介绍 jdbc子包是如何实现这个操作的。
BaseJdbcLogger作为基类提供了一些基本功能,而其子类则为相应的类提供日志能力。并且所有子类都同时实现了InvocationHandler接口。也就是代理实现日志打印能力。
在BaseExecutor中getConnection方法当启用debug等级日志时返回的是一个代理对象。这样connection的所有方法都会经过CollectionLogger的invoker方法。
public abstract class BaseExecutor implements Executor {
/**
* 获取一个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;
}
}
}
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
/**
* 代理方法
* @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);
}
}
}
invoker方法主要完成了:
- 在执行prepareStatement、prepareCall这两个方法执行之前增加了日志打印操作。
- 在需要返回 PreparedStatement 对象、StatementLogger 对象的方法中,返回的是这些对象的具有日志打印功能的代理对象。这样,PreparedStatement 对象、StatementLogger对象中的方法也可以打印日志。
BaseJdbcLogger的其他实现类也是一样的逻辑完成了对JDBC原生对象的增强日志打印。