一 JDBC
1.1 JDBC概述
JDBC:Java Database Connectivity,它是代表一组独立于任何数据库管理系统(DBMS)的API,声明在java.sql与javax.sql包中,是SUN(现在Oracle)提供的一组接口规范。由各个数据库厂商来提供实现类,这些实现类的集合构成了数据库驱动jar。
即JDBC技术包含两个部分:
(1)java.sql包和javax.sql包中的API
因为为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。
(2)各个数据库厂商提供的jar
因为各个数据库厂商的DBMS软件各有不同,那么内部如何通过sql实现增、删、改、查等管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。
1.2 JDBC连接基本步骤
1.2.1 引入mysql驱动jar
(1)在模块路径下建一个文件夹“lib”,把mysql的驱动jar放到里面
MySQL8.0:mysql-connector-java-8.0.25.jar
(2)在jdbclibs文件夹上右键-->Add as Library...
(3)填写库名称、选择这个库应用范围(模块)-->选择使用的具体模块
1.2.2 Java代码连接MySQL数据库
步骤:
1. 注册驱动【依赖的jar包 进行安装】
2. 获取连接【connection建立连接】
3. 创建发送sql语句对象【statement 创建发送sql语句的statement】
4. 发送sql语句,并获取返回结果【statement发送sql语句到数据库 并且取得返回结构】
5. 结果集解析【将result结果解析出来】
6. 资源关闭【释放resultset、statement、connection】
准备:
CREATE DATABASE mytest;
USE mytest;
CREATE TABLE t_user(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
PASSWORD VARCHAR(64) NOT NULL COMMENT '密码',
nickname VARCHAR(20) NOT NULL COMMENT '昵称');
INSERT INTO t_user(account,PASSWORD,nickname) VALUES
('root','123456','经理'),('admin','666666','管理员');
1.3 JDBC连接基本实现
###1.3.1 DQL操作
/**
* Description: 利用jdbc技术,完成用户数据查询工作
*
* TODO: 步骤总结 (6步)
* 1. 注册驱动
* 2. 获取连接
* 3. 创建statement
* 4. 发送SQL语句,并获取结果
* 5. 结果集解析
* 6. 关闭资源
*/
public class JdbcBasePart {
public static void main(String[] args) throws SQLException {
//1.注册驱动
/**
* TODO: 注意
* 8+ Driver -> com.mysql.cj.jdbc.Driver
*/
DriverManager.registerDriver(new Driver());
//2.获取连接
/**
* 面向接口编程
* java.sql 接口 = 实现类
*/
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mytest",
"root",
"root");
//3.创建小车
Statement statement = connection.createStatement();
//4.发送SQL语句
String sql = "select id,account,password,nickname from t_user ;";
ResultSet resultSet = statement.executeQuery(sql);
//5.结果集解析
while (resultSet.next()){
int id = resultSet.getInt("id");
String account = resultSet.getString("account");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
System.out.println(id+"::"+account+"::"+password+"::"+nickname);
}
//6.关闭资源 【先开后关】
resultSet.close();
statement.close();
connection.close();
}
}
1.3.2 DML操作
@Test
public void testDml() throws SQLException, ClassNotFoundException {
//1.注册驱动
/**
* DriverManager.registerDriver(new Driver());注册两次驱动!
*/
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mytest",
"root",
"root");
//3.创建小车
Statement statement = connection.createStatement();
//4.发送SQL语句
String sql = "delete from t_user where id = 1 ;";
int rows = statement.executeUpdate(sql);
System.out.println("rows = " + rows);
statement.close();
connection.close();
}
二 轻松处理各种问题
2.1 产生sql拼接问题(Statement)
模拟登录,控制台输入账号和密码,判断是否登陆成功成功!
/**
* Description: 输入账号密码,模拟用户登录!
*/
public class JdbcStatementLoginPart {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1.输入账号和密码
Scanner scanner = new Scanner(System.in);
String account = scanner.nextLine();
String password = scanner.nextLine();
scanner.close();
//2.jdbc的查询使用
//1:DriverManager.registerDriver(new Driver());调用两次。不用
//2:new Driver() 频繁修改不优雅
//注册一次驱动 方法三
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
//固定方法固定剂
//创建statement
Statement statement = connection.createStatement();
//执行SQL语句 [动态SQL语句,需要字符串拼接]
String sql = "select * from t_user where account = '"+account+"' and password = '"+password+"' ;";
/**
* sql分类: DDL(容器创建,修改,删除) DML(插入,修改,删除) DQL(查询) DCL(权限控制) TPL(事务控制)
* ResultSet 结果集对象 = executeQuery(DQL语句)
* int响应行数 = executeUpdate(非DQL语句)
*/
ResultSet resultSet = statement.executeQuery(sql);
//进行结果集对象解析
if (resultSet.next()){
//只要向下移动,就是有数据 就是登录成功!
System.out.println("登录成功!");
}else{
System.out.println("登录失败!");
}
//关闭资源
resultSet.close();
statement.close();
connection.close();
}
}
2.2 避免sql注入问题(PreparedStatement)
/**
* Description: 使用预编译Statement解决注入攻击问题
*/
public class JdbcPreparedStatementLoginPart {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1.输入账号和密码
Scanner scanner = new Scanner(System.in);
String account = scanner.nextLine();
String password = scanner.nextLine();
scanner.close();
//2.jdbc的查询使用
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
//创建preparedStatement
//connection.createStatement();
//TODO 需要传入SQL语句结构
//TODO 要的是SQL语句结构,动态值的部分使用 ? , 占位符!
//TODO ? 不能加 '?' ? 只能替代值,不能替代关键字和容器名
String sql = "select * from t_user where account = ? and password = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
//给占位符赋值! 从左到右,从1开始!
/**
* int 占位符的下角标
* object 占位符的值
*/
preparedStatement.setObject(2,password);
preparedStatement.setObject(1,account);
//这哥们内部完成SQL语句拼接!
//执行SQL语句即可
ResultSet resultSet = preparedStatement.executeQuery();
//preparedStatement.executeUpdate()
//进行结果集对象解析
if (resultSet.next()){
//只要向下移动,就是有数据 就是登录成功!
System.out.println("登录成功!");
}else{
System.out.println("登录失败!");
}
//关闭资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
2.3 基于preparedStatement演示curd
/**
* 插入一条用户数据!
* 账号: test
* 密码: test
* 昵称: 测试
*/
@Test
public void testInsert() throws Exception{
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
//TODO: 切记, ? 只能代替 值!!!!! 不能代替关键字 特殊符号 容器名
String sql = "insert into t_user(account,password,nickname) values (?,?,?);";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setString(1, "test");
preparedStatement.setString(2, "test");
preparedStatement.setString(3, "测试");
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
System.out.println(rows);
//关闭资源close
preparedStatement.close();
connection.close();
}
/**
* 修改一条用户数据!
* 修改账号: test的用户,将nickname改为tomcat
*/
@Test
public void testUpdate() throws Exception{
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
//TODO: 切记, ? 只能代替 值!!!!! 不能代替关键字 特殊符号 容器名
String sql = "update t_user set nickname = ? where account = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setString(1, "tomcat");
preparedStatement.setString(2, "test");
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
if(rows>0){
System.out.println("修改成功”);
}else{
System.out.println("修改失败”);
}
//关闭资源close
preparedStatement.close();
connection.close();
}
/**
* 删除一条用户数据!
* 根据账号: test
*/
@Test
public void testDelete() throws Exception{
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
//TODO: 切记, ? 只能代替 值!!!!! 不能代替关键字 特殊符号 容器名
String sql = "delete from t_user where account = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setString(1, "test");
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
System.out.println(rows);
//关闭资源close
preparedStatement.close();
connection.close();
}
/**
* 查询全部数据!
* 将数据存到List<Map>中
* map -> 对应一行数据
* map key -> 数据库列名或者别名
* map value -> 数据库列的值
* TODO: 思路分析
* 1.先创建一个List<Map>集合
* 2.遍历resultSet对象的行数据
* 3.将每一行数据存储到一个map对象中!
* 4.将对象存到List<Map>中
* 5.最终返回
*注:如何获取列名?!!!!!!!!!????重要!!!
* TODO:
* 初体验,结果存储!
* 学习获取结果表头信息(列名和数量等信息)
*/
@Test
public void testQueryMap() throws Exception{
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
//TODO: 切记, ? 只能代替 值!!!!! 不能代替关键字 特殊符号 容器名
String sql = "select id,account,password,nickname from t_user ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值 本次没有占位符,省略
//发送查询语句
ResultSet resultSet = preparedStatement.executeQuery();
//注意只有查询是executeQuery(); 其他都是executeUpdate();
//创建一个集合
List<Map> mapList = new ArrayList<>();
//获取列信息对象
//metaData装的是当前列的信息对象(通过他可以获取列对应的下角标,或者是列的数量)
// 也可以通过下角标获取列的名称
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
//可以水平遍历列
while (resultSet.next()) {
//一行数据对应一个map
//取值用resultSet
//取位置(下角标)用metadata
Map map = new HashMap();
for (int i = 1; i <= columnCount; i++) {
//笨蛋写法:
//map.put("id",resultSet.getInt("id"));好几行,写法固定
//value:获取指定下角标的值 用resultSet key;用metaData
//getColumnLabel先获取列的别名,没有别名用列名,而getColumnName只是列名
//getObject获得当前行行的数据作为一个对象
map.put(metaData.getColumnLabel(i), resultSet.getObject(i));
}
mapList.add(map);
}
System.out.println(mapList);
//关闭资源close
preparedStatement.close();
connection.close();
resultSet.close();
}
2.4 获取自增长键值
/**
* 返回插入的主键!
* 主键:数据库帮助维护的自增长的整数主键!
* @throws Exception
*/
@Test
public void returnPrimaryKey() throws Exception{
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection(
"jdbc:mysql:///mytest?user=root&password=root");
//3.编写SQL语句结构
String sql = "insert into t_user (account,password,nickname) values (?,?,?);";
//4.创建预编译的statement,传入SQL语句结构
/**
* TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
* 告诉statement携带回数据库生成的主键!
*/
PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
//5.占位符赋值
statement.setObject(1,"towgog");
statement.setObject(2,"123456");
statement.setObject(3,"二狗子");
//6.执行SQL语句 【注意:不需要传入SQL语句】 DML
int i = statement.executeUpdate();
//7.结果集解析
System.out.println("i = " + i);
//一行一列的数据!里面就装主键值!固定用getGeneratedKeys!!!
ResultSet resultSet = statement.getGeneratedKeys();
resultSet.next();
//移动光标
int anInt = resultSet.getInt(1);//指向第一列
System.out.println("anInt = " + anInt);
//8.释放资源
statement.close();
connection.close();
}
2.5 批量数据插入处理
/**
*改动了三处:(1)路径(2)必写values,且后面不加;(3)装货addBatch()最后executeBatch();
* 批量细节:
* 1.url?rewriteBatchedStatements=true
* 2.insert 语句必须使用 values
* 3.语句后面不能添加分号;
* 4.语句不能直接执行,每次需要装货 addBatch() 最后 executeBatch();
*
* 批量插入优化!
* @throws Exception
*/
@Test
public void batchInsertYH() throws Exception{
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection
("jdbc:mysql:///mytest?rewriteBatchedStatements=true","root","root");
//3.编写SQL语句结构
String sql = "insert into t_user (account,password,nickname) values (?,?,?)";
//4.创建预编译的statement,传入SQL语句结构
/**
* TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
* 告诉statement携带回数据库生成的主键!
*/
long start = System.currentTimeMillis();
PreparedStatement statement = connection.prepareStatement(sql);
for (int i = 0; i < 10000; i++) {
//5.占位符赋值
statement.setObject(1,"ergouzi"+i);
statement.setObject(2,"lvdandan");
statement.setObject(3,"驴蛋蛋"+i);
//6.装车
statement.addBatch();
}
//发车! 批量操作!
statement.executeBatch();
long end = System.currentTimeMillis();
System.out.println("消耗时间:"+(end - start));
//7.结果集解析
//8.释放资源
connection.close();
}
2.6 事务处理
-
数据库表结构
-- 继续在mytest的库中创建银行表 CREATE TABLE t_bank( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键', account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号', money INT UNSIGNED COMMENT '金额,不能为负值') ; INSERT INTO t_bank(account,money) VALUES ('ergouzi',1000),('lvdandan',1000); -
代码结构设计
- 测试类
public class BankTest {
@Test
public void testBank() throws Exception {
BankService bankService = new BankService();
bankService.transfer("ergouzi", "lvdandan",
500);
}
}
- 业务类
public class BankService {
//一个事物最基本的是在同一个连接中connection,一个转账方法是一个事物,将connection传入dao
//实现层即可,dao层不用关闭connection,由事物统一关闭
/**
* 转账业务方法
* @param addAccount 加钱账号
* @param subAccount 减钱账号
* @param money 金额
*/
public void transfer(String addAccount,String subAccount, int money) throws ClassNotFoundException, SQLException {
System.out.println("addAccount = " + addAccount + ", subAccount = " + subAccount + ", money = " + money);
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection
("jdbc:mysql:///mytest", "root", "root");
int flag = 0;
//利用try代码块,调用dao
try {
//开启事务(关闭事务自动提交)
connection.setAutoCommit(false);
BankDao bankDao = new BankDao();
//调用加钱 和 减钱
bankDao.addMoney(addAccount,money,connection);
System.out.println("--------------");
bankDao.subMoney(subAccount,money,connection);
flag = 1;
//不报错,提交事务
connection.commit();
}catch (Exception e){
//报错回滚事务
connection.rollback();
throw e;
}finally {
connection.close();
}
if (flag == 1){
System.out.println("转账成功!");
}else{
System.out.println("转账失败!");
}
}
}
- 持久类
public class BankDao {
/**
* 加钱方法
* @param account
* @param money
* @param connection 业务传递的connection和减钱是同一个! 才可以在一个事务中!
* @return 影响行数
*/
public int addMoney(String account, int money,Connection connection) throws ClassNotFoundException, SQLException {
String sql = "update t_bank set money = money + ? where account = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setObject(1, money);
preparedStatement.setString(2, account);
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
System.out.println("加钱执行完毕!");
//关闭资源close
preparedStatement.close();
return rows;
}
/**
* 减钱方法
* @param account
* @param money
* @param connection 业务传递的connection和加钱是同一个! 才可以在一个事务中!
* @return 影响行数
*/
public int subMoney(String account, int money,Connection connection) throws ClassNotFoundException, SQLException {
String sql = "update t_bank set money = money - ? where account = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
preparedStatement.setObject(1, money);
preparedStatement.setString(2, account);
//发送SQL语句
int rows = preparedStatement.executeUpdate();
//输出结果
System.out.println("减钱执行完毕!");
//关闭资源close
preparedStatement.close();
return rows;
}
}
三 数据库连接池
3.1 什么是数据库连池
连接对象的缓冲区。负责申请,分配管理,释放连接的操作。
3.2 为什么使用数据库连接池
(1)不使用数据库连接池,每次都通过DriverManager获取新连接,用完直接抛弃断开,连接的利用率太低,太浪费。 (2)对于数据库服务器来说,压力太大了。我们数据库服务器和Java程序对连接数也无法控制,很容易导致数据库服务器崩溃。
我们就希望能管理连接。
- 我们可以建立一个连接池,这个池中可以容纳一定数量的连接对象,一开始,我们可以先替用户先创建好一些连接对象,等用户要拿连接对象时,就直接从池中拿,不用新建了,这样也可以节省时间。然后用户用完后,放回去,别人可以接着用。
- 可以提高连接的使用率。当池中的现有的连接都用完了,那么连接池可以向服务器申请新的连接放到池中。 直到池中的连接达到“最大连接数”,就不能在申请新的连接了,如果没有拿到连接的用户只能等待。
3.3 数据库连接池技术种类
- JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口(通常被称为数据源),该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
- DBCP 是Apache提供的数据库连接池,速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持
- C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
- Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
- BoneCP 是一个开源组织提供的数据库连接池,速度快
- Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池
3.4 如何使用德鲁伊数据库连接池
(1)引入jar包和引入mysql驱动jar方式一样
(2)创建数据库连接池对象
(3)获取连接
package com.mytest.pool;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class TestPool {
static int count = 0;
public static void main(String[] args) throws Exception {
Properties pro = new Properties();
pro.setProperty("driverClassName","com.mysql.cj.jdbc.Driver");
pro.setProperty("url","jdbc:mysql://localhost:3306/mytest");
pro.setProperty("username","root");
pro.setProperty("password","123456");
pro.setProperty("initialSize","5");//表示预先缓存5个连接
pro.setProperty("maxActive","10");//最多的连接数量10
pro.setProperty("maxWait","1000");//等待连接的时间,超过时间就报异常
DataSource ds = DruidDataSourceFactory.createDataSource(pro);
/* Connection connection = ds.getConnection();
System.out.println("connection = " + connection);*/
/* for(int i=1; i<=15; i++){
Connection connection = ds.getConnection();
System.out.println("第" + i+ "个connection = " + connection);
//这里没有写connection.close(),连接没有关闭,没有还给连接池,一直占用
// connection.close();
}*/
for(int i=1; i<=15; i++){
new Thread(){
@Override
public void run() {
try {
Connection connection = ds.getConnection();
System.out.println("第" + (count++)+ "个connection = " + connection);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
| 配置 | 缺省 | 说明 |
|---|---|---|
| name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:”DataSource-” + System.identityHashCode(this) | |
| jdbcUrl | 连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | |
| username | 连接数据库的用户名 | |
| password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:github.com/alibaba/dru… | |
| driverClassName | 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) | |
| initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
| maxActive | 8 | 最大连接池数量 |
| maxIdle | 8 | 已经不再使用,配置了也没效果 |
| minIdle | 最小连接池数量 | |
| maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
| poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
| maxOpenPreparedStatements | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
| validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 | |
| testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
| testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 |
| testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
| timeBetweenEvictionRunsMillis | 有两个含义: 1)Destroy线程会检测连接的间隔时间2)testWhileIdle的判断依据,详细看testWhileIdle属性的说明 | |
| numTestsPerEvictionRun | 不再使用,一个DruidDataSource只支持一个EvictionRun | |
| minEvictableIdleTimeMillis | ||
| connectionInitSqls | 物理连接初始化的时候执行的sql | |
| exceptionSorter | 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接 | |
| filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | |
| proxyFilters | 类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |
四 JDBCTools工具类
4.1 druid.properties文件
#key=value
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mytest?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=123456
initialSize=5
maxActive=10
maxWait=1000
4.2 JDBCTools工具类1.0版
4.2.1 JDBCToolsVersion1类
package com.mytest.tools;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCToolsVersion1 {
private static DataSource ds;
static{//静态代码块,JDBCToolsVersion1类初始化执行
try {
Properties pro = new Properties();
pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
public static void free(Connection conn) throws SQLException {
conn.close();//还给连接池
}
}
4.3 ThreadLocal类
其中ThreadLocal的介绍如下:
JDK 1.2的版本中就提供java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session等
ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, Object>,其key就是一个ThreadLocal,而Object即为该线程的共享变量。而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
1、ThreadLocal对象.get: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal对象.set: 设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal对象.remove: 移除ThreadLocal中当前线程共享变量的值。
线程(事务)结束后别忘了移除共享对象。
package com.mytest.tools;
public class TestThreadLocal {
private static ThreadLocal<String> tl = new ThreadLocal<>();
public static void print(){
System.out.println(Thread.currentThread().getName() +"->" +tl.get());
}
/*
这里有两个线程,每一个线程都有自己的ThreadLocalMap对象,
它们此时都有一个key为tl的键值对,但是两个map中tl对应的value不一样,一个是mytest,一个是mysql
*/
public static void main(String[] args) {
new Thread("线程1"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"set之前:" );
print();
tl.set("mytest");
System.out.println(Thread.currentThread().getName()+"set之后:" );
print();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"休眠1秒之后:" );
print();
tl.remove();
System.out.println(Thread.currentThread().getName()+"移出之后:" );
print();
}
}.start();
new Thread("线程2"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"set之前:" );
print();
tl.set("mysql");
System.out.println(Thread.currentThread().getName()+"set之后:" );
print();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"休眠1秒之后:" );
print();
tl.remove();
System.out.println(Thread.currentThread().getName()+"移出之后:" );
print();
}
}.start();
}
}
4.4 JDBCTools工具类2.0版
4.4.1 JDBCToolsVersion1类
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
//事物时,Service和dao属于同一线程,不用再传参数了
/*
这个工具类的作用就是用来给所有的SQL操作提供“连接”,和释放连接。
这里使用ThreadLocal的目的是为了让同一个线程,在多个地方getConnection得到的是同一个连接。
这里使用DataSource的目的是为了(1)限制服务器的连接的上限(2)连接的重用性等
*/
public class JDBCTools {
private static DataSource ds;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
static{//静态代码块,JDBCToolsVersion1类初始化执行
try {
Properties pro = new Properties();
pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
//获取连接
Connection connection = tl.get();
if(connection == null){//当前线程还没有拿过连接,就给它从数据库连接池拿一个
connection = ds.getConnection();
//连接存入共享变量中
tl.set(connection);
}
return connection;
}
public static void free() throws SQLException {
Connection connection = tl.get();
if(connection != null){
tl.remove();
//避免还给数据库连接池的连接不是自动提交模式(建议)
//因为其在业务类中会关闭事务的自动提交
connection.setAutoCommit(true);
connection.close();
}
}
}
五 封装DAO层代码
5.1 DAO层类
Java是面向对象语言,数据在Java中通常以对象的形式存在。
数据库中的记录 <-- DAO类 --> Java的对象对应起来。
我们把访问数据库的代码封装起来,方便数据库访问的复用等,封装的这些类称为DAO(Data Access Object)。它相当于是一个数据访问接口,夹在业务逻辑与数据库资源中间。
5.2 反射封装BaseDAOImpl类
基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,我们称为BaseDAOImpl.
public abstract class BaseDao {
/*
通用的增、删、改的方法
String sql:sql
Object... args:给sql中的?设置的值列表,可以是0~n
*/
protected int update(String sql,Object... args) throws SQLException {
// 创建PreparedStatement对象,对sql预编译
Connection connection = JDBCTools.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
//设置?的值
if(args != null && args.length>0){
for(int i=0; i<args.length; i++) {
ps.setObject(i+1, args[i]);//?的编号从1开始,不是从0开始,数组的下标是从0开始
}
}
//执行sql
int len = ps.executeUpdate();
ps.close();
//这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
//connection.getAutoCommit()为false,不要在这里回收connection,由开启事务的地方回收
//connection.getAutoCommit()为true,正常回收连接
//没有开启事务的话,直接回收关闭即可!
if (connection.getAutoCommit()) {
//回收
JDBCTools.free();
}
return len;
}
/*
通用的查询多个Javabean对象的方法,例如:多个员工对象,多个部门对象等
这里的clazz接收的是T类型的Class对象,
如果查询员工信息,clazz代表Employee.class,
如果查询部门信息,clazz代表Department.class,
返回List<T> list
*/
protected <T> ArrayList<T> query(Class<T> clazz,String sql, Object... args) throws Exception {
// 创建PreparedStatement对象,对sql预编译
Connection connection = JDBCTools.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
//设置?的值
if(args != null && args.length>0){
for(int i=0; i<args.length; i++) {
ps.setObject(i+1, args[i]);//?的编号从1开始,不是从0开始,数组的下标是从0开始
}
}
ArrayList<T> list = new ArrayList<>();
ResultSet res = ps.executeQuery();
/*
获取结果集的元数据对象。
元数据对象中有该结果集一共有几列、列名称是什么等信息
*/
ResultSetMetaData metaData = res.getMetaData();
int columnCount = metaData.getColumnCount();//获取结果集列数
//遍历结果集ResultSet,把查询结果中的一条一条记录,变成一个一个T 对象,放到list中。
while(res.next()){
//循环一次代表有一行,代表有一个T对象
T t = clazz.newInstance();//要求这个类型必须有公共的无参构造
//把这条记录的每一个单元格的值取出来,设置到t对象对应的属性中。
for(int i=1; i<=columnCount; i++){
//for循环一次,代表取某一行的1个单元格的值
Object value = res.getObject(i);
//这个值应该是t对象的某个属性值
//获取该属性对应的Field对象
//String columnName = metaData.getColumnName(i);//获取第i列的字段名
//这里再取别名可能没办法对应上
String columnName = metaData.getColumnLabel(i);//获取第i列的字段名或字段的别名
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);//这么做可以操作private的属性
field.set(t, value);
}
list.add(t);
}
res.close();
ps.close();
//这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
//没有开启事务的话,直接回收关闭即可!
if (connection.getAutoCommit()) {
//回收
JDBCTools.free();
}
return list;
}
protected <T> T queryBean(Class<T> clazz,String sql, Object... args) throws Exception {
ArrayList<T> list = query(clazz, sql, args);
if(list == null || list.size() == 0){
return null;
}
return list.get(0);
}
}
补充问题: jar包版本不兼容
使用低版本jar,例如:mysql-connector-java-8.0.19.jar
Exception in thread "main" com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:408)
at com.mysql.jdbc.Util.getInstance(Util.java:383)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1023)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:997)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:983)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:928)
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2576)
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2309)
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:834)
at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:46)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:408)
at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:419)
at com.mysql.jdbc.NonRegisteringDriver.connect(Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
Exception in thread "main" java.sql.SQLException: The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:76)
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:835)
at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:455)
at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:240)
at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:199)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
.java:344)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
看异常好像是无事务连接异常,无法创建连接。将MySQL驱动改为了最新的8.0版本的MySQL驱动。显示那个驱动类已经过时了,新的驱动类是“com.mysql.cj.jdbc.Driver”,而不是“com.mysql.jdbc.Driver”了,并且还说我没有配置时区,查了一下,原来从JDBC6.0开始驱动类使用了新的,并且url中必须要设置时区,否侧会报错。
如果继续使用旧版jar包,注意驱动类名改为:com.mysql.cj.jdbc.Driver
url后加入时区设置:jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC