首页 > WEB开发 > 数据库 > 数据库连接池(DataSource)
2014
08-25

数据库连接池(DataSource)

04. 数据库连接池(DataSource)19

1、概念

在三层架构中,DAO层直接与数据库交互,首先要建立与数据库的连接,如果采用下图(a)所示,则用户每次的请求都要创建连接,用完又关闭,而数据库连接的创建和关闭需要消耗较大的资源,因此实际开发中常采用图(b)所示,在应用程序启动时创建一个包含多个Connection对象的连接池,DAO层使用时直接从池子里取一个Connection对象,用完后放回池子,避免了重复创建关闭数据库连接造成的开销。

04. 数据库连接池(DataSource)222

2、数据库连接池原理

下面的代码模拟了数据库连接池的原理(代码中的JDBCUtil工具类见《MySQL(JDBC)》),池子里保持了10个Connection对象,并提供了getConnection和release方法:

public class ConnectionPoolDemo {
	//连接池实际上就是一个List
	private static List<Connection> pool = new LinkedList<Connection>();

	static{//加载连接池类时在池子中放入10个连接
		for(int i = 0;i < 10;i ++){
			Connection conn;
			try {
				conn = JDBCUtil.getConnection();
				pool.add(conn);
			} catch (Exception e) {
				e.printStackTrace();
			}

		}
	}

	//从池子中取出一个连接
	public synchronized Connection getConnection(){
		return pool.remove(0);
	}

	//把连接还回池子中
	public static void release(Connection conn){
		pool.add(conn);
	}
}

3、编写一个符合规范的连接池

上节模拟数据库连接池原理的代码也实现了一个简单连接池,但是不符合规范(Sun公司制定)。编写一个符合规范的连接池需要实现javax.sql.DataSource接口。(DataSource接口中定义了两个重载的getConnection方法)

编程难点 ☆:当用户使用完Connection,执行conn.close()时,Connection对象应保证将自己还给连接池,而不要把conn关闭。之所由Connection对象保证将自己返回到LinkedList中,是因为DataSource接口中并未定义上节例子中类似release的方法。所以必须改写Connection中的close方法,使得用户执行conn.close()时,将Connection对象还给连接池。

解决方案 ☆ :改写驱动程序中Connection类的close方法。对已知类的某些方法进行功能上的改变,有以下几种编码方案(☆):

1)编写子类,覆写需要改变的方法。此处行不通,原因有:① 程序中不知道继承哪个驱动的Connection实现类 ② 数据库驱动对Connection接口的实现类是final的,不允许被继承。

2)装饰(包装)设计模式(静态代理)

① 定义包装类:MyConnection,该类完成了对com.mysql.jdbc.Connection类的包装。

关键词:保持被包装对象的原有信息、对某个/某些方法进行改写。包装类的编写过程如下:

/**
 * 目前要包装的类是:com.mysql.jdbc.Connection
 * @author flyne
 */
//1、编写一个类,实现与被包装类相同的接口。
public class MyConnection implements Connection {
	//2、定义一个变量,引用被包装类的实例(保持被包装对象的原有信息)
	private Connection conn;

	private List<Connection> pool;//close方法中需要用

	//3、在构造方法中传入被包装类的实例
	public MyConnection(Connection conn,List<Connection> pool){
		this.conn = conn;
		this.pool = pool;
	}

	//4、对于需要改写的方法,编写自己的代码即可
	public void close() throws SQLException {
		pool.add(conn);
	}

	//5、对于不需要改写的方法,调用被包装对象的对应方法
	public <T> T unwrap(Class<T> iface) throws SQLException {
		return conn.unwrap(iface);
	}
	……//其他代码从略
}

②实现DataSource接口。

public class MyDataSource implements DataSource {
	private static List<Connection> pool = new LinkedList<Connection>();

	static{
		try {
			for(int i=0;i<10;i++){
				Connection conn = JDBCUtil.getConnection();//创建的新连接
				pool.add(conn);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public Connection getConnection() throws SQLException {
		if(pool.size()>0){
			Connection conn = pool.remove(0);
			MyConnection mconn = new MyConnection(conn,pool);
			return mconn;
		}else{
			throw new RuntimeException("服务器忙");
		}
	}

	…… //其他代码从略
}

3)默认适配器

Connection接口中有多个方法,如果直接实现此接口,那么需要在实现类中实现所有的方法,但往往可能只用到接口中一个或者几个方法(如上面的MyConnection类),显然用这样的实现类会造成资源的浪费,系统开销的加大。此时可以在中间引入一个默认适配器。

① 默认适配器

默认适配器本身也是一个包装类,但并没有对任何的方法进行改写:

public class ConnectionAdapter implements Connection {

	private Connection conn;

	public ConnectionAdapter(Connection conn){
		this.conn = conn;
	}

	public <T> T unwrap(Class<T> iface) throws SQLException {
		return this.unwrap(iface);
	}

	…… //其他代码从略
}

② MyConnection类的改写,此时直接继承ConnectionAdapter,并改写close()方法即可。

public class MyConnection extends ConnectionAdapter {

	private Connection conn;

	private List<Connection> pool;

	public MyConnection(Connection conn,List<Connection> pool) {
		super(conn);
		this.conn = conn;
		this.pool = pool;
	}

	public void close() throws SQLException {
		pool.add(conn);
	}
}

③MyDataSource类的实现同上。

4)动态代理(Proxy)

利用动态代理改写上面的MyDataSource类,代码如下:

//在getConnection方法中返回Connection对象的代理对象,其他部分同MyDataSource类
public Connection getConnection() throws SQLException {
	if (pool.size() > 0) {
		final Connection conn = pool.remove(0);

		Connection connProxy = (Connection)Proxy.newProxyInstance(conn.getClass().getClassLoader(),
				conn.getClass().getInterfaces(),
				new InvocationHandler() {//此处为内部类,当close方法被调用时将conn还回池中,其它方法直接执行
					public Object invoke(Object proxy, Method method,
							Object[] args) throws Throwable {
						if("close".equals(method.getName())){
							return pool.add(conn);
						}else{
							return method.invoke(conn, args);
						}
					}
				});
		return connProxy;
	} else {
		throw new RuntimeException("服务器忙");
	}
}

注:InvocationHandler:调用处理程序。它是一个接口,说明如何代理,是典型的策略设计模式。有关动态代理的知识可参考动态代理

4、开源的数据库连接池

大多数Web服务器都实现了DataSource接口,如WebLogic、Tomcat,另外,一些开源组织也提供了DataSource的实现,如DBCP、C3P0和Druid(阿里巴巴)。

1)DBCP数据源(在Java Project中使用)

DBCP:DataBase Connection Pool,由Apache提供实现。本文采用配置文件的方式读取数据库连接的参数,使用步骤如下:

① 拷贝jar包:commons-dbcp.jar、commons-pool.jar

② 配置数据库连接池(src/dbcpconfig.properties)

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb2
username=root
password=

initialSize=10
maxActive=50
minIdle=5

③ 将使用DBCP时的共同操作封装到DBCPUtil中,便于下次调用:

public class DBCPUtil {
	private static DataSource dataSource;

	static{
		try {
			InputStream in = DBCPUtil.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
			Properties props = new Properties();
			props.load(in);

			dataSource = BasicDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			throw new ExceptionInInitializerError(e);
		}
	}

	public static DataSource getDataSource(){
		return dataSource;
	}

	public static Connection getConnection() throws SQLException{
		return dataSource.getConnection();
	}
}

完成上述步骤后,就可以在程序中直接使用DBCP了,下面用JUnit简单的模拟了一下:

@Test
public void test1() throws Exception{
	Connection conn = DBCPUtil.getConnection();
	System.out.println(conn.getClass().getName());
	conn.close();
}

2)C3P0数据源

导入jar包:c3p0-0.9.x.x.jar

配置数据库连接池(src/c3p0-config.xml)

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
	<default-config>
		<property name="driverClass">com.mysql.jdbc.Driver</property>
		<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb2</property>
		<property name="user">root</property>
		<property name="password"></property>

		<property name="initialPoolSize">10</property>
		<property name="acquireIncrement">5</property>
		<property name="maxIdleTime">30</property>
		<property name="maxPoolSize">100</property>
		<property name="minPoolSize">10</property>
	</default-config>
	<named-config name="mysql">
		<!-- 除了设置默认配置外还可以自定义多套配置,方便切换 -->
		<property name="xxx">……</property>
	</named-config>
	<named-config name="oracle">
		<property name="xxx">……</property>
	</named-config>
</c3p0-config>

③ 同使用DBCP一样,封装一个C3P0Util类:

public class C3P0Util {
	private static ComboPooledDataSource dataSource = new ComboPooledDataSource();

	public static DataSource getDataSource(){
		return dataSource;
	}

	public static Connection getConnection(){
		try {
			return dataSource.getConnection();
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}
}

完成上述步骤后,就可以在程序中直接使用C3P0了,下面用JUnit简单的模拟了一下:

@Test
public void test1() throws Exception{
	Connection conn = C3P0Util.getConnection();
	System.out.println(conn.getClass().getName());
	conn.close();
}

3)使用JNDI管理数据源

上面介绍的两个开源数据源可以在Java工程中使用,如果是一个Web工程,就可以直接使用服务器实现的数据源。Tomcat实现的数据源即DBCP(同属Apache),它整合了commons-dbcp和commons-pools(见%TOMCAT_HOME%\lib文件夹下的tomcat-dbcp.jar)

Tomcat服务器中使用JNDI管理数据源,要想得到一个数据源,需要使用JNDI进行查找。

JNDI:Java Naming and Directory Interface。属于JavaEE技术之一。
通俗的讲JNDI就是对Web容器中所有的资源和组件进行管理的花名册【JNDI容器】,通过该服务,可以很方便的找到想要的资源。JNDI使用哈希表(HashMap)存储对象,然后,开发人员可以使用key(也就是一个字符串)来获取这个对象。这里就包括取JNDI的两个最主要操作,bind和lookup。bind操作负责往哈希表里存对象,存对象的时候要定义好对象的键值字符串,lookup操作则根据这个键值字符串往外取对象。

java.naming.*包中定义了JNDI的操作,Context是该包的核心API,代表JNDI容器,lookup() 是最常使用的操作。当向 lookup() 提供想要查询的对象的名称时,它将返回绑定到该名称的对象。

要想使用Tomcat自带数据源,首先要在Web容器的JNDI中注册数据源,有两步:

① 拷贝数据库的驱动到Tomcat\lib目录下。(因为此时数据源在Tomcat上进行配置的)

② 在web应用的META-INF目录下建立一个名称为context.xml的配置文件,内容如下:

04. 数据库连接池(DataSource)7696

当Tomcat启动后,就会在JNDI中注册一个名为jdbc/mydb2的数据源,在Servlet/JSP获取DataSource和Connection对象的方式如下:

public class JNDIUtil {
	//JNDI中找数据源的全名为:路径+名称(配置文件中指定)
	private static String DSName = "java:comp/env/jdbc/mydb2";

	public static DataSource getDataSource() throws NamingException{
		Context context = new InitialContext();
		DataSource ds = (DataSource)context.lookup(DSName);
		return ds;
	}

	public static Connection getConnection() throws Exception{
		return getDataSource().getConnection();
	}
}

注:上面的JNDIUtil虽然在一个普通Java类中定义,但是只能在Servlet和JSP中使用该类。


留下一个回复

你的email不会被公开。