基于druid连接池的学习,如果每次都要创建连接池并没有减少麻烦,我们需要封装成一个工具类来彻底优化。
工具类封装v1.0
我们封装一个工具类,内部包含连接池对象,同时对外提供连接的方法和回收连接的方法。
总结
v1.0版本工具类
内部包含一个连接池对象,并且对外提供获取连接和回收连接方法!
小建议:
工具类的方法,推荐写成静态,外部调用会更加方便!
实现:
属性 连接池对象 [实例化一次]
单例模式
static{
全局实例化调用一次
}
方法
对外提供连接的方法
回收外部传入连接的方法
封装流程
- 创建一个成员属性 dataSource 设定为私有化且静态的,初始化值为null
private static DataSource dataSource = null;//连接池对象,全局的静态的
- 书写 对外提供连接 的成员方法,返回值先设置为null
public static Connection getConnection() throws SQLException {
return null;
}
- 书写 对外提供回收 的成员方法,无返回值,有参数列表,方法体只用关闭参数中传递进来的连接池就可
public static void freeConnnection(Connection connection) throws SQLException {
connection.close();//连接池的连接,调用close就是回收!
}
- 书写一个 静态代码块 ,置于成员方法上,成员属性下。方法中完成创建连接池对象的流程,创建Properties对象,通过类加载器的方式加载properties文件,调用load方法。
public class JdbcUtils {
private static DataSource dataSource = null;//连接池对象,全局的静态的
static {
//初始化连接对象
Properties properties = new Properties();
InputStream ips = JdbcUtils.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
//实例化连接池
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- 继续静态代码块,给成员变量dataSource赋值
DruidDataSourceFactory.createDataSource(properties);
,修改对外提供连接的方法,把返回值改为dataSource.getConnection();
这样调用方法就可以获取一个连接池了。
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
下面是演示工具类使用案例
工具类封装v2.0
我们每次调用getConnection这样就又会使得事务中 Service层 和 Dao层 使用不同连接。我们明确知道当前的Service层和Dao层是同一线程,我们能不能再同一个线程中不同位置的方法,获取相同连接,Service层以后就不用再参数列表中传递连接。
考虑事务的情况下!如何一个线程的不同方法获取同一个连接
这里我们是需要使用 ThreadLocal线程本地变量 。ThreadLocal可以为同一个线程存储共享变量。
ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个 ThreadLocalMap<ThreadLocal,Object>
,其key就是一个ThreadLocal,而Object即为该线程的共享变量。而这个map是通过ThreadLocald的 set 和 get方法 操作的。对于同一个static ThreadLocal,不同线程只能从get,set,remove自己的变量,而不会影响其他变量。
- ThreadLocal对象.get :获取ThreadLocal中当前线程共享变量的值。
- ThreadLocal对象.set :设置ThreadLocal中当前线程共享变量的值。
- ThreadLocal对象.remove :移除ThreadLocal中当前线程共享变量的值
改写工具类
- 全局声明一个线程本地变量
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
改写对外提供连接的方法
- 通过 get方法 得到连接
- 如果是第一次没有的情况判断。如果没有连接池获取,在用 set方法 存放到线程本地变量中
- 返回连接
//先去查看线程本地变量中是否存在
Connection connection = tl.get();//通过get方法获取线程本地变量连接
//第一次没有
if(connection == null){
//线程本地变量没有,连接池获取
connection = dataSource.getConnection();
//获取出来的连接存入线程本地变量
tl.set(connection);
}
return connection;
改写对外提供回收连接的方法
- 删除参数列表, 因为get方法和回收方法一定在统一个连接
- 通过 get方法 得到连接
- 判断不是空,那么remove()清空本地变量数据
- 事务状态回归,因为连接池可能被开启事务了false,要回到默认的状态
- 回收到连接池
//不需要传回,因为get方法和回收方法一定在统一个连接
public static void freeConnnection() throws SQLException {
Connection connection = tl.get();
if(connection != null){
tl.remove();//清空线程本地变量数据
connection.setAutoCommit(true);//事务状态回归 为什么?因为连接池可能被开启事务了false,要回到默认的状态
connection.close();//回收到连接池即可
}
}
最终源码:
public class JdbcUtilsV2 {
//1.声明一个连接池
private static DataSource dataSource = null;
//2.创建一个线程本地变量
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
//3.静态代码块实例化连接池对象,同v1.0
static {
Properties properties = new Properties();
InputStream ips = JdbcUtilsV2.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//4.书写对外提供连接的方法
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
//第一此获取连接池对象的判断
if (connection == null) {
connection = dataSource.getConnection();
tl.set(connection);
}
return connection;
}
//5.对外回收连接的方法
public static void freeConnecion() throws SQLException {
Connection connection = tl.get();
if(connection != null){
//把连接从线程本地变量移除
tl.remove();
//事务状态回归
connection.setAutoCommit(true);
//关闭线程
connection.close();
}
}
}
我们重新调整Dao和Service层
- 删除Dao层中的创建连接,全部添加
Connection connection = JdbcUtilsV2.getConnection();
,并删去参数列表中的connnection。 - 在Service中transfer中改写创建连接的方式
Connection connection = JdbcUtilsV2.getConnection();
,并删去参数列表中的connnection, finally 中调用JdbcUtilsV2.freeConnnection();
在第一次运行Service层中首先会通过工具类找到线程本地变量,返回一个连接并存入线程本地变量;然后设置事务提交变量为false,开启事务。然后执行Dao层的方法,使用的同一个连接。最后执行finally方法回收
Service层:
public class BankService {
@Test
public void start() throws Exception {
//直接在测试方法中调用Service层
//二狗子 给驴蛋蛋转500
transefer("lvdandan","ergouzi",500);
//现在我们把减钱独立一个事务中,加钱独立在另一个事务中,它们两互不印象,会出现什么问题呢?
}
/*
事务添加是在业务方法中!
利用try—catch代码块,开启事务和提交事务,和事务回滚!
将connection传入dao层即可,而dao只负责使用,不要close();
*/
public void transefer(String addAccount,String subAccount,int money) throws Exception {
//创建Dao类的对象
BankDao bankDao = new BankDao();
//一个事务的最基本要求,必须是同一个链接对象,connection
//一个转账方法属于一个事务,(加钱 减钱)
Connection connection = JdbcUtilsV2.getConnection();
try{
//开启事务,关闭MySQL的自动事务提交
//关闭事务提交
connection.setAutoCommit(false);
//执行数据库动作
//执行加钱减钱
bankDao.add(addAccount,money);
System.out.println("----------------------");
bankDao.sub(subAccount,money);
//事务提交
connection.commit();
}catch (Exception e){
//事务回滚
connection.rollback();
//我们捕捉异常为了做事务回滚,但是不能隐藏异常信息,我们选择抛出
//抛出
throw e;
}finally {
JdbcUtilsV2.freeConnnection();
}
}
}
Dao层:
/*
表的数据库方法存储类
*/
public class BankDao {
/*
加钱的数据库操作方法(具体的JDBC动作)
String account 加钱的符号
int money 加钱的金额
*/
public void add(String account, int money) throws Exception {
Connection connection = JdbcUtilsV2.getConnection();
//加钱就是所谓的修改
//3.编写SQL语句结构
String sql = "update t_bank set money = money + ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1, money);
preparedStatement.setObject(2, account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
System.out.println("加钱成功!");
}
/*
减钱的数据库操作方法(具体的JDBC动作)
String account 减钱的符号
int money 减钱的金额
*/
public void sub(String account, int money) throws Exception {
Connection connection = JdbcUtilsV2.getConnection();
//3.编写SQL语句结构
String sql = "update t_bank set money = money - ? where account = ?";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1, money);
preparedStatement.setObject(2, account);
//6.发送sql语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
System.out.println("减钱成功!");
}
}
工具类最终写法
public class JdbcUtilsV2 {
//1.声明一个连接池
private static DataSource dataSource = null;
//2.创建一个线程本地变量
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
//3.静态代码块实例化连接池对象,同v1.0
static {
Properties properties = new Properties();
InputStream ips = JdbcUtilsV2.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//4.书写对外提供连接的方法
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
//第一此获取连接池对象的判断
if (connection == null) {
connection = dataSource.getConnection();
tl.set(connection);
}
return connection;
}
//5.对外回收连接的方法
public static void freeConnecion() throws SQLException {
Connection connection = tl.get();
if(connection != null){
//把连接从线程本地变量移除
tl.remove();
//事务状态回归
connection.setAutoCommit(true);
//关闭线程
connection.close();
}
}
}
高级应用层封装
针对于 注册驱动 , 获取连接 , 回收资源 我们在上面进行了一定程度的优化,但是还有剩下其他步骤我们使用 BaseDao 再次进行优化。
思路:我们把增删改查重复度很高的代码抽取成一个父类BaseDao然后父类中添加前面的工具类JdbcUtilsV2即可。
注意:需要将BaseDao设置为抽象类 ,抽象类可以包含具体的方法实现,子类可以直接继承这些方法,从而避免了代码的重复编写。抽象类的主要目的是为了被子类继承和扩展,而不是被直接使用。
我们只需要把封装两类,一类是DQL(查询)语句,一类是非DQL(增删改)语句
非DQL(增删改)语句封装
/*
封装简化DQL语句 参数一:sql语句 参数二:可变参数代替占位符 可变参数必须存在参数列表的最后一位
sql: 带占位符的SQL语句
params 占位符赋值 注意,传入占位符的值,必须等于SQL语句?位置!
return 返回影响的行数
*/
public int executeUpdate(String sql,Object...params) throws SQLException {
//获取连接
Connection connection = JdbcUtilsV2.getConnection();//工具类是不捕获异常
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
//可变参数可以当作数组使用
//从1开始 到params.length的长度
if(params != null && params.length != 0){
for (int i = 1; i <= params.length; i++) {
preparedStatement.setObject(i, params[i-1]);
}
}
//6.发送SQL语句
//DML类型
int rows = preparedStatement.executeUpdate();
preparedStatement.close();
//是否回收连接,需要考虑是不是事务!
if (connection.getAutoCommit()) {
//没有开启事务
//没有开启事务,正常回收连接,Dao层管
JdbcUtilsV2.freeConnnection();
}
//connection.setAutoCommit(false);//开启事务了,不要管连接即可!业务层处理
return rows;
}
这里需要注意 preparedStatement.setObject(i,params[i-1]);
赋值i,最后取值要-1.防止越界
在对应实现类中,我们需要完成以下步骤
- 让该类 继承BaseDao类 ,拥有父类的抽象方法
- 保留sql语句删除jdbc流程中的其他操作
- 调用executeUpdate方法传入sql语句,然后盯着占位符一个一个给占位符复制
- 返回int结果
改写后的PSCURDPart部分代码
public class PSCURDPart extends BaseDao {
@Test
public void testInsert() throws ClassNotFoundException, SQLException {
String sql = "insert into t_user(account,password,nickname) values( ? , ? , ? )";
int i = executeUpdate(sql, "测试333", "3333", "ergouzi");
System.out.println("i = " + i);//返回影响函数
}
@Test
public void testUpdate() throws ClassNotFoundException, SQLException {
String sql = "update t_user set nickname = ? where id = ?";
int rows = executeUpdate(sql, "新的nickname", 3);
}
@Test
public void testDelete() throws ClassNotFoundException, SQLException {
String sql = "delete from t_user where id = ?";
int i = executeUpdate(sql, 3);
}
}
DQL(查询)语句封装
/*
非DQL语句封装方法 —>返回值 固定为int
DQL语句封装方法 —>返回值 是什么类型呢?是某一个类型的实体类集合
并不是List<Map> map key和value自定义!不用先设定好
map 没有数据校验机制
map 不支持反射操作
数据库中的数据 -> java实体类
table
t_user
id
account
password
nickname
java
User
id
account
password
nickname
表中 -> 一行 -> java类的一个对象 -> 多行 -> List<Java实体类> list;
DQL -> List<Map> -> 一行 -> map -> List<Map>
<T> 声明一个方法泛型,不确定类型
1.确定泛型User.class T = User
2.要使用反射技术属性赋值,实例化对象装值
第一个<T>声明一个泛型是<T>
第二个<T>表示我声明一个集合返回值类型是一个<T>
第三个<T>用于代表class泛型对应的值
public <T> List<T> executeQuery(Class<T> clazz,String sql,Object...params){
//书写反射代码实现具体类
}
*/
/*
将查询结果封装到一个实体类集合
clazz 要接值的实体类集合的模板对象
sql 查询语句,要求列名或者别名等于实体类的属性名! u_id as uId -> uId
params 占位符的值,要和?位置对应传递
return 查询的实体类集合
<T> 声明的结果泛型
*/
public <T> List<T> executeQuery(Class<T> clazz, String sql, Object... params) throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//获取连接
Connection connection = JdbcUtilsV2.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
//为了程序的兼容性 做一个非空判断
if (params != null && params.length != 0) {
for (int i = 1; i <=params.length; i++) {
preparedStatement.setObject(i,params[i - 1]);
}
}
//6.发送SQL语句,返回一个结果集
ResultSet resultSet = preparedStatement.executeQuery();
//7.结果及解析
List<T> list = new ArrayList<>();
//获取列的信息对象
//metaData装的当前结果集列的信息对象!将列的信息和列的值分开存放(他可以获取列的名称根据下角标,可以获取列的数量)
ResultSetMetaData metaData = resultSet.getMetaData();
//获取列的数量
//有了它以后,我们就可以水平遍历列!
int columnCount = metaData.getColumnCount();
while (resultSet.next()) {
//调用类的无参构造函数实例化对象,所以说类必须要有无参构造
T t = clazz.newInstance();
//一行数据,对应一个 T 类型对象
//自动遍历列,注意,要从1开始,并且小于等于总列数!
for (int i = 1; i <= columnCount; i++) {
//对象的属性值
Object value = resultSet.getObject(i);
//获取指定列下角标的列的名称!ResultSetMetaData
//对象的属性名
String propertyName = metaData.getColumnLabel(i);//获取i对应列的标识
//反射,给对象的属性值进行赋值
Field field = clazz.getDeclaredField(propertyName);
field.setAccessible(true);//属性可以设置,打破private私有化修饰限制
/*
参数1:要赋值的对象 如果属性是静态,第一个参数可以为空
参数2:具体的属性值
*/
field.set(t,value);
}
//一行数据的所有列全部存到了map中!
//将map存储到集合中即可
list.add(t);
}
//关闭资源
resultSet.close();
preparedStatement.close();
if(connection.getAutoCommit()){
//没有事务,可以关闭
JdbcUtilsV2.freeConnnection();
}
return list;
}
在项目中我们 改造JDBC 需要四步操作
- 导入依赖的jar包(配置文件) druid和mysql8+驱动
- 编写配置文件 druid.properties 根据需要去调整对应value值。配置文件必须写在 src 下方(类加载器)
# key = value => java Properties读取 (key | value)
# durid配置的key固定命名
# druid连接池需要的配置参数,key固定命名
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=abc123
url=jdbc:mysql:///atguigu
initialSize=5
- 创建util文件夹,导入 工具类JdbcUtilsV2 BaseDao类。
- 创建dao文件夹,创建 Dao类 ,针对于Service层进行一定的修改,根据参数调Dao层方法(没有就alt+回车创建),在 Dao类 中写增删改查的数据库操作,并且继承 BaseDao类。相当于把 Service(业务)层 的方法全部调 Dao层 的实现, Dao层 实现数据库的操作。
我书写这篇文章的初衷就是总结学习的进度,遗忘之际可以拿出来翻看,如有不对的地方还望指正,多多海涵。
[...]笔记基于Spring6演示CURD操作,不同于以前JDBC,现在的事务操作时基于注解和Spring技术完成之前有过对于JDBC的介绍和工具类的封装JDBC工具类封装准备工作:父工程中添加依赖<dependencies>[...]