mybatis源码解读(三)——数据源的配置
在mybatis-configuration.xml 文件中,我们进行了如下的配置:
<!-- 可以配置多个运行环境,但是每个 SqlSessionFactory 实例只能选择一个运行环境常用: 一、development:开发模式 二、work:工作模式 -->
<environments default="development">
<!--id属性必须和上面的default一样 -->
<environment id="development">
<!--使用JDBC的事务管理机制-->
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
其中 <transactionManager type="JDBC" /> 是对事务的配置,下篇博客我们会详细介绍。
本篇博客我们介绍 <dataSource type="POOLED"> 对于数据源的配置。
1、解析 environments 标签
在 XMLConfigBuilder.java 的 parseConfiguration(XNode root) 中:
进入 environmentsElement(root.evalNode("environments")) 方法:
1 private void environmentsElement(XNode context) throws Exception { 2 //如果<environments>标签不为null 3 if (context != null) { 4 //如果 environment 值为 null 5 if (environment == null) { 6 //获取<environments default="属性值">中的default属性值 7 environment = context.getStringAttribute("default"); 8 } 9 //遍历<environments />标签中的子标签<environment /> 10 for (XNode child : context.getChildren()) { 11 //获取<environment id="属性值">中的id属性值 12 String id = child.getStringAttribute("id"); 13 //遍历所有<environment>的时候一次判断相应的id是否是default设置的值 14 if (isSpecifiedEnvironment(id)) { 15 //获取配置的事务管理器 16 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); 17 //获取配置的数据源信息 18 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); 19 DataSource dataSource = dsFactory.getDataSource(); 20 Environment.Builder environmentBuilder = new Environment.Builder(id) 21 .transactionFactory(txFactory) 22 .dataSource(dataSource); 23 configuration.setEnvironment(environmentBuilder.build()); 24 } 25 } 26 } 27 } 28 29 private boolean isSpecifiedEnvironment(String id) { 30 if (environment == null) { 31 throw new BuilderException("No environment specified."); 32 } else if (id == null) { 33 throw new BuilderException("Environment requires an id attribute."); 34 } else if (environment.equals(id)) { 35 return true; 36 } 37 return false; 38 }
①、第 3 行代码:if (context != null) 也就是说我们可以不在 mybatis-configuration.xml 文件中配置<environments />标签,这是为了和spring整合时,在spring容器中进行配置。
②、第 5 行——第 8 行代码:获取<environments default="属性值">中的default属性值,注意第 5 行 首先判断 environment == null 。因为我们可以配置多个环境,也就是连接多个数据库。
不过需要记住:尽管可以配置多个环境,每个 SqlSessionFactory 实例只能选择其一,也就是说每个数据库对应一个 SqlSessionFactory 实例。
可以用如下方法进行区别:
1 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment); 2 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
③、第 10 行代码:遍历<environments />标签中的子标签<environment />,可以配置多个<environment />标签。
④、第 14 行代码:遍历所有<environment />的时候判断相应的id是否是default设置的值,选择相等的 <environment />标签进行数据源的配置。
⑤、第 16 行代码:进行事务的配置(下篇博客进行详解)。
⑥、第 18 行代码:进行数据源的配置,下面我们详细讲解。
2、mybatis 的数据源类图
mybatis 对于数据源的所有类都在如下包中:
注意:DataSource 接口不是mybatis包下的,是JDK的 javax.sql 包下的。
3、mybatis 三种数据源类型
前面我们在 mybatis-configuration.xml 文件中配置了数据源的类型:
mybatis 支持三种数据源类型(也就是 type=”[UNPOOLED|POOLED|JNDI]”):
①、UNPOOLED:(不使用连接池)这个数据源的实现只是每次被请求时打开和关闭连接。虽然有点慢,但对于在数据库连接可用性方面没有太高要求的简单应用程序来说,是一个很好的选择。 不同的数据库在性能方面的表现也是不一样的,对于某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。
②、POOLED:(使用连接池)这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
③、JNDI : 这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。
ps:关于连接池的概念请看下面详细介绍。
这三种数据源的类型在 mybatis 在上面所讲的类图中正好对应。那么 mybatis 是如何产生数据源的呢?
4、mybatis 初始化数据源
看上面的类图,我们可以看到 DataSourceFactory 接口,这是一个工厂方法,mybatis 就是通过工厂模式来创建数据源 DataSource 对象。我们先看看该接口:
1 public interface DataSourceFactory { 2 3 void setProperties(Properties props); 4 5 DataSource getDataSource(); 6 7 }
通过调用其 getDataSource() 方法返回数据源DataSource。
而这个工厂方法也对应上面讲的三种数据源类型的工厂方法。它们分别都实现了 DataSourceFactory 接口(使用连接池的数据源类型 PooledDataSourceFactory 是继承 UnpooledDataSourceFactory,而UnpooledDataSourceFactory 实现了 DataSourceFactory 接口)。
1 public class JndiDataSourceFactory implements DataSourceFactory 2 public class UnpooledDataSourceFactory implements DataSourceFactory 3 public class PooledDataSourceFactory extends UnpooledDataSourceFactory
这里的三个数据源工厂也是通过工厂模式来产生对应的三种数据源。
为什么要这样设计,下面我们来详细介绍。
5、不使用连接池 UnpooledDataSource
在 mybatis-configuration.xml 文件中,type="UNPOOLED"
回到本篇博客第一小节:解析 environments 标签 的第 18 行代码:通过配置的 <datasource>标签,来实例化一个 DataSourceFactory 工厂。该工厂实际上是根据配置的 type 类型,产生对应的数据源类型工厂。
1 private DataSourceFactory dataSourceElement(XNode context) throws Exception { 2 if (context != null) { 3 String type = context.getStringAttribute("type"); 4 Properties props = context.getChildrenAsProperties(); 5 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); 6 factory.setProperties(props); 7 return factory; 8 } 9 throw new BuilderException("Environment declaration requires a DataSourceFactory."); 10 }
下面我们来看 UnPooledDataSource 的 getConnection() 方法实现:
1 public Connection getConnection() throws SQLException { 2 return doGetConnection(username, password); 3 } 4 5 public Connection getConnection(String username, String password) throws SQLException { 6 return doGetConnection(username, password); 7 } 8 private Connection doGetConnection(String username, String password) throws SQLException { 9 //将用户名、密码、驱动都封装到Properties文件中 10 Properties props = new Properties(); 11 if (driverProperties != null) { 12 props.putAll(driverProperties); 13 } 14 if (username != null) { 15 props.setProperty("user", username); 16 } 17 if (password != null) { 18 props.setProperty("password", password); 19 } 20 return doGetConnection(props); 21 } 22 23 /** 24 * 获取数据库连接 25 */ 26 private Connection doGetConnection(Properties properties) throws SQLException { 27 //1、初始化驱动 28 initializeDriver(); 29 //2、从DriverManager中获取连接,获取新的Connection对象 30 Connection connection = DriverManager.getConnection(url, properties); 31 //3、配置connection属性 32 configureConnection(connection); 33 return connection; 34 }
前面的代码比较容易看懂,最后面一个方法获取数据库连接有三步。
①、初始化驱动:判断driver驱动是否已经加载到内存中,如果还没有加载,则会动态地加载driver类,并实例化一个Driver对象,使用DriverManager.registerDriver()方法将其注册到内存中,以供后续使用。
1 private synchronized void initializeDriver() throws SQLException { 2 if (!registeredDrivers.containsKey(driver)) { 3 Class<?> driverType; 4 try { 5 if (driverClassLoader != null) { 6 driverType = Class.forName(driver, true, driverClassLoader); 7 } else { 8 driverType = Resources.classForName(driver); 9 } 10 // DriverManager requires the driver to be loaded via the system ClassLoader. 11 // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html 12 Driver driverInstance = (Driver)driverType.newInstance(); 13 DriverManager.registerDriver(new DriverProxy(driverInstance)); 14 registeredDrivers.put(driver, driverInstance); 15 } catch (Exception e) { 16 throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); 17 } 18 } 19 }
②、创建Connection对象: 使用DriverManager.getConnection()方法创建连接。
③、配置Connection对象: 设置是否自动提交autoCommit和隔离级别isolationLevel。
1 private void configureConnection(Connection conn) throws SQLException { 2 if (autoCommit != null && autoCommit != conn.getAutoCommit()) { 3 conn.setAutoCommit(autoCommit); 4 } 5 if (defaultTransactionIsolationLevel != null) { 6 conn.setTransactionIsolation(defaultTransactionIsolationLevel); 7 } 8 }
④、返回Connection对象。
也就是说,使用 UnpooledDataSource 类型的数据源,每次需要连接的时候都会调用 getConnection() 创建一个新的连接Connection返回。实际上创建一个Connection对象的过程,在底层就相当于和数据库建立的通信连接,在建立通信连接的过程会消耗一些资源。有时候我们可能只是一个简单的 SQL 查询,然后抛弃掉这个连接,这实际上是很耗资源的。
那么怎么办呢?答案就是使用数据库连接池。
6、数据库连接池
其实对于共享资源,有一个很著名的设计理念:资源池。该理念正是为了解决资源的频繁分配、释放所造成的问题。
具体思想:初始化一个池子,里面预先存放一定数量的资源。当需要使用该资源的时候,将该资源标记为忙状态;当该资源使用完毕后,资源池把相关的资源的忙标示清除掉,以示该资源可以再被下一个请求使用。
对应到上面数据库连接的问题,我们可以这样解决:先建立一个池子,里面存放一定数量的数据库连接。当需要数据库连接时,只需从“连接池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。这样就能避免频繁的进行数据库连接和断开耗资源操作。
7、使用连接池 PooledDataSource
先了解一下 PooledDataSource 的实现原理:
①、PooledDataSource将java.sql.Connection对象包裹成PooledConnection对象放到了PoolState类型的容器中维护。 MyBatis将连接池中的PooledConnection分为两种状态: 空闲状态(idle)和活动状态(active),这两种状态的PooledConnection对象分别被存储到PoolState容器内的idleConnections和activeConnections两个List集合中。
②、idleConnections:空闲(idle)状态PooledConnection对象被放置到此集合中,表示当前闲置的没有被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从此集合中取PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此集合中。
③、activeConnections:活动(active)状态的PooledConnection对象被放置到名为activeConnections的ArrayList中,表示当前正在被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从idleConnections集合中取PooledConnection对象,如果没有,则看此集合是否已满,如果未满,PooledDataSource会创建出一个PooledConnection,添加到此集合中,并返回。
下面我们看看PooledDataSource 的getConnection()方法获取Connection对象的实现:
1 public Connection getConnection() throws SQLException { 2 return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); 3 } 4 5 @Override 6 public Connection getConnection(String username, String password) throws SQLException { 7 return popConnection(username, password).getProxyConnection(); 8 }
再看看 popConnection() 方法的实现:
①、 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第②步。
②、查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第③步;
③、 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第④步。
④、 线程等待,循环2步
1 private PooledConnection popConnection(String username, String password) throws SQLException { 2 boolean countedWait = false; 3 PooledConnection conn = null; 4 long t = System.currentTimeMillis(); 5 int localBadConnectionCount = 0; 6 7 while (conn == null) { 8 synchronized (state) { 9 if (!state.idleConnections.isEmpty()) { 10 // Pool has available connection 11 conn = state.idleConnections.remove(0); 12 if (log.isDebugEnabled()) { 13 log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); 14 } 15 } else { 16 // Pool does not have available connection 17 if (state.activeConnections.size() < poolMaximumActiveConnections) { 18 // Can create new connection 19 conn = new PooledConnection(dataSource.getConnection(), this); 20 if (log.isDebugEnabled()) { 21 log.debug("Created connection " + conn.getRealHashCode() + "."); 22 } 23 } else { 24 // Cannot create new connection 25 PooledConnection oldestActiveConnection = state.activeConnections.get(0); 26 long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); 27 if (longestCheckoutTime > poolMaximumCheckoutTime) { 28 // Can claim overdue connection 29 state.claimedOverdueConnectionCount++; 30 state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; 31 state.accumulatedCheckoutTime += longestCheckoutTime; 32 state.activeConnections.remove(oldestActiveConnection); 33 if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { 34 try { 35 oldestActiveConnection.getRealConnection().rollback(); 36 } catch (SQLException e) { 37 log.debug("Bad connection. Could not roll back"); 38 } 39 } 40 conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); 41 conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); 42 conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); 43 oldestActiveConnection.invalidate(); 44 if (log.isDebugEnabled()) { 45 log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); 46 } 47 } else { 48 // Must wait 49 try { 50 if (!countedWait) { 51 state.hadToWaitCount++; 52 countedWait = true; 53 } 54 if (log.isDebugEnabled()) { 55 log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); 56 } 57 long wt = System.currentTimeMillis(); 58 state.wait(poolTimeToWait); 59 state.accumulatedWaitTime += System.currentTimeMillis() - wt; 60 } catch (InterruptedException e) { 61 break; 62 } 63 } 64 } 65 } 66 if (conn != null) { 67 if (conn.isValid()) { 68 if (!conn.getRealConnection().getAutoCommit()) { 69 conn.getRealConnection().rollback(); 70 } 71 conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); 72 conn.setCheckoutTimestamp(System.currentTimeMillis()); 73 conn.setLastUsedTimestamp(System.currentTimeMillis()); 74 state.activeConnections.add(conn); 75 state.requestCount++; 76 state.accumulatedRequestTime += System.currentTimeMillis() - t; 77 } else { 78 if (log.isDebugEnabled()) { 79 log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); 80 } 81 state.badConnectionCount++; 82 localBadConnectionCount++; 83 conn = null; 84 if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) { 85 if (log.isDebugEnabled()) { 86 log.debug("PooledDataSource: Could not get a good connection to the database."); 87 } 88 throw new SQLException("PooledDataSource: Could not get a good connection to the database."); 89 } 90 } 91 } 92 } 93 94 } 95 96 if (conn == null) { 97 if (log.isDebugEnabled()) { 98 log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); 99 } 100 throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); 101 } 102 103 return conn; 104 }
对于PooledDataSource的getConnection()方法内,先是调用类PooledDataSource的popConnection()方法返回了一个PooledConnection对象,然后调用了PooledConnection的getProxyConnection()来返回Connection对象。
问题:对于连接池,我们会遇到java.sql.Connection对象的回收问题。当我们的程序中使用完Connection对象时,如果不使用数据库连接池,我们一般会调用 connection.close()方法,关闭connection连接,释放资源。但是
调用过close()方法的Connection对象所持有的资源会被全部释放掉,Connection对象也就不能再使用。
那么,如果我们使用了连接池,我们在用完了Connection对象时,需要将它放在连接池中,该怎样做呢?
也就是说,我们在调用con.close()方法的时候,不调用close()方法,将其换成将Connection对象放到连接池容器中的代码!
很容易想到代理模式。为真正的Connection对象创建一个代理对象,代理对象所有的方法都是调用相应的真正Connection对象的方法实现。当代理对象执行close()方法时,要特殊处理,不调用真正Connection对象的close()方法,而是将Connection对象添加到连接池中。
MyBatis的PooledDataSource的PoolState内部维护的对象是PooledConnection类型的对象,而PooledConnection则是对真正的数据库连接java.sql.Connection实例对象的包裹器。PooledConnection对象内持有一个真正的数据库连接java.sql.Connection实例对象和一个java.sql.Connection的代理:
其部分定义如下:
PooledConenction实现了InvocationHandler接口,并且 proxyConnection对象也是根据这个它来生成的代理对象:
class PooledConnection implements InvocationHandler { 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); } } } }
从上述代码可以看到,当我们使用了pooledDataSource.getConnection()返回的Connection对象的close()方法时,不会调用真正Connection的close()方法,而是将此Connection对象放到连接池中。
8、JNDI类型的数据源 JndiDataSource
对于JNDI类型的数据源DataSource的获取就比较简单,MyBatis定义了一个JndiDataSourceFactory工厂来创建通过JNDI形式生成的DataSource。
这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。这种数据源配置只需要两个属性:
①、initial_context – 个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么 data_source 属性将会直接从 InitialContext 中寻找。
②、data_source – 这是引用数据源实例位置的上下文的路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
1 public void setProperties(Properties properties) { 2 try { 3 InitialContext initCtx; 4 Properties env = getEnvProperties(properties); 5 if (env == null) { 6 initCtx = new InitialContext(); 7 } else { 8 initCtx = new InitialContext(env); 9 } 10 11 if (properties.containsKey(INITIAL_CONTEXT) 12 && properties.containsKey(DATA_SOURCE)) { 13 Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT)); 14 dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE)); 15 } else if (properties.containsKey(DATA_SOURCE)) { 16 dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE)); 17 } 18 19 } catch (NamingException e) { 20 throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e); 21 } 22 }
参考文档:https://blog.csdn.net/luanlouis/article/details/37992171
http://www.mybatis.org/mybatis-3/zh/configuration.html#environments