JDBC连接池&动态代理

一 PreparedStatement

1.1 登录案例

上一篇博文中的最后的用户登录案例,使用Statement对象执行SQL语句会出现一个问题:

用户不输入密码,只输入用户名,一样可以登录成功

问题

SELECT * FROM USER WHERE username = 'admin ' #' and password = '';
-- 使用#号将后半段的密码验证消除,#号在mysql中是备注

我们将用户输入的实际参数与sql字符串进行了拼接,发送给数据库先编译后执行,改变了sql原有的意义,这个现象我们称为sql注入

JDBC连接池&动态代理

1.2 解决方案

要想解决sql注入的问题就不能用户输入的实际参数与sql字符串进行了拼接,使用PreparedStatement【预编译】

SELECT * FROM USER WHERE username ="admin'#" AND PASSWORD ="";
-- 同 SELECT * FROM USER WHERE username =? AND PASSWORD =?;

JDBC连接池&动态代理

1.3 api介绍

通过Connection对象创建预编译语句执行者

// 使用?占位符代替 sql实际参数
String sql = "select * from user where username = ? and password = ?";
connection.prepareStatement(sql);

给问号设置实际参数

preparedStatement.setXxx(int 问号的位置,Object 实际参数);
	例如:
		Int
		Double
		String
		Object

执行sql语句

// 执行sql 了解
public boolean execute();
// 执行dml
public int executeUpdate(); 影响行数
// 执行dql
public ResultSet executeQuery();

1.4 修复登录案例

// idea 中使用@Test 结合scanner 会有bug
public static void main(String[] args) throws Exception {
    // 在控制台输入用户名和密码 scanner技术
    Scanner scanner = new Scanner(System.in);
    System.out.println("欢迎来到德莱联盟");
    System.out.println("请输入用户名:");
    String username = scanner.nextLine();
    System.out.println("请输入密码:");
    String password = scanner.nextLine();
    // 使用增强版的jdbc 防止sql注入
    Connection connection = JDBCUtils.getConnection();
    // 编写sql  使用问号占位符代替实际的参数
    String sql = "select * from user where username = ? and password = ?;";
    System.out.println(sql);
    // 创建预编译语句执行者【高铁】
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    // 给sql语句设置实际参数
    preparedStatement.setString(1, username);
    preparedStatement.setObject(2, password);
    // 执行sql语句
    ResultSet resultSet = preparedStatement.executeQuery();
    // 处理结果 判断用户是否登录成功
    if (resultSet.next()) {
        System.out.println("用户登录成功");
    } else {
        System.out.println("用户名或密码错误");
    }
    // 关闭资源
    JDBCUtils.closeResource(resultSet, preparedStatement, connection);

}

1.5 优点

  1. 可以防止sql注入,提高安全性
  2. 实际参数与SQL字符串分离,提高程序可读性
  3. sql语句仅编译一次,提高效率

1.6 使用预编译对象实现增删改查

步骤分析

// 获取连接

// 编写sql【使用问号占位符】

// 创建预编译对象【高铁】

// 设置sql的实际参数

// 执行sql并返回结果

// 处理结果

// 关闭资源

新增

// 新增
@Test
public void test01() throws Exception {
    // 获取连接
    Connection connection = JDBCUtils.getConnection();
    // 编写sql【使用问号占位符】
    String sql = "insert into user values(null,?,?);";
    // 创建预编译对象【高铁】
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    // 添加一个用户
    preparedStatement.setObject(1, "宝强");
    preparedStatement.setObject(2, "123");
    System.out.println(preparedStatement.executeUpdate()); // 返回1 成功
    // 再添加一个用户
    preparedStatement.setObject(1, "乃亮");
    preparedStatement.setString(2, "456");
    System.out.println(preparedStatement.executeUpdate());
    // 。。。。。
    // 关闭资源
    JDBCUtils.closeResource(preparedStatement, connection);
}

修改

// 修改
@Test
public void test02() throws Exception {
    // 获取连接
    Connection connection = JDBCUtils.getConnection();
    // 编写sql【使用问号占位符】
    String sql = "update user set password = ? where id = ? ;";
    // 创建预编译对象【高铁】
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    // 设置sql的实际参数
    preparedStatement.setString(1, "789");
    preparedStatement.setInt(2, 5);
    // 执行sql并返回结果
    int i = preparedStatement.executeUpdate();
    // 处理结果
    if (i > 0) {
        System.out.println("修改成功");
    } else {
        System.out.println("修改失败");
    }
    // 关闭资源
    JDBCUtils.closeResource(preparedStatement, connection);
}

删除

// 删除
@Test
public void test03() throws Exception {
    // 获取连接
    Connection connection = JDBCUtils.getConnection();
    // 编写sql【占位符】
    String sql = "delete from user where id= ?;";
    // 创建预编译语句执行者
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    // 设置实际参数
    preparedStatement.setInt(1, 5);
    // 执行sql并返回结果
    int i = preparedStatement.executeUpdate();
    // 处理结果
    System.out.println(i);
    // 关闭资源
    JDBCUtils.closeResource(preparedStatement, connection);
}

查询

// 查询
@Test
public void test03() throws Exception {
    // 获取连接
    Connection connection = JDBCUtils.getConnection();
    // 编写sql【占位符】
    String sql = "select * from user where id = ?";
    // 创建预编译语句执行者
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    // 设置实际参数
    preparedStatement.setInt(1, 5);
    // 执行sql并返回结果
    ResultSet resultSet = preparedStatement.executeQuery();
    // 处理结果
    while(resultSet.next()){
        int id = resultSet.getInt("id");
        String name = resultSet.getString("name");
        System.out.println(id+" "+name);
    }
    // 关闭资源
    JDBCUtils.closeResource(preparedStatement, connection);
}

二 连接池

2.1 概述

我们在使用JDBC操作数据库时,每次都需要创建连接、销毁连接,这些操作是消耗时间和浪费资源的,性能不好,我们使用连接池来优化;在使用连接时从连接池中获得,使用完毕归还到连接池,提高连接的复用性,减轻服务器压力

JDBC连接池&动态代理

DataSource

是java对数据库连接池提供的一套规范(接口),咱们只需要学习接口的方法即可

实现类由连接池厂商提供(jar)

2.2 c3p0

他是一个开源连接池,目前hibernate【JDBC】等框架推荐使用

a)硬编码【了解】

步骤分析

1 导入jar包

2 编写代码

​ // 创建连接池对象

​ // 设置参数

​ // 获取连接

​ // 归还到连接池

代码片段

@Test
public void test01() throws Exception {
    // 创建连接池对象 组合池
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    // 设置参数
    // 数据库基本四项
    dataSource.setDriverClass("com.mysql.jdbc.Driver");
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db_day04");
    dataSource.setUser("root");
    dataSource.setPassword("root");
    // 基本参数 这些都有默认值 开发一般不用,结合自己网站实际情况
    dataSource.setInitialPoolSize(5); // 初始化连接池5个
    dataSource.setMaxPoolSize(20); // 最大连接个数
    dataSource.setAcquireIncrement(5); // 一次性创建5个连接
    dataSource.setMaxIdleTime(1000000);// 单位是毫秒  最大空闲销毁时间
    dataSource.setCheckoutTimeout(3000); // 最大等待时间3秒

    // 获取连接
    Connection connection = dataSource.getConnection();
    System.out.println(connection);

    // 归还到连接池
    connection.close(); // 之前咱们学习JDBC close是销毁连接,现在使用了连接时是归还到连接池,连接池厂商对此方法进行了增强  【动态代理】
}

b)配置文件【推荐】

步骤分析

1 导入jar包

2 定义一个配置文件【由连接池提供】

要求:

​ 文件名必须为:c3p0-config.xml 【不需要修改】

​ 文件位置必须在:src根目录下

3 编写代码

​ // 创建连接池对象

​ // 获取连接

​ // 归还到连接池

代码片段

@Test
public void test01() throws Exception {
    // 创建连接池对象
    // new ComboPooledDataSource(); 它会去 src目录下加载 c3p0-config.xml 配置文件  <default-config>标签的信息
    // 底层技术我昨天讲解的 类加载器
    //  ComboPooledDataSource dataSource = new ComboPooledDataSource();
    // 它会去 src目录下加载 c3p0-config.xml 配置文件    <named-config name="dev">
    ComboPooledDataSource dataSource = new ComboPooledDataSource("test");
    // 获取连接
    Connection connection = dataSource.getConnection();
    System.out.println(connection);

    // 归还到连接池
    connection.close(); // 是归还到连接池
}

2.3 druid

druid(德鲁伊),它是阿里巴巴旗下开源产品,它号称目前最好的连接池没有之一,支持日志监控

目前主要的互联网网站,都在使用druid作为第三方连接池

a)配置文件【推荐】

步骤分析

1 导入jar包

2 定义一个配置文件【由连接池提供】

建议:

​ 名称为:druid.properties 【见名知意】

​ 位置在:src根目录下

3 编写代码

// 创建连接池对象

// 获取连接

// 归还到连接池

代码片段

@Test
public void test01() throws Exception {
    // 获取druid.properteis文件 io流  通过类加载器
    InputStream is = DemoUse.class.getClassLoader().getResourceAsStream("druid.properties");
    // 创建配置文件对象  key=value
    Properties properties = new Properties();
    properties.load(is);
    // 创建连接池对象
    // druid 提供了一个工具类
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);

    // 获取连接
    Connection connection = dataSource.getConnection();
    System.out.println(connection);

    // 打印连接池
    System.out.println(dataSource);

    // 归还到连接池
    connection.close(); // 此方法在使用连接池后,是被增强的(动态代理)
}

2.4 连接池工具类

连接池是一个重量级的对象,连接创建是非常消耗时间和浪费资源的,一般进入企业开发一个项目只有一个连接池,并且初始化一次,提取到工具类放入静态代码块,简化代码,提供效率

步骤分析

// 提供获取连接池的方法

// 提供获取连接的方法

// 提供关闭资源的方法【connection是归还到连接池,其他为关闭】

代码片段

public class DataSourceUtils {
    // 声明变量 static
    public static DataSource dataSource;

    // 初始化连接池(仅一次)
    static {
        // 加载 druid.properteis 使用类加载
        InputStream is = DataSourceUtils.class.getClassLoader().getResourceAsStream("druid.properties");

        // 创建连接池对象 使用工具类
        try {
            // 创建配置文件对象
            Properties properties = new Properties();
            // 加载io流 key = value
            properties.load(is);
            // 使用第三方连接池 druid
            dataSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 提供获取连接池的方法
    public static DataSource getDataSource() {
        return dataSource;
    }

    // 提供获取连接的方法
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    // 提供关闭资源的方法【connection是归还到连接池】
    // 提供关闭资源的方法 【方法重载】3 dql
    public static void closeResource(ResultSet resultSet, Statement statement, Connection connection) {
        // 关闭结果集
        // ctrl+alt+m 将java语句抽取成方法
        closeResultSet(resultSet);
        // 关闭语句执行者
        closeStatement(statement);
        // 关闭连接
        closeConnection(connection);
    }

    // 提供关闭资源的方法 【方法重载】 2 dml
    public static void closeResource(Statement statement, Connection connection) {
        // 关闭语句执行者
        closeStatement(statement);
        // 关闭连接
        closeConnection(connection);
    }

    private static void closeConnection(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    private static void closeStatement(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    private static void closeResultSet(ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

补充

采用第三方 druid

1 导入druid的jar包

2 在src目录下放一个 druid.properties 配置文件

三 动态代理

刚才我们学习完了常用的连接池【c3p0,druid,DBCP】,我们知晓了如果连接需要归还给连接池,那么直接调用close方法即可。 但是我们在学习jdbc时调用connection.close() 是关闭连接,而现在是将连接还回连接池。这个是怎么做到的呢 ? 其实是对Connection的close方法进行了增强。

3.1 增强

a)继承

针对于目标对象,创建一个子类,重写父类的方法

缺点:

​ java是单继承,多实现,如果说我们只为了增强其中一个一个方法,就要浪费一个继承位,这种设计不好,子类还需要我们自己手写,更加繁琐,这种方案我们不采纳

b)动态代理

代理模式【掌握】

JDBC连接池&动态代理

班长【听歌】 – 豆豆老师【唱歌】

班长【听歌】-- 宋哲【收费】-- 豆豆老师【唱歌】

调用者—代理对象【增强】–目标对象【唱歌】

动态代理

在程序运行期间【反射】,由JVM虚拟机来创建代理对象,对其目标对象方法增强

动态代理技术

JDK、【GCLIB、JAVA ASSIST】spring框架

JDK动态代理

jdk根据接口规范来创建代理对象,目标对象也需要实现此接口

3.2 案例:动态代理

需求

班长【听歌】 – 豆豆老师【唱歌】

班长【听歌】-- 宋哲【收费】-- 豆豆老师【唱歌】

需求分析

针对唱歌的方法进行增强,使用jdk的动态代理,目标对象是由接口,根据接口规范创建代理对象

步骤分析

1 创建接口 singer

​ 唱歌、吃饭

public interface Singer {

    public void sing();

    public void eat();
}

2 创建目标类 豆豆老师 实现singer接口

​ 唱歌:单身情歌

​ 吃饭:珍珠翡翠白玉汤

public class DouDou implements Singer {
    @Override
    public void sing() {
        System.out.println("单身情歌");
    }

    @Override
    public void eat() {
        System.out.println("珍珠翡翠白玉汤");
    }
}

3 针对目标对象进行增强 【宋哲】

public class TestProxy {
    // 版本一
    @Test
    public void test01() throws Exception {
        // 现有目标对象
        DouDou douDou = new DouDou();
 /*       douDou.sing();
        douDou.eat();*/
        // 创建代理对象  proxy   唱歌方法 :先收费后唱歌
        /*
         * 参数一:目标对象类加载器
         * 参数二:目标类接口数组
         * 参数三:调用处理器接口 -- 增强逻辑
         *
         * */
        // 多态效果实现
        Singer songzhe = (Singer) Proxy.newProxyInstance(douDou.getClass().getClassLoader(), new Class[]{Singer.class}, new InvocationHandler() {

            // 重写 invoke  具体实现增强的功能
            /*
             * 参数一: proxy  --- this
             * 参数二: method当前执行的方法  sing == method,eat == method
             * 参数三:当前调用方法传递的参数列表 【数组】
             *
             * */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // System.out.println("我是invoke");
                // System.out.println(method.getName());
                // 进行增强
                if ("sing".equals(method.getName())) {
                    System.out.println("收费。。9999");
                }
                // 调用目标对象去执行方法
                Object obj = method.invoke(douDou, args);
                return obj;
            }
        });
        // 调用代理对象方法
        songzhe.sing();
        songzhe.eat();
    }

    // 版本二
    @Test
    public void test02() throws Exception {
        // 创建目标对象
        DouDou douDou = new DouDou();
        // 创建代理对象  Proxy  jdk要求根据接口规范
        /*
         * 参数一  目标类加载器
         * 参数二  目标类接口数组
         * 参数三  执行处理器接口 -- 增强
         *
         * */
        Singer songzhe = (Singer) Proxy.newProxyInstance(douDou.getClass().getClassLoader(), douDou.getClass().getInterfaces(), (proxy, method, args) -> {
            // 对唱歌增强
            if ("sing".equals(method.getName())) {
                System.out.println("收费。。8888");
            }
            // 调用目标对象 反射
            return method.invoke(douDou, args);
        });
        // 调用代理对象的方法,实现增强
        songzhe.sing();
        songzhe.eat();
    }
}

jdk提供了一个工具类 Proxy 专门创建代理对象

复习

  • 能够通过PreparedStatement完成增、删、改、查

    String str = "sql语句,使用占位符?给字段赋值"
    PreparedStatement preparedStatement = connection.preparedStatement(str);
    preparedStatement.setXXX(占位符位置,实际参数);
    
  • 能够完成PreparedStatement改造登录案例

  • 能够理解连接池解决现状问题的原理

  • 能够使用C3P0连接池

  • 能够使用DRUID连接池

  • 能够编写连接池工具类

  • 能够说出动态代理的好处

    可以对其目标对象的方法进行增强

  • 能够使用动态代理