MyBatis10-《通用源码指导书:MyBatis源码详解》笔记-executor包(上)

文章详细介绍了MyBatis中的执行器包,包括基于反射和cglib的动态代理,以及如何使用javassist框架。同时,文章讨论了序列化与主键自增功能,特别是Jdbc3KeyGenerator和SelectKeyGenerator在主键生成中的作用。此外,还深入探讨了MyBatis的懒加载机制,包括配置、实现原理和对序列化支持的处理。
摘要由CSDN通过智能技术生成

本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是CSDN中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为CSDN博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/onlinedct/article/details/107306041

executor 包,顾名思义为执行器包,它作为 MyBatis 的核心将其他各个包凝聚在了一起。在该包的工作中,会调用配置解析包解析出的配置信息,会依赖基础包中提供的基础功能。最终,executor包将所有的操作串接在了一起,通过 session包向外暴露出一套完整的服务。

1. 背景知识

1.1 基于cglib的动态代理

基于反射的动态代理的一个制约条件,即被代理的类必须有一个父接口。但是有些类确实没有父接口,对于这些类而言,基于反射的动态代理是不适用的。
基于 cglib(Code Generation Library,代码生成库)的动态代理。
我们知道一个类必须通过类加载过程将类文件加载到 JVM后才能使用。那么是否能够直接修改 JVM中的字节码信息来修改和创建类呢?

答案是可以的,cglib就是基于这个原理工作的。cglib使用字节码处理框架 ASM来转换字节码并生成被代理类的子类,然后这个子类就可以作为代理类展开工作。ASM是一个底层的框架,除非你对 JVM内部结构包括 class文件的格式和指令集都很熟悉,否则不要直接使用 ASM。

下面我们通过示例介绍一下如何用 cglib实现动态代理。首先要在项目中引入 cglib工具包,以使用 Maven为例,在pom文件中增加依赖

<dependency>
	<groupId>cglib</groupId>
	<artifactId>cglib</artifactId>
	<version>3.2.9</version>
</dependency>

被代理类不需要实现任何接口

public class User {
    public String sayHello(String name) {
        System.out.println("hello " + name);
        return "OK";
    }
}

接下来编写一个实现了org.springframework.cglib.proxy.MethodInterceptor 接口的类。被代理类中的方法被拦截后,会进入该类的 intercept 方法。在该类的intercept方法中,我们在被代理对象的方法执行前后各增加了一句输出语句。

public class ProxyHandler<T> implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before speak");
        Object ans = methodProxy.invokeSuper(o, objects);
        System.out.println("after speak");
        return ans;
    }
}

之后就可以建立代理对象,实现动态代理操作。创建一个代理对象(即变量 user 对应的对象),然后调用了其中的方法。可见,在被代理对象的方法执行前后,均输出了代理对象中增加的语句。

public static void main(String[] args) throws Exception {
    Enhancer enhancer = new Enhancer();
    // 设置enhancer的回调对象
    enhancer.setCallback(new ProxyHandler<>());
    // 设置enhancer对象的父类
    enhancer.setSuperclass(User.class);
    // 创建代理对象,实际为User的子类
    User user = (User) enhancer.create();

    // 通过代理对象调用目标方法
    String ans = user.sayHello("易哥");
    System.out.println(ans);
}

1.2 javassist框架的使用

cglib基于底层的ASM框架来实现 Java字节码的修改。而javassist和ASM类似,它也是一个开源的用来创建、修改Java字节码的类库,能实现类的创建、方法的修改、继承关系的设置等一系列的操作。
相比于 ASM,javassist的优势是学习成本低,可以根据 Java代码生成字节码,而不需要直接操作字节码。javassist 的使用虽然比 ASM 简单,但也并不是太容易。下面通过一个示例来展示javassist的使用,以“无中生有”的方式创建一个类,并给类设置属性和方法。

public static void main(String[] args) throws Exception {
    ClassPool pool = ClassPool.getDefault();
    // 定义一个类
    CtClass userCtClazz = pool.makeClass("com.github.yeecode.mybatisdemo.User");
    // 创建name属性
    CtField nameField = new CtField(pool.get("java.lang.String"), "name", userCtClazz);
    userCtClazz.addField(nameField);
    // 创建name的setter
    CtMethod setMethod = CtNewMethod.make("public void setName(String name) { this.name = name;}", userCtClazz);
    userCtClazz.addMethod(setMethod);
    // 创建sayHello方法
    CtMethod sayHello = CtNewMethod.make("public String sayHello() { return \"Hello, I am \" + this.name ;}", userCtClazz);
    userCtClazz.addMethod(sayHello);

    Class<?> userClazz = userCtClazz.toClass();
    // 创建一个对象
    Object user = userClazz.newInstance();
    // 为对象设置name值
    Method[] methods = userClazz.getMethods();
    for (Method method: methods){
        if (method.getName().equals("setName")) {
            method.invoke(user,"易哥");
        }
    }
    // 调用对象sayHello方法
    for (Method method: methods){
        if (method.getName().equals("sayHello")) {
            String result = (String) method.invoke(user);
            System.out.println(result);

        }
    }
}

凭空创建了一个 User对象,并为其赋予了name属性和 setName方法、sayHello 方法;然后实例化该类的对象后,调用了对象的相关方法。这些操作都是直接针对JVM中的字节码展开的。这充分说明了直接操作字节码这种方式的灵活与强大,但因为它涉及较多的底层操作,并不是很容易驾驭。
但无论如何,javassist 这个强大的工具是可以直接修改字节码的。因此,我们可以使用它创建被代理类的子类从而实现动态代理,也可以使用它创建被代理类接口的子类从而实现动态代理。

1.3 序列化与反序列化中的方法

  1. writeExternal方法和 readExternal方法
    一般情况下要实现对象的序列化我们只需要继承 Serializable 接口实现序列化和反序列化是非常简单的,目标类除了继承Serializable接口外不需要任何其他的操作,整个序列化和反序列化的过程由 Java内部的机制完成。而继承Externalizable接口实现序列化和反序列化则支持自定义序列化和反序列化的方法。Externalizable接口包含以下两个抽象方法。
  • void writeExternal(ObjectOutput out):该方法在目标对象序列化时调用。方法中可以调用 DataOutput(输入参数ObjectOutput的父类)方法来保存其基本值,或调用ObjectOutput的 writeObject方法来保存对象、字符串和数组。
  • void readExternal(ObjectInput in):该方法在目标对象反序列化时调用。方法中调用DataInput(输入参数 ObjectInput的父类)方法来恢复其基础类型,或调用 readObject方法来恢复对象、字符串和数组。需要注意的是,readExternal 方法读取数据时,必须与 writeExternal方法写入数据时的顺序和类型一致。
public class UserModel02 implements Externalizable {
    private static final long serialVerisionUID = 1L;

    private Integer id;
    private String name;
    private String description;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("writeExternal doing ...");
        out.write(id); // DataOutput中的方法
        out.writeObject(name + "(from writeExternal)");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("readExternal doing ...");
        id = in.read();
        name = (String) in.readObject();
        System.out.println("name in file is:" + name);
        name = name + "(from readExternal)";
    }
}
    private static void demo02() throws Exception {
        System.out.println("run demo02:");
        UserModel02 userModel02 = new UserModel02();
        userModel02.setId(1);
        userModel02.setName("易哥");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("m2.tempdata"));
        oos.writeObject(userModel02);
        oos.flush();
        oos.close();

        System.out.println("↑write;↓read");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("m2.tempdata"));
        UserModel02 newUser = (UserModel02) ois.readObject();
        System.out.println("newUser:" + newUser.getId() + "-" + newUser.getName());
        System.out.println();
    }

对于实现了 Externalizable 接口的类,会在对象序列化时调用 writeExternal 方法,而在对象反序列化时调用readExternal方法。我们可以通过自定义writeExternal方法和readExternal方法的具体实现,来控制对象的序列化和反序列化行为,这也使得继承Externalizable接口实现序列化和反序列化更为自由和强大。

  1. writeReplace方法和 readResolve方法在进行序列化和反序列化的目标类(可以继承 Serializable 接口,也可以继承Externalizable接口)中,还可以定义两个方法:writeReplace方法和 readResolve方法。
  • writeReplace:如果一个类中定义了该方法,则对该类的对象进行序列化操作前会先调用该方法。最终该方法返回的对象将被序列化。
  • readResolve:如果一个类中定义了该方法,则对该类的对象进行反序列化操作后会调用该方法。最终该方法返回的对象将作为反序列化的结果。

下面以 writeReplace方法为例,演示一下这种能力。在UserModel03类中定义了writeReplace方法,并在writeReplace方法中返回一个全新的对象

public class UserModel03 implements Serializable {
    private static final long serialVerisionUID = 123L;

    private Integer id;
    private String name;
    private String description;

    private Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace doing ...");
        UserModel03 userModel = new UserModel03();
        userModel.setId(2);
        userModel.setName("yeecode");
        userModel.setDescription("description from writeReplace");
        return userModel;
    }
}

然后对 UserModel03的对象进行序列化和反序列化操作

private static void demo03() throws Exception {
    System.out.println("run demo03:");
    UserModel03 userModel03 = new UserModel03();
    userModel03.setId(1);
    userModel03.setName("易哥");

    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("m3.tempdata"));
    oos.writeObject(userModel03);
    oos.flush();
    oos.close();

    System.out.println("↑write;↓read");

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("m3.tempdata"));
    UserModel03 newUser = (UserModel03) ois.readObject();
    System.out.println("newUser:" + newUser.getId() + "-" + newUser.getName());
    System.out.println();
}
/*
run demo03:
writeReplace doing ...
↑write;↓read
newUser:2-yeecode
*/

可见,无论实际 UserModel03对象如何,最终的序列化都是按照 writeReplace方法输出的对象展开的。writeReplace方法确实在序列化过程中起到了“偷梁换柱”的效果。readResolve也有类似的能力,只不过是在反序列化阶段生效。

  1. 序列化方法和反序列化方法的执行顺序
    上面我们了解了 writeExternal、readExternal和writeReplace、readResolve四个方法,那么这四个方法具体的执行顺序如何呢?
public class UserModel05 implements Externalizable {
    private static final long serialVerisionUID = 1L;

    private Integer id;
    private String name;
    private String description;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("writeExternal doing ...");
        out.write(id);
        out.writeObject(name + "(from writeExternal)");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("readExternal doing ...");
        id = in.read();
        name = (String) in.readObject();
        System.out.println("name in file is:" + name);
        name = name + "(from readExternal)";
    }

    private Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace doing ...");
        UserModel05 userModel = new UserModel05();
        userModel.setId(2);
        userModel.setName(name + "(from writeReplace)");
        userModel.setDescription("description from writeReplace");
        return userModel;
    }

    private Object readResolve() throws ObjectStreamException {
        System.out.println("readResolve doing ...");
        UserModel05 userModel = new UserModel05();
        userModel.setId(2);
        userModel.setName(name + "(from readResolve)");
        userModel.setDescription("description from readResolve");
        return userModel;
    }
}

然后对 UserModel05的对象进行序列化和反序列化操作

 private static void demo05() throws Exception {
        System.out.println("run demo05:");
        UserModel05 userModel05 = new UserModel05();
        userModel05.setId(1);
        userModel05.setName("易哥");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("m5.tempdata"));
        oos.writeObject(userModel05);
        oos.flush();
        oos.close();

        System.out.println("↑write;↓read");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("m5.tempdata"));
        UserModel05 newUser = (UserModel05) ois.readObject();
        System.out.println("newUser:" + newUser.getId() + "-" + newUser.getName());
        System.out.println("description:" + newUser.getDescription());
        System.out.println();
    }
/*

run demo05:
writeReplace doing ...
writeExternal doing ...
↑write;↓read
readExternal doing ...
name in file is:易哥(from writeReplace)(from writeExternal)
readResolve doing ...
newUser:2-易哥(from writeReplace)(from writeExternal)(from readExternal)(from readResolve)
description:description from readResolve
*/

四个方法的执行顺序依次为:writeReplace、writeExternal、readExternal、readResolve。
在这里插入图片描述

继承 Serializable接口的类的序列化和反序列化流程相对简单一些
在这里插入图片描述

1.4 ThreadLocal

防止一个对象被多个线程同时读写是多线程编程中非常重要的工作,通常可以使用加锁等方式来实现。ThreadLocal可以用来彻底避免这种情况。

一个对象会被多个线程访问,是因为多个线程共享了这个对象。我们只要把对象转变为线程独有的,就可以避免这种情况。这是一种以“空间换时间”的思路。

  • 当一个对象被多个线程共享时,节约了存储该对象的空间;但是在访问该对象时需要多个线程排队进行,这样便浪费了时间。
  • 当我们将对象设置为线程独享时,每个线程都可以无须排队而自由访问对象,节约了时间;但是同一个对象可能在多个线程中存在拷贝,这样就浪费了存储空间。
    当然,有一些数据在多个线程之间共享是多个线程之间通信的需要,这种情况不在此列。时间与空间的矛盾在程序设计中会经常出现,我们需要根据不同的场景选择不同的方案。而 ThreadLocal 是典型的“时间换空间”思路的应用,每个线程都独有一个ThreadLocal,可以在其中存放独属于该线程的数据。

ThreadLocal的主要方法有:

  • T get():从 ThreadLocal中读取数据;
  • void set(T value):向 ThreadLocal中写入数据;
  • void remove():从 ThreadLocal中删除数据。

以下代码创建了 threadLocalNumber 和threadLocalString 这两个ThreadLocal 变量。共有三个线程,分别是 main 方法所在的主线程、执行Task01任务的 thread01线程、执行 Task02任务的 thread02线程。在每个线程中,我们都对这两个 ThreadLocal变量进行读写操作。

public class DemoApplication {
    // 创建了两个ThreadLocal变量
    private static ThreadLocal<Integer> threadLocalNumber = new ThreadLocal<>();
    private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();

    public static void main(String[] args) {
        try {
            Thread thread01 = new Thread(new Task01());
            Thread thread02 = new Thread(new Task02());
            thread01.start();
            thread02.start();
            Thread.sleep(2L);
            System.out.println("Main-number: " + threadLocalNumber.get());
            System.out.println("Main-string: " + threadLocalString.get());
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    private static class Task01 implements Runnable {
        @Override
        public void run() {
            System.out.println("Thread01-number: " + threadLocalNumber.get());
            System.out.println("Set Thread01-number : 3001");
            threadLocalNumber.set(3001);
            System.out.println("Thread01-number: " + threadLocalNumber.get());
            System.out.println("Set Thread01-string : hello thread01");
            threadLocalString.set("hello thread01");
            System.out.println("Thread01-string: " + threadLocalString.get());
        }
    }

    private static class Task02 implements Runnable {
        @Override
        public void run() {
            System.out.println("Set Thread02-number : 3002");
            threadLocalNumber.set(3002);
            System.out.println("Thread02-number: " + threadLocalNumber.get());
            System.out.println("Thread02-string: " + threadLocalString.get());
            System.out.println("Set Thread02-string : hello thread02");
            threadLocalString.set("hello thread02");
            System.out.println("Thread02-string: " + threadLocalString.get());
        }
    }
}
/*
Thread01-number: null
Set Thread02-number : 3002
Set Thread01-number : 3001
Thread02-number: 3002
Thread01-number: 3001
Thread02-string: null
Set Thread01-string : hello thread01
Set Thread02-string : hello thread02
Thread01-string: hello thread01
Thread02-string: hello thread02
Main-number: null
Main-string: null
*/

通过打印出来的变量结果可知,每个线程操作的 ThreadLocal 变量都是线程内部的变量,不会对其他线程造成任何干扰。在多线程程序中,当我们需要保存一些线程独有的数据时,可以借助 ThreadLocal来实现。

1.5 Statement及其子接口

Statement 接口中定义了一些抽象方法能用来执行静态 SQL 语句并返回结果,通常返回的结果是一个结果集 ResultSet。
Statement 有一个子接口 PreparedStatement,而PreparedStatement 又有一个子接口CallableStatement。
在这里插入图片描述
在继承关系中,通常子类会继承父类的方法并在此基础上进行扩充,从而使得子类的功能成为父类功能的超集。Statement 接口及其子接口就是这样的,从 Statement 接口到CallableStatement接口,功能逐渐增强。
Statement 接口、PreparedStatement 接口、CallableStatement 接口依次对应我们在设置SQL语句时的简单语句、预编译语句、存储过程语句。
PreparedStatement 子接口除了继承 Statement 接口的全部方法外,还新定义了一些方法。这些方法主要是一些 set方法,如下面的 setInt方法。

void setInt(int parameterIndex, int x) throws SQLException;

这些新增的 set方法(setLong、setString、setObject等)使得预编译的 SQL语句具有了按照参数位置对参数赋值的功能。
CallableStatement 则在 PreparedStatement 的基础上进一步增加了方法,这些方法主要包括以下四类。

  • 按照参数名称赋值方法:这一类方法能够为存储过程中指定名称的参数赋值。例如,setInt(String,int)方法就属于这一类。
  • 注册输出参数方法:这一类方法能够向存储过程注册输出参数。例如,registerOutParameter(int,int)方法就属于这一类。
  • 按照参数位置读取值方法:这一类方法能够读取存储过程指定位置的参数值。例如,getInt(int)方法就属于这一类方法。
  • 按照参数名称读取值方法:这一类方法能够读取存储过程指定名称的参数的值。例如,getInt(String)方法就属于这一类方法。
    因此,从 Statement接口到 PreparedStatement接口再到CallableStatement接口,功能越来越强大。这就意味着SQL语句中,从简单语句到预编译语句再到存储过程语句,它们支持的功能越来越多。

在这里插入图片描述

2.主键自增功能

在进行数据插入操作时,经常需要一个自增生成的主键编号,这既能保证主键的唯一性,又能保证主键的连续性。许多数据库都支持主键自增功能,如 MySQL数据库、SQL Server数据库等。当然也有一些数据库不支持主键自增功能,如 Oracle数据库。MyBatis的 executor包中的 keygen子包兼容以上这两种情况。
在这里插入图片描述
KeyGenerator作为接口提供了两个方法,即 processBefore方法和 processAfter方法。关于这两个方法的实现细节我们会在下面分别介绍。NoKeyGenerator不提供任何主键自增功能,其 processBefore方法和 processAfter方法均为空方法。

2.1 主键自增的配置与生效

通过 KeyGenerator 的类图我们知道,MyBatis 中的KeyGenerator 实现类共有三种:Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator。在实际使用时,这三种实现类中只能有一种实现类生效。而如果生效的是NoKeyGenerator,则代表不具有任何的主键自增功能。
要启用 Jdbc3KeyGenerator,可以在配置文件中增加如下配置。

<setting name="useGeneratedKeys" value="false"/>
或者
<insert id="addUser_A" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO `user`
    (`name`,`email`,`age`,`sex`,`schoolName`)
    VALUES
    (#{name},#{email},#{age},#{sex},#{schoolName})
</insert>

如果要启用 SelectKeyGenerator,则需要在 SQL语句前加一段 selectKey标签

<insert id="addUser_B" parameterType="User">
    <selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
    INSERT INTO `user`
    (`name`,`email`,`age`,`sex`,`schoolName`)
    VALUES
    (#{name},#{email},#{age},#{sex},#{schoolName})
</insert>

如果某一条语句中同时设置了 useGeneratedKeys和selectKey,则后者生效。
以上各个配置项的作用范围、优先级等结论,均可以通过阅读源码得出。在XMLStatementBuilder类中,在集成开发软件的帮助下,我们可以通过查找 KeyGenerator的引用找到。这段代码就是主键自增功能被解析的地方。

    // 处理SelectKey节点,在这里会将KeyGenerator加入到Configuration.keyGenerators中
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 此时,<selectKey> 和 <include> 节点均已被解析完毕并被删除,开始进行SQL解析
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // 判断是否已经有解析好的KeyGenerator
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      // 全局或者本语句只要启用自动key生成,则使用key生成
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

processSelectKeyNodes 方法最终解析了 selectKey 节点的信息并在解析完成后将selectKey节点从 XML中删除了,而解析出来的信息则放入了 configuration的 keyGenerators中。之后,如果没有解析好的 KeyGenerator,则会根据useGeneratedKeys 判断是否使用Jdbc3KeyGenerator。
最终,KeyGenerator信息会被保存在整个 Statement中。在Statement执行时,直接调用 KeyGenerator 中的processBefore 方法和 processAfter 方法即可,必然会有Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator三者中的一个来实际执行这两个方法。

2.1 Jdbc3KeyGenerator类的功能

Jdbc3KeyGenerator类是为具有主键自增功能的数据库准备的。说到这里大家可能会疑惑,既然数据库已经支持主键自增了,那 Jdbc3KeyGenerator类存在的意义是什么呢?它存在的意义是提供自增主键的回写功能。

首先对 user表中的 id字段启用主键自增功能,其次配置XML映射文件。这里并没有启用主键自增功能。

<insert id="addUser_D" parameterType="User">
    INSERT INTO `user`
    (`name`,`email`,`age`,`sex`,`schoolName`)
    VALUES
    (#{name},#{email},#{age},#{sex},#{schoolName})
</insert>
 User user04 = new User("李二壮", "lierzhuang@sample.com",21,0, "KEYUAN SCHOOL");
// 使用NoKeyGenerator,即不使用主键自增功能
System.out.println("user04:");
System.out.println("before insert :" + user04.toString());
result = session.insert("com.github.yeecode.mybatisdemo.dao.UserDao.addUser_D", user04);
System.out.println("insert result : " + result);
System.out.println("after insert :" + user04.toString());

因为在数据库中对 id字段启用了自增功能,所以在数据插入操作结束后,数据库中的user04的 id字段会被设置为一个数值。然而,Java程序中的 user04对象却不会被更新,因此输出的user04的 id值仍然为 null。

Jdbc3KeyGenerator类提供的回写功能能够将数据库中产生的id值回写给 Java对象本身。我们可以通过下面的设置启用Jdbc3KeyGenerator类,

<insert id="addUser_A" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO `user`
    (`name`,`email`,`age`,`sex`,`schoolName`)
    VALUES
    (#{name},#{email},#{age},#{sex},#{schoolName})
</insert>

因此 Jdbc3KeyGenerator类所做的工作就是在 Java对象插入完成后,将数据库自增产生的 id读取出来,然后回写给 Java对象本身。
在这里插入图片描述

2.2 Jdbc3KeyGenerator类的原理

Jdbc3KeyGenerator类的工作是在数据库主键自增结束后,将自增出来的主键读取出来并赋给 Java 对象。这些工作都是在数据插入完成后进行的,即在 processAfter 方法中进行。而processBefore方法中不需要进行任何操作。
processAfter方法直接调用了 processBatch方法。在阅读processBatch方法前我们先复习一个小的知识点:Statement对象的 getGeneratedKeys方法能返回此语句操作自增生成的主键,如果此语句没有产生自增主键,则结果为空 ResultSet对象。
在这里插入图片描述

processBatch 方法源码。该方法的主要工作就是调用Statement对象的 getGeneratedKeys方法获取数据库自增生成的主键,然后将主键赋给实参以达到回写的目的。

  public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    // 拿到主键的属性名
    final String[] keyProperties = ms.getKeyProperties();
    if (keyProperties == null || keyProperties.length == 0) {
      // 没有主键则无需操作
      return;
    }
    // 调用Statement对象的getGeneratedKeys方法获取自动生成的主键值
    try (ResultSet rs = stmt.getGeneratedKeys()) {
      // 获取输出结果的描述信息
      final ResultSetMetaData rsmd = rs.getMetaData();
      final Configuration configuration = ms.getConfiguration();
      if (rsmd.getColumnCount() < keyProperties.length) {
        // 主键数目比结果的总字段数目还多,则发生了错误。
        // 但因为此处是获取主键这样的附属操作,因此忽略错误,不影响主要工作
      } else {
        // 调用子方法,将主键值赋给实参
        assignKeys(configuration, rs, rsmd, keyProperties, parameter);
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    }
  }

2.3 SelectKeyGenerator类

Jdbc3KeyGenerator类其实并没有真正地生成自增主键,而只是将数据库自增出的主键值回写到了 Java对象中。因此,面对不支持主键自增功能的数据库时,Jdbc3KeyGenerator类将无能为力。这时就需要 SelectKeyGenerator类,因为它可以真正地生成自增的主键。
SelectKeyGenerator类实现了 processBefore和 processAfter这两个方法。这两个方法均直接调用了子方法 processGeneratedKeys,这可能会让看到源码的我们感到疑惑。

  /**
   * 数据插入前进行的操作
   * @param executor 执行器
   * @param ms 映射语句对象
   * @param stmt Statement对象
   * @param parameter SQL语句实参对象
   */
  @Override
  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (executeBefore) {
      processGeneratedKeys(executor, ms, parameter);
    }
  }

  /**
   * 数据插入后进行的操作
   * @param executor 执行器
   * @param ms 映射语句对象
   * @param stmt Statement对象
   * @param parameter SQL语句实参对象
   */
  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (!executeBefore) {
      processGeneratedKeys(executor, ms, parameter);
    }
  }

SelectKeyGenerator 类的功能描述起来很简单:先执行一段特定的 SQL 语句获取一个值,然后将该值赋给 Java对象的自增属性。
然而,SelectKeyGenerator类这一功能的执行时机分为以下两种,这两种执行时机通过配置二选一。

  1. 在数据插入之前执行。执行完特定的 SQL 语句并将值赋给对象的自增属性后,再将这个完整的对象插入数据库中。而这种操作又分为两种情况:
  • 如果数据库没有设置或者不支持主键自增,则完整的对象会被完整地插入数据库中。这是 SelectKeyGenerator类最常用的使用场景。
  • 如果数据库设置了主键自增,则刚才特定 SQL语句生成的自增属性值会被数据库自身的自增值覆盖掉。这种情况下,Java对象的自增属性值可能会和数据库中的自增属性值不一致,因此是错误的。这种情况下,建议使用 Jdbc3KeyGenerator类的回写功能。
  1. 在数据插入之后执行。对象插入数据库结束后,Java对象的自增属性被设置成特定SQL语句的执行结果。这种操作也分为两种情况:
  • 如果数据库不支持主键自增,则之前被插入数据库中的对象的自增属性是没有被赋值的,而 Java 对象的自增属性却被赋值了,这会导致不一致。这种操作是错误的。
  • 如果数据库设置了主键自增,则数据库自增生成的值和 SQL语句执行产生的值可能不一样。不过我们一般通过设置特定的SQL语句来保证两者一致,这其实和 Jdbc3KeyGenerator类的回写功能类似。

可见 SelectKeyGenerator类的功能描述起来简单又灵活,但是因为执行时机、数据库状况等不同可能产生多种情况,需要使用者自己把握。
在这里插入图片描述
processBefore和 processAfter这两个方法都直接调用了processGeneratedKeys方法,所以 processGeneratedKeys方法的功能就是执行一段 SQL语句后获取一个值,然后将该值赋给 Java对象的自增属性。

// 用户生成主键的SQL语句的特有标志,该标志会追加在用于生成主键的SQL语句的id的后方
public static final String SELECT_KEY_SUFFIX = "!selectKey";
// 插入前执行还是插入后执行
private final boolean executeBefore;
// 用户生成主键的SQL语句
private final MappedStatement keyStatement;
  /**
   * 执行一段SQL语句后获取一个值,然后将该值赋给Java对象的自增属性
   *
   * @param executor 执行器
   * @param ms 插入操作的SQL语句(不是生成主键的SQL语句)
   * @param parameter 插入操作的对象
   */
  private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
    try {
      // keyStatement为生成主键的SQL语句;keyStatement.getKeyProperties()拿到的是要自增的属性
      if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
        // 要自增的属性
        String[] keyProperties = keyStatement.getKeyProperties();
        final Configuration configuration = ms.getConfiguration();
        final MetaObject metaParam = configuration.newMetaObject(parameter);
        if (keyProperties != null) {
          // 为生成主键的SQL语句创建执行器keyExecutor。
          // 原注释:不要关闭keyExecutor,因为它会被父级的执行器关闭
          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
          // 执行SQL语句,得到主键值
          List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
          // 主键值必须唯一
          if (values.size() == 0) {
            throw new ExecutorException("SelectKey returned no data.");
          } else if (values.size() > 1) {
            throw new ExecutorException("SelectKey returned more than one value.");
          } else {
            MetaObject metaResult = configuration.newMetaObject(values.get(0));
            if (keyProperties.length == 1) {
              // 要自增的主键只有一个,为其赋值
              if (metaResult.hasGetter(keyProperties[0])) {
                // 从metaResult中用getter得到主键值
                setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
              } else {
                // 可能返回的直接就是主键值本身
                setValue(metaParam, keyProperties[0], values.get(0));
              }
            } else {
              // 要把执行SQL得到的值赋给多个属性
              handleMultipleProperties(keyProperties, metaParam, metaResult);
            }
          }
        }
      }
    } catch (ExecutorException e) {
      throw e;
    } catch (Exception e) {
      throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
    }
  }

这样,我们对 SelectKeyGenerator 类的功能及如何实现这些功能进行了详细的介绍。因此,我们可以将SelectKeyGenerator类作为 Jdbc3KeyGenerator类的升级版或自由定制版。

  • SelectKeyGenerator类可以设置为插入前执行并实现主键的主动生成,而且可以通过SQL语句设置主键生成方式。这是Jdbc3KeyGenerator类没有的功能。
  • SelectKeyGenerator类可以设置为插入后执行。通过将主键生成 SQL语句设置为类似“SELECT LAST_INSERT_ID()”的语句便可以实现主键回写功能。

3.懒加载功能

3.1 懒加载功能的使用

在进行跨表数据查询的时候,常出现先查询表A,再根据表A的输出结果查询表B的情况。而有些时候,我们从表A中查询出来的数据,只有部分需要查询表B。
例如,我们需要从 user表查询用户信息并打印所有用户的姓名列表。而查询出的用户中,只有满足“user.getAge()==18”的用户才需要查询该用户在 task表中的信息。

private static void nestedQuery(SqlSessionFactory sqlSessionFactory) {
   try (SqlSession session = sqlSessionFactory.openSession()) {
       User userParam = new User();
       userParam.setSex(0);
       // 查询满足条件的全部用户
       List<User> userList = session.selectList("com.github.yeecode.mybatisdemo.dao.UserDao.nestedQuery", userParam);
       // 打印全部用户姓名列表
       System.out.println("users: ");
       for (User user : userList) {
           System.out.println(user.getName() + ", age = " + user.getAge());
       }
       // 打印用户任务
       System.out.println("userDetail: ");
       for (User user : userList) {
           System.out.println(user.getName() + ":");
           for (Task task : user.getTaskList()) {
               System.out.println(task.getTaskName());
           }
       }
   }
}

我们可以先从 user表获取用户信息,然后再从 task表查询所有用户的任务信息。这一定是可行的,但是这样操作会查询出许多多余的结果,所有不满足“user.getAge()==18”的用户任务信息都是多余的。
一种更好的方案是先从 user表获取用户信息,然后根据需要(即是否满足“user.getAge()==18”)决定是否查询该用户在task表中的信息。

这种先加载必需的信息,然后再根据需要进一步加载信息的方式叫作懒加载。MyBatis支持数据的懒加载。要想使用懒加载,需要在 MyBatis的配置文件中启用该功能。

<settings>
    <!--true即:全局启用惰性加载-->
    <setting name="lazyLoadingEnabled" value="true" />
    <!--false即:惰性加载时,每个属性都按需加载-->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

aggressiveLazyLoading 是激进懒加载设置,我们对该属性进行一些说明。当aggressiveLazyLoading设置为 true时,对对象任一属性的读或写操作都会触发该对象所有懒加载属性的加载;当 aggressiveLazyLoading设置为 false时,对对象某一懒加载属性的读操作会触发该属性的加载。无论aggressiveLazyLoading的设置如何,调用对象的“equals”“clone”“hashCode”“toString”中任意一个方法都会触发该对象所有懒加载属性的加载。在后面的源码阅读中,我们会清晰地看到 aggressiveLazyLoading设置项如何生效。

接下来还需要设置好映射文件,在“id=“lazyLoadQuery””的查询中,查询 user表是必需的操作,而在结果的映射中又需要查询 task表,因此它涉及两个表的查询。而只要不访问 User对象的 taskList属性,则 task表的查询操作就是可以省略的。因此,User对象的 taskList就是可以懒加载的属性。

<resultMap id="associationUserMap" type="User">
   <result property="id" column="id"/>
   <result property="name" column="name"/>
   <result property="email" column="email"/>
   <result property="age" column="age"/>
   <result property="sex" column="sex"/>
   <result property="schoolName" column="schoolName"/>
   <association property="taskList" javaType="ArrayList"
                select="com.github.yeecode.mybatisdemo.dao.UserDao.selectTask" column="id"/>
</resultMap>
<select id="lazyLoadQuery" resultMap="associationUserMap">
 SELECT * FROM `user` WHERE `sex` = #{sex}
</select>

<select id="selectTask" resultType="Task">
 SELECT * FROM `task` WHERE `userId` = #{id}
</select>

MyBatis 先从 user 表查询了所有的用户信息,然后仅对满足“user.getAge()==18”的“易哥”调用了 selectTask语句从 task表查询了任务信息,而没有对不符合条件的“杰克”等人调用 selectTask语句。因此,整个过程是存在懒加载的。

3.2 懒加载功能的实现

懒加载功能的实现还是相对复杂的,为便于理解,我们先简要给出 MyBatis中懒加载的实现原理,这对后面的源码阅读有着重要的帮助。

  1. 先查询 user表,获得 User对象。
  2. 将返回的 User对象替换为 User对象的代理对象UserProxy对象,并返回上层应用。UserProxy对象有以下特点。
  • 当属性的写方法被调用时,直接将属性值写入被代理对象。
  • 当属性的读方法被调用时,判断是否为懒加载属性。如果不是懒加载属性,则直接由被代理对象返回;如果是懒加载属性,则根据配置加载该属性,然后再返回。

在这里插入图片描述
在了解了懒加载的基本实现原理之后,我们参照 loader子包的类图对懒加载功能中涉及的类进行源码阅读。

3.3 代理工厂

ProxyFactory是创建代理类的工厂接口,其中的 setProperties方法用来对工厂进行属性设置。但是 MyBatis内置的两个实现类均没有实现该接口,故不支持属性设置。createProxy方法用来创建一个代理对象。

ProxyFactory接口有两个实现类,即 CglibProxyFactory类和JavassistProxyFactory类。这两个实现类整体结构高度一致,甚至内部类、方法设置都一样,只是实现原理不同,一个基于cglib实现,另一个基于 Javassist实现。接下来我们以CglibProxyFactory类为例进行源码分析。
CglibProxyFactory类中提供了两个创建代理对象的方法。其中createProxy方法重写了ProxyFactory 接口中的方法,用来创建一个普通的代理对象;createDeserializationProxy 方法用来创建一个反序列化的代理对象。

createProxy方法创建的代理对象是内部类EnhancedResultObjectProxyImpl的实例。

  private static class EnhancedResultObjectProxyImpl implements MethodInterceptor {
    // 被代理类
    private final Class<?> type;
    // 要懒加载的属性Map
    private final ResultLoaderMap lazyLoader;
    // 是否是激进懒加载
    private final boolean aggressive;
    // 能够触发懒加载的方法名“equals”, “clone”, “hashCode”, “toString”。这四个方法名在Configuration中被初始化。
    private final Set<String> lazyLoadTriggerMethods;
    // 对象工厂
    private final ObjectFactory objectFactory;
    // 被代理类构造函数的参数类型列表
    private final List<Class<?>> constructorArgTypes;
    // 被代理类构造函数的参数列表
    private final List<Object> constructorArgs;
}

代理类中最核心的方法是 intercept方法。当被代理类的方法被调用时,都会被拦截进该方法。在介绍 intercept方法之前,我们先了解两个方法:finalize方法和 writeReplace方法。因为在intercept方法中,对这两种方法进行了排除。

  • finalize方法:在 JVM进行垃圾回收前,允许使用 finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
  • writeReplace方法:前面已经介绍过,不再赘述。
    /**
     * 代理类的拦截方法
     * @param enhanced 代理对象本身
     * @param method 被调用的方法
     * @param args 每调用的方法的参数
     * @param methodProxy 用来调用父类的代理
     * @return 方法返回值
     * @throws Throwable
     */
    @Override
    public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      // 取出被代理类中此次被调用的方法的名称
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) { // 防止属性的并发加载
          if (WRITE_REPLACE_METHOD.equals(methodName)) { // 被调用的是writeReplace方法
            // 创建一个原始对象
            Object original;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            // 将被代理对象的属性拷贝进入新创建的对象
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) { // 存在懒加载属性
              // 则此时返回的信息要更多,不仅仅是原对象,还有相关的懒加载的设置等信息。因此使用CglibSerialStateHolder进行一次封装
              return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              // 没有未懒加载的属性了,那直接返回原对象进行序列化
              return original;
            }
          } else {
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { // 存在懒加载属性且被调用的不是finalize方法
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { // 设置了激进懒加载或者被调用的方法是能够触发全局懒加载的方法
                // 完成所有属性的懒加载
                lazyLoader.loadAll();
              } else if (PropertyNamer.isSetter(methodName)) { // 调用了属性写方法
                // 则先清除该属性的懒加载设置。该属性不需要被懒加载了
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
              } else if (PropertyNamer.isGetter(methodName)) { // 调用了属性读方法
                final String property = PropertyNamer.methodToProperty(methodName);
                // 如果该属性是尚未加载的懒加载属性,则进行懒加载
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        // 触发被代理类的相应方法。能够进行到这里的是除去writeReplace方法外的方法,例如读写方法、toString方法等
        return methodProxy.invokeSuper(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

被代理对象的 finalize方法被调用时,代理对象不需要做任何特殊处理。而被代理对象的其他方法被调用时,intercept方法的处理方式如下。

  • 如果设置了激进懒加载或者被调用的是触发全局加载的方法,则直接加载所有未加载的属性。
  • 如果被调用的是属性写方法,则将该方法从懒加载列表中删除,因为此时数据库中的数据已经不是最新的了,没有必要再去加载。然后进行属性的写入操作。
  • 如果被调用的是属性读方法,且该属性尚未被懒加载的话,则加载该属性;如果该属性已经懒加载过,则直接读取该属性。
    以上整个逻辑和上述的简化逻辑基本一致,只是在细节上考虑了更多的情况。

3.4 ResultLoaderMap类

被代理对象可能会有多个属性可以被懒加载,这些尚未完成加载的属性是在ResultLoaderMap 类的实例中存储的。ResultLoaderMap 类主要就是一个 HashMap 类,该HashMap类中的键为属性名的大写,值为 LoadPair对象。

LoadPair 类是 ResultLoaderMap 类的内部类,它能够实现对应属性的懒加载操作。

public static class LoadPair implements Serializable {
    // 用来根据反射得到数据库连接的方法名
    private static final String FACTORY_METHOD = "getConfiguration";
    // 判断是否经过了序列化的标志位,因为该属性被设置了transient,经过一次序列化和反序列化后会变为null
    private final transient Object serializationCheck = new Object();
    // 输出结果对象的封装
    private transient MetaObject metaResultObject;
    // 用以加载未加载属性的加载器
    private transient ResultLoader resultLoader;
    // 日志记录器
    private transient Log log;
    // 用来获取数据库连接的工厂
    private Class<?> configurationFactory;
    // 未加载的属性的属性名
    private String property;
    // 能够加载未加载属性的SQL的编号
    private String mappedStatement;
    // 能够加载未加载属性的SQL的参数
    private Serializable mappedParameter;
 /**
  * 进行加载操作
  * @param userObject 需要被懒加载的对象(只有当this.metaResultObject == null || this.resultLoader == null才生效,否则会采用属性metaResultObject对应的对象)
  * @throws SQLException
  */
 public void load(final Object userObject) throws SQLException {
   if (this.metaResultObject == null || this.resultLoader == null) { // 输出结果对象的封装不存在或者输出结果加载器不存在
     // 判断用以加载属性的对应的SQL语句存在
     if (this.mappedParameter == null) {
       throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
               + "required parameter of mapped statement ["
               + this.mappedStatement + "] is not serializable.");
     }

     final Configuration config = this.getConfiguration();
     // 取出用来加载结果的SQL语句
     final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
     if (ms == null) {
       throw new ExecutorException("Cannot lazy load property [" + this.property
               + "] of deserialized object [" + userObject.getClass()
               + "] because configuration does not contain statement ["
               + this.mappedStatement + "]");
     }

     // 创建结果对象的包装
     this.metaResultObject = config.newMetaObject(userObject);
     // 创建结果加载器
     this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
             metaResultObject.getSetterType(this.property), null, null);
   }

   /* We are using a new executor because we may be (and likely are) on a new thread
    * and executors aren't thread safe. (Is this sufficient?)
    *
    * A better approach would be making executors thread safe. */
   // 只要经历过持久化,则可能在别的线程中了。为这次惰性加载创建的新线程ResultLoader
   if (this.serializationCheck == null) {
     // 取出原来的ResultLoader中的必要信息,然后创建一个新的
     // 这是因为load函数可能在不同的时间多次执行(第一次加载属性A,又过了好久加载属性B)。
     // 而该对象的各种属性是跟随对象的,加载属性B时还保留着加载属性A时的状态,即ResultLoader是加载属性A时设置的
     // 则此时ResultLoader中的Executor在ResultLoader中被替换成了一个能运行的Executor,而不是ClosedExecutor
     // 能运行的Executor的状态可能不是close,这将导致它被复用,从而引发多线程问题
     // 是不是被两次执行的一个关键点就是有没有经过序列化,因为执行完后会被序列化并持久化
     final ResultLoader old = this.resultLoader;
     this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
             old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
   }

   this.metaResultObject.setValue(property, this.resultLoader.loadResult());
 }

上述方法的设计包含很多非常巧妙的点,我们一一进行介绍。
首先,懒加载的过程就是执行懒加载 SQL语句后,将查询结果使用输出结果加载器赋给输出结果元对象的过程。因此,load 方法首先会判断输出结果元对象 metaResultObject和输出结果加载器 resultLoader是否存在。如果不存在的话,则会使用输入参数 userObject重新创建上述二者。
然后,介绍 ClosedExecutor类的设计。ClosedExecutor类是ResultLoaderMap类的内部类。该类只有一个 isClosed方法能正常工作,其他所有的方法都会抛出异常。然而就是这样的一个类,在创建 ResultLoader时还是被使用。

this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
             old.parameterObject, old.targetType, old.cacheKey, old.boundSql);

这是因为 ClosedExecutor类存在的目的就是通过 isClosed方法返回 true来表明自己是一个关闭的类,以保证让任何遇到ClosedExecutor 对象的操作都会重新创建一个新的有实际功能的 Executor。例如,在 ResultLoader中我们可以找到源码:

// 初始化ResultLoader时传入的执行器
Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
  // 执行器关闭,或者执行器属于其他线程,则创建新的执行器
  localExecutor = newExecutor();
}

可以看出,传入的 ClosedExecutor 对象总会触发ResultLoader 创建新的 Executor 对象。所以,没有任何实际功能的 ClosedExecutor对象起到了占位符的作用。

最后,介绍 load方法中与序列化和反序列化相关的设计。

经过一次序列化和反序列化后,对象可能处在了全新的线程中;序列化和反序列化的时间间隔可能很长,原来的缓存信息也极有可能没有了意义。这些情况都需要懒加载过程进行特殊的处理。

我们知道,在继承了 Serializable接口的类中,如果对某个属性使用 transient关键字修饰,就会使序列化操作忽略该属性。那么对序列化的结果进行反序列化操作时,就会导致该属性变为null。基于此,LoadPair 中的 serializationCheck 属性被设计成了一个序列化标志位。只要 LoadPair对象经历过序列化和反序列化过程,就会使得 serializationCheck属性的值变为 null。

如果经历过序列化与反序列化,则当前的 LoadPair对象很有可能处在一个新的线程之中,因此继续使用之前的 ResultLoader 可能会引发多线程问题。所以,LoadPair 对象只要检测出自身经历过持久化,就会依赖老 ResultLoader 对象中的信息重新创建一个新ResultLoader对象。

ResultLoader对象也被 transient修饰,因此真正老ResultLoader对象也在序列化和反序列化的过程中消失了,与之一起消失的还有 MetaObject对象和 ResultLoader对象。因此这里所谓的老 ResultLoader对象实际是在该 load方法中进入“(this.metaResultObjectnull||this.resultLoadernull)”对应的分支后重新组建的。

而重新组建的所谓的老 ResultLoader 对象与真正的老ResultLoader 对象相比缺少了cacheKey和 boundSql这两个参数。其中 cacheKey是为了加速查询而存在的,非必要并且缓存可能早已失效;而 boundSql 会在后面的查询阶段重新补足,在 BaseStatementHandler的构造方法中就可以找到相关的代码片段。

这样,序列化和反序列化引入的问题才被一一解决了。可见,牵涉序列化和反序列化之后,懒加载操作会变得十分复杂。

3.5 ResultLoader类

ResultLoader 类是一个结果加载器类,它负责完成数据的加载工作。因为懒加载只涉及查询,而不需要支持增、删、改的工作,因此它只有一个查询方法 selectList来进行数据的查询。

3.6 懒加载功能对序列化和反序列化的支持

仍然以基于 cglib实现的懒加载为例。如果要对查询结果对象进行序列化,实际上是对代理对象即EnhancedResultObjectProxyImpl对象进行序列化,因为EnhancedResultObject-ProxyImpl已经替换了被代理对象。

我们查看 EnhancedResultObjectProxyImpl类的属性后会发现一个问题,即这些属性中并不包含已加载完成的属性(非懒加载的属性和已懒加载完的属性)。这意味着,只要对查询结果对象进行一次序列化和反序列化操作,则所有已加载完成的属性都会丢失。这种事情是不应该发生的。

为了保证懒加载操作支持序列化和反序列化,则必须保证在序列化时将被代理对象和代理对象的所有信息全都保存。为此,load 子包中准备了一整套的机制。接下来我们就介绍这套机制。

在 CglibProxyFactory中创建代理对象时,无论是创建EnhancedResultObjectProxyImpl类型的代理对象还是创建EnhancedDeserializationProxyImpl 类型的代理对象,都会在它们的构造方法中调用createProxy方法。

  /**
   * 创建代理对象
   * @param type 被代理对象类型
   * @param callback 回调对象
   * @param constructorArgTypes 构造方法参数类型列表
   * @param constructorArgs 构造方法参数类型
   * @return 代理对象
   */
  static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    Enhancer enhancer = new Enhancer();
    enhancer.setCallback(callback);
    // 创建的是代理对象是原对象的子类
    enhancer.setSuperclass(type);
    try {
      // 获取类中的writeReplace方法
      type.getDeclaredMethod(WRITE_REPLACE_METHOD);
      if (LogHolder.log.isDebugEnabled()) {
        LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
      }
    } catch (NoSuchMethodException e) {
      // 如果没找到writeReplace方法,则设置代理类继承WriteReplaceInterface接口,该接口中有writeReplace方法
      enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
    } catch (SecurityException e) {
      // 什么都不做
    }
    Object enhanced;
    if (constructorArgTypes.isEmpty()) {
      enhanced = enhancer.create();
    } else {
      Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
      Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
      enhanced = enhancer.create(typesArray, valuesArray);
    }
    return enhanced;
  }

createProxy 方法中一个重要的操作是校验被代理类中是否含有writeReplace方法。如果被代理类没有该方法,则会让代理类继承 WriteReplaceInterface从而获得一个writeReplace方法。通过前面1.3节的介绍我们知道,writeReplace方法会在对象序列化前被调用,起到“偷梁换柱”的作用。

在被代理类中植入了 writeReplace 方法后,在被代理对象被序列化时,则会调用该方法。而在EnhancedResultObjectProxyImpl类的 intercept方法中,已经对 writeReplace方法进行了特殊处理:

if (WRITE_REPLACE_METHOD.equals(methodName)) { // 被调用的是writeReplace方法
 // 创建一个原始对象
 Object original;
 if (constructorArgTypes.isEmpty()) {
   original = objectFactory.create(type);
 } else {
   original = objectFactory.create(type, constructorArgTypes, constructorArgs);
 }
 // 将被代理对象的属性拷贝进入新创建的对象
 PropertyCopier.copyBeanProperties(type, enhanced, original);
 if (lazyLoader.size() > 0) { // 存在懒加载属性
   // 则此时返回的信息要更多,不仅仅是原对象,还有相关的懒加载的设置等信息。因此使用CglibSerialStateHolder进行一次封装
   return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
 } else {
   // 没有未懒加载的属性了,那直接返回原对象进行序列化
   return original;
 }
} 

对代理对象进行持久化操作时,如果被代理对象还有尚未懒加载的属性,则最终持久化的是一个CglibSerialStateHolder 对象。这一切是基于writeReplace提供的“偷梁换柱”功能实现的。

CglibSerialStateHolder是 AbstractSerialStateHolder类的子类。可见其中既包含了被代理对象的信息,又包含了尚未加载属性的信息。而 CglibSerialStateHolder类作为其子类会继承这些属性。

// 整个状态保持器可以序列化,保存了原始对象、原始对象中未加载的属性Map、用来反射生成对象的相关信息
public abstract class AbstractSerialStateHolder implements Externalizable {
  private static final long serialVersionUID = 8940388717901644661L;
  private static final ThreadLocal<ObjectOutputStream> stream = new ThreadLocal<>();
  // 序列化后的对象
  private byte[] userBeanBytes = new byte[0];
  // 原对象
  private Object userBean;
  // 未加载的属性
  private Map<String, ResultLoaderMap.LoadPair> unloadedProperties;
  // 对象工厂,创建对象时使用
  private ObjectFactory objectFactory;
  // 构造函数的属性类型列表,创建对象时使用
  private Class<?>[] constructorArgTypes;
  // 构造函数的属性列表,创建对象时使用
  private Object[] constructorArgs;

  /**
   * 对对象进行序列化
   * @param out 序列化结果将存入的流
   * @throws IOException
   */
  @Override
  public final void writeExternal(final ObjectOutput out) throws IOException {
    // 判断是不是该线程的第一轮写入
    boolean firstRound = false;
    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream os = stream.get();
    if (os == null) {
      // 之前没有结果,所以是第一轮写入
      os = new ObjectOutputStream(baos);
      firstRound = true;
      stream.set(os);
    }

    os.writeObject(this.userBean);
    os.writeObject(this.unloadedProperties);
    os.writeObject(this.objectFactory);
    os.writeObject(this.constructorArgTypes);
    os.writeObject(this.constructorArgs);

    final byte[] bytes = baos.toByteArray();
    out.writeObject(bytes);

    if (firstRound) {
      stream.remove();
    }
  }

将序列化的原理研究清楚后,我们再研究反序列化的过程。反序列化时,会调用AbstractSerialStateHolder中的readResolve方法:

  /**
   * 反序列化时被调用,给出反序列化的对象
   * @return 最终给出的反序列化对象
   * @throws ObjectStreamException
   */
  @SuppressWarnings("unchecked")
  protected final Object readResolve() throws ObjectStreamException {
    // 非第一次运行,直接输出已经解析好的被代理对象
    if (this.userBean != null && this.userBeanBytes.length == 0) {
      return this.userBean;
    }

    // 第一次运行时,反序列化输出
    try (ObjectInputStream in = new LookAheadObjectInputStream(new ByteArrayInputStream(this.userBeanBytes))) {
      this.userBean = in.readObject();
      this.unloadedProperties = (Map<String, ResultLoaderMap.LoadPair>) in.readObject();
      this.objectFactory = (ObjectFactory) in.readObject();
      this.constructorArgTypes = (Class<?>[]) in.readObject();
      this.constructorArgs = (Object[]) in.readObject();
    } catch (final IOException ex) {
      throw (ObjectStreamException) new StreamCorruptedException().initCause(ex);
    } catch (final ClassNotFoundException ex) {
      throw (ObjectStreamException) new InvalidClassException(ex.getLocalizedMessage()).initCause(ex);
    }

    final Map<String, ResultLoaderMap.LoadPair> arrayProps = new HashMap<>(this.unloadedProperties);
    final List<Class<?>> arrayTypes = Arrays.asList(this.constructorArgTypes);
    final List<Object> arrayValues = Arrays.asList(this.constructorArgs);

    // 创建一个反序列化的代理输出,因此还是一个代理
    return this.createDeserializationProxy(userBean, arrayProps, objectFactory, arrayTypes, arrayValues);
  }

在这里会将之前序列化的结果反序列化,最终给出一个EnhancedDeserializationProxy-Impl对象,它也是一个代理对象。EnhancedDeserializationProxyImpl类是AbstractEnhanced-DeserializationProxy的子类。反序列化过程中还对结果进行了缓存。这样,对同一个对象多次反序列化时除了第一次需要进行实际的反序列化操作外,之后只需将属性中缓存的结果直接返回即可,提高了反序列化的效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值