文章目录
六. 数据库事务
transaction:交易; 处理; 业务
我们以通用的增删改为例
假设有这样一个场景,对于下表
希望AA给BB转账,这就意味着有两行记录都要发生变化。而且我们希望两条记录要么都改,要么都不改
这有点像触发器的应用场景。在有触发器之前,我们是将两个操作用事务包裹起来,使得这两个操作变成一个原子操作(其实就是封装)
正常情况
这是正常情况下执行两个关联操作,最终AA变为900,BB变为1100
/*
* 场景
* 针对于数据表user_table来说:
* AA用户给BB用户转账100
*
* update user_table set balance = balance - 100 where user = 'AA';
* update user_table set balance = balance + 100 where user = 'BB';
*/
@Test
public void testUpdate(){
String sql1 = "update user_table set balance = balance - 100 where user = ?";
update(sql1, "AA");
String sql2 = "update user_table set balance = balance + 100 where user = ?";
update(sql2, "BB");
System.out.println("转账成功");
}
/**
* @description:通用的增删改 --version1.0
*/
关联操作出现异常
加入两个关联操作之间出现了异常,比如网络卡顿,或者插入其他操作
public void testUpdate(){
String sql1 = "update user_table set balance = balance - 100 where user = ?";
update(sql1, "AA");
//模拟网络异常
System.out.println(10 / 0);
String sql2 = "update user_table set balance = balance + 100 where user = ?";
update(sql2, "BB");
System.out.println("转账成功");
}
只有AA成功更改
这个时候应该将更改AA的数据这个操作进行回滚
数据库事务
定义:一组逻辑操作单元,也就是一个或多个DML操作
原则:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式
(1)要么所有的事务都被提交(commit),那么这些修改就永久地保存下来
(2)要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态(回滚默认情况下是回滚到最近的一次DML操作,每进行一次DML都会默认提交一次,这在之前批量插入查询语句时碰到过)
数据一旦提交,就不可回滚
不可回滚的操作
(1)DDL操作一旦执行,都会自动提交。
set autocommit = false 对DDL操作失效
(2)DML默认情况下,一旦执行,就会自动提交。
我们可以通过set autocommit = false的方式取消DML操作的自动提交。
(3)默认在关闭连接时,会自动的提交数据
考虑数据库事务的通用增删改
我们需要做两件事:
(1)手动控制数据的提交
保证同一个事物中的DML操作是在同一个连接中执行
为了避免DML的自动提交,set autocommit = false
为了避免关闭连接的时候自动提交,执行完一次DML,不关闭连接
(2)手动控制数据的回滚
出现异常,连接调用rollback方法进行回滚
可以看到,在下面的程序中,负责增删改的方法改动很小:
(1)从外部传入连接
(2)关闭资源时,不要关闭从外部传入的连接,因为关闭连接会自动提交数据
主要的改动发生在调用增删改方法之外:
(1)事务中的所有操作在同一个连接中完成
(2)主动关闭DML的自动提交
(3)事务执行完之后主动进行一次提交。已经关闭了连接的自动提交功能
(4)关闭了连接的自动提交功能之后,关闭连接也不会提交数据
(5)最后要恢复连接的自动提交功能。因为以后是从数据库连接池获取连接,就像线程池,我们需要在交还连接的时候恢复连接的状态
public void testUpdateWithTransaction(){
Connection conn = null;
try {
conn = JDBCUtils.getConnection();
//取消DML的自动提交
conn.setAutoCommit(false);
String sql1 = "update user_table set balance = balance - 100 where user = ?";
update(conn, sql1, "AA");
//模拟网络异常
System.out.println(10 / 0);
String sql2 = "update user_table set balance = balance + 100 where user = ?";
update(conn, sql2, "BB");
System.out.println("转账成功");
conn.commit();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
//主动回滚数据
try {
conn.rollback();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}finally{
conn.setAutoCommit(true);
JDBCUtils.closeResources(conn, null);
}
}
public int update(Connection conn, String sql, Object ...args){
//PreparedStatement object take in charge of curd operation
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(sql);
//填充占位符
for(int i = 0;i < args.length;i++){
ps.setObject(i+1, args[i]);
}
return ps.executeUpdate();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//close all of resources
JDBCUtils.closeResources(null, ps);
}
return 0;
}
这种主动回滚很像DFS,DFS回退的时候需要回滚answer数组的数据
innodb存储引擎中一条sql写入的详细流程–提交数据
第0步:会先去看缓冲区有没有这条数据,如果有就不进行缓存,直接进入第三步。
第1步:会将要修改的那一行数据所在的一整页加载到缓冲池Buffer Pool中
第2步:将旧值写入undo日志中,便于回滚以及mvcc机制的运作
第3步:将Buffer Pool中的数据更新为新的数据。
第4步:写入redo日志缓冲池,redo日志的内容实际和binlog差不多,但是作用不同
// 这一步之前都是在缓存中进行的,十分的高效。下面的需要和磁盘交互了。
至于什么时候刷新buffer log到redo log日志,有innodb_flush_log_at_trx_commit参数可以控制
同时innodb写入磁盘用了两段提交。具体可看:MySQL两阶段提交
第5步:准备提交事务前,将redo日志写入磁盘
第6步:准备提交日志前,将binlog日志写入磁盘
第7步:将commit标记写到redo日志中,事务提交完成。该操作时为了保证事务提交后redo和binlog数据一致性
第8步:将缓冲区的数据写入磁盘,注意这里的写入不是及时写入的,而是随机的。
可以看到,一旦提交数据,紧接着就是写入磁盘。
数据库事务的ACID属性
- 原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发
生。 - 一致性(Consistency) 事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
- 隔离性(Isolation) 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的
数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 - 持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其
他操作和数据库故障不应该对其有任何影响。
关于这个一致性:
(1)有比较才有一致
(2)事务执行前后数据库中的数据的完整性和一致性进行比较
(3)比如,两个用户转账,转账前后的总额不能发生变化
关于隔离性:
(1)基于事务的原子性,一个事务不能干扰另一个事务的执行
(2)高并发对事务的隔离构成了挑战,如果直接锁定事务,并发性能下降
(3)类似于多线程,数据表中相同位置的数据就相当于是多线程的共享资源
数据库事务的隔离性
隔离性有他的适用范围,在购物这个场景下,我们搜索一个商品,但是没有。如果使用最强的隔离,那么你一直刷新都是没有,这显然不符合现实要求。这种场景下不需要那么强的隔离
数据库的并发问题
与其说是问题,不如说是需求,并发带来的新的需求:
(1)一个事务能否读取另一个事务未提交的数据
(2)一个事务在他的生命周期里能不能对同一个位置的数据读取的结果都是一样的
(3)一个事务在他的生命周期里能不能对某张数据表的查询结果始终一样
数据库事务并发带来的问题:
(1)脏读: 对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, T1读取的内容就是临时且无效的
(2)不可重复读: 对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字段, 值就不同了
(3)幻读:对于两个事务T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行。之后, 如果 T1 再次读取同一个表, 就会多出几行
四种隔离级别
为了解决上面的三个问题,设计了隔离等级:
按顺序说明不同隔离级别解决了什么问题:
(1)都没解决
(2)解决脏读
(3)解决脏读,不可重复读
(4)都解决了
一般只用得到(2)(3),默认为(3)
oracle数据库支持(2)(4)默认为(2)
mysql数据库设置事务的隔离级别
//读取当前事务的隔离级别
System.out.println(conn.getTransactionIsolation());
//主动设置隔离级别
conn.setTransactionIsolation(4);
七. DAO及其实现类
DAO:数据访问对象
将对数据库的各种操作进行了封装,原本的结构是java->JDBC->mysql
改了之后,java->DAO->JDBC->mysql
为什么要进一步抽象?
避免重复造轮子。使代码模块化,更易于维护和扩展。之前我们就遇到这样的场景,在不改变原有系统功能的情况下,通过抽象出的结构为系统添加新功能
BaseDAO
将其设计为一个抽象父类,里面包含增删改查相关操作的通用方法
实现类
在BaseDAO这个抽象父类的基础上,为每张表设计再设计一套规范,也就是接口。最后再设计一个类,一方面继承BaseDAO,一方面实现接口
也就是说,对数据库中的每张表设计一个映射类xxx,一个接口xxxDAO,一个xxxDAOImpl类.比如说对于customers数据表,映射类Cutomer,接口CutomerDAO,实现类CustomerDAOImpl
综合来说,实现类要利用继承于抽象父类的方法去重写接口中的抽象方法
BaseDAO抽象类
封装了增删改查等通用操作,从外部传入连接,sql语句以及填充占位符的数据,通用操作也适用于不同的数据表,而且支持事务
其中以查询最为复杂,需要利用反射机制来支持对不同数据表的查询
public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object ...args){
//获取预编译的sql语句
PreparedStatement ps = null;
//接收返回的结果集
ResultSet rs = null;
try {
//获取连接
conn = JDBCUtils.getConnection();
//获取预编译的sql语句
ps = conn.prepareStatement(sql);
//填充占位符
for(int i = 0;i < args.length;i++){
ps.setObject(i+1, args[i]);
}
//接收返回的结果集
rs = ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//从元数据中得到列数
int columnCount = rsmd.getColumnCount();
//创建容器List
ArrayList<T> list = new ArrayList<T>();
//从结果集中取数据
while(rs.next()){
//接收数据的映射类对象
T t = clazz.newInstance();
//循环取出一行中每个字段的数据
for(int i = 1;i <= columnCount;i++){
//获取每个字段的数据
Object columnValue = rs.getObject(i);
//通过元数据取得列名
//String columnName = rsmd.getColumnName(i);
String columnLabel = rsmd.getColumnLabel(i);
//通过反射为order对象的属性赋值
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
//向集合中添加映射类的对象
list.add(t);
}
return list;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//关闭资源
JDBCUtils.closeResources(null, ps, rs);
}
return null;
}
xxxDAO接口
接口中定义了一系列将用于xxx数据表的可能需要的操作
注意每个抽象方法的形参,都没有sql语句
其实这就是一种设计思路,将sql语句封装了,隐藏了
/**
* @description:此接口用于规范针对于Customers表的常用操作
*/
public interface CustomerDAO {
/**
* @description:将Customer实例添加到数据库中
*/
void insert(Connection conn, Customer cust);
/**
* @description:根据指定id删除表中的一条记录
*/
void deleteById(Connection conn, int id);
/**
* @description:使用内存中的Customer实例修改表中指定的记录
*/
void update(Connection conn, Customer cust);
/**
* @description:查询customers表中指定id的一条记录
*/
Customer getCustomerById(Connection conn, int id);
/**
* @description:查询表中的所有记录
*/
List<Customer> getAll(Connection conn);
/**
* @description:返回表中记录的总数
*/
Long getCount(Connection conn);
/**
* @description:返回数据表中最大的生日
*/
Date getMaxBirth(Connection conn);
}
xxxDAOImpl实现类
使用继承于抽象父类BaseDAO的方法重写接口中的抽象方法
注意,sql语句被封装到了接口里面,不允许从外部传入sql语句
public class CustomerDAOImpl extends BaseDAO implements CustomerDAO{
@Override
public void insert(Connection conn, Customer cust) {
String sql = "insert into customers(name,email,birth) values(?,?,?)";
//占位符填充所用到的数据来自于映射类Customer的一个实例
update(conn, sql, cust.getName(),cust.getEmail(),cust.getBirth());
}
@Override
public void deleteById(Connection conn, int id) {
String sql = "delete from customers where id = ?";
update(conn, sql,id);
}
@Override
public void update(Connection conn, Customer cust) {
String sql = "update customers set name = ?, email = ?, birth = ? where id = ?";
update(conn, sql, cust.getName(),cust.getEmail(),cust.getBirth(),cust.getId());
}
@Override
public Customer getCustomerById(Connection conn, int id) {
String sql = "select id,name,email,birth from customers where id = ?";
Customer customer = getInstance(conn, Customer.class, sql, id);
return customer;
}
@Override
public List<Customer> getAll(Connection conn) {
String sql = "select id,name,email,birth from customers";
List<Customer> list = getForList(conn, Customer.class, sql);
return list;
}
@Override
public Long getCount(Connection conn) {
String sql = "select count(*) from customers";
return getValue(conn, sql);
}
@Override
public Date getMaxBirth(Connection conn) {
String sql = "select max(birth) from customers";
return getValue(conn, sql);
}
}
总结
我认为就是几句话:
BaseDAO作为一个抽象父类,被xxxDAOImpl实现类继承
每张数据表设计一个映射类,一个接口,一个实现类
实现类通过继承于抽象父类的方法重写接口中的方法
将sql语句封装到接口中,不允许从外部传入sql语句
由此,当我们有新的增删改查的需求,就到接口中增加相应的抽象方法,再去实现类中重写这个抽象方法
对DAO的升级
前文已经说了,每张数据表都要设计一个映射类,一个接口,一个实现类
如何让实现类支持对某一张表的通用操作呢??
增删改不需要知道具体操作的哪一张表
// 通用的增删改操作---version 2.0 (考虑上事务)
public int update(Connection conn,String sql, Object... args)
但是,查询因为要接受返回的结果集,所以要用反射来确定使用哪一个映射类的实例来接收返回的数据
public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object ...args)
List<Customer> list = getForList(conn, Customer.class, sql);
将BaseDAO声明为泛型类
回顾实现类的程序,我们在设计时已经确定每个实现类都有对应的数据表,但是在上文的代码中,我们仍然显示地在声明映射类的类型
因此,我们将BaseDAO声明为泛型类public abstract class BaseDAO<T>
在实例化实现类时,获取实现类父类的泛型类型,这样一来就不需要在方法的形参里面再显示地声明映射类的类型
//在声明BaseDAO子类的时候,声明BaseDAO的泛型类型
public class CustomerDAOImpl extends BaseDAO<Customer> implements CustomerDAO
//在父类中添加属性用于保存泛型类型
//增加代码块用于获取泛型类型
public abstract class BaseDAO<T> {
//用于获取抽象父类的泛型
private Class<T> clazz = null;
{
// this.getClass().getGenericSuperclass() 当前对象(xxxDAOImpl的实例)的类型(xxxDAOImpl)的父类的泛型
Type genericSuperclass = this.getClass().getGenericSuperclass();
//强转为参数化类型
ParameterizedType paramType = (ParameterizedType)genericSuperclass;
Type[] typeArguments = paramType.getActualTypeArguments();
clazz = (Class<T>)typeArguments[0];//泛型的第一个参数
}
}
这样一来BaseDAO中的方法声明就变为
public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object ...args)
public List<T> getForList(Connection conn, String sql, Object ...args)
//*********************************************************
public <T> T getInstance(Connection conn, Class<T> clazz, String sql, Object ...args)
public T getInstance(Connection conn, String sql, Object ...args)
八. 数据库连接池
背景
前面提到过web项目的架构,客户端发送请求给服务器,服务器将需求传输给数据库服务器,去执行相应的操作,结果返回给服务器,服务器在返回给客户端
从一开始学习面向对象我就提到,面向对象是用来处理超大数量代码的,如果不理解面向对象为什么这样设计,先将项目的代码量扩大几个数量级
在这里也是一样,如果同时有几十万用户都向服务器发送访问数据库的请求会发生什么???
(1)数据库的连接资源并没有得到很好的重复利用。每次向数据库建立连接的时候都要将 Connection加载到内存中,再验证用户名和密码(得花费0.05s~1s的时间)。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间
(2)对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。java中的内存泄漏指的是丢失对象的地址导致其无法被回收
(3)不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃
数据库连接池技术
概述
就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去
比如说,购物软件向下翻页,每一次到达底部需要联网获取数据,有了连接池之后,就可以直接从连接池中取出一个连接
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个
开源的数据库连接池
JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现
(1)DBCP 是Apache提供的数据库连接池。tomcat 服务器自带dbcp数据库连接池。速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持。
(2)C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以。hibernate官方推荐使用
(3)Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
(4)BoneCP 是一个开源组织提供的数据库连接池,速度快
(5)Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池,但是速度不确定是否有BoneCP快
1,3,5
DataSource接口
DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池
DataSource用来取代DriverManager来获取Connection,获取速度快,同时可以大幅度提高数据库访问速度
数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可
关于开源连接池的使用
和mysql驱动一样,使用数据库连接池也需要导入jar包
前文提到java设计了DataSource接口,连接池厂商需要提供接口的实现类
DataSource接口又继承了javax.sql中的CommonDataSource接口
C3P0连接池的实现
导入jar包
暴露第三方API的写法
用了好几个类
ComboPooledDataSource类:来自API
DataSources类:来自API
public void testGetConnection() throws Exception{
//cpds就是我们获取到的C3P0数据库连接池
ComboPooledDataSource cpds = new ComboPooledDataSource();
//com.mysql.jdbc.Driver是java中Driver接口的实现类
//相当于加载并注册驱动
cpds.setDriverClass( "com.mysql.jdbc.Driver" ); //loads the jdbc driver
//url
cpds.setJdbcUrl( "jdbc:mysql://localhost:13306/test" );
//设置用户名和密码
cpds.setUser("root");
cpds.setPassword("123456");
//设置连接数量
//初始连接数
cpds.setInitialPoolSize(10);
Connection conn = cpds.getConnection();
System.out.println(conn);
//销毁连接池,DataSources不是DataSource,前者是jar包中的类,后者是java中的接口
// DataSources.destroy(cpds);
}
使用配置文件隐藏第三方API
配置文件的编写c3p0提供了相应的写法
(1)默认的配置文件名为:c3p0-config.xml
(2)底层默认访问c3p0-config.xml文件,因此这个不能改
获取连接时,读取的字段是,从而获取基本信息和相关设置
(3)基本信息的name严格限定,只能是下面的四个
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<named-config name="helloc3p0">
<!-- 提供获取连接的四个基本信息:用户名,密码,url,Driver接口的实现类 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:13306/test</property>
<property name="user">sfsdf</property>
<property name="password">sdfsf</property>
<!-- 进行数据库管理的基本信息 -->
<!-- 当数据库连接池中的连接数不够时,c3p0一次性向数据库服务器申请的连接数 -->
<property name="acquireIncrement">5</property>
<!-- c3p0数据库连接池中初始化时的连接数 -->
<property name="initialPoolSize">10</property>
<!-- c3p0数据库连接池维护的最少连接数 -->
<property name="minPoolSize">10</property>
<!-- c3p0数据库连接池维护的最多的连接数 -->
<property name="maxPoolSize">100</property>
<!-- c3p0数据库连接池最多维护的Statement的个数 -->
<property name="maxStatements">50</property>
<!-- 每个连接中可以最多使用的Statement的个数 -->
<property name="maxStatementsPerConnection">2</property>
</named-config>
</c3p0-config>
有个配置文件,获取连接池中的连接就很方便了
在实例化ComboPooledDataSource类的时候,在带参构造器中声明要访问的字段
public void testGetConnection1() throws Exception{
//ComboPooledDataSource是java中DataSource接口的实现类
//cpds就是我们获取到的C3P0数据库连接池
ComboPooledDataSource cpds = new ComboPooledDataSource("helloc3p0");
Connection conn = cpds.getConnection();
System.out.println(conn);
}
上面的写法有问题,相当于没调用一次getConnection方法就造一个连接池
//cpds就是我们获取到的C3P0数据库连接池
private static ComboPooledDataSource cpds = new ComboPooledDataSource("helloc3p0");
public static Connection getConnection1() throws SQLException{
Connection conn = cpds.getConnection();
System.out.println("成功从连接池获取连接");
return conn;
}
DBCP连接池的实现
基本上和C3P0的实现一致
从继承树上来说更为清晰,BasicDataSource实现类直接实现了javax.sql.DataSource接口
而C3P0使用的ComboPooledDataSource类继承了多个父类,实现了多个接口public final class ComboPooledDataSource extends AbstractPoolBackedDataSource implements PooledDataSource, Serializable, Referenceable
,往深一层,由PooledDataSource接口继承DatSource接口
另外,配置文件的编写也更为简单,和DriverManager采用同样的方法,都利用了java中的Properties类,编写一个.properties格式的配置文件
配置文件
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:13306/test
username=root
password=123456
initialSize=10
两种获取连接的方法
/**
* @description:测试DBCP数据库连接池
* @author : cale
* @throws SQLException
* @date 2022年12月7日下午8:59:02
*/
@Test
public void testGetConnection() throws SQLException{
//BasicDataSource类是javax.sql.DataSource接口的实现类
//创建了一个DBCP数据库连接池
// DataSource source = new BasicDataSource();
BasicDataSource source = new BasicDataSource();
//设置基本信息,url,Driver接口的实现类,用户名,密码。不同的数据库连接池对这四个属性的命名
//有差异
source.setDriverClassName("com.mysql.jdbc.Driver");
source.setUrl("jdbc:mysql://localhost:13306/test");
source.setUsername("root");
source.setPassword("123456");
//还可以设置数据库管理的相关属性
source.setInitialSize(10);
source.setMaxActive(50);
Connection conn = source.getConnection();
System.out.println(conn);
}
/**
* @description:使用配置文件创建DBCP数据库连接池
* @author : cale
* @throws Exception
* @date 2022年12月7日下午9:15:22
*/
@Test
public void testGetConnection1() throws Exception{
Properties pros = new Properties();
//获取dbcp配置文件的输入流的两种方式
//使用类加载器
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("dbcp.properties");
//使用节点输入字节流
// FileInputStream is = new FileInputStream(new File("src/dbcp.properties"));
pros.load(is);
DataSource source = BasicDataSourceFactory.createDataSource(pros);
Connection conn = source.getConnection();
System.out.println(conn);
}