欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Jdbc进阶篇

程序员文章站 2022-07-15 11:09:08
...

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进阶篇

我们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));
}

结果

Jdbc进阶篇

由于我们预编译了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进阶篇

原子性操作的基本格式

我们的Jdbc代码按照以下伪代码的格式可以实现原子性,其中我们...的位置可以执行多条SQL,这些SQL是一个事物。

connection.setAutoCommit(false);
try{
    .....
    connection.commit();
        
}
catch{
    connection.rollback();
}
finally{
    connection.close();
}

JDBC批处理

批处理就是一次执行多条语句的技术,我们前面知道Jdbc是支持事物的,而事物的一大核心就是多条SQL同时成功或者同时失败。

Jdbc进阶篇

批处理相关方法

名字带有Batch的方法就是批处理方法:

  • addBatch()

    将当前statement的sql语句加入到批处理队列中

  • addBatch(String sql)

    添加一条sql语句加入到批处理队列中

  • clearBatch()

    清除批处理队列

  • executeBatch()

    执行批处理,返回值是int

  • executeLargeBatch()

    执行批处理,返回值是Long,表示处理大量SQL

简单看一下StatementPrepareStatement的分别的执行流程

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[]的对应索引值。

Jdbc进阶篇

PrepareStatement的addBatc()方法也可以传入一个SQL,不过这个SQL必须是完整的,因为我们创建PrepareStatement的时候已经预编译好了一个SQL模板。

Jdbc进阶篇

Statement

statement传入的SQL必须是已经拼接好的完整sql,所以addBatch()方法加参数。

Jdbc进阶篇

优化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();
                }
            }
        }
    }
}

我们发现,我们真正要变化的代码只有三行,其他及其复杂的部分都是重复的。

Jdbc进阶篇

解决思路,封装

将异常的捕获以及资源的关闭都封装起来

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%。

Jdbc进阶篇

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");
    }
}