JDBC笔记

目录

基础篇

一、引言

1.1 数据的存储

1.2 数据库的操作

二、JDBC

2.1 JDBC的概念

2.2 JDBC的核心组成

三、JDBC快速入门

3.1 JDBC搭建步骤

3.2 代码实现

3.2.1 数据库

3.2.2 java代码

四、核心API理解

4.1 注册驱动

4.2 Connection

4.3 Statement

4.4 PreparedStatement

4.5 ResultSet

五、基于PreparedStatement实现CRUD

5.1 查询单行单列

5.2 查询单行多列

5.3 查询多行多列

5.4 新增

5.5 修改

5.6 删除

六、常见问题

6.1 资源的管理

6.2 SQL语句问题

6.3 SQL语句未设置参数问题

6.4 用户名或密码错误问题

6.5 通信异常

进阶篇

七、JDBC扩展

7.1 实体类和ORM

7.2 主键回显

7.3 批量操作

八、连接池

8.1 现有问题

8.2 连接池

8.3 常见连接池

8.4 Druid连接池使用

8.5 Hikari连接池的使用

高级篇

九、JDBC优化及工具类封装

9.1 现有问题

9.2 JDBC工具类封装V1.0

9.3 ThreadLocal

9.4 JDBC工具类封装V1.0

十、DAO封装及BaseDAO工具类

10.1 DAO概念

10.1 DAO搭建

10.1 BaseDAO概念

10.2 BaseDAO搭建

10.3 BaseDAO通用的增删改方法

10.4 BaseDAO通用的查询方法

10.5 DAO结合BaseDAO完成CRUD


基础篇

一、引言

1.1 数据的存储

我们在开发Java程序时,数据都是存储在内存中,属于临时存储,当程序停止或重启时,内存中的数据就丢失了!我们为了解决数据的长期存储问题,有如下解决方案:

1.数据通过1/0流技术,存储在本地磁盘中,解决了持久化问题,但是没有结构和逻辑,不方便管理和维 护。

2.通过关系型数据库,将数据按照特定的格式交由数据库管理系统维护。关系型数据库是通过库和表分隔 不同的数据,表中数据存储的方式是行和列,区分相同格式不同值的数据

数据库存储数据
1.2 数据库的操作

数据存储在数据库,仅仅解决了我们数据存储的问题,但当我们程序运行时,需要读取数据,以及对数据做增删改的操作,那么我们如何通过Java程序对数据库中的数据做增删改查呢?

Java程序读取数据库

二、JDBC

2.1 JDBC的概念
  • JDBC:Java Database Connectivity,意为Java数据库连接。

  • JDBC是Java提供的一组独立于任何数据库管理系统的API。

  • Java提供接口规范,由各个数据库厂商提供接口的实现,厂商提供的实现类封装成jar文件,也就是我们俗称的数据库驱动iar包。

  • 学习JDBC,充分体现了面向接口编程的好处,程序员只关心标准和规范,而无需关注实现过程。

JDBC简单执行过程

2.2 JDBC的核心组成
  • 接口规范:

    1.为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。

    2.接口存储在java.sql和iavax.sql包下。

  • 实现规范:

    1.因为各个数据库厂商的DBMS软件各有不同,那么各自的内部如何通过SQL实现增、删、改、查等操作管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。

    2.厂商将实现内容和过程封装成jar文件,我们程序员只需要将jar文件引入到项目中集成即可,就可以开发调用实现过程操作数据库了。

三、JDBC快速入门

3.1 JDBC搭建步骤
  1. 准备数据库。

  2. 官网下载数据库连接驱动jar包。 MySQL :: Download MySQL Connector/J (Archived Versions)

  3. 创建Java项目,在项目下创建lib文件夹,将下载的驱动jar包复制到文件夹里

  4. 选中lib文件夹右键->Add as Library,与项目集成。

  5. 编写代码

3.2 代码实现
3.2.1 数据库
create table t_emp ( 
emp_id int auto_increment comment "员工编号" primary key, 
emp_name varchar(100) not null comment "员工姓名", 
emp_salary double(10,5) not null comment "员工薪资", 
emp_age int not null comment "员工年龄");
​
insert into t_emp (emp_name,emp_salary,emp_age) 
values 
("andy",777.77,32), 
("大风哥",666.66,41), 
("康师傅",111,23), 
("Gavin",123,26), 
("小鱼儿",123,28);
3.2.2 java代码
//        1.注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
​
//        2.获取连接对象
        String url = "jdbc:mysql://localhost:3306/atguigu";
        String username = "root";
        String password = "123456";
        Connection connection = DriverManager.getConnection(url, username, password);
​
//        3.获取执行SQL语句的对象
        Statement statement = connection.createStatement();
​
//        4.编写SQL语句,并执行,接受返回的结果
        String sql = "select * from t_emp";
        ResultSet resultSet = statement.executeQuery(sql);
​
//        5.处理结果,遍历resultSet结果集
        while (resultSet.next()) {
            int empId = resultSet.getInt("emp_id");
            String empName = resultSet.getString("emp_name");
            double empSalary = resultSet.getDouble("emp_salary");
            int empAge = resultSet.getInt("emp_age");
            System.out.println(empId + "\t" + empName + "\t" + empSalary + "\t" + empAge);
        }
​
//        6.释放资源(先开后关原则)
        resultSet.close();
        statement.close();
        connection.close();

四、核心API理解

4.1 注册驱动
  • Class.forName("com.mysql.cj.jdbc.Driver");
    ​
    DriverManager.registerDriver(new Driver());
  • 在 Java 中,当使用JDBC(Java Database connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得JDBCAPI能够识别并与特定的数据库进行交互。

  • 从JDK6开始,不再需要显式地调用 class.forName()来加载JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。

4.2 Connection
  • Connection接口是JDBC API的重要接口,用于建立与数据库的通信通道。换而言之,connection对象不为空则代表一次数据库连接。

  • 在建立连接时,需要指定数据库URL、用户名、密码参数。

    1. URL:jdbc:mysql://localhost:3306/atguigu

    2. "jdbc:mysq!://IP地址:端口号/数据库名称?参数键值对1&参数键值对2

  • Connection 接口还负责管理事务,Connection 接口提供了 commitrollback 方法,用于提交事务和回滚事务。

  • 可以创建 Statement 对象,用于执行 SQL语句并与数据库进行交互。

  • 在使用JDBC技术时,必须要先获取Connection对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。

4.3 Statement
  • Statement 接口用于执行 SQL语句并与数据库进行交互。它是 JDBC API 中的一个重要接口。通过

  • Statement 对象,可以向数据库发送 SQL语句并获取执行结果。

  • 结果可以是一个或多个结果。

    1.增删改:受影响行数单个结果。

    2.查询:单行单列、多行多列、单行多列等结果。

  • 但是 statement 接口在执行SQL语句时,会产生 SQL注入攻击问题:

    当使用 statement 执行动态构建的 SQL 查询时,往往需要将査询条件与 SQL 语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。

4.4 PreparedStatement
  • PreparedStatementStatement 接口的子接口,用于执行 预编译的 SQL 查询,作用如下:

    1.预编译SQL语句:在创建Preparedstatement时,就会预编译SQL语句,也就是SQL语句已经固定。

    2.防止SQL注入: Preparedstatement 支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来",无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。

    3.性能提升:Preparedstatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。

  • 后续的学习我们都是基于Preparedstatement进行实现,更安全、效率更高!

4.5 ResultSet
  • ResultSet 是 JDBC API中的一个接口,用于表示从数据库中 执行查询语句所返回的结果集。它提供了一种用于遍历和访问查询结果的方式。

  • 遍历结果:ResultSet可以使用 next()方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean类型,true代表有下一行结果,false则代表没有。

  • 获取单列结果:可以通过getXxx的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。

五、基于PreparedStatement实现CRUD

5.1 查询单行单列
    @Test
    public void testQuerySingleAndCol() throws Exception {
//        1.注册驱动
//        DriverManager.registerDriver(new Driver());
​
//        2.获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
//        3.预编译SQL语句得到PreparedStatement对象
        PreparedStatement preparedStatement = connection.prepareStatement("select count(*) as count from t_emp");
​
//        4.执行SQL语句,获取结果
        ResultSet resultSet = preparedStatement.executeQuery();
​
//        5.处理结果(如果自己明确一定只有一个结果,那么resultSet最少要做依次next的判断,才能拿到我们要的列的结果)
        if (resultSet.next()) {
            int count = resultSet.getInt("count");
            System.out.println(count);
        }
​
//        6.释放资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }

5.2 查询单行多列
    @Test
    public void testQuerySingleRow() throws Exception {
//        1.注册驱动
​
//        2.获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
//        3.预编译SQL语句获得PreparedStatement对象
        PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp where emp_id=?");
​
//        4.执行,并接受结果
        preparedStatement.setInt(1, 5);
        ResultSet resultSet = preparedStatement.executeQuery();
​
//        5.处理结果
        while (resultSet.next()) {
            int id = resultSet.getInt(1);
            String name = resultSet.getString(2);
            double salary = resultSet.getDouble(3);
            int age = resultSet.getInt(4);
            System.out.println(name + "\t" + id + "\t" + salary + "\t" + age);
        }
​
//        6.释放资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }

5.3 查询多行多列
    @Test
    public void testQueryMoreRow() throws Exception {
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
        PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp where emp_age>?");
​
//        为占位符赋值,执行SQL语句,接收结果
        preparedStatement.setInt(1, 25);
        ResultSet resultSet = preparedStatement.executeQuery();
​
        while (resultSet.next()) {
            int empId = resultSet.getInt("emp_id");
            String empName = resultSet.getString("emp_name");
            double empSalary = resultSet.getDouble("emp_salary");
            int empAge = resultSet.getInt("emp_age");
            System.out.println(empId + "\t" + empName + "\t" + empSalary + "\t" + empAge);
        }
​
//        释放资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }

5.4 新增
    @Test
    public void testInsert() throws Exception {
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
        PreparedStatement preparedStatement = connection.prepareStatement("insert into t_emp(emp_name,emp_salary,emp_age) values (?,?,?)");
​
        // 为?占位符赋值,并执行SQL语句,并接收返回对象
        preparedStatement.setString(1, "rose");
        preparedStatement.setDouble(2, 345.67);
        preparedStatement.setInt(3, 28);
​
        int result = preparedStatement.executeUpdate();
​
//        根据受影响行数,做判断,得到成功或失败
        if (result > 0) {
            System.out.println("成功");
        } else {
            System.out.println("失败");
        }
​
//        6.释放资源
        preparedStatement.close();
        connection.close();
    }

5.5 修改
    @Test
    public void testUpdate() throws Exception {
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
        PreparedStatement preparedStatement = connection.prepareStatement("update t_emp set emp_salary = ? where emp_id = ?");
​
        // 为?占位符赋值,并执行SQL语句,并接收返回对象
        preparedStatement.setDouble(1,888.88);
        preparedStatement.setInt(2,6);
        int result = preparedStatement.executeUpdate();
​
//        根据受影响行数,做判断,得到成功或失败
        if (result > 0) {
            System.out.println("成功");
        } else {
            System.out.println("失败");
        }
​
//        6.释放资源
        preparedStatement.close();
        connection.close();
    }

5.6 删除
    @Test
    public void testDelete() throws Exception {
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
        PreparedStatement preparedStatement = connection.prepareStatement("delete from t_emp where emp_id = ?");
​
        // 为?占位符赋值,并执行SQL语句,并接收返回对象
        preparedStatement.setInt(1, 6);
        int result = preparedStatement.executeUpdate();
​
//        根据受影响行数,做判断,得到成功或失败
        if (result > 0) {
            System.out.println("成功");
        } else {
            System.out.println("失败");
        }
​
//        6.释放资源
        preparedStatement.close();
        connection.close();
    }

六、常见问题

6.1 资源的管理

在使用JDBC的相关资源时,比如Connection、Preparedstatement、ResultSet,使用完毕后,要及时关闭这些资源以释放数据库服务器资源和避免内存泄漏是很重要的。

6.2 SQL语句问题

java.sql.SQLSyntaxErrorException:SQL语句错误异常,一般有几种可能:

  1. SQL语句有错误,检查SQL语句!建议SQL语句在SQL工具中测试后再复制到Java程序中!

  2. 连接数据库的URL中,数据库名称编写错误,也会报该异常!

6.3 SQL语句未设置参数问题

java.sql.SQLException: No value specified for parameter 1

在使用预编译SQL语句时,如果有?占位符,要为每一个占位符赋值,否则报该错误!

6.4 用户名或密码错误问题

连接数据库时,如果用户名或密码输入错误,也会报SQLException,容易混淆!所以一定要看清楚异常后面的原因描述

6.5 通信异常

在连接数据库的URL中,如果IP或端口写错了,会报如下异常:

com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

进阶篇

七、JDBC扩展

7.1 实体类和ORM
  • 在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Java是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类!

  • ORM(0bject Relational Mapping)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!

  • 当下JDBC中这种过程我们称其为手动ORM。后续我们也会学习0RM框架,比如MyBatis、JPA等。

public class Employee {
    private Integer empId;  //emp_id
    private String empName;  //emp_name
    private Double empSalary;  //emp_salary
    private Integer empAge;  //emp_age
​
    public Employee() {
    }
​
    public Employee(Integer empId, String empName, Double empSalary, Integer empAge) {
        this.empId = empId;
        this.empName = empName;
        this.empSalary = empSalary;
        this.empAge = empAge;
    }
​
    public Integer getEmpId() {
        return empId;
    }
​
    public void setEmpId(Integer empId) {
        this.empId = empId;
    }
​
    public String getEmpName() {
        return empName;
    }
​
    public void setEmpName(String empName) {
        this.empName = empName;
    }
​
    public Double getEmpSalary() {
        return empSalary;
    }
​
    public void setEmpSalary(Double empSalary) {
        this.empSalary = empSalary;
    }
​
    public Integer getEmpAge() {
        return empAge;
    }
​
    public void setEmpAge(Integer empAge) {
        this.empAge = empAge;
    }
​
    @Override
    public String toString() {
        return "Employee{" +
                "empId=" + empId +
                ", empName='" + empName + '\'' +
                ", empSalary=" + empSalary +
                ", empAge=" + empAge +
                '}';
    }

封装代码:

    // 查询单行数据
    @Test
    public void testORM() throws Exception {
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
        PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp where emp_id = ?");
​
        preparedStatement.setInt(1, 1);
​
        ResultSet resultSet = preparedStatement.executeQuery();
​
        Employee employee = null;
​
        if (resultSet.next()) {
            employee = new Employee();
            int empId = resultSet.getInt("emp_id");
            String empName = resultSet.getString("emp_name");
            double empSalary = resultSet.getDouble("emp_salary");
            int empAge = resultSet.getInt("emp_age");
//            为对象的属性赋值
            employee.setEmpId(empId);
            employee.setEmpName(empName);
            employee.setEmpSalary(empSalary);
            employee.setEmpAge(empAge);
        }
        System.out.println(employee);
​
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
    @Test
    public void testORMList() throws Exception {
        //    查询多行数据
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
        PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp");
​
        ResultSet resultSet = preparedStatement.executeQuery();
​
        Employee employee = null;
​
        List<Employee> employeeList = new ArrayList<>();
​
        while (resultSet.next()) {
            employee = new Employee();
            int empId = resultSet.getInt("emp_id");
            String empName = resultSet.getString("emp_name");
            double empSalary = resultSet.getDouble("emp_salary");
            int empAge = resultSet.getInt("emp_age");
​
            //            为对象的属性赋值
            employee.setEmpId(empId);
            employee.setEmpName(empName);
            employee.setEmpSalary(empSalary);
            employee.setEmpAge(empAge);
//            将每次循环封装的一行数据的对象存储在集合里
            employeeList.add(employee);
        }
//        处理结果,遍历集合
//        for (Employee emp : employeeList) {
//            System.out.println(emp);
//        }
        for (int i = 0; i < employeeList.size(); i++) {
            System.out.println(employeeList.get(i));
        }
​
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }

7.2 主键回显
  • 在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到,但是在Java程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作为主键回显。

  • 代码实现:

    @Test
    public void testReturnPK() throws Exception {
//        获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "123456");
​
//        预编译SQL语句对象,告知preparedStatement,返回新增数据的主键列的值
        String sql = "insert into t_emp(emp_name,emp_salary,emp_age) values(?,?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
​
//        创建对象,将对象的属性值填充在?占位符上
        Employee employee = new Employee(null, "jack", 123.45, 29);
        preparedStatement.setString(1, employee.getEmpName());
        preparedStatement.setDouble(2, employee.getEmpSalary());
        preparedStatement.setInt(3, employee.getEmpAge());
​
//        执行sql,并接收返回结果
        int result = preparedStatement.executeUpdate();
        ResultSet resultSet = null;
​
//        处理结果
        if (result > 0) {
            System.out.println("成功");
​
//            获取当前新增数据的主键列,回显到Java中employee对象的empId属性上
//            返回的主键值,是一个单行单列的结果存储在resultSet里
            resultSet = preparedStatement.getGeneratedKeys();
            if (resultSet.next()) {
                int empId = resultSet.getInt(1);
                employee.setEmpId(empId);
            }
            System.out.println(employee);
        } else {
            System.out.println("失败");
        }
​
//        释放资源
        if (resultSet != null) {
            resultSet.close();
        }
        preparedStatement.close();
        connection.close();
    }

7.3 批量操作
  • 插入多条数据时,一条一条发送给数据库执行,效率低下!

  • 通过批量操作,可以提升多次操作效率!

  • 代码实现:

    @Test
    public void testBatch() throws Exception {
//        1.注册驱动
//        Class.forName("com.mysql.cj.jdbc.Driver");
​
//        2.获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?rewriteBatchedStatements=true", "root", "123456");
​
//        3.编写SQL语句
        /*
         * 注意:
         * 1. 必须在连接数据库的URL后面追加rewriteBatchedStatements=true。允许批量操作
         * 2. 新增SQL必须用values,且语句最后不要追加;结束
         * 3. 调用addBatch()方法,将SQL语句进行批量添加的操作调用
         * 4. 统一执行批量操作,调用executeBatch()方法
         * */
        String sql = "insert into t_emp(emp_name,emp_salary,emp_age) values (?,?,?)";
​
//        4.创建预编译的PreparedStatement对象,传入sql
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
​
//        获取当前代码执行的时间
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
//            5.为占位符赋值
            preparedStatement.setString(1, "marry" + i);
            preparedStatement.setDouble(2, 100.0 + i);
            preparedStatement.setInt(3, 20 + i);
​
            preparedStatement.addBatch();
        }
​
//        执行批量操作
        preparedStatement.executeBatch();
​
        long end = System.currentTimeMillis();
        System.out.println("消耗时间:" + (end - start));
​
//        6.释放资源
        preparedStatement.close();
        connection.close();
    }

八、连接池

8.1 现有问题

每次操作数据库都要获取新连接,使用完毕后就close释放,频繁的创建和销毁造成资源浪费

连接的数量无法把控,对服务器来说压力巨大。

8.2 连接池

连接池就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接、管理连接、释放连接等操作。

预先创建数据库连接放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率。

当池中无连接可用,且未达到上限时,连接池会新建连接。

池中连接达到上限,用户请求会等待,可以设置超时时间。

8.3 常见连接池

JDBC 的数据库连接池使用 javax.sql.DataSource接口进行规范,所有的第三方连接池都实现此接口,自行添加具体实现!也就是说,所有连接池获取连接的和回收连接方法都一样,不同的只有性能和扩展功能。

  • DBCP 是Apache提供的数据库连接池,速度相对C3P0较快,但自身存在一些BUG。

  • C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以。

  • Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较吃c3p0差一点

  • Druid 是阿里提供的数据库连接池,是集DBCP、C3P0、Proxool优点于一身的数据库连接池,性能、扩展性、易用性都更好,功能丰富

  • Hikari(ひかり[shi ga li])取自日语,是光的意思,是SpringBoot2.x之后内置的一款连接池,基于 BoneCP(已经放弃维护,推荐该连接池)做了不少的改进和优化,口号是快速、简单、可靠。

主流连接池的性能对比

8.4 Druid连接池使用
  • 使用步骤

    1. 引入jar包

    2. 编码

  • 代码实现

    1. 硬编码方式(了解)

        @Test
        public void testHardDruid() throws Exception {
            /*
             * 硬编码:将连接池的配置信息和Java代码耦合在一起
             * 1. 创建DruidDataSource连接池对象
             * 2. 设置连接池的配置信息 【必须 | 非必须】
             * 3. 通过连接池获取连接对象
             * 4. 回收连接【不是释放连接,而是将连接归还给连接池,给其他线程进行复用】
             * */
    ​
    //        1.创建DruidDataSource连接池对象
            DruidDataSource druidDataSource = new DruidDataSource();
    ​
    //        2.设置连接池的配置信息 【必须 | 非必须】
    //        2.1 必须设置的配置
            druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Drive");
            druidDataSource.setUrl("jdbc:mysql:///atguigu");
            druidDataSource.setUsername("root");
            druidDataSource.setPassword("123456");
    ​
    //        2.2 非必须设置的配置
            druidDataSource.setInitialSize(10);
            druidDataSource.setMaxActive(20);
    ​
    //        3.通过连接池获取连接对象
            DruidPooledConnection connection = druidDataSource.getConnection();
    ​
    //        基于connection进行CRUD
    ​
    //        4.回收连接
            connection.close();
        }

2.软编码方式(推荐):

  • 在项目目录下创建resources文件夹,标识该文件夹为资源目录,创建db.properties配置文件,将连接信息定义在该文件中。

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///atguigu
username=root
password=123456
initialSize=10
maxActive=20

Java代码:

    @Test
    public void testResourcesDruid() throws Exception {
//        1.创建properties集合,用于存储外部配置文件的key和value值
        Properties properties = new Properties();
​
//        2.读取外部配置文件,获取输入流,加载到properties集合里
        InputStream inputStream = DruidTest.class.getClassLoader().getResourceAsStream("db.properties");
        properties.load(inputStream);
​
//        3.基于properties集合构建DruidDataSource连接池
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
​
//        4.通过连接池获取连接对象
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
//        5.开发CRUD
​
//        6.回收连接
        connection.close();
    }

8.5 Hikari连接池的使用
  • 使用步骤

    1. 引入jar包

    2. 编码

  • 代码实现

    1. 硬编码方式(了解)

        @Test
        public void testHardCodeHikari() throws Exception {
            /*
             * 硬编码:将连接池的配置信息和Java代码耦合在一起
             * 1. 创建HikariDataSource连接池对象
             * 2. 设置连接池的配置信息 【必须 | 非必须】
             * 3. 通过连接池获取连接对象
             * 4. 回收连接【不是释放连接,而是将连接归还给连接池,给其他线程进行复用】
             * */
    ​
    //        1.创建HikariDataSource连接池对象
            HikariDataSource hikariDataSource = new HikariDataSource();
    ​
    //        2.设置连接池的配置信息 【必须 | 非必须】
    //        2.1 必须设置的配置
            hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Drive");
            hikariDataSource.setJdbcUrl("jdbc:mysql:///atguigu");
            hikariDataSource.setUsername("root");
            hikariDataSource.setPassword("123456");
    ​
    //        2.2 非必须设置的配置
            hikariDataSource.setMinimumIdle(10);
            hikariDataSource.setMaximumPoolSize(20);
    ​
    //        3.通过connection获取连接对象
            Connection connection = hikariDataSource.getConnection();
    ​
    //        基于connection进行CRUD
    ​
    //        4.回收连接
            connection.close();
        }

    2.软编码方式(推荐):

    • 在项目目录下创建resources文件夹,标识该文件夹为资源目录,创建db.properties配置文件,将连接信息定义在该文件中

      driverClassName=com.mysql.cj.jdbc.Driver
      jdbcUrl=jdbc:mysql:///atguigu
      username=root
      password=123456
      initialSize=10
      maxActive=20

      Java代码:

          @Test
          public void testResourcesHikari() throws Exception {
      //        1.创建properties集合,用于存储外部配置文件的key和value值
              Properties properties = new Properties();
      ​
      //        2.读取外部配置文件,获取输入流,加载到properties集合里
              InputStream inputStream = HikariTest.class.getClassLoader().getResourceAsStream("hikari.properties");
              properties.load(inputStream);
      ​
      //        3.创建HikariConfig连接池配置对象,将properties集合传进去
              HikariConfig hikariConfig = new HikariConfig(properties);
      ​
      //        4.基于HikariConfig连接池配置对象,构建hikariDataSource
              HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);
              Connection connection = hikariDataSource.getConnection();
      ​
      //        5.开发CRUD
      ​
      //        6.回收连接
              connection.close();
          }

高级篇

九、JDBC优化及工具类封装

9.1 现有问题

我们在使用JDBC的过程中,发现部分代码存在冗余的问题:

  • 创建连接池

  • 获取连接

  • 连接的回收

9.2 JDBC工具类封装V1.0
  • resources/db.properties配置文件:

    • driverClassName=com.mysql.cj.jdbc.Driver
      url=jdbc:mysql:///atguigu
      username=root
      password=123456
      setMinimumIdle=10
      setMaximumPoolSize=20

  • 工具类代码:

    • /*
       * JDBC工具类(V1.0)
       * 1. 维护一个连接池对象
       * 2. 对外提供在连接池中获取连接的方法
       * 3. 对外提供回收连接的方法
       *
       * 注意:工具类仅对外提供共性的功能代码,所以方法均为静态方法
       * */
      public class JDBCUtil {
          //    创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的
          private static DataSource dataSource;
      ​
          //    项目启动时,即创建连接池对象,赋值给dataSource
          static {
              try {
                  Properties properties = new Properties();
                  InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
                  properties.load(inputStream);
      ​
                  dataSource = DruidDataSourceFactory.createDataSource(properties);
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      ​
          //    对外提供在连接池中获取连接的方法
          public static Connection getConnection() {
              try {
                  return dataSource.getConnection();
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      ​
          //    driverClassName=com.mysql.cj.jdbc.Driver
          //url=jdbc:mysql:///atguigu
          //username=root
          //password=123456
          //setMinimumIdle=10
          //setMaximumPoolSize=20对外提供回收连接的方法
          public static void release(Connection connection) {
              try {
                  connection.close();
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      }

  • Java测试类代码

    •     @Test
          public void testGetConnection() {
              Connection connection = JDBCUtil.getConnection();
              System.out.println(connection);
      //        CRUD
      ​
              JDBCUtil.release(connection);
          }

9.3 ThreadLocal

JDK 1.2的版本中就提供java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session等。

ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, 0bject>,其key就是一个ThreadLocal,而object即为该线程的共享变量。

而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static ThreadLocal,不同线程只能从中get, set,remove自己的变量,而不会影响其他线程的变量。

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

  • 线程间数据隔离。

  • 进行事务操作,用于存储线程事务信息。

  • 数据库连接,Session 会话管理。

  1. ThreadLocal对象.get: 获取ThreadLocal中当前线程共享变量的值。

  2. ThreadLocal对象.set: 设置ThreadLocal中当前线程共享变量的值。

  3. ThreadLocal对象,remove: 移除ThreadLocal中当前线程共享变量的值。

9.4 JDBC工具类封装V1.0
  • resources/db.properties配置文件:

    • driverClassName=com.mysql.cj.jdbc.Driver
      url=jdbc:mysql:///atguigu
      username=root
      password=123456
      setMinimumIdle=10
      setMaximumPoolSize=20

  • 工具类代码:

    • /*
       * JDBC工具类(V2.0)
       * 1. 维护一个连接池对象,维护了一个线程绑定变量的ThreadLocal对象
       * 2. 对外提供在ThreadLocal中获取连接的方法
       * 3. 对外提供回收连接的方法,回收过程中将要回收的连接从ThreadLocal中移除
       *
       * 注意:工具类仅对外提供共性的功能代码,所以方法均为静态方法
       * 注意:使用ThreadLocal就是为了一个线程在多次数据库操作过程中,使用的时同一个连接
       * */
      ​
      public class JDBCUtilV2 {
          //    创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的
          private static DataSource dataSource;
          private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
      ​
          //    项目启动时,即创建连接池对象,赋值给dataSource
          static {
              try {
                  Properties properties = new Properties();
                  InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
                  properties.load(inputStream);
      ​
                  dataSource = DruidDataSourceFactory.createDataSource(properties);
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      ​
          //    对外提供在连接池中获取连接的方法
          public static Connection getConnection() {
              try {
      //            在ThreadLocal中获取Connection
                  Connection connection = threadLocal.get();
      //            threadLocal里没有存储connection,也就是第一次
                  if (connection == null) {
                      connection = dataSource.getConnection();
                      threadLocal.set(connection);
                  }
                  return connection;
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      ​
          //      对外提供回收连接的方法
          public static void release() {
              try {
                  Connection connection = threadLocal.get();
                  if (connection != null) {
      //                从threadLocal中一处当前存储的Connection对象
                      threadLocal.remove();
      //                将Connection对象归还给连接池
                      connection.close();
                  }
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      }

  • Java测试代码

    •     @Test
          public void testJDBCV2() {
      //        Connection connection1 = JDBCUtil.getConnection();
      //        Connection connection2 = JDBCUtil.getConnection();
      //        Connection connection3 = JDBCUtil.getConnection();
      ​
      //        System.out.println(connection1);
      //        System.out.println(connection2);
      //        System.out.println(connection3);
      ​
      //        CRUD
      ​
              Connection connection1 = JDBCUtilV2.getConnection();
              Connection connection2 = JDBCUtilV2.getConnection();
              Connection connection3 = JDBCUtilV2.getConnection();
      ​
              System.out.println(connection1);
              System.out.println(connection2);
              System.out.println(connection3);
          }

十、DAO封装及BaseDAO工具类

10.1 DAO概念

DA0:Data Access Object,数据访问对象。

Java是面向对象语言,数据在Java中通常以对象的形式存在。一张表对应一个实体类,一张表的操作对应一个DAO对象!

在Java操作数据库时,我们会将对同一张表的增删改查操作统一维护起来,维护的这个类就是DAO层。

DAO层只关注对数据库的操作,供业务层Service调用,将职责划分清楚!

10.1 DAO搭建
  • 新建dao包,在dao包内新建接口 EmployeeDao

    • p
      ublic interface EmployeeDao {
          /**
           * 数据库对应的查询所有的操作
           *
           * @return
           */
          List<Employee> selectAll();
      ​
          /**
           * 数据库对应的根据empId查询单个员工数据的操作
           * @param empId 主键列
           * @return 一个员工对象(一行数据)
           */
          Employee selectByEmpId(Integer empId);
      ​
          /**
           * 数据库对应的新增一条员工数据
           * @param employee ORM思想中的一个员工对象
           * @return 受影响的行数
           */
          int insert(Employee employee);
      ​
          /**
           * 数据库对应的修改一条员工数据
           * @param employee ORM思想中的一个员工对象
           * @return 受影响的行数
           */
          int update(Employee employee);
      ​
          /**
           * 数据库对应的根据empId删除一条员工数据
           * @param empId 主键列
           * @return 受影响的行数
           */
          int delete(Integer empId);
      }

  • 在dao包内新建impl包,在impl包内新建 EmployeeImpl类并实现 EmployeeDao接口

    • public class EmployeeImpl implements EmployeeDao {
          @Override
          public List<Employee> selectAll() {
      //        1.注册驱动
      ​
      //        2.获取连接
      ​
      //        3.预编译SQL语句
      ​
      //        4.为占位符赋值,执行SQL,接受返回结果
      ​
      //        5.处理结果
      ​
      //        6.释放资源
              return null;
          }
      ​
          @Override
          public Employee selectByEmpId(Integer empId) {
              return null;
          }
      ​
          @Override
          public int insert(Employee employee) {
              return 0;
          }
      ​
          @Override
          public int update(Employee employee) {
              return 0;
          }
      ​
          @Override
          public int delete(Integer empId) {
              return 0;
          }
      }

10.1 BaseDAO概念

基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DA0的实现类可以抽取一个公共的父类,复用增删改查的基本操作,我们称为BaseDAO。

10.2 BaseDAO搭建
public class BaseDAO {
    /**
     * 通用的增删改的方法
     *
     * @param sql    调用要执行的SQL语句
     * @param params SQL语句中占位符要赋值的参数
     * @return 受影响额行数
     */
    public int executeUpdate(String sql, Object... params) throws Exception {
//        1.通过JDBCUtilV2获取连接
        Connection connection = JDBCUtilV2.getConnection();
​
//        2.预编译SQL语句
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
​
//        4.为占位符赋值,执行SQL,接受返回结果
        if (params != null && params.length > 0) {
            for (int i = 0; i < params.length; i++) {
//                占位符从1开始的,参数的组是从0开始的
                preparedStatement.setObject(i + 1, params[i]);
            }
        }
        int row = preparedStatement.executeUpdate();
​
//        5.释放资源
        preparedStatement.close();
        JDBCUtilV2.release();
​
//        6.处理结果
        return row;
    }
}

10.3 BaseDAO通用的增删改方法
public class BaseDAO {
    /**
     * 通用的增删改的方法
     *
     * @param sql    调用要执行的SQL语句
     * @param params SQL语句中占位符要赋值的参数
     * @return 受影响额行数
     */
    public int executeUpdate(String sql, Object... params) throws Exception {
//        1.通过JDBCUtilV2获取连接
        Connection connection = JDBCUtilV2.getConnection();

//        2.预编译SQL语句
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

//        4.为占位符赋值,执行SQL,接受返回结果
        if (params != null && params.length > 0) {
            for (int i = 0; i < params.length; i++) {
//                占位符从1开始的,参数的组是从0开始的
                preparedStatement.setObject(i + 1, params[i]);
            }
        }
        int row = preparedStatement.executeUpdate();

//        5.释放资源
        preparedStatement.close();
        JDBCUtilV2.release();

//        6.处理结果
        return row;
    }
10.4 BaseDAO通用的查询方法

 

/**
     * 通用的查询:多行多列、单行多列、单行单列
     * <p>
     * 多行多列:<List><Employee>
     * 单行多列:Employee
     * 单行单列:封装的一个结果,Double,Integer......
     * <p>
     * 封装过程:
     * 1.返回的结果:泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了
     * 2.返回的结果:通用:LIst  可以存储多个结果,也可以存储一个结果 get(0)
     * 3.结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象,Class
     */

    public <T> List<T> executeQuery(Class<T> clazz, String sql, Object... params) throws Exception {
//        1.获取连接
        Connection connection = JDBCUtilV2.getConnection();

//        2.预编译SQL语句
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

//        3.设置占位符的值
        if (params != null && params.length > 0) {
            for (int i = 0; i < params.length; i++) {
                preparedStatement.setObject(i + 1, params[i]);
            }
        }
//        4.执行SQL,并接受返回的结果
        ResultSet resultSet = preparedStatement.executeQuery();

//        5.获取结果集中的元数据对象
//        包含了:列的数量,列的名称
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();

        List<T> list = new ArrayList<>();

//        6.处理结果
        while (resultSet.next()) {
//            循环一次,代表有一行数据,通过反射创建一个对象
            T t = clazz.newInstance();
//            循环遍历当前行的列,循环几次,看有多少列
            for (int i = 1; i <= columnCount; i++) {
//                通过下标获取列的值
                Object value = resultSet.getObject(i);

//                获取到的列的value值,这个值就是t这个对象中的某个属性
//                获取当前拿到的列的名字 = 对象的属性名
                String filedName = metaData.getColumnLabel(i);
//                通过类对象和filedName获取要封装的对象的属性
                Field field = clazz.getDeclaredField(filedName);
//                突破封装的private
                field.setAccessible(true);
                field.set(t, value);
            }
            list.add(t);
        }

        resultSet.close();
        preparedStatement.close();
        JDBCUtilV2.release();

        return list;
    }
    /**
     * 通用查询:在上面查询的集合结果中获取第一个结果。 简化了获取单行单列的获取、单行多列的获取
     */

    public <T> T executeQueryBean(Class<T> clazz, String sql, Object... params) throws Exception {
        List<T> list = this.executeQuery(clazz, sql, params);
        if (list == null && list.size() == 0) {
            return null;
        } else {
            return list.get(0);
        }
    }
10.5 DAO结合BaseDAO完成CRUD
  • 在dao包下新建EmployeeDao接口,添加增删改查方法

public interface EmployeeDao {
    /**
     * 数据库对应的查询所有的操作
     *
     * @return
     */
    List<Employee> selectAll();
​
    /**
     * 数据库对应的根据empId查询单个员工数据的操作
     * @param empId 主键列
     * @return 一个员工对象(一行数据)
     */
    Employee selectByEmpId(Integer empId);
​
    /**
     * 数据库对应的新增一条员工数据
     * @param employee ORM思想中的一个员工对象
     * @return 受影响的行数
     */
    int insert(Employee employee);
​
    /**
     * 数据库对应的修改一条员工数据
     * @param employee ORM思想中的一个员工对象
     * @return 受影响的行数
     */
    int update(Employee employee);
​
    /**
     * 数据库对应的根据empId删除一条员工数据
     * @param empId 主键列
     * @return 受影响的行数
     */
    int delete(Integer empId);
}

  • 在dao包下新建impl包,在impl包下新建EmployeeImpl类,在EmployeeImpl类继承BaseDAO以及接口EmployeeDao,并重写增删改查方法

public class EmployeeImpl extends BaseDAO implements EmployeeDao {
    @Override
    public List<Employee> selectAll() {
        try {
            String sql = "select emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge from t_emp";
            return executeQuery(Employee.class, sql, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
​
    @Override
    public Employee selectByEmpId(Integer empId) {
        try {
            String sql = "select emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge from t_emp where emp_id=?";
            return executeQueryBean(Employee.class, sql, empId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
​
    @Override
    public int insert(Employee employee) {
        try {
            String sql = "insert into t_emp(emp_name,emp_salary,emp_age) values(?,?,?)";
            return executeUpdate(sql, employee.getEmpName(), employee.getEmpSalary(), employee.getEmpAge());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
​
    @Override
    public int update(Employee employee) {
        try {
            String sql = "update t_emp set emp_salary = ? where emp_id = ? ";
            return executeUpdate(sql, employee.getEmpSalary(), employee.getEmpId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
​
    @Override
    public int delete(Integer empId) {
        try {
            String sql="delete from t_emp where emp_id = ?";
            return executeUpdate(sql,empId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
  • 测试

    @Test
    public void testEmployeeDaoSelectMore() {
//        1.创建DAO实现类对象
        EmployeeDao employeeDao = new EmployeeImpl();

//        2.调用查询所有方法
        List<Employee> employeeList = employeeDao.selectAll();

//        3.处理结果
        for (Employee employee : employeeList) {
            System.out.println("employee = " + employee);
        }
    }

    @Test
    public void testEmployeeDaoSelect() {
//        1.创建DAO实现类对象
        EmployeeDao employeeDao = new EmployeeImpl();

//        2.调用根据id查询单个员工方法
        Employee employee = employeeDao.selectByEmpId(1);

//        3.处理结果
        System.out.println("employee = " + employee);
    }

    @Test
    public void testEmployeeDaoInsert() {
//        1.创建DAO实现类对象
        EmployeeDao employeeDao = new EmployeeImpl();

//        2.调用添加员工方法
        Employee employee = new Employee(null, "tom", 300.65, 30);
        int insert = employeeDao.insert(employee);

//        3.处理结果
        System.out.println("insert = " + insert);
    }

    @Test
    public void testEmployeeDaoUpdate() {
//        1.创建DAO实现类对象
        EmployeeDao employeeDao = new EmployeeImpl();

//        2.调用添加员工方法
        Employee employee = new Employee(1, "andy", 999.36, 32);
        int update = employeeDao.update(employee);

//        3.处理结果
        System.out.println("update = " + update);
    }

    @Test
    public void testEmployeeDaoDelete() {
//        1.创建DAO实现类对象
        EmployeeDao employeeDao = new EmployeeImpl();

//        2.调用删除员工方法
        int delete = employeeDao.delete(2000);

//        3.处理结果
        System.out.println("delete = " + delete);
    }
一、概述: JDBC从物理结构上说就是Java语言访问数据库的一套接口集合。从本质上来说就是调用者(程序员)和实现者(数据库厂商)之间的协议。JDBC的实现由数据库厂商以驱动程序的形式提供。JDBC API 使得开发人员可以使用纯Java的方式来连接数据库,并进行操作。 ODBC:基于C语言的数据库访问接口。 JDBC也就是Java版的ODBC。 JDBC的特性:高度的一致性、简单性(常用的接口只有4、5个)。 1.在JDBC中包括了两个包:java.sql和javax.sql。 ① java.sql 基本功能。这个包中的类和接口主要针对基本的数据库编程服务,如生成连接、执行语句以及准备语句和运行批处理查询等。同时也有一些高级的处理,比如批处理更新、事务隔离和可滚动结果集等。 ② javax.sql 扩展功能。它主要为数据库方面的高级操作提供了接口和类。如为连接管理、分布式事务和旧有的连接提供了更好的抽象,它引入了容器管理的连接池、分布式事务和行集等。 注:除了标出的Class,其它均为接口。 API 说明 java.sql.Connection 与特定数据库的连接(会话)。能够通过getMetaData方法获得数据库提供的信息、所支持的SQL语法、存储过程和此连接的功能等信息。代表了数据库。 java.sql.Driver 每个驱动程序类必需实现的接口,同时,每个数据库驱动程序都应该提供一个实现Driver接口的类。 java.sql.DriverManager (Class) 管理一组JDBC驱动程序的基本服务。作为初始化的一部分,此接口会尝试加载在”jdbc.drivers”系统属性中引用的驱动程序。只是一个辅助类,是工具。 java.sql.Statement 用于执行静态SQL语句并返回其生成结果的对象。 java.sql.PreparedStatement 继承Statement接口,表示预编译的SQL语句的对象,SQL语句被预编译并且存储在PreparedStatement对象中。然后可以使用此对象高效地多次执行该语句。 java.sql.CallableStatement 用来访问数据库中的存储过程。它提供了一些方法来指定语句所使用的输入/输出参数。 java.sql.ResultSet 指的是查询返回的数据库结果集。 java.sql.ResultSetMetaData 可用于获取关于ResultSet对象中列的类型和属性信息的对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值