Jdbc进阶篇
JDBC进阶部分
上一篇讲了Jdbc的基本使用,常用对象,方法的讲解,这篇博客主要对一些Jdbc实际使用场景的问题和优化展开的。
SQL注入问题
SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
简单的示例:
这是一条基础的登录语句,用户传过来两个参数,我们验证一下数据库是否有这个账号密码,如果有则返回查询结果。
select * from user where username='参数1' and password='参数2';
数据库
mysql> select * from admin;
+----------+----------+
| username | password |
+----------+----------+
| dulao | 123456 |
| jack | 123456 |
| mahe | 123456 |
| root | 123456 |
+----------+----------+
对应写一个JDBC
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///kaikeba", "root", "");
Statement statement = connection.createStatement();
String username="dulao";
String password="123456";
ResultSet resultSet = statement.executeQuery("select * from admin where username='" + username + "' and password='" + password + "'");
//打印结果
while (resultSet.next()){
System.out.println(resultSet.getString(1)+resultSet.getString(2));
结果
dulao123456
我们看看非法输入:
- String username=“haha”
- String password=“1’ or ‘1’ ='1”;
结果
dulao123456
jack123456
mahe123456
root123456
很奇怪,为什么呢?
对比一下,我们发现非法注入的SQL已经改变了我们的语句结构,变成了A=B or 1=1这种恒等式。
我们JDBC还提供了一个statement来解决SQL注入的问题。
PrepareStatement
PrepareStatement也是通过Connection对象创建的另一种statement,特点是预编译SQL。
connection.prepareStatement(String sql);
基本使用:
- 需要获取的用户参数用
?
代替 - 随后再设置参数
Connection connection = DriverManager.getConnection("jdbc:mysql:///kaikeba", "root", "");
//创建时传入SQL
PreparedStatement statement = connection.prepareStatement("select * from admin where username=? and password=?");
//参数设置
String username="dulao";
String password="1' or '1' ='1";
statement.setString(1,username);
statement.setString(2,password);
//打印结果
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()){
System.out.println(resultSet.getString(1)+resultSet.getString(2));
}
结果
由于我们预编译了sql,所以不会改变sql的结构。我们找不到用户名名为haha,密码为1’ or ‘1’ ='1的用户。
如何预防SQL注入:
- 前端字符串验证
- ‘,; and or 等符号SQL语句验证
- 使用PrepareStatement
JDBC事物支持
和MySQL操作一致,JDBC也可以关闭自动提交。默认是开启的
connection.setAutoCommit(false);
关闭自动提交之后,我们改为手动提交
connection.commit();
我们可以用try–finally代码块来执行JDBC代码,出现异常时再finally代码中执行connection.rollback()方法回滚数据。
connection.rollback();
示例
数据表:
mysql> select * from admin;
+----------+----------+
| username | password |
+----------+----------+
| dulao | 123456 |
| jack | 12345 |
| mahe | 123456 |
| root | 123456 |
+----------+----------+
JDBC代码:
public class JdbcDemo3 {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection=DriverManager.getConnection("jdbc:mysql:///kaikeba","root","");
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
int i = statement.executeUpdate("insert into admin values('jack','123456')");
System.out.println(i);
}
}
结果
1
我们再查询数据库,发现实际数据没变,由于没有自动提交。
原子性操作的基本格式
我们的Jdbc代码按照以下伪代码的格式可以实现原子性,其中我们...
的位置可以执行多条SQL,这些SQL是一个事物。
connection.setAutoCommit(false);
try{
.....
connection.commit();
}
catch{
connection.rollback();
}
finally{
connection.close();
}
JDBC批处理
批处理就是一次执行多条语句的技术,我们前面知道Jdbc是支持事物的,而事物的一大核心就是多条SQL同时成功或者同时失败。
批处理相关方法
名字带有Batch的方法就是批处理方法:
-
addBatch()
将当前statement的sql语句加入到批处理队列中
-
addBatch(String sql)
添加一条sql语句加入到批处理队列中
-
clearBatch()
清除批处理队列
-
executeBatch()
执行批处理,返回值是int
-
executeLargeBatch()
执行批处理,返回值是Long,表示处理大量SQL
简单看一下Statement和PrepareStatement的分别的执行流程
PrepareStatement
由于我们在构造的时候一定要传入一个SQL,所以我们的批处理addBatch()不用传参数,我么setString()补充完参数之后,addBatch()是将参数填充之后的SQL交到批处理队列,注意如果参数没填充完,编译通过,运行报错。
PreparedStatement statement = connection.prepareStatement("delete from admin where username=?");
for (int i = 0; i < 10; i++) {
statement.setString(1,"user"+i);
statement.addBatch();
}
int[] ints = statement.executeBatch();
System.out.println(Arrays.toString(ints));
简单流程:
PrepareStement构造的时候已经预编译好了SQL,每次我们填充完参数之后,使用addBatch()将图中的加工好的SQL加入到批处理队列。
最后执行SQL时,将每个SQL的执行结果都封装进int[]的对应索引值。
PrepareStatement的addBatc()方法也可以传入一个SQL,不过这个SQL必须是完整的,因为我们创建PrepareStatement的时候已经预编译好了一个SQL模板。
Statement
statement传入的SQL必须是已经拼接好的完整sql,所以addBatch()方法加参数。
优化JDBC的代码
封装
简单看一下完整的Jdbc流程:
public class JdbcDemo3 {
public static void main(String[] args) {
Connection connection=null;
Statement statement=null;
ResultSet resultSet=null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql:///kaikeba", "root", "");
connection.setAutoCommit(false);
statement = connection.createStatement();
resultSet = statement.executeQuery("select * from admin");
while (resultSet.next()) {
System.out.println("username" + resultSet.getString(1) + "password" + resultSet.getString(2));
}
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement!=null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection!=null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
我们发现,我们真正要变化的代码只有三行,其他及其复杂的部分都是重复的。
解决思路,封装
将异常的捕获以及资源的关闭都封装起来
public class JdbcUtils {
public static Connection getConnection() {
try {
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection("jdbc:mysql:///kaikeba", "root", "");
} catch (SQLException | ClassNotFoundException e) {
throw new RuntimeException();
}
}
public static void close(ResultSet resultSet, Statement statement, Connection connection) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
业务代码
public class JdbcDemo3 {
public static void main(String[] args) {
Connection connection=null;
Statement statement=null;
ResultSet resultSet=null;
try {
JdbcUtils.getConnection();
statement = connection.createStatement();
resultSet = statement.executeQuery("select * from admin");
while (resultSet.next()) {
System.out.println("username" + resultSet.getString(1) + "password" + resultSet.getString(2));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.close(resultSet, statement, connection);
}
}
}
理论上以及减少了不少,但是我们的有效代码还是不到30%。
JdbcTemplate
Spring框架提供了一种比较简便的Jdbc封装类,将异常处理,资源申请的所有业务逻辑都进行封装,只保留最核心的业务代码需要我们编写。
由于涉及到Spring的知识,这里就抛砖引玉了,大家有空可以去了解一下。
我们看到核心代码缩减到一行了。
public class JdbcDemo3 {
//创建template
private final static JdbcTemplate template=new JdbcTemplate(DruidUtils.getDataSource());
public static void main(String[] args) {
//删除
int delete = template.update("delete from admin where id=?", 1);
//添加
int insert = template.update("insert into admin values(?,?)", "root", "password");
//修改
int update = template.update("update admin set password=? where username=?", "123","jack");
//查询
Map<String, Object> select = template.queryForMap("select * from admin");
}
}