第 2 章 基础支持层

22 阅读16分钟

在这里插入图片描述

2.1 解析器模块

常见的 XML 处理方式

  1. DOM,基于树形结构的 XML 解析方式,它会将整个 XML 文档读入内存并构建一个 DOM 树,基于这棵树形结构对各个节点(Node)进行操作。

  2. SAX,基于事件模型的 XML 解析方式,它不需要将整个 XML 文档加载到内存中,而只需将 XML 文档的一部分加载到内存中。

  3. StAX,JAXP 的后来版本,解析方式与 SAX 类似,也是把 XML 文档作为一个事件流进行处理。不同之处在于 StAX 采用的是“拉模式”。

    在这里插入图片描述

2.1.1 XPath简介

MyBatis 在初始化过程中处理 mybatis-config.xml 配置文件以及映射文件时,使用的是 DOM 解析方式,并结合使用 XPath 解析 XML 配置文件。

2.1.2 XPathParse

MyBatis 提供的 XPathParser 类封装了 XPath、Document 和 EntityResolver。

2.2 反射工具箱

2.2.1 Reflector&ReflectorFactory

  • Reflector 是 MyBatis 中反射模块的基础,每个 Reflector 对象都对应一个类,在 Reflector 中缓存了反射操作需要使用的类的元信息。
  • ReflectorFactory 接口主要实现了对 Reflector 对象的创建和缓存。

TIPS:关于 JavaBean 规范

类中定义的成员变量也称为“字段”,属性则是通过 getter/setter 方法得到的,属性只与类中的方法有关,与是否存在对应成员变量没有关系。例如,存在 getA() 方法和 setA(String) 方法,无论类中是否定义了字段 String a,我们都认为该类中存在属性 a。

2.2.2 TypeParameterResolver

TypeParameterResolver 是一个工具类,它提供了一系列静态方法来解析指定类的字段、方法返回值或方法参数的类型。

2.2.3 ObjectFactory

ObjectFactory 接口提供了多个 create() 方法的重载,通过这些方法可以创建指定类型的对象。

2.2.4 Property 工具集

  1. PropertyTokenizer

    <resultMap id="rm4testProTool" type="User">
        <id column="id" property="id"/>
        <result property="orders[0].items[0].name" column="item1"/>
        <result property="orders[0].items[1].name" column="item2"/>
    </resultMap>
    

    orders[0].items[0].name 这种由 “.” 和 “[]” 组成的表达式是由 PropertyTokenizer 进行解析的。

  2. PropertyNamer,完成方法名到属性名的转换

  3. PropertyCopier,实现相同类型的两个对象之间的属性值的拷贝

2.2.5 MetaClass

MetaClass 通过 Reflector 和 PropertyTokenizer 的组合使用,实现了对复杂的属性表达式的解析,并实现了获取指定属性描述信息的功能。

2.2.6 ObjectWrapper

ObjectWrapper 接口是对对象的包装,抽象了对象的属性信息,它定义了一系列查询对象属性信息的方法。

2.2.7 MetaObject

MetaObject 负责解析属性表达式。

2.3 类型转换

JDBC 数据类型与 Java 语言中的数据类型并不是完全对应的,所以在使用时需要对二者进行转换。

在这里插入图片描述

MyBatis 中使用 JdbcType 枚举代表 JDBC 中的数据类型,该枚举类型中定义了 TYPE_CODE 字段,记录了 JDBC 类型在 java.sql.Types 中相应的常量编码,并通过一个静态集合 codeLookup(HashMap<Integer, JdbcType>) 维护了常量编码与 JdbcType 之间的对应关系。

2.3.1 TypeHandler

MyBatis 中所有的类型转换器都继承了 TypeHandler 接口,在TypeHandler 接口中定义了两类四个方法,其中 setParameter() 方法将数据由 Java 类型转换成 JdbcType 类型;getResult() 方法及其重载负责将数据由 JdbcType 类型转换成 Java 类型。

2.3.2 TypeHandlerRegistry

在 MyBatis 初始化过程中,会为所有已知的 TypeHandler 创建对象,并实现注册到 TypeHandlerRegistry 中,由 TypeHandlerRegistry 负责管理这些对象。

2.3.3 TypeAliasRegistry

MyBatis 将 SQL 语句中别名的概念进行了延伸和扩展,可以为一个类添加一个别名,之后就可以通过别名引用该类。并通过 TypeAliasRegistry 完成对别名的注册和管理。

2.4 日志模块

2.4.1 适配器模式

适配器模式的主要目的是解决由于接口不能兼容而导致类无法使用的问题,适配器模式会将需要甜酸的类转换成调用者能够使用的目标接口。涉及以下角色:

  1. 目标接口(Target):调用者能够直接使用的接口
  2. 需要适配的类(Adaptee):一般情况下,Adaptee 类中有真正的业务逻辑,但是其接口不能被调用者直接使用。
  3. 适配器(Adapter):Adapter 实现了 Target 接口,并包装了一个 Adaptee 对象。Adapter 在实现 Target 接口中的方法时,会将调用委托给 Adaptee 对象,由后者完成具体的业务。

使用适配器模式的好处就是复用现有组件。这符合开闭原则,当有新的 Adaptee 需要被复用时,只要添加新的 Adapter 即可。

在 MyBatis 的日志模块中,就使用了适配器模式。MyBatis 使用内部接口(logging.Log)完成日志模块。并提供多种 Adapter 来集成和复用 Log4j,Log4j2 等三方日志组件。

2.4.2 日志适配器

MyBatis 统一提供了 trace、debug、warn、error 四个日志级别,可以满足绝大多数场景的日志需求。

2.4.3 代理模式与 JDK 动态代理

在这里插入图片描述

如图,Subject 是程序中的业务逻辑接口,RealSubject 是实现了 Subject 接口的真正业务类,Proxy 是实现了 Subject 接口的代理类,其中封装了 RealSubject 对象。程序中不会直接调用 RealSubject 对象的方法,而是使用 Proxy 对象实现相关的功能。这就是所谓的“代理模式”。

除了上述的“静态代理模式”(在编译阶段就为每个被代理类创建一个 Proxy 类),还可以使用动态代理。JDK 通过 InvocationHandler 接口,提供了动态代理的解决方案。

package com.example.chapter2.section4;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class TestInvokerHandler implements InvocationHandler {

    private Object target; // 真正的业务对象,也就是 RealSubject 对象

    public TestInvokerHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // ……在执行真正的业务方法之前的预处理操作……
        Object result = method.invoke(target, args);// 调用真正的业务方法
        // ……在执行真正的业务方法之后的后续处理操作……
        return result;
    }

    public Object getProxy() {
        // 创建代理对象
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), target.getClass().getInterfaces(), this);
    }

    public static void main(String[] args) {
        // 创建真正的业务对象
        Subject realSubject = new RealSubject();
        // 创建 InvocationHandler 对象
        TestInvokerHandler handler = new TestInvokerHandler(realSubject);
        // 创建代理对象
        Subject proxySubject = (Subject) handler.getProxy();
        // 调用代理对象的方法
        proxySubject.operation();
    }
}

2.4.4 JDBC调试

MyBatis 日志模块中有一个 Jdbc 包,通过 JDK 动态代理的方式,将 JDBC 操作通过指定的日志框架打印出来。这个功能通常在开发阶段使用,它可以输出 SQL 语句、用户传入的绑定参数、SQL 语句影响行数等信息。

2.5 资源加载

2.5.1 类加载器简介

  1. JVM 中的类加载器(ClassLoader)负责加载来自文件系统、网络或其他来源的类文件

  2. 有三种默认使用的类加载器

    Bootstrap ClassLoader负责加载 JDK 自带的 rt.jar 包中的类文件所有类加载器的父加载器,没有任何父加载器
    Extension ClassLoader负责加载 Java 的扩展类库,即 jre/lib/ext 或 java.ext.dirs 指定的目录下的类文件System ClassLoader 的子加载器
    System(Application) ClassLoader负责从 classpath 环境变量中加载类文件,classpath 通常由 “-classpath” 或 “-cp” 命令行选项定义,或是由 JAR 中 Manifest 文件的 classpath 属性指定。Extension ClassLoader 的子加载器
  3. JVM 类加载器默认使用双亲委派模式——在加载类文件时,子加载器首先会将加载请求委托给它的父加载器。父加载器会检测自己是否已经加载过该类,如果已经加载过则加载过程结束;如果没有加载过,则请求继续向上传递直到 Bootstrap ClassLoader,最终由 Bootstrap ClassLoader 加载该类文件,如果加载失败则由子加载器尝试加载,直接发起加载请求的子加载器为止。双亲委派模式可以保证:1)子加载器可以使用父加载器已加载的类;2)父加载器已加载过的类无法被子加载器再次加载。保证了 JVM 的安全性和稳定性。

    在这里插入图片描述

  4. 可以通过继承 java.lang.ClassLoader 类的方式实现自定义类加载器,以满足特殊需要。

2.5.2 ClassLoaderWrapper

MyBatis 的 IO 包中封装了 ClassLoader 以及读取资源文件的相关 API。ClassLoaderWrapper 是一个 ClassLoader 的包装器,其中包含了多个 ClassLoader 对象。通过调整多个类加载器的使用顺序,ClassLoaderWrapper 可以确保返回 给系统使用的是正确的类加载器。

2.5.3 ResolverUtil

ResolverUtil 可以根据指定的条件查找指定包下的类,其中使用的条件由 Test 接口表示。

2.5.4 单例模式

所谓单例模式,是指在整个系统中,单例类只能有一个实例对象,且需要自行完成示例,并始终对外提供同一实例对象。

写法一:双端检锁

package com.example.chapter2.section5;

public class Singleton {
    
    // 使用 volatile 修饰 instance 变量
    private static volatile Singleton instance = null;
    
    // 私有构造方法
    private Singleton() {
    }
    
    // 静态方法
    public static Singleton getInstance() {
        // 第一次检查 instance 是否为 null
        if (instance == null) {
            // 加锁
            synchronized (Singleton.class) {
                // 第二次检查 instance 是否为 null
                if (instance == null) {
                    // 创建实例
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

写法二:静态内部类

package com.example.chapter2.section5;

public class Singleton6 {

    // 私有的静态内部类,该静态内部类只会在newInstance()方法中被加载
    private static class SingletonHolder {
        // 静态初始化器,由JVM来保证线程安全
        private static final Singleton6 instance = new Singleton6();
    }

    // 私有构造方法
    private Singleton6() {
    }

    // 静态方法
    public static Singleton6 newInstance() {
        return SingletonHolder.instance;
    }
}

2.5.5 VFS

虚拟文件系统(Virtual File System),它用来查找指定路径下的资源。MyBatis 提供了 JBoss6VFS 和 DefaultVFS 两个 VFS 的实现。

2.6 DataSource

MyBatis 提供了两个 javax.sql.DataSource 接口实现,分别是 PooledDataSource 和 UnpooledDataSource。MyBatis 使用不同的 DataSourceFactory 接口实现创建不同类型的 DataSource。

2.6.1 工厂方法模式

在工厂方法模式中,定义一个用于创建对象的工厂接口,并根据工厂接口的具体实现类决定具体实例化哪一个产品类。

在这里插入图片描述

工厂方法由四个角色构成:

  1. 工厂接口(Factory):核心接口,调用者会直接与工厂接口交互用于获取具体的产品实现类
  2. 具体工厂类(ConcreteFactory):实现类,用于实例化产品对象
  3. 产品接口(Product):用于定义产品类的功能,具体工厂类产生的所有产品对象都必须实现该接口
  4. 具体产品类(ConcreteProduct):实现产品接口的实现类

当需要添加新的第三方数据源组件时,只需要添加对应的工厂实现类,新数据源就能被 MyBatis 使用。

2.6.2 DataSourceFactory

在数据源模块中,DataSourceFactory 接口扮演工厂接口的角色。UnpooledDataSourceFactory 和 PooledDataSourceFactory 则扮演着具体工厂类的角色。

在这里插入图片描述

2.6.3 UnpooledDataSource

javax.sql.DataSource 接口在数据源模块中扮演了产品接口的角色,MyBatis 提供了两个 DataSource 接口的实现类,分别是 UnpooledDataSource 和 PooledDataSource,它们扮演了具体的产品类的角色。

在这里插入图片描述

2.6.4 PooledDataSource

数据库连接的创建过程是非常耗时的,数据库能够建立的连接数也非常有限。使用数据库连接池就显得尤为必要。

PooledDataSource 实现了简易数据库连接池的功能,如图

在这里插入图片描述

public class PoolState {

  protected PooledDataSource dataSource;

  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  protected final List<PooledConnection> activeConnections = new ArrayList<>();
  
}

2.7 Transaction

MyBatis 使用 Transaction 接口对数据库事务进行抽象,它有 JdbcTransaction 和 ManagedTransaction 两个实现。

在这里插入图片描述

JdbcTransaction 依赖于 JDBC Connection 控制事务的提交和回滚。

ManagedTransaction 事务和提交和回滚都是空实现,依靠窗口管理的。

2.8 binding 模块

binding 模块提供 Mapper 接口,定义 SQL 语句对应的方法,这些方法在 MyBatis 初始化过程中会与映射配置文件中定义的 SQL 语句相关联。如果存在无法关系的 SQL 语句,就会抛出异常。

2.8.1 MapperRegistry & MapperProxyFactory

MapperRegistry 是 Mapper 接口及其对应的代理对象工厂的注册中心。

public class MapperRegistry {
    // Configuration 对象,MyBatis 全局唯一的配置对象,包含了所有配置信息
    private final Configuration config;
    // 记录 Mapper 接口与对应 MapperProxyFactory 之间的关系
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
}

MyBatis 初始化过程中人读取映射配置文件以及 Mapper 接口中的注解信息,填充到 knownMappers 集合中。

在需要执行 SQL 语句时,会通过 knownMappers 获取 MapperProxyFactory,并使用 JDK 动态代理生成代理对象执行 SQL 语句。

2.8.2 MapperProxy

MapperProxy 实现了 InvocationHandler 接口,核心功能如下

public class MapperProxy<T> implements InvocationHandler, Serializable {
    // 记录了关联的 SqlSession 对象
    private final SqlSession sqlSession;
    // Mapper 接口对应的 Class 对象
    private final Class<T> mapperInterface;
    // 用于缓存 MapperMethod 对象,key:Mapper 接口中方法对应的 Method 对象,value: 对应的 MapperMethod 对象。
    private final Map<Method, MapperMethodInvoker> methodCache;
}

2.8.3 MapperMethod

MapperMethod 中封装了 Mapper 接口中对应方法的信息,以及对应 SQL 语句的信息。MapperMethod 可以理解为连接 Mapper 接口以及映射配置文件中定义的 SQL 语句的桥梁。

public class MapperMethod {
	// 记录了 SQL 语句的名称和类型
    private final SqlCommand command;
    // Mapper 接口中对应方法的相关信息
    private final MethodSignature method;
}

MapperMethod 中最核心的方法是 execute(),它会根据 SQL 语句的类型调用 SqlSession 对应的方法完成数据库操作。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
  }

2.9 缓存模块

MyBatis 中的缓存是两层结构,分为一级缓存和二级缓存,但在本质是都是 Cache 接口的实现。

2.9.1 装饰器模式

装饰器可以动态地为对象添加功能,它是基于组合的方式实现该功能的。

在这里插入图片描述

  • Component(组件):定义了全部组件实现类以及所有装饰器实现的行为
  • ConcreteComponent(具体组件实现类):实现了 Component 接口。通常情况下就是被装饰器装饰的原始对象。
  • Decorator(装饰器):所有装饰器的父类,它是一个实现了 Component 接口的抽象类,
  • ConcreteDecorator:具体的装饰器实现类,该实现类要向被装饰对象添加某些功能。

使用装饰器模式有两个明显优点:

  1. 相较于承继,装饰器模式更灵活,也更容易扩展
  2. 当有新功能需要添加时,只需要添加新的装饰器实现类,无须修改已有类的代码,符合开闭原则

2.9.2 Cache 接口及其实现

public interface Cache {
	// 缓存对象的 id
    String getId();
	// 向缓存中添加数据,key:cacheKey,value:查询结果
    void putObject(Object key, Object value);
	// 根据指定的 key,在缓存中查询结果
    Object getObject(Object key);
	// 删除 key 对应的缓存项
    Object removeObject(Object key);
	// 清空缓存
    void clear();
	// 缓存项的个数,不会被 MyBatis 核心代码使用
    int getSize();
	// 获取读写锁,不会被 MyBatis 核心代码使用
    default ReadWriteLock getReadWriteLock() {
    	return null;
    }
}

实现类 PerpetualCache 提供了 Cache 接口的基本实现。底层使用 HashMap 记录缓存项,并实现 Cache 接口中定义的相应方法。

public class PerpetualCache implements Cache {

	private final String id;
	private final Map<Object, Object> cache = new HashMap<>();

	public PerpetualCache(String id) {
		this.id = id;
	}

	@Override
	public String getId() {
		return id;
	}

	@Override
	public int getSize() {
		return cache.size();
	}

	@Override
	public void putObject(Object key, Object value) {
		cache.put(key, value);
	}

	@Override
	public Object getObject(Object key) {
		return cache.get(key);
	}

	@Override
	public Object removeObject(Object key) {
		return cache.remove(key);
	}

	@Override
	public void clear() {
		cache.clear();
	}
}

Cache 的其他实现类,会在 PerpetualCache 的基础上提供一些额外功能,通过多个组合后满足一个特定的需求。

BlockingCache:阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据。

public class BlockingCache implements Cache {
	// 阻塞超时时长
    private long timeout;
    // 被装饰的底层 Cache 对象
    private final Cache delegate;
    // 每个 key 都有对应的 ReentrantLock 对象
    private final ConcurrentHashMap<Object, CountDownLatch> locks;
}

FifoCache & LruCache

public class FifoCache implements Cache {
	// 被装饰的底层 Cache 对象
	private final Cache delegate;
	// 用于记录 key 进入缓存的先后顺序
	private final Deque<Object> keyList;
	// 记录了缓存项的上限,超过该值,则需要清理最老的缓存项
	private int size;

	public FifoCache(Cache delegate) {
		this.delegate = delegate;
		this.keyList = new LinkedList<>();
		this.size = 1024;
	}
}
public class LruCache implements Cache {
	// 被装饰的底层 Cache 对象
	private final Cache delegate;
	// LinkedHashMap,记录 key 最近的使用情况
	private Map<Object, Object> keyMap;
	// 记录最少被使用的缓存项的 key
	private Object eldestKey;

	public LruCache(Cache delegate) {
		this.delegate = delegate;
		setSize(1024);
	}
}

SoftCache & WeakCache

关于强、软、弱、虚引用,会在其他笔记中整理

public class SoftCache implements Cache {
	// 在 SoftCache 中,保存最近使用的一部分缓存项不被 GC 回收
	private final Deque<Object> hardLinksToAvoidGarbageCollection;
    // 引用队列,用于记录已经被 GC 回收的缓存项所对应的 SoftEntry 对象
	private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
    // 被装饰的底层 Cache 对象
	private final Cache delegate;
    // 强连接的个数
	private int numberOfHardLinks;

	public SoftCache(Cache delegate) {
		this.delegate = delegate;
		this.numberOfHardLinks = 256;
		this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
		this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
	}
}

ScheduledCache & LoggingCache & SynchronizedCache & SerializedCache

  • ScheduledCache 是周期性清理缓存的装饰器,默认一小时清理一次
  • LoggingCache 在 Cache 的基础上提供了日志功能,通过 requestshits 计算缓存的命中率
  • SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能。
  • SerializedCache 提供了将 value 对象序列化的功能。添加缓存时,会将 value 序列化为 byte[] 存入缓存;获取缓存时,会将 byte[] 反序列化成 Java 对象。

Tips:perpetual(adj. 永恒的),delegate(n. 代表)

2.9.3 CacheKey

在 Cache 中唯一确定一个缓存项需要使用缓存项的 key,这个 key 不能仅仅通过一个 String 表示,所以需要定义 CacheKey 类型

public class CacheKey implements Cloneable, Serializable {

	private static final int DEFAULT_MULTIPLIER = 37;
	private static final int DEFAULT_HASHCODE = 17;

    // 参与计算 hashcode,默认 37
	private final int multiplier;
    // CacheKey 对象的 hashcode,初始 17
	private int hashcode;
    // 校验和
	private long checksum;
    // updateList 集合的个数
	private int count;
    // 由该集合中的所有对象共同决定两个 CacheKey 是否相同
	private List<Object> updateList;

	public CacheKey() {
		this.hashcode = DEFAULT_HASHCODE;
		this.multiplier = DEFAULT_MULTIPLIER;
		this.count = 0;
		this.updateList = new ArrayList<>();
	}
}

实际上,CacheKey 对象的 updateList 由四部分组成,分别是:

  1. MappedStatement 的 id
  2. 指定查询结果集的范围,也就是 RowBounds.offset 和 RowBounds.limit
  3. 查询所使用的 SQL 语句,也就是 boundSql.getSql() 方法返回的 SQL 语句,其中可能包含“?”占位符
  4. 用户传递给上述 SQL 语句的实际参数值

2.10 本章小结

  1. XML 解析的基础知识以及解析器模块的具体实现
  2. MyBatis 对 Java 反射的封装,Type 接口的基础知识以及复杂属性表达式在类层面和对象层面的处理
  3. MyBatis 如何实现数据在 Java 类型与 JDBC 类型之间的转换以及别名功能
  4. 日志模块的相关实现
  5. MyBatis 中 JDK 动态代理的应用
  6. DataSource 模块的实现和原理
  7. Transaction 模块如何实现事务
  8. binding 模块如何将 Mapper 与映射配置信息相关联
  9. Cache 模块的实现