浅入浅出 MyBatis

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

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。

    171 引用 • 414 回帖 • 375 关注
  • 代码
    467 引用 • 586 回帖 • 9 关注
  • JDBC
    18 引用 • 24 回帖
  • Java

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

    3194 引用 • 8214 回帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • footmanff via macOS

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

    1 回复
  • crick77 via macOS
    作者

    是的 感觉这个点很难找

  • z201 1 via macOS

    思路清晰