JDBC
数据库连接池
数据库连接池是管理并发访问数据库连接的理想解决方案.
DriverManager管理数据库连接适合单线程使用,在多线程并发情况下,为了能够重用数据库连接,控制并发连接总数,保护数据库避免连接过载,一定要使用数据库连接池
连接池原理:
使用DBCP连接池(DataBase Connection Pool)
数据库连接池的开源实现非常多,DBCP是常用的连接池之一。
导入DBCP pom.xml:
commons-dbcp
commons-dbcp
1.4
使用DBCP:
- 导入连接池jar
- 创建连接池对象
- 设置数据库必须的连接参数
- 设置可选的连接池管理策略参数
- 从连接池中获得活动的数据库连接
- 使用连接范围数据库
- 使用以后关闭数据库连接
- 这个关闭不是真的关闭连接,而是将使用过的连接归还给连接池.
案例:
public class Demo01 {
public static void main(String[] args) throws SQLException {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/tedustore";
String username = "root";
String password = "";
BasicDataSource ds = new BasicDataSource();
//设置必须的参数
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
//设置连接池的管理策略参数
ds.setInitialSize(2);
ds.setMaxActive(100);
//使用连接池中的数据库连接
Connection conn = ds.getConnection();
//执行SQL
String sql = "select 'hello' as a from dual ";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
while(rs.next()) {
String str = rs.getString("a");
System.out.println(str);
}
//归还连接到数据库连接池!!
conn.close();
}
}
为了便捷的使用连接池,经常将连接池封装为一个连接管理工具类:
/**
* 连接池版本的 数据库 连接管理工具类
* 适合于并发场合
*/
public class DBUtils {
private static String driver;
private static String url;
private static String username;
private static String password;
private static int initSize;
private static int maxActive;
private static BasicDataSource ds;
static {
ds = new BasicDataSource();
Properties cfg = new Properties();
try {
InputStream in = DBUtils.class.getClassLoader().getResourceAsStream("db.properties");
cfg.load(in);
//初始化参数
driver = cfg.getProperty("jdbc.driver");
url = cfg.getProperty("jdbc.url");
username = cfg.getProperty("jdbc.username");
password = cfg.getProperty("jdbc.password");
initSize = Integer.parseInt(cfg.getProperty("initSize"));
maxActive = Integer.parseInt(cfg.getProperty("maxActive"));
in.close();
//初始化连接池
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setInitialSize(2);
ds.setMaxActive(maxActive);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static Connection getConnection() {
try {
//getConnection()从连接池中获取重用
//的连接,如果连接池满了,则等待.
//如果有连接归还,则获取重用的连接
Connection conn = ds.getConnection();
return conn;
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void close(Connection conn) {
if(conn!=null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
其配套参数配置文件 db.properties:
# db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/tedustore
jdbc.username=root
jdbc.password=
# paramter for BasicDataSource
initSize=2
maxActive=2
测试:
public class Demo02 {
public static void main(String[] args) {
Connection conn = null;
try {
conn = DBUtils.getConnection();
String sql = "select 'Hello' as a from dual";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
while(rs.next()) {
String str = rs.getString("a");
System.out.println(str);
}
rs.close();
st.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(conn);
}
}
}
DBUtils 简化了数据库访问
连接池并发性测试:
测试原理:
测试案例:
/**
* 连接池并发测试
*/
public class Demo03 {
public static void main(String[] args) {
Thread t1 = new DemoThread(5000);
Thread t2 = new DemoThread(6000);
Thread t3 = new DemoThread(1000);
t1.start();
t2.start();
t3.start();
}
}
class DemoThread extends Thread{
int wait;
public DemoThread(int wait) {
this.wait=wait;
}
public void run() {
Connection conn = null;
try {
//getConnection()方法在连接池中没有
//连接可以使用时候,会阻塞等待
conn = DBUtils.getConnection();
System.out.println("获取了连接:"+conn);
Thread.sleep(wait);
String sql = "select 'Hello' as a from dual";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
while(rs.next()){
System.out.println(rs.getString("a"));
}
System.out.println(wait+"结束");
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(conn);
}
}
}
PreparedStatement
PreparedStatement 对象用于执行带参数的预编译执行计划,其原理是:
关于执行计划:
任何SQL执行过程都是先编译“执行计划”,再执行“执行计划”
-
数据库为了优化性能,载SQL相同时候,会重用执行计划
- 执行计划编译较慢
- 重用执行计划可以可以提高数据库性能
-
数据库只有在SQL语句完全一样时候才重用相同的执行计划
- 如果SQL语句中有一个字符的更改,也会执行不同的执行计划
- SQL中一个空格或者一个大小写不同也会创建不同的执行计划
PreparedStatement 好处是可以重复使用执行计划,提过DB效率
使用步骤
- 将带参数的SQL发送到数据库创建执行计划
- 替换执行计划中参数
- 执行执行计划,得到结果
案例:
/**
* 演示预编译的SQL执行计划
* @author soft01
*
*/
public class Demo04 {
public static void main(String[] args) {
Connection conn = null;
try {
conn = DBUtils.getConnection();
//创建带参数的SQL语句.
String sql = "insert into robin_demo (id,name) values (?,?)";
//将SQL发送数据库,创建执行计划
//返回值 ps 就代表执行计划
PreparedStatement ps = conn.prepareStatement(sql);
//替换执行计划中的参数,2个参数
//按照序号发送参数
ps.setInt(1, 9);
ps.setString(2, "Andy");
//执行“执行计划”
int n = ps.executeUpdate();
System.out.println(n);
//再次重复使用执行计划
ps.setInt(1, 100);
ps.setString(2, "Wang");
n = ps.executeUpdate();
System.out.println(n);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(conn);
}
}
}
案例:
/**
* 使用执行计划更新语句
* @author soft01
*
*/
public class Demo05 {
public static void main(String[] args) {
Connection conn = null;
try {
conn = DBUtils.getConnection();
String sql = "update robin_demo set name=? where id=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "Lao wang");
ps.setInt(2, 100);
int n = ps.executeUpdate();
System.out.println(n);//1
ps.setString(1, "Jerry");
ps.setInt(2,8);
n = ps.executeUpdate();
System.out.println(n);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(conn);
}
}
}
案例: 带参数的查询功能
/**
* 利用PS 实现查询功能
*/
public class Demo06 {
public static void main(String[] args) {
Connection conn = null;
try {
conn = DBUtils.getConnection();
String sql = "select id,name from robin_demo where name like ? ";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "%w%");
ResultSet rs = ps.executeQuery();
while(rs.next()) {
//getInt(列的序号),利用序号获取值
int id = rs.getInt(1);
String name = rs.getString(2);
System.out.println(id+","+name);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(conn);
}
}
}
PreparedStatement 可以避免SQL注入攻击
何为注入攻击:
用户输入了 含有SQL成分的参数,参数在拼接SQL时候造成SQL语句的语意改变!
改变SQL语句的执行计划!最后的执行结果就完全变了!!!
如何避免:
- 拦截用户输入的SQL成分
- 固定执行计划,避免改变执行逻辑
原理:
一个注入攻击的例子:
create table robin_user(
id int(10),
name varchar(100),
pwd varchar(100)
);
insert into robin_user (id,name,pwd) values (1,'tom','123');
insert into robin_user (id,name,pwd) values (2,'jreey','123');
案例代码:
/**
SQL 注入演示
-
/
public class Demo07 {
public static void main(String[] args) {
//获取用户输入
Scanner in = new Scanner(System.in);
System.out.print("用户名:");
String name = in.nextLine();
System.out.print("密码:");
String pwd = in.nextLine();
//检查登陆情况
boolean pass = login(name,pwd);
if(pass) {
System.out.println("欢迎你!"+name);
} else {
System.out.println("用户名或密码错误");
}
}//检查用户是否能够登陆 public static boolean login(String name,String pwd) { String sql = "select count(*) as c from robin_user where name='"+name+"' and pwd='"+pwd+"' "; System.out.println(sql); Connection conn = null; try { conn = DBUtils.getConnection(); Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql); while(rs.next()) { int n = rs.getInt("c"); return n>=1; } } catch (Exception e) { e.printStackTrace(); } finally { DBUtils.close(conn); } return false; }
}
测试:
在输入用户名 tom 和密码: 1‘ or '1'='1 时候出现注入攻击现象
使用PS 就可以避免注入攻击,更新login方法如下:
/**
* 利用 PS 就可以避免注入攻击
* @param name
* @param pwd
* @return
*/
public static boolean login(String name,String pwd) {
String sql = "select count(*) as c from robin_user where name=? and pwd=? ";
Connection conn = DBUtils.getConnection();
try {
conn = DBUtils.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, name);
ps.setString(2, pwd);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
int n = rs.getInt("c");
return n>=1;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(conn);
}
return false;
}
测试结果不会发生注入攻击
作业
实现课堂全部案例代码
- 测试数据库连接池的使用
- 测试数据库连接池的并发性
- 封装连接管理工具类 DBUtils
- 利用PS实现数据库插入功能
- 利用PS实现数据库更新功能
- 利用PS实现数据库删除功能
- 利用PS实现数据库查询功能
- 完整SQL注入案例