JDBC, SQL注入攻击, 批处理, 连接池(DBCP, C3P0)

前期准备

JDBC : java数据库连接,
使用它时,我们需要在工程中导入.jar包,
例如 : mysql-connector-java-5.1.40-bin.jar
我们可以在工程下创建一个lib目录, lib目录与src目录同级别,
将所有导入的.jar包都可以放入lib包中, 从而进行统一管理

1. JDBC实现步骤

1.注册数据库驱动
2.获取数据库连接
3.创建传输器
4.传输sql语句, 并返回结果
5.遍历返回结果
6.关闭资源

1.1 JDBC连接基础版

import com.mysql.jdbc.Driver;

import java.sql.*;

/**
 * JDBC实现步骤
 * 1.注册数据库驱动
 * 2.获取数据库连接
 * 3.创建传输器
 * 4.传输sql语句, 并返回结果
 * 5.遍历返回结果
 * 6.关闭对象,释放资源
 */
public class JDBCDemo1 {
    public static void main(String[] args) throws SQLException {
        //1.注册数据库驱动(暂时抛出异常)
        DriverManager.registerDriver(new Driver());

        //2.获取数据库连接,并接收返回值
        //连接方式一
        //Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school?user=root&password=123456");
        //连接方式二
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "123456");

        //3.创建传输器,并接收返回值
        Statement stat = conn.createStatement();

        //4.传输sql查询语句,并接收返回值
        ResultSet rs = stat.executeQuery("select * from student");

        //5.遍历返回结果
        while (rs.next()) {//将游标从列名那一行开始往下一行依次移动,判断下一行是否有数据
            System.out.print(rs.getString(1));//通过每一列的下标找到每一行对应位置的元素,数据库中列下标从1开始
            System.out.println(rs.getString("student_name"));//通过列名找到每一行对应位置的元素
        }

        //6.关闭对象,释放资源(后创建对象的先关闭)
        rs.close();
        rs = null;

        stat.close();
        stat=null;

        conn.close();
        conn=null;
    }
}

1.2 改进一 : 异常处理

IDEA异常处理快捷键 :
选中所有异常的部分, 然后点击快捷键 ctrl + alt + T

//异常处理后
import com.mysql.jdbc.Driver;

import java.sql.*;

public class JDBCDemo2 {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;
        try {
            //1.注册数据库驱动(暂时抛出异常)
            DriverManager.registerDriver(new Driver());

            //2.获取数据库连接,并接收返回值
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "123456");

            //3.创建传输器,并接收返回值
            stat = conn.createStatement();

            //4.传输sql查询语句,并接收返回值
            rs = stat.executeQuery("select * from student");

            //5.遍历返回结果
            while (rs.next()) {//将游标从列名那一行开始往下一行依次移动,判断下一行是否有数据
                System.out.print(rs.getString(1));//通过每一列的下标找到每一行对应位置的元素,数据库中列下标从1开始
                System.out.println(rs.getString("student_name"));//通过列名找到每一行对应位置的元素
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            //6.关闭对象(后创建对象的先关闭)
            //处理异常
            if (rs != null) {//为了防止rs为空,进而出现空指针异常
                try {
                    //关闭对象
                    rs.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {//无论对象是否正常关闭,都要释放资源,以便垃圾回收
                    //释放资源
                    rs = null;
                }
            }
            if (stat != null) {
                try {
                    stat.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    stat = null;
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    conn = null;
                }
            }
        }
    }
}

1.3 改进二 : 改用反射来加载数据库驱动

  • 注册数据库驱动所用的Driver类的源码

    //注册数据库驱动所用的Driver类的源码
    package com.mysql.jdbc;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {
        }
    
        static {
            try {
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    }
    
  • 我们自己写的注册数据库驱动的代码

    DriverManager.registerDriver(new Driver());
    
  • 我们发现Driver类的静态代码块中已经有了我们自己所写的代码, 而我们又重新写了一遍, 相当于我们执行了两次注册数据库驱动的代码, 代码重复冗余.

  • 我们只需要加载Driver类, 而不创建它的对象, 这样就可以成功注册数据库驱动

  • 我们用反射来加载Driver类
    Class.forName("com.mysql.jdbc.Driver");//处理异常 ClassNotFoundException

//改进二 : 改用反射来加载数据库驱动
import com.mysql.jdbc.Driver;

import java.sql.*;

public class JDBCDemo3 {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;
        try {
            //1.注册数据库驱动
            //DriverManager.registerDriver(new Driver());//多次注册数据库驱动,浪费资源
            //使用反射来加载类只会注册数据库驱动一次
            Class.forName("com.mysql.jdbc.Driver");//处理异常 ClassNotFoundException

            //2.获取数据库连接,并接收返回值
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "123456");

            //3.创建传输器,并接收返回值
            stat = conn.createStatement();

            //4.传输sql查询语句,并接收返回值
            rs = stat.executeQuery("select * from student");

            //5.遍历返回结果
            while (rs.next()) {//将游标从列名那一行开始往下一行依次移动,判断下一行是否有数据
                System.out.print(rs.getString(1));//通过每一列的下标找到每一行对应位置的元素,数据库中列下标从1开始
                System.out.println(rs.getString("student_name"));//通过列名找到每一行对应位置的元素
            }
        } catch (SQLException | ClassNotFoundException throwables) {
            throwables.printStackTrace();
        } finally {
            //处理异常
            if (rs != null) {//为了避免空指针异常
                try {
                    rs.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    rs = null;
                }
            }
            if (stat != null) {
                try {
                    stat.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    stat = null;
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    conn = null;
                }
            }
        }
    }
}

1.4 改进三 : 将JDBC封装成一个工具类

为了避免每次都需要重写连接数据库的代码, 我们可以将重复使用的代码封装成一个工具类

//JDBC工具类
import java.sql.*;

//该类的所有方法设置为静态方法的目的,是为了能直接使用类名调用该类的方法,不需要单独创建对象
public class JDBCUtils {
    //私有化构造方法,禁止其他类创建本类的对象
    private JDBCUtils() {
    }

    //1.加载数据库驱动  2.连接数据库
    public static Connection getConnection() throws ClassNotFoundException, SQLException {
        //由于我们不知道具体使用我们这个方法的类会遇到那些异常,
        //因此我们抛出异常,让具体使用该方法的类去自己单独处理异常
        Class.forName("com.mysql.jdbc.Driver");//抛出异常 ClassNotFoundException
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "123456");//抛出异常 SQLException
    }

    //6.关闭对象
    public static void close(Connection conn, Statement stat, ResultSet rs) {
        //处理异常
        if (rs != null) {//为了避免空指针异常
            try {
                rs.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                rs = null;
            }
        }
        if (stat != null) {
            try {
                stat.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                stat = null;
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                conn = null;
            }
        }
    }
}

测试JDBC工具类

//测试JDBC工具类
class JdbcTest {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;

        try {
            //1.加载数据库驱动  2.连接数据库
            conn = JDBCUtils.getConnection();//处理异常

            //3.创建传输器
            stat = conn.createStatement();

            //4.通过sql语句对数据库进行操作
            rs = stat.executeQuery("select * from student");

            //5.遍历返回结果
            while (rs.next()) {
                System.out.print(rs.getString(1));//通过每一列的下标找到每一行对应位置的元素,数据库中列下标从1开始
                System.out.println(rs.getString("student_name"));//通过列名找到每一行对应位置的元素
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            //6.关闭对象,释放资源
            JDBCUtils.close(conn, stat, rs);
        }
    }
}

1.5 改进四 : 利用properties配置文件来避免代码后期的频繁改动

Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "123456");
上面代码中的内容容易经常改动, 为了避免我们写的代码被频繁的修改, 我们将里面的数据放到 XX.properties 配置文件中, 后期需要修改的话, 我们只需要修改配置文件中的内容即可, 对我们的代码没有影响

  • 在工程中的src目录中新建文件, 命名为db.properties, 里面的内容如下
    driver=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/school
    user=root
    password=123456
    
//改进四 : 利用properties配置文件来避免代码后期的频繁改动
import java.io.File;
import java.io.FileInputStream;

import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class JDBCUtils2 {
    //私有化构造方法,禁止其他类创建本类的对象
    private JDBCUtils2() {
    }

    //创建Properties类对象
    //静态属性(对象)--- 全局统一 --- 只需要创建一次对象就可以读取配置文件中的内容
    private static Properties p = new Properties();

    //静态代码块---保证程序先读取配置文件内容
    static {
        //静态代码块中不能抛出异常, 只能捕获代码块
        try {
            /*方法一
            JDBCUtils2.class---获取当前类的字节码对象
            getClassLoader()---获取类加载器
            getResource("src目录下的文件名称")---获取当前工程的src目录
            getPath()---把URL类型转换成String类型*/
            //p.load(new FileInputStream(new File(JDBCUtils2.class.getClassLoader().getResource("db.properties").getPath())));
            
            //方法二
            p.load(JDBCUtils2.class.getClassLoader().getResourceAsStream("db.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //1.加载数据库驱动  2.连接数据库
    public static Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName(p.getProperty("driver"));//抛出异常 ClassNotFoundException
        return DriverManager.getConnection(p.getProperty("url"), p.getProperty("user"), p.getProperty("password"));//抛出异常 SQLException
    }

    //6.关闭对象
    public static void close(Connection conn, Statement stat, ResultSet rs) {
        //处理异常
        if (rs != null) {//为了避免空指针异常
            try {
                rs.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                rs = null;
            }
        }
        if (stat != null) {
            try {
                stat.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                stat = null;
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                conn = null;
            }
        }
    }
}

测试类

class JdbcTest2 {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;

        try {
            //1.加载数据库驱动  2.连接数据库
            conn = JDBCUtils2.getConnection();//处理异常

            //3.创建传输器
            stat = conn.createStatement();

            //4.通过sql语句对数据库进行操作
            rs = stat.executeQuery("select * from student");

            //5.遍历返回结果
            while (rs.next()) {
                System.out.print(rs.getString(1));//通过每一列的下标找到每一行对应位置的元素,数据库中列下标从1开始
                System.out.println(rs.getString("student_name"));//通过列名找到每一行对应位置的元素
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            //6.关闭对象,释放资源
            JDBCUtils2.close(conn, stat, rs);
        }
    }
}

2. SQL注入攻击

Statement
由于sql语句中的参数部分是由用户手动输入的, 并且我们通过用户给定的参数与sql语句的主干部分进行拼接. 用户可以通过输入sql语句中的关键字来改变sql的语义, 从而执行一些别的操作.

2.1 用Java实现在数据库中创建表,并在表中添加数据

//java实现在数据库中创建表,并在表中添加数据
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class InsertDemo {
    public static void main(String[] args) {

        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;

        try {
            conn = JDBCUtils2.getConnection();//处理异常
            //创建传输器
            stat = conn.createStatement();
            stat.execute("drop table t");//先删除,再创建
            stat.executeUpdate("create table t(name varchar(20),password varchar(20))");
            stat.executeUpdate("insert into t values('lili','123456')");
            stat.executeUpdate("insert into t values('Jack','123456')");
            stat.executeUpdate("insert into t values('Tom','123456')");

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils2.close(conn, stat, rs);
        }
    }
}

2.2 模拟用户登入

通过将用户输入的用户名和密码与数据库中的数据进行匹配, 如果匹配成功, 则登入成功, 否则, 登入失败

判断方法一

通过遍历每一行的数据与用户输入的数据进行比较, 如果匹配成功, 则登入成功, 否则, 登入失败

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;

//模拟用户登入
public class LoginDemo {
    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名 : ");
        String name = sc.nextLine();
        System.out.println("请输入密码 : ");
        String password = sc.nextLine();

        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;

        try {
            conn = JDBCUtils2.getConnection();//处理异常
            //创建传输器
            stat = conn.createStatement();
            rs = stat.executeQuery("select * from t");
            //通过遍历每一行的数据与用户输入的数据进行比较
            boolean b = false;
            while (rs.next()) {
                if (rs.getString("name").equals(name) && rs.getString("password").equals(password)) {
                    b = true;
                }
            }
            if (b) {
                System.out.println("登入成功");
            } else {
                System.out.println("登录失败");
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils2.close(conn, stat, rs);
        }
    }
}

判断方法二

通过sql语句直接查询满足用户输入的数据的内容, 如果能查到数据, 代表登入成功, 否则, 登入失败

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;

//模拟用户登入
public class LoginDemo2 {
    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名 : ");
        String name = sc.nextLine();
        System.out.println("请输入密码 : ");
        String password = sc.nextLine();

        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;

        try {
            conn = JDBCUtils2.getConnection();//处理异常
            //创建传输器
            stat = conn.createStatement();
            //通过sql语句直接查询满足用户输入的数据的内容
            //通过字符串拼接来形成完整的sql语句
            //注意此处sql语句中的单引号
            rs = stat.executeQuery("select * from t where name='" + name + "' and password='" + password + "'");
            if(rs.next()){//如果下一行有数据,那么登入成功
                System.out.println("登入成功!");
            }else{
                System.out.println("登入失败");
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils2.close(conn, stat, rs);
        }
    }
}

用户通过输入sql语句中的关键字 # , 进入使得原sql语句忽略 # 后面的代码,
rs = stat.executeQuery("select * from t where name='" + name + "' and password='" + password + "'");
原sql语句的意思从原来的根据用户名与密码来查询数据, 变成了现在的只通过用户名来查询数据, 跳过了输入密码的部分, 这就是sql注入攻击所引起的安全问题
在这里插入图片描述

2.3 解决sql注入攻击问题

我们原来使用Statement来创建传输器, 通过传输器来传送sql语句
现在我们换成PreparedStatement来传送sql语句

  • PreparedStatement(避免sql注入攻击)
  • 带有预编译的传输器
  • 先将sql的主干语句部分发送给数据库服务器, 主干语句中的参数位置用 ? 来预留.
  • 主干语句发送到数据库服务器之后就会变成机器码, 这个机器码无法修改.
  • 以纯文本的形式来接收用户发送的参数数据, 如果参数中包含关键字也会被认为是一段文本内容.
  • PreparedStatement是Statement类的子类

在这里插入图片描述

import java.sql.*;
import java.util.Scanner;

//模拟用户登入
public class LoginDemo3 {
    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名 : ");
        String name = sc.nextLine();
        System.out.println("请输入密码 : ");
        String password = sc.nextLine();

        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            conn = JDBCUtils2.getConnection();//处理异常

            //发送sql主干语句,参数部分用?预留位置
            ps = conn.prepareStatement("select * from t where name=? and password=?");

            //第一个参数代表选择第几个问号(?), 第二个参数是传入的数据(即问号位置所传入的具体的值)
            ps.setString(1, name);
            ps.setString(2, password);

            //执行sql语句
            rs = ps.executeQuery();

            if (rs.next()) {//如果下一行有数据,那么登入成功
                System.out.println("登入成功!");
            } else {
                System.out.println("登入失败");
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            //此处ps是PreparedStatement类的对象, 而此处要求传入的对象应该是Statement类的对象,
            //此处没有出错, 是因为PreparedStatement是Statement的子类
            JDBCUtils2.close(conn, ps, rs);
        }
    }
}

3. 批处理

将多条sql语句加载好, 达到一定数量后统一发送给数据库,
避免了系统频繁的一条一条的向数据库发送sql语句,
这样一批一批的向数据库发送大大减少了系统与数据库交互的次数,节省了与数据库交互所花费的时间

需求 : 以50条sql语句为一批, 一共向数据库发送510条sql语句, 用批处理实现

3.1 Statement实现批处理

addBatch() : 向当前批处理中添加(加载)一条sql语句
executeBatch() : 执行批处理
clearBatch() : 清空批处理, 释放批处理中的空间

//以50条sql语句为一批, 一共向数据库发送510条sql语句, 用批处理实现
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class StatementBatch {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stat = null;
        ResultSet rs = null;

        try {
            conn = JDBCUtils2.getConnection();
            stat = conn.createStatement();
            System.out.println("开始执行批处理");

            //以50条sql语句为一批, 一共向数据库发送510条sql语句, 用批处理实现
            for (int i = 1; i <= 510; i++) {
                stat.addBatch("insert into t values('" + i + "','123456')");//向当前批处理中添加(加载)一条sql语句
                if (i % 50 == 0) {//每加载50条sql语句,执行一次批处理
                    stat.executeBatch();//执行批处理, 即将加载好的一批sql语句一起发送给数据库
                    stat.clearBatch();//清空批处理, 清空所有加载到批处理中的SQL语句
                    System.out.println("执行了" + i / 50 + "次批处理");
                }
            }
            
            System.out.println("批处理执行完毕");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils2.close(conn, stat, rs);
        }
    }
}

关键代码分析 :

for (int i = 1; i <= 510; i++) {
    stat.addBatch("insert into t values('" + i + "','123456')");//向当前批处理中添加(加载)一条sql语句
    if (i % 50 == 0) {//每加载50条sql语句,执行一次批处理
       stat.executeBatch();//执行批处理, 即将加载好的一批sql语句一起发送给数据库
       stat.clearBatch();//清空批处理, 清空所有加载到批处理中的SQL语句
       System.out.println("执行了" + i / 50 + "次批处理");
    }
}

如果将上面代码中的 stat.clearBatch(); 语句放在 stat.executeBatch(); 前面, 那么将不会往数据库中发送sql语句. 原因是 clearBatch() 方法会清空所有已经加载到批处理中的sql语句, 轮到 executeBatch() 方法执行的时候, 已经没有需要发送的sql语句了.

问题分析 :
上面的代码改进, 上面的代码只能向数据库发送500条sql语句, 无法满足我们的向数据库发送510条sql语句的需求, 因为剩余的10条sql语句不足50条sql语句, 因此没有被执行, 具体的修改方法请看下面 PreparedStatement 实现的批处理(Stetement参照下面的代码修改即可)

3.2 PreparedStatement实现批处理

addBatch() : 向当前批处理中添加(加载)一条sql语句
executeBatch() : 执行批处理
clearBatch() : 清空批处理, 释放批处理中的空间

上面问题的改进 :

//在for循环外部添加下面的操作
//如果剩余的一些sql语句不够50条语句,那么执行下面的代码
ps.executeBatch();//执行之前没有执行的批处理
ps.clearBatch();//清空批出六
System.out.println("批处理执行完毕");
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class PreparedStatementBatch {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            conn = JDBCUtils2.getConnection();
            ps = conn.prepareStatement("insert into t values(?,?)");

            System.out.println("开始执行批处理");
            for (int i = 1; i <= 510; i++) {
                ps.setString(1, "" + i);//为了确保传入数据库的数据为字符串,利用数据库拼接将int类型,转换为字符串
                ps.setString(2, "123456");
                ps.addBatch();//添加sql语句到批处理中
                if (i % 50 == 0) {
                    ps.executeBatch();//执行批处理
                    ps.clearBatch();//清空批处理
                    System.out.println("执行了" + i / 50 + "次批处理");
                }
            }

            //如果剩余的一些sql语句不够50条语句,那么执行下面的代码
            ps.executeBatch();//执行之前没有执行的批处理
            ps.clearBatch();//清空批出六
            System.out.println("批处理执行完毕");

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils2.close(conn, ps, rs);
        }
    }
}

4. 连接池

开源连接池(DBCP、C3P0)
在这里插入图片描述
上述模型表示,如果有多个用户访问服务器,服务器需要通过其中的程序与数据库建立连接,使用完连接之后,要销毁连接。若是面对海量的访问,频繁的创建和销毁连接十分占用资源。应该将创建和销毁的过程消除掉。

在这里插入图片描述
为了消除创建和销毁连接的过程,可以使用连接池。连接池中会保留一些连接,用户需要使用时,直接取出使用,使用完成后归还到连接池中即可。通过取出和归还代替了创建和销毁。若是面对海量访问,也能降低服务器和数据库的压力

连接池原理 :
在服务器启动的时候,会主动向数据库服务器索要一批连接连接数量。在连接被取完之后,连接池还可以再次初始化更多连接。用户使用完连接之后,将连接归还到连接池中。若是较长一段时间,无人操作连接,则连接池会销毁其中一半的连接。

4.1 手动实现简易的连接池

创建一个类来实现 DataSource 接口

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.*;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

public class TestDataSource implements DataSource {
    //使用集合来存储数据库连接
    private static List<Connection> list = new LinkedList<>();

    //保证类加载时初始化一定数量的数据库连接
    static {
        //静态代码块中只能捕获异常
        try {
            Connection conn = JDBCUtils2.getConnection();
//            Class.forName("com.mysql.jdbc.Driver");//异常处理
//            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school","root","123456");//异常处理

            for (int i = 0; i < 5; i++) {//一次循环存放一个数据库连接
                list.add(conn);//将数据库连接放到集合中
            }
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
    }


    //取出数据库连接
    @Override
    public Connection getConnection() throws SQLException {
        if (list.isEmpty()) {//判断集合中有没有数据库连接,如果没有,则执行if中的代码
            try {
                Connection conn = JDBCUtils2.getConnection();
                for (int i = 0; i < 5; i++) {
                    list.add(conn);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        Connection conn = list.remove(0);//remove(下标索引):返回值为删除元素的数据
        System.out.println("取出连接,连接池中还剩" + list.size() + "个连接");
        return conn;
    }

    //归还连接
    public void returnConnection(Connection conn) throws SQLException {
        //如果接收的连接已经被关闭,或者接收的值为null,都不能归还连接
        if (conn != null && (!conn.isClosed())) {//isClosed()方法是判断接收的连接是否已经被关闭,需要处理异常
            list.add(conn);
            System.out.println("归还连接,连接池中还剩" + list.size() + "个连接");
        }
    }


    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }
}

测试类

class TestData {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        //创建连接池实现类的对象
        TestDataSource source = new TestDataSource();
        try {
            //调用连接的方法来获取连接--取出连接
            conn = source.getConnection();
            ps = conn.prepareStatement("select * from student where student_id=?");
            ps.setInt(1, 2018);
            rs = ps.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString(2));
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            if (rs != null) {//为了防止rs为空,进而出现空指针异常
                try {
                    //关闭对象
                    rs.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {//无论对象是否正常关闭,都要释放资源,以便垃圾回收
                    //释放资源
                    rs = null;
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    ps = null;
                }
            }
            if (conn != null) {
                try {
                    //归还连接
                    source.returnConnection(conn);
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    conn = null;
                }
            }
        }
    }
}

4.2 DBCP

需要在工程下导入两个.jar包, 一般在工程下创建lib文件夹, lib文件夹与src文件夹一个级别, 将.jar包放入lib文件夹下

  • 导入包的名称为 :
    commons-dbcp-1.4.jar
    commons-pool-1.5.6.jar

  • 在src文件夹下创建 XX.properties 文件,
    dbcp.properties文件内容为

    driverClassName=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/school
    username=root
    password=123456
    
import org.apache.commons.dbcp.BasicDataSourceFactory;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

public class DBCPDemo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            //读取dbcp的配置文件
            Properties p = new Properties();
            //p.load(new FileInputStream(new File(DBCPDemo.class.getClassLoader().getResource("dbcp.properties").getPath())));
            p.load(DBCPDemo.class.getClassLoader().getResourceAsStream("dbcp.properties"));//处理异常

            //创建工厂对象
            BasicDataSourceFactory factory = new BasicDataSourceFactory();
            //获取连接池对象
            DataSource source = factory.createDataSource(p);//处理异常
            //通过连接池对象获取连接
            conn = source.getConnection();

            ps = conn.prepareStatement("select * from student");
            rs = ps.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString(2));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (rs != null) {//为了防止rs为空,进而出现空指针异常
                try {
                    //关闭对象
                    rs.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {//无论对象是否正常关闭,都要释放资源,以便垃圾回收
                    //释放资源
                    rs = null;
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    ps = null;
                }
            }
            if (conn != null) {
                try {
                    //归还连接(不是原来的关闭连接了)
                    conn.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                } finally {
                    conn = null;
                }
            }
        }
    }
}

4.3 C3P0

需要在工程中导入一个.jar包

  • 导入的包名为 :
    c3p0-0.9.1.2.jar

  • 在src文件夹下创建 c3p0.properties文件, 该文件的文件名称必须是 c3p0, 文件中的内容如下 :

    c3p0.driverClass=com.mysql.jdbc.Driver
    c3p0.jdbcUrl=jdbc:mysql://localhost:3306/school
    c3p0.user=root
    c3p0.password=123456
    
import com.mchange.v2.c3p0.ComboPooledDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class C3P0Demo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        //创建连接池对象
        //默认去读取src目录下c3p0.properties文件的内容
        ComboPooledDataSource source = new ComboPooledDataSource();
        try {
            //取出连接
            conn = source.getConnection();

            ps = conn.prepareStatement("select  * from student");
            rs = ps.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString(2));
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            JDBCUtils.close(conn, ps, rs);
        }
    }
}

5. 用C3P0对JDBC工具类做最后的修改

import com.mchange.v2.c3p0.ComboPooledDataSource;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class C3P0Utils {
    //私有化构造方法,禁止其他类创建本类的对象
    private C3P0Utils() {
    }

    //创建c3p0连接池对象,全局统一
    private static ComboPooledDataSource source = new ComboPooledDataSource();

    //1.加载数据库驱动  2.连接数据库
    public static Connection getConnection() throws ClassNotFoundException, SQLException {
        return source.getConnection();
    }

    //6.关闭对象
    public static void close(Connection conn, Statement stat, ResultSet rs) {
        //处理异常
        if (rs != null) {//为了避免空指针异常
            try {
                rs.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                rs = null;
            }
        }
        if (stat != null) {
            try {
                stat.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                stat = null;
            }
        }
        if (conn != null) {
            try {
                //归还连接(不是关闭连接)
                conn.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            } finally {
                conn = null;
            }
        }
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值