JDBC源码解析

JDBC源码分析


前言

JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。各个厂商实现,但是里面并没有很厉害的东西可以让我们学习,最厉害的还是各个厂商的实现,但是他们的实现都很复杂,所以没有多少人去解析他们的源码,拿Mysql驱动来说,他需要创建Socket和目标服务器进行通信,我们每执行的sql语句,都会发送到远程服务端执行,执行后返回数据并转换成ResultSet,这其实是很复杂的事情,复杂的原因是要定义一套信息交互格式,虽然我们可以不用MySql驱动照样可以和Mysql服务端通信,但是这是很难的,如果你非常了解mysql交互协议的话,也并不是不可能。

其他厂商也是。

这里的驱动是指Driver类,任何厂商都会有实现,他是用来创建Connection的,但这里就有一个问题,Java是怎么知道使用哪个驱动的?
其实Java也不知道,在初学JDBC的时候,首先会使用Class.forName("com.mysql.cj.jdbc.Driver"),这条语句其实是可以不需要的,因为有SPI机制,具体mysql在那个版本增加了对SPI的支持,我就不知道了,不想找资料了,但是8.0以后肯定是有的,在8.0后的驱动包中有META-INF/services/java.sql.Driver这个文件,这是关键,这个文件的内容是com.mysql.cj.jdbc.Driver。接下来调用ServiceLoader.load(Driver.class)会自动通过Class.forName加载这个类,Class.forName()一个类后,这个类的static代码块就会被执行。
那么驱动类被加载后往往都会在static代码块中执行DriverManager.registerDriver,注册的对象是本身,内部会有一个集合保存所有注册的Driver,接下来在调用getConnection()的时候,依次遍历已经注册后的Driver,看哪个Driver的connect()方法不返回null,表示这个Driver对传递的参数感兴趣,可以对他建立一个连接,所以每个厂商的url都不一样。

下面还是分析一下JdbcTemplate。

JdbcTemplate的作用是省去了一些过程,直接扔给他一个DataSource就可以工作了,最重要的还为我们封装了对ResultSet的转换,但是可气的是,没有一个方法可以直接把结果转换成对应的实体,倒是提供了一大堆接口,通过接口,我们可以间接实现,这方面大不如Mybatis方便

SPI机制:SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦


使用JDBC从数据库提取出数据,需要六步:

  1. 加载驱动(JDK6以后会自动注册,得益于SPI机制)
  2. 获取用户信息和URL
  3. 得到数据库对象
  4. 得到SQL对象(Statement
  5. 执行SQL对象
  6. 释放连接

那么整个JDBC的执行流程似乎都离不开一个SQL对象(Statement)

一、Statement对象详解

JDBC中的Statement对象用于向数据库发送SQL语句,数据库的增删改查通过这个对象来完成(向数据库中发送指令)。

该对象主要有两个方法:

  • statement.executeUpdate()方法,用于向数据库发送增,删,改的sql语句,executeUpdate()执行完后,将返回一个整数(就是增删改语句导致了数据库几行数据发生了改变)
  • statement.executeQuery()方法用于向数据库发送查询语句executeQuery()方法返回代表查询结果的结果集ResultSet对象

那么Statement还有个子类就是PreparedSatatement,那么他们呢两者的区别是什么呢?

Statement可以正常访问数据库,适用于运行静态 SQL 语句。 Statement 接口不接受参数。

  • 创建 Statement 对象
public static void main(String[] args) throws Exception {
        //1.提供加载MySQL注册驱动
        //Class.forName("com.mysql.jdbc.Driver");(JDK6之后可以不用写)
        //2.获取Connection连接对象
        Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydb1",
                "root",
                "123456"
        );
        //3.提供静态SQL语句(在写SQL语句时需要注意SQL语句中字符串问题和不用写;)
        String sql = "create table student(" +
                "id int primary key auto_increment," +
                "name varchar(20)," +
                "age int" +
                ")";
        //4.通过Connection对象创建静态语句对象Statement
        Statement statement = connection.createStatement();
        //5.通过Statement对象将SQL语句发送到Mysql数据库中执行并接收方法返回值
        int i = statement.executeUpdate(sql);
        //可以判断这个返回值是否>0 如果>0就证明操作成功,否则就证明操作失败
        //但是有一个特殊的存在使用DDL语言创建表或库 返回值是0 但是创建已经成功了
        System.out.println("返回值是:"+i);
        //6.关闭JDBC连接操作
        statement.close();
        connection.close();
​
​
    }

PreparedStatement计划多次使用 SQL 语句, PreparedStatement 接口运行时接受输入的参数。
PreparedStatement的好处:

  • 防止SQL攻击;
  • 提高代码的可读性,以可维护性;
  • 提高效率。

使用Connection的prepareStatement(String sql):即创建它时就让它与一条SQL模板绑定;调用PreparedStatement的setXXX()系列方法为问号设置值调用executeUpdate()或executeQuery()方法,但要注意,调用没有参数的方法。注意PreparedStatement对象独有的executeQuery()方法是没有参数的,而Statement的executeQuery()是需要参数(SQL语句)的。因为在创建PreparedStatement对象时已经让它与一条SQL模板绑定在一起了,所以在调用它的executeQuery()和executeUpdate()方法时就不再需要参数了。PreparedStatement最大的好处就是在于重复使用同一模板,给予其不同的参数来重复的使用它。这才是真正提高效率的原因。

  • 创建 PreparedStatement 对象
Connection conn= null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
    //1.注册驱动
    //Class.forName("com.mysql.jdbc.Driver");(JDK6之后可以不用写)
    //2.获取连接
    String url ="jdbc:mysql://127.0.0.1:3306/web08?useUnicode=true&characterEncoding=utf8";
    conn = DriverManager.getConnection(url,"root","root");
    //3.编写sql语句
    String sql = "select * from tbl_user where uname=? and upassword=?";
    //4.创建预处理对象
	pstmt = conn.prepareStatement(sql);
	// 5.设置参数(给占位符)
	pstmt.setString(1, username);
	pstmt.setString(2, password);
	// 6.执行查询操作
	ResultSet rs = pstmt.executeQuery();
    //6.处理结果集
    if(rs.next()){
       System.out.println("恭喜您," + username + ",登录成功!");
    }else {
        System.out.println("账号或密码错误!");
    }
} catch (Exception e) {
    e.printStackTrace();
}finally{
    if(rs!=null)
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    if(stmt!=null)
        try {
            pstmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    if(conn!=null)
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
}

二、JDBC核心类

JDBC的核心类除了上面提到的Statement之外还有:DriverManager、Connection、ResultSet
JDBC流程

DriverManager

DriverManager主要用于管理数据库驱动,并为我们提供了获取连接对象的接口。其中,它有一个重要的成员属性 registeredDrivers,是一个 CopyOnWriteArrayList 集合(通过 synchronized关键字实现线程安全),存放的是元素是 DriverInfo 对象。

//存放数据库驱动包装类的集合(线程安全)
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        //调用重载方法,传入的DriverAction对象为null
        registerDriver(driver, null);
    }
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
        if(driver != null) {
            //当列表中没有这个DriverInfo对象时,加入列表。
            //注意,这里判断对象是否已经存在,最终比较的是driver地址是否相等。
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

为什么集合存放的是 Driver 的包装类 DriverInfo 对象,而不是 Driver 对象呢?

通过 DriverInfo 的源码可知,当我们调用 equals 方法比较两个 DriverInfo 对象是否相等时,实际上比较的是 Driver 对象的地址,也就是说,我可以在 DriverManager 中注册多个 MYSQL 驱动。而如果直接存放的是 Driver 对象,就不能达到这种效果(因为没有遇到需要注册多个同类驱动的场景,所以我暂时理解不了这样做的好处)。

DriverInfo 中还包含了另一个成员属性 DriverAction,当我们注销驱动时,必须调用它的 deregister 方法后才能将驱动从注册列表中移除,该方法决定注销驱动时应该如何处理活动连接等(其实一般在构造 DriverInfo 进行注册时,传入的 DriverAction 对象为空,根本不会去使用到这个对象,除非一开始注册就传入非空 DriverAction 对象)。

综上,集合中元素不是 Driver 对象而 DriverInfo 对象,主要考虑的是扩展某些功能,虽然这些功能几乎不会用到。
部分源码:

class DriverInfo {

    final Driver driver;
    DriverAction da;
    DriverInfo(Driver driver, DriverAction action) {
        this.driver = driver;
        da = action;
    }

    @Override
    public boolean equals(Object other) {
        //这里对比的是地址
        return (other instanceof DriverInfo)
                && this.driver == ((DriverInfo) other).driver;
    }

}

为什么JDK6之后可以不用写Class.forName("com.mysql.jdbc.Driver");
具体去看了一下com.mysql.jdbc.Driver这个类的源代码,其中有这么一段静态代码块

static {
	try {
		java.sql.DriverManager.registerDriver(new Driver());
	} catch (SQLException E) {
		throw new RuntimeException(Can’t register driver!);
	}
}

也就是,在Class.forName加载完驱动类,开始执行静态初始化代码时,会自动新建一个Driver的对象,并调用DriverManager.registerDriver把自己注册到DriverManager中去。用Class.forName也是为了注册这个目的.(直接把Driver对象new出来,也是可以连接的,但是浪费空间没必要)
其次,在JDK6当中引入了Service Provider 的概念——SPI机制,即可以在配置文件中配置Service(可能是一个Interface或者Abstract Class)的Provider(即Service的实现类)。配置路径是:/META-INF/services/
大致看一下SPI的原理,以及如何实现的,主要实现的是一个叫ServiceLoader的类,在java.util包下
首先我先给出一个小示例来方便大家理解
现在我们需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。

  • 先定义好接口
public interface Search {
    List<String> searchDoc(String keyword);
}
  • 文件搜索的实现
public class FileSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        List<String> list = new ArrayList<>();
        list.add("文件搜索" + keyword);
        return list;
    }
}
  • 数据库搜索的实现
public class DatabaseSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        List<String> list = new ArrayList<>();
        list.add("数据库搜索" + keyword);
        return list;
    }
}

接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:org.example.Search,里面加上我们需要用到的实现类

#org.example.impl.FileSearch
org.example.impl.DatabaseSearch

以换行符作为分割

  • 测试方法
public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
            Search search = iterator.next();
            List<String> list = search.searchDoc("你好世界");
            System.out.println(list.get(0));
        }
    }

控制台输出:
控制台输出
如果在org.example.Search文件里写上两个实现类,那最后的输出结果就是两行了。

ServiceLoader.load()

动态的调用服务提供需要用到的一个关键类:ServiceLoader
进入静态方法load看一下

	@CallerSensitive
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();//获取类加载器
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);// 通过调用类,服务类和类加载器构造ServiceLoader的信息
    }

可以看到这个方法主要的作用就是构造ServiceLoader对象,并初始化一些属性,就是为了后面加载实现类做了铺垫

ServiceLoader.iterator()

通过查看ServiceLoader源码可以得知其实现了Itertable接口,走进iterator方法

public Iterator<S> iterator() {

        // create lookup iterator if needed
        if (lookupIterator1 == null) {
            lookupIterator1 = newLookupIterator();//创建查找迭代器
        }

        return new Iterator<S>() {

            // record reload count
            final int expectedReloadCount = ServiceLoader.this.reloadCount;

            // index into the cached providers list
            int index;

            /**
             * Throws ConcurrentModificationException if the list of cached
             * providers has been cleared by reload.
             */
            private void checkReloadCount() {
                if (ServiceLoader.this.reloadCount != expectedReloadCount)
                    throw new ConcurrentModificationException();
            }

            @Override
            public boolean hasNext() {
                checkReloadCount();
                if (index < instantiatedProviders.size())
                    return true;
                return lookupIterator1.hasNext();
            }

            @Override
            public S next() {
                checkReloadCount();
                S next;
                if (index < instantiatedProviders.size()) {
                    next = instantiatedProviders.get(index);
                } else {
                    next = lookupIterator1.next().get();
                    instantiatedProviders.add(next);
                }
                index++;
                return next;
            }

        };
    }

那么作为首次调用的时候会根据需要创建一个查找迭代器,继续跟进newLookupIterator方法

private Iterator<Provider<S>> newLookupIterator() {
        assert layer == null || loader == null;
        if (layer != null) {
            return new LayerLookupIterator<>();
        } else {
            Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
            Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
            return new Iterator<Provider<S>>() {
                @Override
                public boolean hasNext() {
                    return (first.hasNext() || second.hasNext());
                }
                @Override
                public Provider<S> next() {
                    if (first.hasNext()) {
                        return first.next();
                    } else if (second.hasNext()) {
                        return second.next();
                    } else {
                        throw new NoSuchElementException();
                    }
                }
            };
        }
    }

根据我Debug的一步步跟进发现在new一个LazyClassPathLookupIterator对象的时候指定了获取配置文件的路径static final String PREFIX = "META-INF/services/"; ,从这个类名见名知意叫做惰性类路径查找迭代器,就是根据全限定类名去查找Java所需要的指定厂商提供的Driver实现类,这个路径也是各大数据库厂商为了JDBC指定的标准而定制的

private final class LazyClassPathLookupIterator<T>
        implements Iterator<Provider<T>>
    {
        static final String PREFIX = "META-INF/services/";

        Set<String> providerNames = new HashSet<>();  // to avoid duplicates
        Enumeration<URL> configs;
        Iterator<String> pending;

        Provider<T> nextProvider;
        ServiceConfigurationError nextError;

        LazyClassPathLookupIterator() { }
        ....省略部分源码

那么LazyClassPathLookupIterator重写了hasNext()方法

@Override
        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

发现其中有个方法必然执行,就是hasNextService(),继续跟进

private boolean hasNextService() {
            while (nextProvider == null && nextError == null) {
                try {
                    Class<?> clazz = nextProviderClass();//找到服务提供商的实现类字节码
                    if (clazz == null)
                        return false;

                    if (clazz.getModule().isNamed()) {
                        // ignore class if in named module
                        continue;
                    }

                    if (service.isAssignableFrom(clazz)) {
                        Class<? extends S> type = (Class<? extends S>) clazz;
                        Constructor<? extends S> ctor
                            = (Constructor<? extends S>)getConstructor(clazz);
                        ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
                        nextProvider = (ProviderImpl<T>) p;
                    } else {
                        fail(service, clazz.getName() + " not a subtype");
                    }
                } catch (ServiceConfigurationError e) {
                    nextError = e;
                }
            }
            return true;
        }

映入眼帘的就是nextProviderClass()方法

private Class<?> nextProviderClass() {
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();//做一个文件路径拼接,获取需要加载的不同厂商的实现类,在初次load方法调用的时候我们就传入了Search.class,初始化的内容就是当前的service,getName()=>org.example.Search
                    if (loader == null) {
                        configs = ClassLoader.getSystemResources(fullName);
                    } else if (loader == ClassLoaders.platformClassLoader()) {
                        // The platform classloader doesn't have a class path,
                        // but the boot loader might.
                        if (BootLoader.hasClassPath()) {
                            configs = BootLoader.findResources(fullName);
                        } else {
                            configs = Collections.emptyEnumeration();
                        }
                    } else {
                        configs = loader.getResources(fullName);
                    }
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return null;
                }
                pending = parse(configs.nextElement());
            }
            String cn = pending.next();
            try {
                return Class.forName(cn, false, loader);//通过上面提供的类加载器和实现类完全限定名调用底层native方法获取实现类并返回。
            } catch (ClassNotFoundException x) {
                fail(service, "Provider " + cn + " not found");
                return null;
            }
        }

可以看到这个类前面都是再找到具体实现的类加载器,主要的是在他调用的parse方法开始解析配置文件中的实现类的完全限定名

private Iterator<String> parse(URL u) {
            Set<String> names = new LinkedHashSet<>(); // preserve insertion order
            try {
                URLConnection uc = u.openConnection();
                uc.setUseCaches(false);
                try (InputStream in = uc.getInputStream();
                     BufferedReader r
                         = new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE)))
                {
                    int lc = 1;
                    while ((lc = parseLine(u, r, lc, names)) >= 0);//获取配置的详细信息
                }
            } catch (IOException x) {
                fail(service, "Error accessing configuration file", x);
            }
            return names.iterator();
        }

之后在nextProviderClass中通过Class.forName的方式开始加载实现类
通过分析过程可以看出,SPI使用懒加载机制,只有在调用ServiceLoader的遍历接口,才会真正的去加载实现类。

Connection

获取连接对象的入口是 DriverManager.getConnection,调用时需要传入 url、username 和 password。

获取连接对象需要调用 java.sql.Driver 实现类(即数据库驱动)的方法,而具体调用哪个实现类呢?

正如前面讲到的,注册的数据库驱动被存放在 registeredDrivers 中,所以只有从这个集合中获取就可以了。

部分源码:

public static Connection getConnection(String url, String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }
        /*参数:
	    url:指定连接的路径
	    	语法:jdbc:mysql://ip地址(域名):端口号/数据库名称
			例子:jdbc:mysql://localhost:3306/employee
			细节:如果连接是本机Mysql服务器,并且默认的端口是3306,则url可以		  简写为:jdbc:mysql:///数据库名
		*/
		     //传入url、包含username和password的信息类、当前调用类
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        //遍历所有注册的数据库驱动
        for(DriverInfo aDriver : registeredDrivers) {
            //先检查这当前类加载器是否有权限加载这个驱动,如果是才进入
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                //这一步是关键,会去调用Driver的connect方法
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    return con;
                }
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }
    }

由于使用的是 mysql 的数据驱动,这里实际调用的是 com.mysql.cj.jdbc.NonRegisteringDriver 的方法。
从以下代码可以看出,MySQL支持多节点部署的策略,本文仅对单机版进行扩展

以下是部分经过修改的源码:

//mysql支持多节点部署的策略,根据架构不同,url格式也有所区别。
    	SINGLE_CONNECTION("jdbc:mysql:", ConnectionUrl.HostsCardinality.SINGLE),
        FAILOVER_CONNECTION("jdbc:mysql:", ConnectionUrl.HostsCardinality.MULTIPLE),
        LOADBALANCE_CONNECTION("jdbc:mysql:loadbalance:", ConnectionUrl.HostsCardinality.ONE_OR_MORE),
        REPLICATION_CONNECTION("jdbc:mysql:replication:", ConnectionUrl.HostsCardinality.ONE_OR_MORE),
        XDEVAPI_SESSION("mysqlx:", ConnectionUrl.HostsCardinality.ONE_OR_MORE);

    public Connection connect(String url, Properties info) throws SQLException {
        //根据url的类型来返回不同的连接对象,这里仅考虑单机版
        ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
        switch (conStr.getType()) {
            case SINGLE_CONNECTION:
                //调用ConnectionImpl.getInstance获取连接对象
                return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

            case LOADBALANCE_CONNECTION:
                return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

            case FAILOVER_CONNECTION:
                return FailoverConnectionProxy.createProxyInstance(conStr);

            case REPLICATION_CONNECTION:
                return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

            default:
                return null;
        }
    }

写法很简洁,就是创建了一个 MySQL 的数据库连接对象, 传入 host, port, database 等连接信息,在 com.mysql.jdbc.Connection 的构造方法里面有个 createNewIO () 方法,主要会做两件事情,一、建立和 MysqlServer 的 Socket 连接,二、连接成功后,进行登录校验,发送用户名、密码、当前数据库连接默认选择的数据库名。

连接数据库的URL地址格式:
协议名:子协议://服务器名或IP地址:端口号/数据库名?参数=参数值

ConnectionImpl.getInstance

这个类有个比较重要的字段 session,可以把它看成一个会话,和我们平时浏览器访问服务器的会话差不多,后续我们进行数据库操作就是基于这个会话来实现的。

部分修改过后的代码:

private NativeSession session = null;
    public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
        //调用构造
        return new ConnectionImpl(hostInfo);
    }
    public ConnectionImpl(HostInfo hostInfo) throws SQLException {
        //先根据hostInfo初始化成员属性,包括数据库主机名、端口、用户名、密码、数据库及其他参数设置等等,这里省略不放入。
        //最主要看下这句代码 
        createNewIO(false);
    }
    public void createNewIO(boolean isForReconnect) {
        if (!this.autoReconnect.getValue()) {
            //这里只看不重试的方法
            connectOneTryOnly(isForReconnect);
            return;
        }

        connectWithRetries(isForReconnect);
    }
    private void connectOneTryOnly(boolean isForReconnect) throws SQLException {

        JdbcConnection c = getProxy();
        //调用NativeSession对象的connect方法建立和数据库的连接
        this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
        return;
    }

NativeSession.connect

public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
            throws IOException {
        //首先获得TCP/IP连接
        SocketConnection socketConnection = new NativeSocketConnection();
        socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout);

        // 对TCP/IP连接进行协议包装
        if (this.protocol == null) {
            this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
        } else {
            this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
        }

        // 通过用户名和密码连接指定数据库,并创建会话
        this.protocol.connect(user, password, database);
    }

Connection实例是线程安全的吗

我们在原生的JDBC中,每次与数据库操作,都是先获取Connection对象,然后在方法执行完调用close()方法,释放资源。

能不能只创建一次,共享connection对象?
答案是不能的!Connection不是线程安全的,他会在多线程环境下,导致数据库操作的混乱,特别是在事务存在的情况下:可能一个线程刚开启事务con.setAutoCommit(true);,而另一个线程直接提交事务con.commit();。
对于单独查询的情况,似乎不会出现数据错乱的情况。是因为在JDBC中,使用了锁进行同步。

	@Override
    public java.sql.ResultSet executeQuery() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
        ...
        }
    }

connection本身是线程不安全的,并且connection创建开销比较大,所以一般使用数据库连接池来统一的管理connection对象,例如druid连接池,c3p0连接池等等。

在使用数据库连接池时,一个线程中所有DB操作都是使用同一个Connection实例吗

在Spring环境中,获取connection源码如下:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
       //1. 若是事务情况下,在ConnectionHolder中获取Connection对象
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            return conHolder.getConnection();
        }
         //Else we either got no holder or an empty thread-bound holder here.
        //2. 若未存在事务,直接在数据库连接池中获取Connection连接
        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = fetchConnection(dataSource);

        ....

        return con;
    }
1.非事务场景

在非事务场景中(同时没有使用Spring事务管理器),每一次访问数据库,都是在DataSource中取出一个connection实例,调用完毕之后归还资源,因此多次调用,应该是不同的connection实例。

2.事务场景

在使用事务的情况下 ,实际上是在ConnectionHolder中获取的Connection。而ConnectionHolder是在TransactionSynchronizationManager中获取的resources属性的值,即connection对象信息。

private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

ThreadLocal<Map<Object, Object>>线程上下文共享。即Connection对于与Thread 绑定。因此在事务中无论操作多少次DB,事实上都是操作的同一个Connection对象。

3.Connection关闭自动提交

开启事务操作的关键是con.setAutoCommit(false);,JDBC默认是开启的。即sql执行完毕自动提交。

Connection conn = DriverManager.getConnection(...);
try{
  con.setAutoCommit(false);
  Statement stmt = con.createStatement();
  //1 or more queries or updates
  con.commit();
}catch(Exception e){
  con.rollback();
}finally{
con.close();
}

而关闭自动提交后,需要手动调用con.commit();方法提交事务;若出现异常,则调用con.rollback();方法回滚事务。

数据库连接池的实现及原理

对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用,情况就完全不同了。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。对于共享资源,有一个很著名的设计模式:资源池。该模式正是为了解决资源频繁分配、释放所造成的问题的。把该模式应用到数据库连接管理领域,就是建立一个数据库连接池,提供一套高效的连接分配、使用策略,最终目标是实现连接的高效、安全的复用。

1.前言

数据库应用,在许多软件系统中经常用到,是开发中大型系统不可缺少的辅助。但如果对数据库资源没有很好地管理(如:没有及时回收数据库的游标(ResultSet)、Statement、连接 (Connection)等资源),往往会直接影响系统的稳定。这类不稳定因素,不单单由数据库或者系统本身一方引起,只有系统正式使用后,随着流量、用户的增加,才会逐步显露。在基于Java开发的系统中,JDBC是程序员和数据库打交道的主要途径,提供了完备的数据库操作方法接口。但考虑到规范的适用性,JDBC只提供了最直接的数据库操作规范,对数据库资源管理,如:对物理连接的管理及缓冲,期望第三方应用服务器(Application Server)的提供。本文,以JDBC规范为基础,介绍相关的数据库连接池机制,并就如何以简单的方式,实现有效地管理数据库资源介绍相关实现技术。

2.连接池技术背景
2.1 JDBC连接池

在标准JDBC对应用的接口中,并没有提供资源的管理方法。所以,缺省的资源管理由应用自己负责。虽然在JDBC规范中,多次提及资源的关闭/回收及其他的合理运用。但最稳妥的方式,还是为应用提供有效的管理手段。所以,JDBC为第三方应用服务器(Application Server)提供了一个由数据库厂家实现的管理标准接口:连接缓冲(connection pooling)。引入了连接池( Connection Pool )的概念 ,也就是以缓冲池的机制管理数据库的资源。

前面提到JDBC的核心类有

  • Connection: 数据库连接
  • Statement: 会话声明
  • ResultSet: 结果集游标
    并存在以下关系:
    在这里插入图片描述
    这是一种“爷—父—子”的关系,对Connection的管理,就是对数据库资源的管理。举个例子: 如果想确定某个数据库连接(Connection)是否超时,则需要确定其(所有的)子Statement是否超时,同样,需要确定所有相关的 ResultSet是否超时;在关闭Connection前,需要关闭所有相关的Statement和ResultSet。因此,连接池(Connection Pool)所起到的作用,不仅仅简单地管理Connection,还涉及到 Statement和ResultSet。
2.2 连接池(ConnectionPool)与资源管理

ConnectionPool以缓冲池的机制,在一定数量上限范围内,控制管理Connection,Statement和ResultSet。任何数据库的资源是有限的,如果被耗尽,则无法获得更多的数据服务。
在大多数情况下,资源的耗尽不是由于应用的正常负载过高,而是程序原因。在实际工作中,数据资源往往是瓶颈资源,不同的应用都会访问同一数据源。其中某个应用耗尽了数据库资源后,意味其他的应用也无法正常运行。因此,ConnectionPool的第一个任务是限制:每个应用或系统可以拥有的最大资源。也就是确定连接池的大小(PoolSize)。
ConnectionPool的第二个任务:在连接池的大小(PoolSize)范围内,最大限度地使用资源,缩短数据库访问的使用周期。许多数据库中,连接(Connection)并不是资源的最小单元,控制Statement资源比Connection更重要。以Oracle为例:
每申请一个连接(Connection)会在物理网络(如 TCP/IP网络)上建立一个用于通讯的连接,在此连接上还可以申请一定数量的Statement。同一连接可提供的活跃Statement数量可以达到几百。在节约网络资源的同时,缩短了每次会话周期(物理连接的建立是个费时的操作)。但在一般的应用中,这样有10个程序调用,则会产生10次物理连接,每个Statement单独占用一个物理连接,这是极大的资源浪费。 ConnectionPool可以解决这个问题,让几十、几百个Statement只占用同一个物理连接, 发挥数据库原有的优点。
通过ConnectionPool对资源的有效管理,应用可以获得的Statement总数到达 :

(并发物理连接数)×(每个连接可提供的Statement数量)

例如某种数据库可同时建立的物理连接数为 200个,每个连接可同时提供250个Statement,那么ConnectionPool最终为应用提供的并发Statement总数为: 200 × 250 = 50,000个。这是个并发数字,很少有系统会突破这个量级。所以在本节的开始,指出资源的耗尽与应用程序直接管理有关。
对资源的优化管理,很大程度上依靠数据库自身的JDBC Driver是否具备。有些数据库的JDBC Driver并不支持Connection与Statement之间的逻辑连接功能,如SQLServer,我们只能等待她自身的更新版本了。
对资源的申请、释放、回收、共享和同步,这些管理是复杂精密的。所以,ConnectionPool另一个功能就是,封装这些操作,为应用提供简单的,甚至是不改变应用风格的调用接口。

MyBatis数据源与连接池源码分析

一、MyBatis数据源DataSource分类

MyBatis把数据源DataSource分为三种:
1、UNPOOLED 不使用连接池的数据源
2、POOLED 使用连接池的数据源
3、JNDI 使用JNDI实现的数据源

相应地,MyBatis内部分别定义了实现了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource类来表示UNPOOLED、POOLED类型的数据源。
在这里插入图片描述

二、数据源DataSource的创建过程

MyBatis数据源DataSource对象的创建发生在MyBatis初始化的过程中。下面让我们一步步地了解MyBatis是如何创建数据源DataSource的。

在mybatis的XML配置文件中(spring-jdbc.xml),使用元素来配置数据源:

<bean id="dataSource" class="com.xxxxx.jdbc.GroupDataSource"  type="POOLED"
          init-method="init" destroy-method="close">
        <property name="jdbcRef" value="${jdbc_ref}" />
        <property name="poolType" value="tomcat-jdbc" />
        <property name="minPoolSize" value="5" />
        <property name="maxPoolSize" value="20" />
        <property name="initialPoolSize" value="5" />
        <property name="checkoutTimeout" value="1000" />
        <property name="connectionInitSql" value="set names utf8mb4" />
        <property name="lazyInit" value="${jdbc_lazyinit}" />
        <!--每60秒运行一次空闲连接回收器
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>-->
        <!--池中的连接空闲180秒后被回收-->
        <property name="minEvictableIdleTimeMillis" value="180000"/>
        <property name="socketTimeout" value="120000"/>
    </bean>

MyBatis在初始化时,解析此文件,根据的type属性来创建相应类型的的数据源DataSource,即:

  • type=”POOLED” :MyBatis会创建PooledDataSource实例
  • type=”UNPOOLED” :MyBatis会创建UnpooledDataSource实例
  • type=”JNDI” :MyBatis会从JNDI服务上查找DataSource实例,然后返回使用

顺便说一下,MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口:org.apache.ibatis.datasource.DataSourceFactory,通过其getDataSource()方法返回数据源DataSource:

public interface DataSourceFactory {   
  void setProperties(Properties props);  
  //生产DataSource  
  DataSource getDataSource();
}

不同类型,对应不同的dataSource工厂如下图:
在这里插入图片描述
你也可以通过实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现

三、数据连接池的实现原理
3.1 PooledDataSource 的实现原理

构造函数
PooledDataSource 的构造函数如下,可以看到在创建 PooledDataSource 对象的时候,会创建 UnpooledDataSource 对象。
同时,在实例化 PooledDataSource 对象的时候,会创建 PoolState 实例。


  private final PoolState state = new PoolState(this); // 用于存储数据库连接对象

  private final UnpooledDataSource dataSource; // 用于创建数据库连接对象
  private int expectedConnectionTypeCode; // 数据库连接标识,url+username+password 字符串的哈希值

  public PooledDataSource() {
    this.dataSource = new UnpooledDataSource();
  }

  public PooledDataSource(UnpooledDataSource dataSource) {
    this.dataSource = dataSource;
  }

  public PooledDataSource(String driver, String url, String username, String password) {
    this.dataSource = new UnpooledDataSource(driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(String driver, String url, Properties driverProperties) {
    this.dataSource = new UnpooledDataSource(driver, url, driverProperties);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
    this.dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
    this.dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

数据库的连接地址、用户名密码信息,保存在 UnpooledDataSource 对象之中。
在这里插入图片描述
在 PooledDataSource 对象中,为什么要保存一个 UnpooledDataSource 对象呢?
这是为了利用 UnpooledDataSource 来向数据库建立连接。
比如在使用 MySQL 驱动的情况下,会向 MySQL 服务器建立 Socket 连接,并返回一个 com.mysql.cj.jdbc.ConnectionImpl 连接对象。

org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
org.apache.ibatis.datasource.unpooled.UnpooledDataSource#doGetConnection(java.lang.String, java.lang.String)
org.apache.ibatis.datasource.unpooled.UnpooledDataSource#doGetConnection(java.util.Properties)

  private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver();
    Connection connection = DriverManager.getConnection(url, properties); // 利用数据库驱动包,创建连接对象
    configureConnection(connection);
    return connection;
  }

PooledDataSource 中的 PoolState,是一个内部类,用于存储数据库连接对象,以及记录统计信息。

数据库连接池的大小,由 PoolState 中两个集合的容量决定:

空闲连接集合中,存储的是没有被使用的、可以直接拿去使用的连接。
活动连接集合中,存储的是正在使用中的连接。

public class PoolState {

  protected PooledDataSource dataSource;

  protected final List<PooledConnection> idleConnections = new ArrayList<>();   // 空闲的连接
  protected final List<PooledConnection> activeConnections = new ArrayList<>(); // 活动的连接
  protected long requestCount = 0;            // 请求次数
  protected long accumulatedRequestTime = 0;  // 总请求时间
  protected long accumulatedCheckoutTime = 0; // 总的检出时间(从池中取出连接,称为检出)
  protected long claimedOverdueConnectionCount = 0;               // 声明为已过期的连接数
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 总的已过期的连接数
  protected long accumulatedWaitTime = 0;     // 总等待时间
  protected long hadToWaitCount = 0;          // 要等待的次数
  protected long badConnectionCount = 0;      // 坏的连接次数

  public PoolState(PooledDataSource dataSource) {
    this.dataSource = dataSource;
  }
}  

PoolState 中并不是存储原始的连接对象com.mysql.cj.jdbc.ConnectionImpl,而是存储 PooledConnection 对象。

这里采用了 JDK 的动态代理,每次创建 PooledConnection 的时候,都会为原始的连接对象,创建一个动态代理。

使用代理的目的是,改变 Connection 的行为:

  1. 把连接关闭行为 Connection#close,改为将连接归还连接池。
  2. 每次使用连接之前,检查 PooledConnection#valid 属性是否有效(只是检查代理对象是否有效,并没有检查原始连接)。

org.apache.ibatis.datasource.pooled.PooledConnection

class PooledConnection implements InvocationHandler { // 相当于一个工具类

  private static final String CLOSE = "close";
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };

  private final int hashCode;
  private final PooledDataSource dataSource;
  private final Connection realConnection;  // 原始类-数据库连接
  private final Connection proxyConnection; // 代理类-数据库连接
  private long checkoutTimestamp; // 从连接池中检出的时间戳
  private long createdTimestamp;  // 创建的时间戳
  private long lastUsedTimestamp; // 上一次使用的时间戳
  private int connectionTypeCode;
  private boolean valid; // 连接是否有效

  /**
   * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in.
   *
   * @param connection
   *          - the connection that is to be presented as a pooled connection
   * @param dataSource
   *          - the dataSource that the connection is from
   */
  public PooledConnection(Connection connection, PooledDataSource dataSource) { // 传入原始类,获取代理类
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true;
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); // JDK 动态代理
  }
  
  /**
   * Required for InvocationHandler implementation.
   *
   * @param proxy
   *          - not used
   * @param method
   *          - the method to be executed
   * @param args
   *          - the parameters to be passed to the method
   * @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 代理方法
    String methodName = method.getName();
    if (CLOSE.equals(methodName)) { // 将关闭连接的行为,改为放回连接池
      dataSource.pushConnection(this);
      return null;
    }
    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);
    }

  }
  
  private void checkConnection() throws SQLException {
    if (!valid) {
      throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
    }
  }
  
  public boolean isValid() { // 校验连接是否有效
    return valid && realConnection != null && dataSource.pingConnection(this);
  }

PooledDataSource#pingConnection
PooledDataSource 的使用过程中,会调用 PooledConnection#isValid 方法来检查连接是否有效。

PooledDataSource 类中与连接检查相关的属性:

// 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。
protected String poolPingQuery = "NO PING QUERY SET";  
// 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
protected boolean poolPingEnabled;     
// 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
protected int poolPingConnectionsNotUsedFor;     

当满足以下条件时,会向数据库发送 poolPingQuery 所配置的 SQL 语句。

  1. 数据库连接未关闭。
  2. 在 MyBatis XML 中配置 poolPingEnabled 为 true。
  3. 距离上一次使用连接的时间,大于连接检查频率。

org.apache.ibatis.datasource.pooled.PooledDataSource#pingConnection

  /**
   * Method to check to see if a connection is still usable
   *
   * @param conn
   *          - the connection to check
   * @return True if the connection is still usable
   */
  protected boolean pingConnection(PooledConnection conn) { // 校验连接是否有效
    boolean result = true;

    try {
      result = !conn.getRealConnection().isClosed(); // 校验数据库连接会话是否已关闭 eg. com.mysql.cj.jdbc.ConnectionImpl.isClosed
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }

    if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0         // 配置了需要检查连接
        && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) { // 距离上一次使用连接的时间,大于连接检查频率
      try {
        if (log.isDebugEnabled()) {
          log.debug("Testing connection " + conn.getRealHashCode() + " ...");
        }
        Connection realConn = conn.getRealConnection();
        try (Statement statement = realConn.createStatement()) {
          statement.executeQuery(poolPingQuery).close(); // 发送简单语句,检查连接是否有效
        }
        if (!realConn.getAutoCommit()) {
          realConn.rollback();
        }
        result = true;
        if (log.isDebugEnabled()) {
          log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
        }
      } catch (Exception e) {
        log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage()); // 连接检查失败
        try {
          conn.getRealConnection().close(); // 尝试关闭连接
        } catch (Exception e2) {
          // ignore
        }
        result = false;
        if (log.isDebugEnabled()) {
          log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
        }
      }
    }
    return result;
  }

PooledConnection 对象中会记录上一次使用连接的时间戳(毫秒级)。

org.apache.ibatis.datasource.pooled.PooledConnection#getTimeElapsedSinceLastUse

  /**
 1. Getter for the time since this connection was last used.
 2.  3. @return - the time since the last use
   */
  public long getTimeElapsedSinceLastUse() {
    return System.currentTimeMillis() - lastUsedTimestamp;
  }

PooledDataSource#popConnection
从池中取出数据库连接,代码流程:

  1. 采用 while 循环从数据库连接池中取出连接(该操作称为检出 checkout),每次循环开始都需要获取 PoolState state 对象锁。
  2. 检测 PoolState 中的空闲连接集合和活动连接集合,并从中获取连接对象,分为几种情况:
    2.1 空闲连接集合非空,则从中取出一个连接。
    2.2 空闲连接集合为空,活动连接集合未满,则利用数据库驱动包建立新连接,并包装为 PooledConnection 对象(生成动态代理)。
    2.3 空闲连接集合为空,活动连接集合已满,则需要对最早的连接进行检查:
    2.3.1 如果该连接已超时(代理对象的检出时间大于 poolMaximumCheckoutTime,但是原始连接可能还存活),此时将代理对象 PooledConnection 标记为失效,将原始连接封装为新的 PooledConnection 对象。
    2.3.2 如果该连接未超时,则当前的检出线程进入等待。
  3. 来到这一步,说明从 PoolState 检出 PooledConnection 对象成功,需要检查该连接是否有效:
    3.1 如果连接有效,则设置相关时间戳,并存入活动连接集合,结束 while 循环。
    3.2 如果连接无效,重新进入 while 循环。
    3.3 重新进入 while 循环的次数是有限的,不可超过(空闲连接数 + 坏连接忍受阈值),否则抛出异常。

org.apache.ibatis.datasource.pooled.PooledDataSource#popConnection

  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;

    while (conn == null) { // 循环检出连接
      synchronized (state) { // 每次循环,都要重新获取锁!
        if (!state.idleConnections.isEmpty()) { // 空闲连接集合非空,则从中取出连接(都是有效的)
          // Pool has available connection
          conn = state.idleConnections.remove(0); // 移除并返回
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // Pool does not have available connection // 空闲连接集合为空,则需要检查活跃连接集合
          if (state.activeConnections.size() < poolMaximumActiveConnections) { // 活跃连接集合未满,则建立新的数据库连接
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this); // 利用数据库驱动包,创建连接对象,再包装为代理对象
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // Cannot create new connection // 空闲连接集合为空,且活跃连接集合已满,则需要处理过期的活跃连接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) { // 对于活跃连接集合中最早放入的连接,如果它的检出的时间已超时(也就是说从池中出来太久了)
              // Can claim overdue connection
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              state.activeConnections.remove(oldestActiveConnection); // 从活跃连接集合移除
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  /*
                     Just log a message for debug and continue to execute the following  // 回滚失败,当作无事发生
                     statement like nothing happened.
                     Wrap the bad connection with a new PooledConnection, this will help         // 将坏连接包装为一个新的 PooledConnection 对象
                     to not interrupt current executing thread and give current thread a         // 不会中断当前执行任务的线程,该线程后续可以从连接池中,取出其他的有效连接
                     chance to join the next competition for another valid/good database
                     connection. At the end of this loop, bad {@link @conn} will be set as null. // 本次循环最后,会把坏连接置为空
                   */
                  log.debug("Bad connection. Could not roll back");
                }
              }
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); // 后续需要识别为坏连接!怎么识别?通过 PooledConnection#isValid
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              oldestActiveConnection.invalidate(); // 设为无效
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {
              // Must wait // 活跃集合已满,且都未超时,只能等待其他线程归还活跃连接
              try {
                if (!countedWait) {
                  state.hadToWaitCount++;
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait); // 等待直到超时,或者被其他线程唤醒(见 PooledDataSource#pushConnection)。接着进入下一次 while 循环
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
        if (conn != null) { // 通过各种方式拿到连接之后,需要检查连接是否有效
          // ping to server and check the connection is valid or not
          if (conn.isValid()) {
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); // 设置连接标识:url+username+password 字符串的哈希值
            conn.setCheckoutTimestamp(System.currentTimeMillis()); // 设置检出时间,注意,这里是从数据库连接池中取出的时间戳!而不是与数据库建立连接的时间!
            conn.setLastUsedTimestamp(System.currentTimeMillis()); // 设置最后一次使用时间
            state.activeConnections.add(conn); // 加入活跃集合(1. 把原连接对象从空闲集合移动到活跃集合;2. 从活跃集合中取出超时连接,又放回活跃集合)
            state.requestCount++;
            state.accumulatedRequestTime += System.currentTimeMillis() - t;
          } else { // 连接无效,则进入下一次循环重新获取连接,或者抛异常
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            state.badConnectionCount++;
            localBadConnectionCount++;
            conn = null;
            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { // 本次循环次数 大于(空闲连接数 + 坏连接忍受阈值),则抛异常不再循环
              if (log.isDebugEnabled()) {
                log.debug("PooledDataSource: Could not get a good connection to the database.");
              }
              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
            }
          }
        }
      }

    }

    if (conn == null) {
      if (log.isDebugEnabled()) {
        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      }
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }

    return conn;
  }

在检出连接的过程中,会利用 PoolState 来记录一些总的耗时。

  protected long requestCount = 0;            // 请求次数
  protected long accumulatedRequestTime = 0;  // 总请求时间
  protected long accumulatedCheckoutTime = 0; // 总的检出时间(从池中取出连接,称为检出)
  protected long claimedOverdueConnectionCount = 0;               // 声明为已过期的连接数
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 总的已过期的连接数
  protected long accumulatedWaitTime = 0;     // 总等待时间
  protected long hadToWaitCount = 0;          // 要等待的次数
  protected long badConnectionCount = 0;      // 坏的连接次数

而在 PooledDataSource 对象中,会设置空闲连接集合、活动连接集合的容量,以及一些最大时间限制。

  protected int poolMaximumActiveConnections = 10; // 在任意时间可存在的活动(正在使用)连接数量
  protected int poolMaximumIdleConnections = 5;    // 任意时间可能存在的空闲连接数
  protected int poolMaximumCheckoutTime = 20000;   // 在被强制返回之前,池中连接被检出的时间。默认值:20000 毫秒(即 20 秒)
  protected int poolTimeToWait = 20000;            // 这是一个底层设置,如果获取连接花费的相当长的时间,它会给连接池打印状态日志,并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志)
  protected int poolMaximumLocalBadConnectionTolerance = 3; // 这是一个关于坏连接容忍度的底层设置,作用于每一个尝试从缓存池获取连接的线程。如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和

PooledDataSource#pushConnection

将连接归还数据库连接池。
代码流程:

  1. 获取 PoolState 对象锁。
  2. 从活跃连接集合中移除 PooledConnection,并检查连接是否有效。
  3. 若连接有效,则判断空闲集合是否已满:
    3.1 空闲集合未满,将原始连接封装为新的 PooledConnection 对 象,加入空闲集合。
    3.2 空闲集合已满,则关闭连接,不再复用。

可以看到,每次归还数据库连接,实际上是归还原始的 com.mysql.cj.jdbc.ConnectionImpl 连接对象,而 PooledConnection 生成的代理对象则是用完就丢,并且设置为失效状态,避免在复用时影响其他线程。

  protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      state.activeConnections.remove(conn); // 从活跃连接集合中移除
      if (conn.isValid()) { // 校验连接是否有效,若有效则进入下一步
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { // 空闲连接集合未满,并且连接标识一致(url+username+password),则需要加入空闲连接集合
          state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 累加总的检出时间(记录连接从出池到入池的总时间)(有效的连接从池中取出时,会记录检出时间戳)
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback(); // 把之前的事务回滚,避免对下次使用造成影响
          }
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); // 为原始连接生成新的 PooledConnection 对象
          state.idleConnections.add(newConn); // 加入空闲连接集合(注意这里不是把旧的 PooledConnection 从活跃集合移动到空闲集合)
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          conn.invalidate(); // 将旧的 PooledConnection 对象设为失效,因为用户可以直接拿到这个实例,避免后续仍使用这个实例操作数据库
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          state.notifyAll(); // 唤醒等待获取数据库连接的线程,见 PooledDataSource#popConnection。被唤醒后只有一个线程会获得对象锁。
        } else { // 空闲连接集合已满,或者连接标识不一致,则关闭连接
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          conn.getRealConnection().close(); // 关闭连接,不再复用
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          conn.invalidate();
        }
      } else { // 连接无效,累加计数
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
      }
    }
  }
3.2 PooledDataSource 的使用流程

PooledDataSource 的使用流程如下:

  1. 解析 mybatis-config.xml 配置文件时,创建 PooledDataSource 连接池对象。
  2. 开启 SqlSession 数据库会话时,创建 JdbcTransaction 事务对象,利用 JdbcTransaction 来维护对数据库连接池的存取操作。
  3. 在一次会话中,JdbcTransaction 只会向连接池获取一个连接。在该会话范围之内,读写数据库的操作都通过该连接来完成。
  4. 关闭 SqlSession 数据库会话时,向数据库连接池归还连接。

数据源配置
使用 SqlSessionFactoryBuilder 解析 mybatis-config.xml 配置文件的时候,会解析其中的 environments 标签。

调用链如下:

org.apache.ibatis.session.SqlSessionFactoryBuilder#build(Reader, String, java.util.Properties)
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration(org.apache.ibatis.parsing.XNode)

MyBatis底层是怎么运行的呢?

public static void main(String[] args) throws IOException {

        String configName = "mybatis_config.xml";

        Reader reader = Resources.getResourceAsReader(configName);

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

}
启动,加载配置文件
  1. new SqlSessionFactoryBuilder().build(reader),SqlSessionFactoryBuilder创建出SqlSessionFactory,reader参数接收一个mybatis-config.xml的流文件。
  2. 创建 XMLConfigBuilder:config.xml解析器。
  3. 实例化 XMLConfigBuilder父类(BaseBuilder)的Configuration类。
  4. 解析config.xml数据,加载到Configuration对象中。
  5. new DefaultSqlSessionFactory(config) 创建一个SqlSessionFactory实例,默认是DefaultSqlSessionFactory。

如图所示:
在这里插入图片描述
new SqlSessionFactoryBuilder().build(reader)
创建SqlSessionFactory对象实例

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
            //主要的配置文件解析在pares()方法,下文会详细讲解
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
            ErrorContext.instance().reset();

            try {
                reader.close();
            } catch (IOException var13) {
            }

        }

        return var5;
    }

new XMLConfigBuilder(reader, environment, properties);
解析 mybatis-config.xml文件

public class XMLConfigBuilder extends BaseBuilder {



    /* 标记是否已经解析过配置文件 */

    private boolean parsed;

    /* 解析器 */

    private final XPathParser parser;

    /**

     * 数据源,SqlSessionFactoryBuilder.build(Reader reader, String environment, Properties properties)

     * 不指定为空

     */

    private String environment;

	public XMLConfigBuilder(Reader reader) {
        this((Reader)reader, (String)null, (Properties)null);
    }

    public XMLConfigBuilder(Reader reader, String environment) {
        this((Reader)reader, environment, (Properties)null);
    }

    public XMLConfigBuilder(Reader reader, String environment, Properties props) {
        this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
    }

    public XMLConfigBuilder(XPathParser parser, String environment, Properties props) {

        /* 初始化 Configuration */

        super(new Configuration());

        ErrorContext.instance().resource("SQL Mapper Configuration");

        /* 设置格外的属性 */

        this.configuration.setVariables(props);

        /* 标记初始化为 false */

        this.parsed = false;

        this.environment = environment;

        this.parser = parser;

    }

}

new Configuration()

创建 Configuration 实例
public class Configuration {

    /**

     * 类型别名注册

     * 比如 <dataSource type="POOLED">

     * 其中的 type="POOLED" 会使用 PooledDataSourceFactory类创建数据源连接池

     */

    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();



    public Configuration() {

        /**

         * JDBC 对应使用的 事务工厂类

         * <transactionManager type="JDBC"></transactionManager>

         */

        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);

        /**

         * MANAGED 对应使用的 事务工厂类

         */

        typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);



        /**

         * JNDI 对应使用的 数据源

         */

        typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);

        /**

         * POOLED 对应使用的 数据源

         * <dataSource type="POOLED">

         */

        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);

        /**

         * UNPOOLED 对应使用的 数据源

         */

        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);



        /**

         * 缓存策略

         */

        typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);

        typeAliasRegistry.registerAlias("FIFO", FifoCache.class);

        typeAliasRegistry.registerAlias("LRU", LruCache.class);

        typeAliasRegistry.registerAlias("SOFT", SoftCache.class);

        typeAliasRegistry.registerAlias("WEAK", WeakCache.class);



        typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);



        typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);

        typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);



        /**

         * 日志

         */

        typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);

        typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);

        typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);

        typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);

        typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);

        typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);

        typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);



        typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);

        typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);



        languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);

        languageRegistry.register(RawLanguageDriver.class);

    }

}

在构建Configuration的时候,会去解析我们的配置文件。

解析 mybatis-config.xml

jdbc.driver=com.mysql.jdbc.Drive·
jdbc.url=jdbc:mysql://localhost:3306/sams?serverTimezone=UTC
jdbc.username=root
jdbc.password=root

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="db.properties"></properties>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"></property>
                <property name="url" value="${jdbc.url}"></property>
                <property name="username" value="${jdbc.username}"></property>
                <property name="password" value="${jdbc.password}"></property>
            </dataSource>
        </environment>
    </environments>
    <!--加载映射文件-->
    <mappers>
        <mapper resource="mapper/adminMap.xml"></mapper>
        <mapper resource="mapper/mapper.xml"></mapper>
    </mappers>
</configuration>

MyBatis的配置文件解析过程是在 org.apache.ibatis.builder.xml.XMLConfigBuilder 类中完成的。下面是解析配置文件的主要过程:

XML文件解析为一个Document 对象

public XMLConfigBuilder(Reader reader, String environment, Properties props)->
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)->
private Document createDocument(InputSource inputSource)
在 MyBatis 中,XML 文件的解析是通过使用 JAXP(Java API for XML Processing)实现的。具体来说,使用了 DOM(Document Object Model)API 和 SAX(Simple API for XML)API 来解析 XML 文件并生成 Document 对象。

DOM API 会将整个 XML 文件加载到内存中,形成一颗基于内存的树结构,以便于程序对树结构中的节点进行操作和遍历。SAX API 则是一种事件驱动的 API,它通过解析 XML 文件中的内容并触发事件来对 XML 文件进行处理。

  1. 通过 DocumentBuilderFactory 类创建一个 DocumentBuilder 对象
  2. 使用 DocumentBuilder 对象的 parse 方法将 XML 文件解析成 Document 对象。
    DocumentBuilder.parse(InputSource is)
public Document parse(File f) throws SAXException, IOException {
        if (f == null) {
            throw new IllegalArgumentException("File cannot be null");
        }

        //convert file to appropriate URI, f.toURI().toASCIIString()
        //converts the URI to string as per rule specified in
        //RFC 2396,
        InputSource in = new InputSource(f.toURI().toASCIIString());
        return parse(in);
}

public abstract Document parse(InputSource is)
        throws SAXException, IOException;

javax.xml.parsers.DocumentBuilder#parse 方法的实现过程:

  1. 首先检查输入的文件参数是否为 null,如果是则抛出 IllegalArgumentException 异常。
  2. 将文件对象转换成合适的 URI 字符串,这里使用了 File 类的 toURI 和 toASCIIString 方法。
  3. 用上一步得到的 URI 字符串构造一个 InputSource 对象,作为 parse 方法的参数。
  4. 调用 parse(InputSource is) 方法,解析传入的 XML 文件。
  5. 返回解析后得到的 Document 对象。

需要注意的是,这里使用了一个 InputSource 对象来将文件作为输入源传递给解析器进行解析。实际上,InputSource 对象可以包装任何形式的输入源,例如一个字符流、字节流、URL、文件路径等等,只要这些输入源可以被转换为 InputStream、Reader 或 SystemId(URI 字符串)等形式即可。因此,我们可以使用不同的方式来获取 InputSource 对象,并传递给 parse 方法进行解析。
com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl#parse

public Document parse(InputSource is) throws SAXException, IOException {
//首先对is参数进行了空引用检查,如果is为空,就会抛出一个IllegalArgumentException异常,异常信息会通过DOMMessageFormatter的formatMessage()方法来生成。
        if (is == null) {
            throw new IllegalArgumentException(
                DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
                "jaxp-null-input-source", null));
        }
        //对fSchemaValidator字段进行检查,如果不为空,则进行一些操作。
        /**
        fSchemaValidator是一个用于进行XML Schema验证的对象。
        
        */
        if (fSchemaValidator != null) {
            if (fSchemaValidationManager != null) {
                fSchemaValidationManager.reset();
                fUnparsedEntityHandler.reset();
            }
            resetSchemaValidator();
        }
        domParser.parse(is);
        Document doc = domParser.getDocument();
        domParser.dropDocumentReferences();
        return doc;
    }

fSchemaValidator是一个用于进行XML Schema验证的对象。它是DocumentBuilderImpl类的一个私有字段,用于存储XMLSchemaValidator实例。在解析XML文件时,如果设置了schema,则需要进行XML Schema验证,fSchemaValidator字段会被初始化。如果fSchemaValidator不为null,就需要进行相关操作。

在parse方法中,如果fSchemaValidator不为null,则重置相关属性(fSchemaValidationManager和fUnparsedEntityHandler)和XML Schema验证器fSchemaValidator。这是为了确保在对下一个XML文件进行解析之前,状态被清除。

接着使用了domParser来解析输入的XML源,然后获取解析后的Document对象,最后清除对Document对象的引用,返回该对象。

需要注意的是,这里使用的domParser对象是com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl内部定义的DOMParser类型的字段,它是一个SAX2解析器。

public void parse(InputSource inputSource)
        throws SAXException, IOException {

        // parse document
        try {
        	// 将 InputSource 转换为 XMLInputSource,设置相关属性
            XMLInputSource xmlInputSource =
                new XMLInputSource(inputSource.getPublicId(),
                                   inputSource.getSystemId(),
                                   null, false);
            xmlInputSource.setByteStream(inputSource.getByteStream());
            xmlInputSource.setCharacterStream(inputSource.getCharacterStream());
            xmlInputSource.setEncoding(inputSource.getEncoding());
            // 调用父类 AbstractXMLDocumentParser 的 parse 方法进行解析
        	// 其中 AbstractXMLDocumentParser 类是 Xerces 内部的一个类,实现了 XML 文档的解析功能。
        	// 它使用 Xerces 内部的 SAXParser API,将 XML 文档解析成相应的文档树结构。
            parse(xmlInputSource);
        }
        
		// 如果解析过程中出现了 XMLParseException 异常,则需要进行异常处理
        // wrap XNI exceptions as SAX exceptions
        catch (XMLParseException e) {
            Exception ex = e.getException();
            // 如果异常信息中没有详细的错误原因,则需要根据 Locator 创建一个 SAXParseException,抛出异常
            if (ex == null || ex instanceof CharConversionException) {
                // must be a parser exception; mine it for locator info and throw
                // a SAXParseException
                LocatorImpl locatorImpl = new LocatorImpl();
                locatorImpl.setPublicId(e.getPublicId());
                locatorImpl.setSystemId(e.getExpandedSystemId());
                locatorImpl.setLineNumber(e.getLineNumber());
                locatorImpl.setColumnNumber(e.getColumnNumber());
                throw (ex == null) ?
                        new SAXParseException(e.getMessage(), locatorImpl) :
                        new SAXParseException(e.getMessage(), locatorImpl, ex);
            }
             // 如果异常是 SAXException,则直接抛出
            if (ex instanceof SAXException) {
                // why did we create an XMLParseException?
                throw (SAXException)ex;
            }
            // 如果异常是 IOException,则直接抛出
            if (ex instanceof IOException) {
                throw (IOException)ex;
            }
            // 如果异常是其他类型的异常,则使用 SAXException 进行包装抛出
            throw new SAXException(ex);
        }
        // 如果解析过程中出现了 XNIException 异常,则需要进行异常处理
        catch (XNIException e) {
            Exception ex = e.getException();
             // 如果异常信息中没有详细的错误原因,则需要根据 Locator 创建一个 SAXParseException,抛出异常
            if (ex == null) {
                throw new SAXException(e.getMessage());
            }
            // 如果异常是 SAXException,则直接抛出
            if (ex instanceof SAXException) {
                throw (SAXException)ex;
            }
            if (ex instanceof IOException) {
                throw (IOException)ex;
            }
            throw new SAXException(ex);
        }

    } // parse(InputSource)

parse() 方法主要的逻辑是将 InputSource 对象转换为 XMLInputSource 对象,在解析过程中,如果出现异常,则会进行相关的异常处理,将异常转换为 SAXException 或 IOException,并抛出。
由于篇幅原因大致说一下,SAX2 解析器是事件驱动的,它通过实现 ContentHandler 接口来解析 XML 文档。当 SAX2 解析器遇到 XML 文档的开始、元素、字符等事件时,它会回调 ContentHandler 接口的方法,比如 startDocument()、startElement()、characters() 等方法,这些方法需要我们根据业务需求自己去实现,从而完成对 XML 文档的处理。
在DocumentBuilderImpl类当中
((XMLDocumentSource)validatorComponent).setDocumentHandler(domParser);给他设置了这个处理器。

最后,如果解析没有出现异常,解析结果将被存储在fDocument对象中,而且所有的相关引用也将被重置,以便进行下一次解析。

实体引用(DTD)解析
  1. 首先使用 XMLMapperEntityResolver 类来解析 mybatis-config.xml 文件中的实体。
    在解析 mybatis-config.xml 文件时,如果该文件包含了实体引用,解析器会尝试通过调用 XMLMapperEntityResolver 的 resolveEntity 方法来解析这些实体引用。在方法中,首先会根据 DTD 的 PUBLIC ID 和 SYSTEM ID 尝试从本地缓存中查找 DTD。如果缓存中找到了 DTD,则直接返回 DTD 的 InputStream。如果在本地缓存中没有找到 DTD,会根据 PUBLIC ID 和 SYSTEM ID 去尝试从类路径中查找 DTD。如果在类路径中找到了 DTD,则返回 DTD 的 InputStream,并将其缓存到本地缓存中。如果在本地缓存和类路径中都没有找到 DTD,则返回 null,这时解析器会使用默认的解析方式来解析实体。需要注意的是,在 MyBatis 中,虽然配置文件中包含了 DTD 引用,但是这些 DTD 文件并不存在,也没有实际的作用,只是为了让解析器不报错而已。因此,在 XMLMapperEntityResolver 中,无论找到了还是没找到 DTD,都只返回空的 InputStream。总之,XMLMapperEntityResolver 类主要作用是解析 mybatis-config.xml 文件中的实体引用,并将其映射为对应的 InputStream,方便解析器对实体进行解析。
public class XMLMapperEntityResolver implements EntityResolver {

  // 每个 DTD 文件所对应的本地缓存路径
  private static final String IBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/ibatis-3-config.dtd";
  private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
  private static final String MYBATIS_CONFIG_XSD = "org/apache/ibatis/builder/xml/mybatis-3-config.xsd";
  private static final String MYBATIS_MAPPER_XSD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.xsd";

  // 用于将 DTD 文件转化为本地缓存路径的映射
  private static final Map<String, String> doctypeMap = new HashMap<>();

  // 将 DTD 文件与其本地缓存路径建立映射关系
  static {
    doctypeMap.put("-//ibatis.apache.org//DTD Config 3.0//EN", IBATIS_CONFIG_DTD);
    doctypeMap.put("-//mybatis.org//DTD Config 3.0//EN", MYBATIS_CONFIG_DTD);
    doctypeMap.put("-//mybatis.org//DTD Mapper 3.0//EN", MYBATIS_MAPPER_DTD);
    doctypeMap.put("http://mybatis.org/dtd/mybatis-3-config.dtd", MYBATIS_CONFIG_DTD);
    doctypeMap.put("http://mybatis.org/dtd/mybatis-3-mapper.dtd", MYBATIS_MAPPER_DTD);
  }

  @Override
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
    if (publicId != null) {
      // 将 DTD 的 publicId 和系统 ID 映射到本地缓存路径中
      publicId = publicId.trim();
    }
    if (systemId != null) {
      systemId = systemId.trim();
    }
    String resource = doctypeMap.get(publicId);
    if (resource == null) {
      resource = doctypeMap.get(systemId);
    }
    if (resource != null) {
      // 将 DTD 资源文件转换为本地 InputStream 输入流
      InputStream in = getResourceAsStream(resource);
      return new InputSource(in);
    }
    return null;
  }

  // 根据资源名称返回对应的输入流
  private InputStream getResourceAsStream(String resource) {
    ClassLoader[] classLoader = getClassLoaders();
    for (ClassLoader cl : classLoader) {
      InputStream inputStream = cl.getResourceAsStream(resource);
      if (inputStream != null) {
        return inputStream;
      }
    }
    return null;
  }

  // 获取所有可能的 ClassLoader
  private ClassLoader[] getClassLoaders() {
    return new ClassLoader[] {
      ClassLoader.getSystemClassLoader(),
      Thread.currentThread().getContextClassLoader()
    };
  }
}

为什么无论找到了还是没找到 DTD,都只返回空的 InputStream?那既然没用,为什么必须要有DTD?没有DTD,MyBatis是如何校验XML文件的合理性?

  • 返回空的 InputStream 是因为 MyBatis 的解析器并不需要真正的 DTD 来解析配置文件,它只是需要一个占位符,来代替 DTD 的位置,从而继续向下解析。相比之下,如果解析器需要真正的 DTD 来解析配置文件,那么就需要网络请求等耗时操作,会拖慢解析速度。因此,MyBatis 的做法是通过返回空的 InputStream,跳过对 DTD 的解析,从而加快解析速度。同时,MyBatis 也提供了自定义的实体解析器,可以根据实际需要去实现。
  • 当 MyBatis 解析 mybatis-config.xml 文件时,它会使用 JAXP(Java API for XML Processing)规范中的 SAX(Simple API for XML)解析器来解析该文件,这个解析器会解析配置文件中的实体引用。说白了是因为规范的约束,必须有DTD,也必须解析
  • 在 MyBatis 中,还可以通过其他方式来验证配置文件的正确性,例如使用 XML Schema 或 Relax NG。这些方式相对于 DTD 来说,更加灵活和强大。
解析<configuration>以及<properties>节点
  1. 然后使用 XPathParser 类来解析 mybatis-config.xml 文件,从 org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration(org.w3c.dom.Node) 方法开始的,首先会解析 <configuration> 标签,然后解析这个节点及其子节点。
  2. 在解析 configuration 节点时,会先解析 properties 子节点,用来替换配置文件中的占位符,例如 ${jdbc.driver} 这样的占位符就可以通过 properties 节点中定义的键值对进行替换。解析 properties 节点的过程在 org.apache.ibatis.builder.xml.XMLConfigBuilder#propertiesElement(org.w3c.dom.Node) 方法中完成。并将解析后的属性设置到 Configuration 对象中。
private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 解析 <properties> 的子节点,也就是 <property> 节点
    Properties defaults = context.getChildrenAsProperties();
    // 获取 <properties> 节点的 resource 属性值,即 properties 配置文件的路径
    String resource = context.getStringAttribute("resource");
    // 获取 <properties> 节点的 url 属性值,即 properties 配置文件的 URL
    String url = context.getStringAttribute("url");
    // resource 和 url 不能同时存在
    if (resource != null && url != null) {
      throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
    }
    // 如果指定了 properties 配置文件的路径,则读取对应的配置文件并将其解析为 Properties 对象
    if (resource != null) {
      // 获取 properties 配置文件对应的输入流
      InputStream inputStream = Resources.getResourceAsStream(resource);
      // 读取输入流并解析为 Properties 对象
      defaults.putAll(PropertiesLoaderUtils.loadProperties(new EncodedResource(inputStream, "UTF-8")));
    } else if (url != null) {
      // 如果指定了 properties 配置文件的 URL,则读取对应的配置文件并将其解析为 Properties 对象
      InputStream inputStream = new URL(url).openStream();
      defaults.putAll(PropertiesLoaderUtils.loadProperties(new EncodedResource(inputStream, "UTF-8")));
    }
    // 将 configuration 中定义的属性覆盖到 defaults 中
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    // 创建 PropertyParser 对象,解析 ${} 占位符,并将解析结果保存到 configuration 对象中
    parser = new PropertyParser(defaults);
  }
}


解析<typeAliases >节点
  1. 解析 typeAliases 节点,将解析出来的类别名注册到 TypeAliasRegistry 中。
// 解析 typeAlias 节点
private void typeAliasesElement(XNode parent) {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();

            while(var2.hasNext()) {
                XNode child = (XNode)var2.next();
                String alias;
                if ("package".equals(child.getName())) {
                    alias = child.getStringAttribute("name");
                    this.configuration.getTypeAliasRegistry().registerAliases(alias);
                } else {
                    alias = child.getStringAttribute("alias");
                    String type = child.getStringAttribute("type");

                    try {
                        Class<?> clazz = Resources.classForName(type);
                        if (alias == null) {
                            this.typeAliasRegistry.registerAlias(clazz);
                        } else {
                            this.typeAliasRegistry.registerAlias(alias, clazz);
                        }
                    } catch (ClassNotFoundException var7) {
                        throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + var7, var7);
                    }
                }
            }
        }

    }

// TypeAliasRegistry 类中的存储别名和Java类型的Map
private final Map<String, Class<?>> typeAliases = new HashMap<String, Class<?>>();

// TypeAliasRegistry 类中解析别名的方法,返回别名对应的Java类型的Class对象
public Class<?> resolveAlias(String alias) {
  try {
    if (alias == null) {
      return null;
    }
    // 别名是否为Java原生类型或者Java基本类型的包装类型
    Class<?> type = typeAliases.get(alias);
    if (type == null) {
      // 尝试用类名加载,例如 java.lang.Integer
      type = Resources.classForName(alias);
    }
    return type;
  } catch (ClassNotFoundException e) {
    throw new TypeException("Could not resolve type alias '" + alias + "'.  Cause: " + e, e);
  }
}
解析环境信息<environments>节点
  1. 根据 <environments> 标签来解析环境信息,首先会解析 <environment> 标签,并根据 <transactionManager><dataSource> 标签来创建事务管理器和数据源。
    在Configuration类中定义了environments属性,用于存储所有解析出来的Environment对象,如下所示:
protected final Map<String, Environment> environments = new HashMap<>();

在Configuration类的environmentsElement方法中解析environments节点,如下所示:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 如果当前 Configuration 对象没有设置默认环境 id,则从环境节点上获取默认环境 id 并设置到 Configuration 对象上
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            // 遍历 environments 下的子节点,即 environment 节点,解析 environment 节点
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
                // 解析 TransactionFactory 节点
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 解析 DataSourceFactory 节点
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                // 创建 DataSource 对象
                DataSource dataSource = dsFactory.getDataSource();
                // 构建 Environment 对象
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                // 将 Environment 对象保存到 environments 集合中
                environments.put(id, environmentBuilder.build());
            }
        }
    }
}

该方法首先判断传入的节点是否为null,如果不为null,则通过调用context.getStringAttribute(“default”)获取default属性的值,并将其赋值给environment属性。

接着,该方法通过遍历environments节点下的所有子节点,即environment节点,依次解析每个environment节点。

对于每个environment节点,该方法首先通过调用child.getStringAttribute(“id”)获取其id属性的值,并判断当前环境是否是指定的环境,即是否需要解析该environment节点。如果需要解析,则依次解析transactionManager节点和dataSource节点,通过调用它们的evalNode方法获取对应的TransactionFactory和DataSourceFactory对象,最后调用它们的getDataSource方法获取DataSource对象。

最后,该方法使用获取到的id、TransactionFactory、DataSource等信息创建一个Environment.Builder对象,并将其构建为一个Environment对象,再将该对象存储到environments属性中。

可以看到,在解析environments节点时,MyBatis会依次解析每个environment节点,并为每个environment节点创建一个Environment对象,并将其存储到environments属性中。当解析完environments节点后,Configuration对象中的environments属性将存储所有的Environment对象,供后续使用。

可以看到几乎解析每一个节点的时候都会用到evalNode 这个方法;
1.根据 XPath 表达式解析出相应的 XML 节点
Node node = (Node) this.evaluate(expression, this.document, XPathConstants.NODE);
这里使用了 Java 内置的 XPath 解析器,并传入了要解析的表达式、要解析的文档(即 document)以及解析出的结果类型(这里是 NODE)。
2. 这里使用了 node.getNodeType() 方法获取节点类型,如果是 CDATA 节点或文本节点,则返回空。否则,使用解析器 parser 和节点 node 以及变量 variables 创建一个新的 XNode 实例并返回。
if (node == null || node.getNodeType() == Node.CDATA_SECTION_NODE || node.getNodeType() == Node.TEXT_NODE) { return null; } else { return new XNode(this.parser, node, this.variables); }
总的来说,evalNode 方法是 MyBatis 中用于解析 XML 节点的核心方法,它利用 Java 内置的 XPath 解析器,根据给定的表达式和文档解析出相应的节点,并将其封装成一个 XNode 对象返回。

解析<mapper>节点
  1. 最后就是解析 <mappers>标签时,会根据 <mapper> 标签中的配置信息来创建 XMLMapperBuilder 类的实例,并调用 XMLMapperBuilder 中的 parse() 方法来解析 Mapper 文件。在解析 Mapper 文件时,会使用 XPathParser 类来解析 Mapper 文件,并根据解析结果来创建 XMLStatementBuilder、XMLMapperBuilderAssistant 等类的实例,并调用相应的方法来构建 SQL 语句,并将解析出来的配置信息保存到 Configuration 类中。由于不属于解释配置文件mybatis-config.xml的范围不再过多赘述。

ResultSet

当创建一个ResultSet时,你可以设置三个属性:

  • 类型
  • 并发
  • 可保存性

在创建Statement或PreparedStatement时已经设置了这些值,如下所示:

Statement statement = connection.createStatement(
    ResultSet.TYPE_FORWARD_ONLY,
    ResultSet.CONCUR_READ_ONLY,
    ResultSet.CLOSE_CURSORS_OVER_COMMIT
   );

PreparedStatement statement = connection.prepareStatement(sql,
    ResultSet.TYPE_FORWARD_ONLY,
    ResultSet.CONCUR_READ_ONLY,
    ResultSet.CLOSE_CURSORS_OVER_COMMIT
   );

基本的ResultSet

之所以说是最基本的ResultSet是因为,这个ResultSet他起到的作用就是完成了查询结果的存储功能,而且只能读去一次,不能够来回的滚动读取.这种结果集的创建方式如下:

public Statement createStatement() throws SQLException {
        try {
            return this.createStatement(1003, 1007);
        } catch (CJException var2) {
            throw SQLExceptionsMapping.translateException(var2, this.getExceptionInterceptor());
        }
    }

由于这种结果集不支持,滚动的读去功能所以,如果获得这样一个结果集,只能使用它里面的next()方法,逐个的读去数据。

方法内部的代码有两个部分。首先,它调用了一个重载的createStatement方法,并传入了两个整数值作为参数,分别是‘1003’和‘1007’。这两个整数参数指定了Statement对象的类型和结果集的类型。
‘1003’表示ResultSet.TYPE_FORWARD_ONLY,这个类型的Statement对象只能在结果集中向前移动,不能向后也不能更新结果集。‘1007’表示ResultSet.CONCUR_READ_ONLY,这个类型的结果集只能读取,不能更新。

可滚动的ResultSet类型

这个类型支持前后滚动取得纪录next(),previous(),回到第一行first(),同时还支持要去的ResultSet中的第几行absolute(int n),以及移动到相对当前行的第几行relative(int n),要实现这样的ResultSet在创建Statement时用如下的方法:

public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        try {
            this.checkClosed();
            StatementImpl stmt = new StatementImpl(this.getMultiHostSafeProxy(), this.database);
            stmt.setResultSetType(resultSetType);
            stmt.setResultSetConcurrency(resultSetConcurrency);
            return stmt;
        } catch (CJException var5) {
            throw SQLExceptionsMapping.translateException(var5, this.getExceptionInterceptor());
        }
    }

其中两个参数的意义:

  • resultSetType是设置ResultSet对象的类型可滚动,或者是不可滚动
    取值如下:
    • ResultSet.TYPE_FORWARD_ONLY(值为 1003):表示结果集中的游标只能向前移动,不能向后移动。这是默认值。
    • ResultSet.TYPE_SCROLL_INSENSITIVE(值为 1004):表示结果集中的游标可以向前和向后移动,但是不受其他程序对数据库所做更改的影响。
    • ResultSet.TYPE_SCROLL_SENSITIVE(值为 1005):表示结果集中的游标可以向前和向后移动,并且会受到其他程序对数据库所做更改的影响。该选项需要数据库驱动程序支持。

要注意的是,ResultSet.TYPE_SCROLL_SENSITIVE是可选的,不是所有的数据库驱动程序都支持它。如果你想要在应用程序中使用它,应该先检查数据库驱动程序是否支持。另外,不同的数据库还可能支持其他的resultSetType取值,你可以查看相应数据库的文档来了解更多信息。

  • resultSetConcurency是设置ResultSet对象能够修改的,也就是并发级别
    取值如下:

    • ResultSet.CONCUR_READ_ONLY(值为 1007):表示结果集只能被读取,不能被修改。这是默认值。
    • ResultSet.CONCUR_UPDATABLE(值为 1008):表示结果集可以被修改。

要注意的是,如果结果集的并发级别为ResultSet.CONCUR_READ_ONLY,则不能对结果集进行修改操作,否则将会抛出异常。另外,某些数据库驱动程序可能不支持对结果集进行修改操作,你可以查看相应数据库驱动程序的文档来了解更多信息。

可更新的ResultSet

这样的ResultSet对象可以完成对数据库中表的修改,但是我知道ResultSet只是相当于数据库中表的视图,所以并不是所有的ResultSet只要设置了可更新就能够完成更新的,能够完成更新的ResultSet的SQL语句必须要具备如下的属性:

  • 数据库表必须包含一个主键列
  • 查询语句必须指定所有需要更新的列,否则将无法更新结果集中的数据
  • 数据库驱动程序必须支持可更新的ResultSet对象,否则将无法创建可更新的ResultSet对象。
  • 不含有join或者group by子句
  • 只引用了单个表

可更新的ResultSet对象的使用场景通常是需要对查询结果进行修改并且实时同步到数据库中的情况,例如在网页上进行数据的增、删、改操作,可以先查询出需要修改的数据,然后将查询结果返回到页面,用户进行修改后,再将修改后的结果集更新回数据库中,以实现数据的同步更新。

更新的方法是,把ResultSet的游标移动到你要更新的行,然后调用updateXXX(),这个方法XXX的含义和getXXX()是相同的.updateXXX()方法,有两个参数,第一个是要更新的列,可以是列名或者序号.第二个是要更新的数据,这个数据类型要和XXX相同.每完成对一行的update要调用updateRow()完成对数据库的写入,而且是在ResultSet的游标没有离开该修改行之前,否则修改将不会被提交。

moveToInsertRow()是把ResultSet移动到插入行,这个插入行是表中特殊的一行,不需要指定具体那一行,只要调用这个方法系统会自动移动到那一行的。

moveToCurrentRow()这是把ResultSet移动到记忆中的某个行,通常当前行.如果没有使用insert操作,这个方法没有什么效果,如果使用了insert操作,这个方法用于返回到insert操作之前的那一行,离开插入行,当然也可以通过next(),previous()等方法离开插入行。

要完成对数据库的插入,首先调用moveToInsertRow()移动到插入行,然后调用updateXXX的方法完成对,各列数据的更新,完成更新后和更新操作一样,要写到数据库,不过这里使用的是insertRow(),也要保证在该方法执行之前ResultSet没有离开插入列,否则插入不被执行,并且对插入行的更新将丢失。

可保持的ResultSet

正常情况下如果使用Statement执行完一个查询,又去执行另一个查询时这时候第一个查询的结果集就会被关闭,也就是说,所有的Statement的查询对应的结果集是一个,如果调用Connection的commit()方法也会关闭结果集。可保持性就是指当ResultSet的结果被提交时,是被关闭还是不被关闭。JDBC2.0和1.0提供的都是提交后ResultSet就会被关闭。不过在JDBC3.0中,我们可以设置ResultSet是否关闭。要完成这样的ResultSet的对象的创建,要使用的Statement的创建要具有三个参数。
当使用ResultSet的时候,当查询出来的数据集记录很多,有一千万条的时候,那rs所指的对象是否会占用很多内存,如果记录过多,那程序会不会把系统的内存用光呢 ?
  不会的,ResultSet表面看起来是一个记录集,其实这个对象中只是记录了结果集的相关信息,具体的记录并没有存放在对象中,具体的记录内容直到你通过next方法提取的时候,再通过相关的getXXXXX方法提取字段内容的时候才能从数据库中得到,这些并不会占用内存,具体消耗内存是由于你将记录集中的数据提取出来加入到你自己的集合中的时候才会发生,如果你没有使用集合记录所有的记录就不会发生消耗内存厉害的情况。

遍历ResultSet

遍历ResultSet跟打印读取流内容一样,它有一个next()方法,它会判断是否该对象里还有数据。

while(result.next()) {
    // ... get column values from this record
}

获取列数据两种方式:

  1. 通过列名
while(result.next()) {

    result.getString    ("name");
    result.getInt       ("age");
    // 等等
}
  1. 通过index值
while(result.next()) {

    result.getString    (1);
    result.getInt       (2);
    // 等等
}

总结

这篇博客深入分析了JDBC的源码,从介绍JDBC的概念开始,到对JDBC的核心类进行详细分析,最后对数据库连接池和MyBatis连接池进行了源码分析。此外,博客还详细解析了MyBatis如何加载解析配置文件mybatis-config.xml,为读者提供了非常实用的技术参考。

总的来说,这篇博客内容非常详细,涉及到的知识点也非常丰富。对于Java开发人员来说,这篇博客是一份非常有价值的学习资料。通过对JDBC的源码分析,读者可以深入了解JDBC的实现原理和内部机制,同时也可以更好地理解JDBC的使用方式。此外,对于数据库连接池和MyBatis连接池的分析,读者也可以学习到连接池的实现原理和使用方法,为数据库操作提供更好的性能和效率。

总之,这篇博客对于Java开发人员学习JDBC和连接池相关知识具有很大的参考价值,是一份非常优秀的技术分享。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值