J2EE学习篇之--JDBC详解(三)

2014-11-23 21:26:05 · 作者: · 浏览: 40
tedKeys(); int id = 0; if(rs.next()){ id = rs.getInt(1); } System.out.println("id:"+id); }catch(Exception e){ e.printStackTrace(); }finally{ JdbcUtils.free(rs,ps,conn); } }我们只要设置一个参数Statement.RETURN_GENERATED_KEYS就可以得到一个主键集合了,这里要注意的是,因为有的表结构中会出现组合主键的情况,所以返回的是一个主键集合。这种方式就和底层数据库摆脱了关系,做到一致性了。


下面在来看一下使用JDBC来实现批处理功能

我们在前面的例子中会发现,每次都是执行一条语句,然后关闭连接,这样效率可能会很低,如果我们想一次插入几千条数据的话,这时候可以使用批处理的功能,所谓批处理就是将多个执行语句进行捆绑然后去执行,但是效率上并非就一定高,因为我们知道这个数据库连接是tcp的,所以在将多个语句捆绑在一起的时候,在传输的过程中也是会进行分包发送的,这个包的大小也不是固定的,这个大小很难掌控的,我们之后经过多次测试之后,才能得到一次批量处理的适宜数量。下面来看一下实例吧:

首先是普通的插入一条数据:

static void create() throws Exception{
		//建立一个连接的是很耗时间的
		//执行一个sql语句也是很耗时间的
		//优化的措施:批处理
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert user(name,birthday,money) values( , , )";
			ps = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
			ps.setString(1,"jiangwei");
			ps.setDate(2,new Date(System.currentTimeMillis()));
			ps.setFloat(3,400);
			ps.executeUpdate();
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs, ps, conn);
		}
	}


然后是批处理插入100条数据:

static void createBatch() throws Exception{
		//建立一个连接的是很耗时间的
		//执行一个sql语句也是很耗时间的
		//优化的措施:批处理
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert user(name,birthday,money) values( , , )";
			ps = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
			
			//打包的话容量也不是越大越好,因为可能会内存溢出的,同时网络传输的过程中也是会进行拆包传输的,这个包的大小是不一定的
			//有时候打包的效率不一定就会高,这个和数据库的类型,版本都有关系的,所以我们在实践的过程中需要检验的
			for(int i=0;i<100;i++){
				ps.setString(1,"jiangwei");
				ps.setDate(2,new Date(System.currentTimeMillis()));
				ps.setFloat(3,400);
				//ps.addBatch(sql);
				ps.addBatch();
			}
			ps.executeBatch();
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs, ps, conn);
		}
	}

测试代码:

public static void main(String[] args) throws Exception{
		long startTime = System.currentTimeMillis();
		for(int i=0;i<100;i++){
			create();
		}
		long endTime = System.currentTimeMillis();
		System.out.println("For Waste Time:"+(endTime-startTime));
		createBatch();
		System.out.println("Batch Waste Time:"+(System.currentTimeMillis()-endTime));
	}
我们在控制台中看到他们分别消耗的时间:

\

< http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+ztLDx7/J0tS/tLW91eK49sX6tKbA7c/7usS1xMqxvOTD98/UutzJ2aGjoaO1sci7ztLDx9Tav6rKvLXEyrG68tKyy7W5/cHLo6zV4rj2xfq0psDttcTX7srK0su1xLTz0KHSqtXGv9i6w6GjPC9wPgo8cD48YnI+CjwvcD4KPHA+1NrAtL+00rvPwkpEQkPW0LXEufa2r73hufu8r7rNt9bSs7y8yvU8L3A+CjxwPs7Sw8fU2seww+a1xMD919PW0L/J0tS/tLW9o6zU2rSmwO294bn7vK+1xMqxuvKjrM7Sw8e2vMrH0rvM9dK7zPXP8rrztKbA7bXEo6y1q8rH09DKsbryztLDx9Do0qrIy86qtcS/2NbGveG5+7yvtcS59ravo6yxyMjnztLDx8/rzfnHsLn2tq+jrM/r1rG907aozru1vcTEuPa94bn7vK+8x8K8tciy2df3o6y1sci7SkRCQ9KyysfM4bmpwcvSu8zXQXBpyMPO0sPHwLSy2df3tcQ8L3A+CjxwPjwvcD4KPHByZSBjbGFzcz0="brush:java;">static void test() throws Exception{ Connection conn = null; Statement st = null; ResultSet rs = null; try{ conn = JdbcUtils.getConnection(); //结果集可滚动的 /** * 参数的含义: * ResultSet.RTYPE_FORWORD_ONLY:这是缺省值,只可向前滚动; ResultSet.TYPE_SCROLL_INSENSITIVE:双向滚动,但不及时更新,就是如果数据库里的数据修改过,并不在ResultSet中反应出来。 ResultSet.TYPE_SCROLL_SENSITIVE:双向滚动,并及时跟踪数据库的更新,以便更改ResultSet中的数据。 ResultSet.CONCUR_READ_ONLY:这是缺省值,指定不可以更新 ResultSet ResultSet.CONCUR_UPDATABLE:指定可以更新 ResultSet */ st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_READ_ONLY); rs = st.executeQuery("select id,name,money,birthday from user"); //开始的时候这个游标的位置是第一条记录之前的一个位置 //当执行rs.next的时候这个游标的位置就到第一条记录了 /*while(rs.next()){ //print result }*/ //上面的代码执行之后,这个游标就到最后一条记录的下一个位置了 //所以这里在调用previous方法之后,这个游标就回到了最后一条记录中,所以打印了最后一条记录的值 /*if(rs.previous()){ System.out.println("id="+rs.getInt("id")+"\tname="+rs.getString("name")+"\tbirthday="+rs.getDate("birthday")+"\tmoney="+rs.getFloat("money")); }*/ //绝对定位到第几行结果集 //这里传递的参数的下标是从1开始的,比如这里查询出来的记录有3条,那么这里的参数的范围是:1-3,如果传递的参数不在这个范围内就会报告异常的 rs.absolute(2); System.out.println("id="+rs.getInt("id")+"\tname="+rs.getString("name")+"\tbirthday="+rs.getDate("birthday")+"\tmoney="+rs.getFloat("money")); //滚到到第一行的前面(默认的就是这种情况) rs.beforeFirst(); //滚动到最后一行的后面 rs.afterLast(); rs.isFirst();//判断是不是在第一行记录 rs.isLast();//判断是不是在最后一行记录 rs.isAfterLast();//判断是不是第一行前面的位置 rs.isBeforeFirst();//判断是不是最后一行的后面的位置 //以上的api可以实现翻页的效果(这个效率很低的,因为是先把数据都查询到内存中,然后再进行分页显示的) //效率高的话是直接使用数据库中的分页查询语句: //select * from user limit 150,10; //以上的api实现的分页功能是针对于那些本身不支持分页查询功能的数据库的,如果一个数据库支持分页功能,上面的代码就不能使用的,因为效率是很低的 }catch(Exception e){ e.printStackTrace(); }finally{ JdbcUtils.free(rs,st,conn); } }我们看到结果集:rs有很多方法的,我们一次来看一下:

next()方法:这个很常用的,就是将结果集向后滚动
previous()方法:这个方法和next是相反的,将结果集向前滚动
absolute(int index)方法:这个方法是将结果集直接定位到指定的记录上,这个参数是从1开始的,不是0,如果不在指定的范围内的话,会报告异常的
beforeFirst()方法:这个方法是将结果集直接滚动到第一条记录的前面的位置(默认情况是这样的,所以我们每次在取出数据的时候,需要使用next方法,将结果集滚动到第一条记录上)
afterLast()方法:这个方法是将结果集直接滚动到最后一条记录的后面的位置
isFirst()方法:判断是不是在第一行记录
isLast()方法:判断是不是在最后一行记录
isAfterLast()方法:判断是不是第一行前面的位置
isBeforeFirst()方法:判断是不是最后一行的后面的位置


当然我们要向实现可滚动的结果集,还要设置一下参数:

st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_READ_ONLY);
参数的含义:

ResultSet.RTYPE_FORWORD_ONLY:这是缺省值,只可向前滚动;
ResultSet.TYPE_SCROLL_INSENSITIVE:双向滚动,但不及时更新,就是如果数据库里的数据修改过,并不在ResultSet中反应出来。
ResultSet.TYPE_SCROLL_SENSITIVE:双向滚动,并及时跟踪数据库的更新,以便更改ResultSet中的数据。
ResultSet.CONCUR_READ_ONLY:这是缺省值,指定不可以更新 ResultSet
ResultSet.CONCUR_UPDATABLE:指定可以更新 ResultSet(这个后面会说到)


同时在这里我们只需要使用absolute方法就可以实现分页的功能,因为他可以随便的定位到指定的记录集中,但是这个是在全部将结果集查询处理的基础上来实现的,就是首先将所有符合条件的结果集查询出来放到内存中,然后再就行分页操作,那么这种分页的效率就很低了,所以我们强烈建议在数据库查询数据的时候就进行分页操作,比如MySql中使用limit关键字进行操作,MSSQL中使用top关键字,Oracle中使用number关键字,但是有的数据库中不支持分页查询操作,所以这时候我们只能使用上面的形式来进行分页了。


下面再来看一下JDBC中的可更新以及对更新敏感的结果集操作

我们有时候可能有这样的需求,就是在查询出结果集的时候,想对指定的记录进行更新操作,说白了,就是将查询和更新操作放到一起进行,来看一下代码:

static void test() throws Exception{
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			//第三个字段的含义是,在读取数据的时候(已经返回了结果集到内存中了),
			//再去修改结果集中的数据,这时候数据库中的数据就可以感知到结果集中的变化了进行修改
			st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
			rs = st.executeQuery("select * from user");
			//这种操作是不可取的,因为查询和更新交互在一起,逻辑就乱了,只有在特定的场合中使用
			while(rs.next()){
				//这里我们获取到name列的值,如果是lisi我们就将结果集中的他的记录中的money变成170,
				//然后再更行行信息,这时候数据库中的这条记录的值也发生变化了,
				//内存中的结果集中的记录的值发生改变了,影响到了数据库中的值
				String name = rs.getString("name");
				if("jiangwei".equals(name)){
					rs.updateFloat("money",170);
					rs.updateRow();
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,st,conn);
		}
	}

我们看到在循环处理结果集的时候,我们将name是jiangwei的记录的钱修改成170,并且反映到数据库中

这里一定要记得设置参数:ResultSet.CONCUR_UPDATABLE,不然会报异常的,这个参数的功能就是将更新的操作同步到数据库中的

这里我们是不建议这种做法的,因为将查询和更新的操作放到一起来操作的话,维护是很差的,我们一定要将CRUD操作进行分开处理,这里只是介绍一下相关知识,不推荐使用的。


下面来看一下通过反射技术,来实现将结果集填充到指定的实体类中,其实这部分的内容很简单的,直接上代码:

/**
	 * 使用泛型
	 * @param 
  
   
	 * @param sql
	 * @param clazz
	 * @return
	 * @throws Exception
	 */
	static 
   
     T test(String sql,Class
    
      clazz) throws Exception{ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try{ conn = JdbcUtils.getConnection(); ps = conn.prepareStatement(sql); rs = ps.executeQuery(); ResultSetMetaData rsmd = rs.getMetaData(); int count = rsmd.getColumnCount(); String[] colNames = new String[count]; for(int i=1;i<=count;i++){ colNames[i-1] = rsmd.getColumnLabel(i);//使用别名,让列名和User中的属性名相同 } T user = clazz.newInstance(); //使用反射获取set方法来进行赋值 if(rs.next()){ Method[] ms = user.getClass().getMethods(); for(int i=0;i
     
      测试代码: 
      

public static void main(String[] args) throws Exception{
		//User user = test("select * from user",User.class);
		//使用别名来规定列名和属性名相同
		User user = test("select id as Id,name as Name,birthday as Birthday,money as Money from user",User.class);
		System.out.println(user);
	}


其实就是使用反射技术将得到实体类中所有属性的set方法,然后通过set方法进行属性值的填充,这里唯一要注意的问题就是返回的结果集中的字段名称必须要和实体类中的属性名称相同,要做到这一点我们又两种方式:

一种是直接将表中的字段名称和实体类中的属性名称相同

一种是使用别名的方式来操作,将别名设置的和实体类中的属性名称相同

其实我们会发现,后面说到的Hibernate框架就是采用这种机制的


下面在来看一下元数据的相关知识

我们知道元数据信息就是标示数据本身的一些数据信息

1.数据库的元数据信息

就是数据库的相关信息,如数据库的版本号,驱动名称,是否支持事务操作等信息,JDBC提供了接口让我们可以获取这些信息的:

static void test() throws Exception{
		Connection conn = JdbcUtils.getConnection();
		//这些信息对于那些框架的编写就很有用了,因为框架是要兼容各个数据库类型的,如Hibernate中有一个方言设置
		//如果没有设置的话,他就会自己使用以下的api进行查找是那个数据库
		DatabaseMetaData metaData = conn.getMetaData();
		System.out.println("databaseName:"+metaData.getDatabaseProductName());
		System.out.println("driverName:"+metaData.getDriverName());
		System.out.println("isSupportBatch:"+metaData.supportsBatchUpdates());
		System.out.println("isSupportTransaction:"+metaData.supportsTransactions());
	}
这些信息对于我们使用人员来说可能没有太大的用处,但是对于开发框架的人来说用处很大的,比如Hibernate框架,他要做到兼容所有的数据库特性的话,必须要将不同特性统一起来,所以他肯定要获取数据库的元数据信息的,后面会说到Hibernate中有一个配置叫做:方言,这个就是用来设置数据库名称的。

2.查询参数的元数据信息

当我们在使用PreparedStatement来进行参数填充的时候,我们想知道参数的一些信息,直接上代码:

static void test(String sql,Object[] params) throws Exception{
		Connection conn = JdbcUtils.getConnection();
		//参数的元数据信息:Statement是没有参数的元数据信息的(因为Statement不支持 ),查看源代码,返回的都是varchar
		PreparedStatement ps = conn.prepareStatement(sql);
		//必须在连接数据库中的时候添加这个参数generateSimpleParameterMetadata=true
		//不然是获取不到参数的,而且会报异常
		ParameterMetaData pMetaData= ps.getParameterMetaData();
		int count = pMetaData.getParameterCount();
		for(int i=1;i<=count;i++){
			//因为mysql没有去查询库,所以不能根据查询的字段就能获取字段的类型,所有都返回varchar
			System.out.println(pMetaData.getParameterClassName(i));
			System.out.println(pMetaData.getParameterType(i));
			System.out.println(pMetaData.getParameterTypeName(i));
			//假定我们传入的参数的顺序和sql语句中的占位符的顺序一样的
			ps.setObject(i,params[i-1]);
		}
		ps.executeQuery();
	}

测试代码:

public static void main(String[] args) throws Exception{
		String sql = "select name,birthday,money from user where name= ";
		Object[] params = new Object[]{"jiangwei"};
		test(sql,params);
	}

我们知道Statement是不支持参数填充的,所以不可能获取到参数的元数据信息的

我们运行测试代码,会看到如下异常信息:

\

这时候我们就要注意了,如果想获取到元数据信息的话,我们还需要在连接数据的url后面添加一个参数:

generateSimpleParameterMetadata=true

添加完之后,我们运行结果如下:

\

我们看到可以获取参数在Java中的类型,12代表的是sql包中的类型:java.sql.Types.VARCHAR,这个字段是个整型值,值就是12,他对应到数据库中varchar类型的

这里要注意一点:有时候我们会发现这里获取到数据库中的类型是错误的,比如这里我们如果将数据库中的name字段的类型修改成char类型的,这里获取到的还是varchar,这一点想一想也是对的,你想想这个是获取查询参数的信息,我们还没有进行查询操作的,系统不可能那么智能的获取到数据库中准确的字段的类型的,所以他这里就做了一个大致的对应关系,将Java中的类型和数据库中的类型对应起来的,因为数据库中char和varchar都是字符串的。所以我们不能相信这里得到的数据库中字段的类型的,需要通过结果集中的元数据类型。

3.结果集中元数据信息

就是查询结果的一般信息,比如字段的相关信息

我们在上面看到要想获取数据库中字段的真实类型的话,只有先进行查询操作才可以,在这里我们就可以获取到正确的类型了,上代码:

static void test(String sql) throws Exception{
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			ps = conn.prepareStatement(sql);
			rs = ps.executeQuery();
			ResultSetMetaData rsmd = rs.getMetaData();
			int count = rsmd.getColumnCount();
			String[] colNames = new String[count];
			for(int i=1;i<=count;i++){
				//这里是可以获取到真实的类型的,因为这个是已经从数据库中查询了
				System.out.println(rsmd.getColumnClassName(i));
				System.out.println(rsmd.getColumnName(i));
				System.out.println(rsmd.getColumnType(i));
				System.out.println(rsmd.getColumnLabel(i));//列的别名:select name as n from user;,有别名的话就返回的是别名,而不是原始的列名了
				colNames[i-1] = rsmd.getColumnName(i);
			}
			//将结果构建一个Map,列名是key,列的值是value
			Map
       
         data = null;
			//假设查询的数据只有一条,如果是多条的话我们可以定义一个List
        
         (); for(int i=0;i
         
          
我们这里可以获取到结果集中字段的总数count,以及字段的类型,名称,别名等信息,同时我们这里还穿插了一段代码,就是将结果集封装成一个HashMap结构,字段名做key,字段值做value


最后再来看一下一个非常重要的概念就是数据源,首先我们要知道什么是数据源,为什么要有数据源,我们从上面的例子看到,我们每次执行操作的时候,都是打开连接,关闭连接,这个连接的建立和关闭是很好资源和时间的,所以我们就在想一个策略怎么才能优化呢?所以数据源的概念就出来的,数据源就是用来管理连接的一个池子,使用高效的算法进行调度,这样在执行操作的时候是很方便的,为了容易理解数据源的相关概念,我们自己编写一个数据源:

package com.weijia.datasource;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;

import javax.sql.DataSource;

/**
 * 大部分时间都是浪费在数据库连接这一块
 * 这个类是我们自己编写的一个数据源
 * @author weijiang204321
 *
 */
public class MyDataSource implements DataSource{

	private static String user = "root";
	private static String password = "123456";
	private static String dbName = "test";
	private static  String url = "jdbc:mysql://localhost:3306/"+dbName+" user="+user+"&password="+password+"&useUnicode=true&characterEncoding=gb2312";
	
	private static int initCount = 5;//初始化的连接数
	private static int maxCount = 10;//最大连接数
	private static int currentCount = 0;//当前的连接数
	//可能频繁的取出连接和删除连接,所以用LinkedList
	private LinkedList
           
             connectionsPool = new LinkedList
            
             (); public MyDataSource(){ try{ for(int i=0;i
             
               0){ return this.connectionsPool.removeFirst(); } if(currentCount < maxCount){ currentCount++; return createConnection(); } //在这里可以让当前线程等待,抛出异常,返回null都是可以的,要视情况而定 throw new SQLException("已经没有连接了"); //不能无限制的创建连接的,因为这样的话对数据库的压力很大,连接越多,最后数据库的运行速度就会变得很慢了(很硬件相关) //如果内存够大,cpu给力的话,数据库可以建立的连接数也会增加的 } } public void free(Connection conn) throws SQLException{ this.connectionsPool.addLast(conn); } private Connection createConnection() throws SQLException{ return DriverManager.getConnection(url); } public Connection getConnection(String username, String password)throws SQLException { return null; } public PrintWriter getLogWriter() throws SQLException { return null; } public int getLoginTimeout() throws SQLException { return 0; } public void setLogWriter(PrintWriter arg0) throws SQLException { } public void setLoginTimeout(int arg0) throws SQLException { } } 
             
            
           

然后修改一下JdbcUtils中的代码:

package com.weijia.firstdemo;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import com.weijia.datasource.MyDataSource;

public class JdbcUtils {
	
	private static String user = "root";
	private static String password = "123456";
	private static String dbName = "test";
	private static  String url = "jdbc:mysql://localhost:3306/"+dbName+" user="+user+"&password="+password+"&useUnicode=true&characterEncoding=gb2312&generateSimpleParameterMetadata=true";
	
	private static MyDataSource dataSource = null;
	
	/**
	 * 加载驱动
	 */
	static{
		try{
			Class.forName("com.mysql.jdbc.Driver");
			dataSource = new MyDataSource();//初始化数据源
		}catch(Exception e){
			System.out.println("Exception:"+e.getMessage()+"");
			throw new ExceptionInInitializerError(e);
		}
	}
	
	private JdbcUtils(){
	}
	
	/**
	 * 获取连接
	 * @return
	 * @throws SQLException
	 */
	public static Connection getConnection() throws SQLException{
		return dataSource.getConnection();
	}
	
	public static DataSource getDataSource(){
		return dataSource;
	}
	
	/**
	 * 释放资源
	 * @param rs
	 * @param st
	 * @param conn
	 */
	public static void free(ResultSet rs,Statement st,Connection conn){
		try{
			if(rs != null){
				rs.close();
			}
		}catch(SQLException e){
			e.printStackTrace();
		}finally{
			try{
				if(st != null){
					st.close();
				}
			}catch(SQLException e){
				e.printStackTrace();
			}finally{
				try{
					dataSource.free(conn);
				}catch(SQLException e){
					e.printStackTrace();
				}
			}
		}
			
	}

}

我们看到,在我们自定义的数据源中,主要有这么几个变量:

初始化连接数,最大连接数,当前的连接数,连接池(因为我们可能需要频繁的添加连接和删除连接所以使用LinkedList,因为这个list是链表结构的,增加和删除效率高)

主要流程是:初始化数据源的时候,初始化一定量的连接放到池子中,当用户使用getConnection()方法取出连接的时候,我们会判断这个连接池中还有没有连接了,有就直接取出第一个连接返回,没有的话,我们在判断当前的连接数有没有超过最大连接数,超过的话,就抛出一个异常(其实这里还可以选择等待其他连接的释放,这个具体实现是很麻烦的),没有超过的话,就创建连接,并且将其放入池子中。

我们自定义的数据源是实现了JDBC中的DataSource接口的,这个接口很重要的,后面我们会说到apache的数据源都是要实现这个接口的,这个接口统一了数据源的标准,这个接口中有很多实现的,所以看到我们的数据源类中有很多没必要的方法,但是那个方法都是要实现的,最重要的就是要实现getConnection方法,其他的实现都只需要调用super.XXX就可以了。


在JdbcUtils类中我们也是需要修改的,首先我们要在静态代码块中初始化我们的数据源,在getConnection方法中调用数据源的getConnection方法,在free方法中调用数据源的free方法即可。


看一下测试类:

package com.weijia.datasource;

import java.sql.Connection;

import com.weijia.firstdemo.JdbcUtils;

public class Test {
	
	public static void main(String[] args) throws Exception{
		for(int i=0;i<10;i++){
			Connection conn = JdbcUtils.getConnection();
			System.out.println(conn);
			JdbcUtils.free(null, null, conn);
		}
	}

}

运行结果:


我们可以看到,我们在测试代码中申请了10个连接,从结果上可以看出前五个是不同的连接,后五个连接和前五个是一样的,这是因为我们在释放连接的时候就是free方法中,是将连接重新放到池子中的,上面显示的是五个,是因为我们初始化的连接数是5个,当第一个连接释放的时候这个连接其实已经放到了池子的第六个位置,以此类推。