浅入浅出 MyBatis

本贴最后更新于 2330 天前,其中的信息可能已经渤澥桑田

1. 关于阅读源码

工作中使用到的框架容易成为最熟悉的陌生代码,可以熟练使用,但是并不知道其中原理。阅读源码可以帮助我们知其然,亦知其所以然,既可以在工作中更好的使用框架特性,也可以借鉴其中优秀的设计思想。

但是面对框架庞大的代码量,往往不知如何下手。总结了一些方法,可以帮助 coder 战胜恐惧,顺利的开始阅读源码。

1.1. 屏蔽干扰

简单粗暴的减法,根据包名猜测作用,排除主流程外代码
如 MyBatis 的包结构如下

  • annotations 注解类,流程实现无关
  • binding 代理 *
  • builder 构造 *
  • cache 缓存相关,后续独立阅读
  • datasource 数据源
  • exceptions 异常封装
  • executor 具体的执行 **
  • io 文件流
  • jdbc
  • logging 日志,后续独立阅读
  • mapping 映射,sql 映射,参数映射,结果映射
  • parsing sql 解析
  • plugin 插件
  • reflection 反射
  • scripting 脚本
  • session 会话请求 *
  • transaction 事务管理,后续独立阅读
  • type 类型转换,后续独立阅读

需要阅读的包列表如下

  • binding 代理 *
  • builder 构造 *
  • datasource 数据源
  • executor 具体的执行 **
  • mapping 映射,sql 映射,参数映射,结果映射
  • parsing sql 解析
  • scripting 脚本
  • session 会话请求 *

1.2. 找到入口

代码是逻辑的实现,找到入口,根据功能逻辑逐步阅读。入口一般为

  1. 启动类
  2. 根据具体的使用方法 debug 跟踪

1.3. 不纠结细节,不扩散思维

根据方法名明确其作用,熟悉整体流程时,不过分纠结具体实现细节(如:锁),明确作用主线,不要扩散到其他实现中

2. 基础知识点

阅读源码前先补充两个知识点

2.1. 代理

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 不破坏开闭原则的前提下,实现了功能的修改。AOP 也是采用了这种设计模式。

1353942400_6301jpg

2.1.1. 静态代理

需求(issue)需要编码(coding)实现,程序员(Coder)负责编码,但是 issue 不应该直接找 coder,产品经理(PM)负责在 coding 前后进行需求梳理、需求验收。但是 PM 不应该直接修改程序员的 coding 过程,所以 PM 就是一层代理,在 coder 无感知的情况下增加了功能与访问控制。

public class StaticProxy {
    public static void main(String[] args) {
        Issue client = new PM();
        client.coding();
    }

    static interface Issue {
        void coding();
    }

    static class Coder implements Issue {

        @Override
        public void coding() {
            System.out.println("coding");
        }

    }

    static class PM implements Issue {

        private static Issue issue = new Coder();

        @Override
        public void coding() {
            this.beforeDoing();
            issue.coding();
            this.afterDoing();
        }

        private void beforeDoing() {
            System.out.println("需求梳理");
        }

        private void afterDoing() {
            System.out.println("需求验收");
        }
    }

}

2.1.2. 动态代理

上面的代码有两个 bug,

  1. 如果我有多个程序员,如 JavaCoder、PythonCoder,那么是不是也需要多个 PM 吗?
  2. PM 不会 coding。。。

为了解决这两个问题,需要用到动态代理

public class DynamicProxy {

    public static void main(String[] args) {
        PM pmWithPython = new PM(new PythonCoder());
        PM pmWithJava = new PM(new JavaCoder());
        Issue pythonIssue = (Issue) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{Issue.class},
                pmWithPython);
        pythonIssue.coding();

        Issue javaIssue = (Issue) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{Issue.class},
                pmWithJava);
        javaIssue.coding();
    }

    interface Issue {
        void coding();
    }

    static class JavaCoder implements Issue {

        @Override
        public void coding() {
            System.out.println("coding by java");
        }
    }
    static class PythonCoder implements Issue {

        @Override
        public void coding() {
            System.out.println("coding by python");
        }
    }

    static class PM implements InvocationHandler {

        private Issue issue;

        public PM(Issue issue) {
            this.issue = issue;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            beforeDoing();
            proxy = method.invoke(issue, args);
            afterDoing();
            return proxy;
        }
        private void beforeDoing() {
            System.out.println("需求梳理");
        }

        private void afterDoing() {
            System.out.println("需求验收");
        }
    }
}

很多人会纠结,代理模式和装饰模式有什么区别,网上大多数的文章说装饰模式是为了增强功能,代理模式是为了控制访问,这是不能赞同的一种说法。我认为区别在于思想,装饰模式是你明确知道要把某个对象做哪些扩展,而代理则是你不和对象直接交互,而是通过第三者沟通。 动态代理,则是可以选择无关的第三者,不需要有共同的接口或父类。

2.2. JDBC

简单的实现代码

import java.sql.*;

public class JdbcApp {

    private static String url = "jdbc:mysql://localhost:3306/mysql";
    private static String username = "root";
    private static String password = "";

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection(url, username, password);
        Statement statement = connection.createStatement();

        ResultSet resultSet = statement.executeQuery("select * from user");
        if (resultSet.next()) {
            String Host = resultSet.getString("Host");
            String User = resultSet.getString("User");
            System.out.println("Host: " + Host + ", User: " + User);
        }
    }
}

2.2.1 JDBC 做了哪些事

JDBCpng

2.2.2 Class.forName 做了什么?

通过反射将驱动注册进 DriverManager 中,用于创建 connection 时使用。

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}
public class DriverManager {
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {

        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
}

反射运行静态块,但是不会生成实例。

2.2.3 可以不写 Class.forName 吗?

可以,因为 DriverManager 中通过 spi 初始化了驱动。

public class DriverManager {
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}

对 spi 做一个简单的介绍

SPI 简介 SPI 全称为(Service Provider Interface) ,是 JDK 内置的一种服务提供发现机制。

简单来说,我们定义了一个服务 interface,jar 包可以编写实现,并通过文件声明实现类,让服务发现并动态替换。

public interface SpiDemoApi {
    void print();
}

public class SpiDemoImplA implements SpiDemoApi {
    @Override
    public void print() {
        System.out.println("spi A impl");
    }
}

public class SpiDemoImplB implements SpiDemoApi {
    @Override
    public void print() {
        System.out.println("spi B impl");
    }
}

在 META-INF/services 目录下以接口全路径名命名的文件,并在其中声明实现类的全路径名。声明的方法可以通过 ServiceLoader 发现

import java.util.ServiceLoader;

public class SpiApp {
    public static void main(String[] args) {
        ServiceLoader<SpiDemoApi> serviceLoader = ServiceLoader.load(SpiDemoApi.class);
        for (SpiDemoApi service : serviceLoader) {
            service.print();
        }
    }
}

dubbo、log、jdbc 都使用了 spi 相关技术。

spi
dubbo log jdbc

SpringFactoriesLoader

2.3. 反射

2.3.1 类

3. MyBatis

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java 对象)映射成数据库中的记录。

3.1. 入口

因为 springboot 给我们带来开发便利的同时,隐藏了太多细节,已经忘记了 MyBatis 的启动方式。

3.2. driver + connection

在实际应用的过程中,每次都创建一次 connection 会极大的影响性能,所以我们会选择连接池技术,简单来说就是维护一个 connection 的 list,每次请求从 list 中取出一个 connection。Mybatis 提供了 3 中方式实现数据源初始化。

_faea1983364b432f9ce19444a1891322png

每次获取 connection 都会判断 driver 是否已注册

  private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver();
    Connection connection = DriverManager.getConnection(url, properties);
    configureConnection(connection);
    return connection;
  }

  private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver)driverType.newInstance();
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

PooledConnection 使用了动态代理,在不改变 Connection 的前提下,使关闭的 connection 加入到闲置连接池内。

class PooledConnection implements InvocationHandler {
...
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          checkConnection();
        }
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw 
ExceptionUtil.unwrapThrowable(t);
      }
    }
  }
  ...
}

3.3. statement + query

mybatis 项目中有 statement 这个包,直接看其中的类。

15209243245923png

StatementHandler 定义了获取 Statement 的接口

public interface StatementHandler {
...
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;
  Statement prepare(Connection connection)
      throws SQLException;
...
}

BaseStatementHandler 实现了 prepare 接口,并留了一个钩子函数 instantiateStatement

public abstract class BaseStatementHandler implements StatementHandler {
...
  @Override
  public Statement prepare(Connection connection) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      setStatementTimeout(statement);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

  protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
...
}

SimpleStatementHandler 继承 BaseStatementHandler 并重写 instantiateStatement

public class SimpleStatementHandler extends BaseStatementHandler {
...
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    String sql = boundSql.getSql();
    statement.execute(sql);
    return resultSetHandler.<E>handleResultSets(statement);
  }
  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    if (mappedStatement.getResultSetType() != null) {
      return connection.createStatement(mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.createStatement();
    }
  }
...
}

这里有一个两个成员变量,我们先 mark 一下,稍后再去阅读。

  1. MappedStatement 现在知道它可以获取 resultSetType。
  2. BoundSql 获取 sql

SimpleStatementHandler 实现了 StatementHandler 提供的 query 接口方法,通过 statement.execute 执行。

3.4. ResultSet

MyBatis 被称为半个 ORM 系统,就是因为它对 ResultSet 的映射。我们来看一下实现,同样有一个包名叫 resultset。。

15209278423663png

  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 384 关注
  • 代码
    467 引用 • 631 回帖 • 9 关注
  • JDBC
    18 引用 • 24 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • crick77
    作者

    是的 感觉这个点很难找

  • 其他回帖
  • footmanff

    源码分析文章最麻烦的一点是,怎么把代码逻辑用简单易懂的语言表达出来,感觉非常难。

    1 回复
  • z201 1

    思路清晰