【2021软件创新实验室暑假集训】JDBC(原理、使用以及实现简单的数据库连接池)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 【2021软件创新实验室暑假集训】JDBC(原理、使用以及实现简单的数据库连接池)

一、JDBC的前世今生

JDBC全称Java DataBase Connectivity(Java数据库连接),是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。


早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范(就是一组接口),并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API出现。SUN提供的规范命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!


很多时候我们往往错误的认识了JDBC,以为它是用于数据库连接的框架。其实不然,它只是sun公司为了规范操作,屏蔽底层数据库之间的差异而定义的一套标准,它位于java.sql包下面。

q1.png


各个数据库厂商自己编写相关的驱动包来实现这套标准。因为规范了接口,所以Java工程师们可以不用关心数据库层面的差异,利用统一的jdbc操作数据库即可。


二、JDBC的使用及原理

1.jdbc简单使用步骤

以下操作以mysql为例


①加载驱动程序

//加载MySql驱动
Class.forName("com.mysql.cj.jdbc.Driver")

或者也可以这么写


driver = new com.mysql.cj.jdbc.Driver();
DriverManager.registerDriver(driver);

其实上面的操作本质是一样,

q1.png


看一看源码,Class.forName("com.mysql.cj.jdbc.Driver")这句话其实就是把就是把这个类加载到内存中,加载的过程中会执行static块里的代码,而该代码实际上和后面一种是一样的。


②获得数据库连接

根据数据库路径、账号、密码获取数据库连接


DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/imooc", "root", "root");

③创建Statement\PreparedStatement对象

Statement statement=conn.createStatement();
PreparedStatement ps=conn.prepareStatement(sql);

④调用executeQuery/executeUpdate方法

//调用Statement的executeQuery方法
ResultSet rs = stmt.executeQuery(sql);
//调用Statement的executeUpdate方法
Integer i=statement.executeUpdate(sql);
//调用PreparedStatement的executeQuery方法
ResultSet rs=ps.executeQuery();
//调用PreparedStatement的executeUpdate方法
Integer i=ps.executeUpdate(sql);

注:这里省略了业务处理流程


⑤关闭ResultSet、Statement/PreparedStatement、Connection

这里一定要注意关闭顺序,应该以ResultSet、Statement/PreparedStatement、Connection的顺序(先开后关)


同时捕获异常时最后每个都单独捕获,这样不至于在关闭时因为前面关闭出错而导致后面资源没关闭。


try {
            ....
} catch (SQLException throwables) {
    throwables.printStackTrace();
}finally {
    try {
        rs.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
    try {
        statement.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
    try {
        connection.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
}


2.简单的例子

//数据库路径
private static String url="jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true";
//数据库账号
private static String user="root";
//数据库密码
private static String password="jinhaolin";
void testQueryStatement(){
Driver driver= null;
Statement statement=null;
Connection connection=null;
ResultSet rs=null;
try {
    //创建驱动类对象
    driver = new com.mysql.cj.jdbc.Driver();
    //注册驱动类
    DriverManager.registerDriver(driver);
    //获取连接
    connection=DriverManager.getConnection(url,user,password);
    //获取Statement
    statement=connection.createStatement();
    //sql语句
    String sql="select * from user";
    //执行查询,获得结果集
    rs=statement.executeQuery(sql);
    //调用next方法,将指针指向下一条记录,一开始调用next方法后,指针指向第一条记录。next返回值为Boolean类型,
    // 表示是否还有下一条记录
    while (rs.next()){
        System.out.println("-----------------------------");
        //获取当条记录的userName字段并打印
        System.out.println("username"+rs.getString("userName"));
        //获取当条记录的password字段并打印
        System.out.println("password"+rs.getString("password"));
        System.out.println("-----------------------------");
    }
} catch (SQLException throwables) {
    throwables.printStackTrace();
}finally {
    try {
        rs.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
    try {
        statement.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
    try {
        connection.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
}


3.Statement/PreparedStatement使用

①Statement

对于Statement,一般都是写好sql语句或者自己拼接好对应的sql语句去执行(自己拼接的话要注意格式,比如字符串得加上‘’,日期格式得以’yyyy-MM-dd HH:mm:ss’形式才行等等)。

一般来讲查询语句调用ResultSet executeQuery(String sql),增删改查都是调用int executeUpdate(String sql)


查询操作

ResultSet executeQuery(String sql)方法会返回一个结果集,结果集的读取调用,模板如下:


//rs中有多条记录,其内置一个指针,每调用一次next方法就会跳转到下一条记录,初始指针指向空。
while (rs.next()){
    //用getXXX(字段名称)的方式获取当前指针指向的记录字段
          int id=rs.getInt("id");
          //....


以下是代码实例


private static String url="jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true";
private static String user="root";
private static String password="jinhaolin";
void testQueryStatement(){
    Driver driver= null;
    Statement statement=null;
    Connection connection=null;
    ResultSet rs=null;
    try {
        //创建驱动类对象
        driver = new com.mysql.cj.jdbc.Driver();
        //注册驱动类
        DriverManager.registerDriver(driver);
        //获取连接
        connection=DriverManager.getConnection(url,user,password);
        //获取Statement
        statement=connection.createStatement();
        //sql语句
        String sql="select * from user";
        //执行查询,获得结果集
        rs=statement.executeQuery(sql);
        //调用next方法,将指针指向下一条记录,一开始调用next方法后,指针指向第一条记录。next返回值为Boolean类型,
        // 表示是否还有下一条记录
        while (rs.next()){
            System.out.println("-----------------------------");
            //获取当条记录的userName字段并打印
            System.out.println("username"+rs.getString("userName"));
            //获取当条记录的password字段并打印
            System.out.println("password"+rs.getString("password"));
            System.out.println("-----------------------------");
        }
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }finally {
        try {
            rs.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        try {
            statement.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        try {
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}



增删改操作

关于增删改一般调用executeUpdate(String sql)`即可,其返回值为当次操作受影响的记录行数。


void testUpdateStatement(){
     Driver driver= null;
     Statement statement=null;
     Connection connection=null;
     try {
         driver = new com.mysql.cj.jdbc.Driver();
         DriverManager.registerDriver(driver);
         connection=DriverManager.getConnection(url,user,password);
         statement=connection.createStatement();
         String sql="update user set userName='李四' where id=2";
         Integer i=statement.executeUpdate(sql);
         System.out.println("当前受影响的记录数:"+i);
     } catch (SQLException throwables) {
         throwables.printStackTrace();
     }finally {
         try {
             statement.close();
         } catch (SQLException throwables) {
             throwables.printStackTrace();
         }
         try {
             connection.close();
         } catch (SQLException throwables) {
             throwables.printStackTrace();
         }
     }
 }



②PreparedStatement

PreparedStatement和Statement操作类似,不过PreparedStatement在创建时就需要传入对应的sql语句,这是为了“预编译”,同时sql语句支持占位符的方式(占位符序号从1开始)。


PreparedStatement操作有两个好处:

1.添加参数时不用操心类型转化

我们自己拼接字符串时总要为参数类型而操心,比如字符串要加’’,日期要改成合适格式。而PreparedStatement会帮我们做了这些事情。

2.防止sql注入(这个后面会讲)


查询操作

void testQueryPreparedStatement(){
    Driver driver= null;
    Connection connection=null;
    PreparedStatement ps=null;
    ResultSet rs=null;
    try {
        driver = new com.mysql.cj.jdbc.Driver();
        DriverManager.registerDriver(driver);
        connection=DriverManager.getConnection(url,user,password);
        String sql="select * from user where id=? or id=?";
        ps=connection.prepareStatement(sql);
        //传参
        ps.setInt(1,1);
        ps.setInt(2,2);
        rs=ps.executeQuery();
        while (rs.next()){
            System.out.println("-----------------------------");
            System.out.println("username"+rs.getString("userName"));
            System.out.println("password"+rs.getString("password"));
            System.out.println("-----------------------------");
        }
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }finally {
        try {
            rs.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        try {
            ps.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        try {
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}



增删改操作

void testUpdatePreparedStatement(){
     Driver driver= null;
     Connection connection=null;
     PreparedStatement ps=null;
     try {
         driver = new com.mysql.cj.jdbc.Driver();
         DriverManager.registerDriver(driver);
         connection=DriverManager.getConnection(url,user,password);
         String sql="update user set userName='李四' where id=?";
         ps=connection.prepareStatement(sql);
         ps.setInt(1,1);
         Integer i=ps.executeUpdate(sql);
         System.out.println("当前受影响的记录数:"+i);
     } catch (SQLException throwables) {
         throwables.printStackTrace();
     }finally {
         try {
             ps.close();
         } catch (SQLException throwables) {
             throwables.printStackTrace();
         }
         try {
             connection.close();
         } catch (SQLException throwables) {
             throwables.printStackTrace();
         }
     }
 }


4.事务的使用

有时候我们希望有些操作要么一起执行,要么一起失败。比如转账业务,需要在转账方账户扣除相应的资金,在转入方增加相应的资金,不能说一方成功了,一方失败了,这是不被允许。


所以我们需要有一种机制能保证某几个操作能一起成功或者一起失败。这就叫事务机制。


数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。


事务使用示例:


void testTransaction(){
        Driver driver= null;
        Connection connection=null;
        PreparedStatement ps1=null,ps2=null;
        try {
            driver = new com.mysql.cj.jdbc.Driver();
            DriverManager.registerDriver(driver);
            connection=DriverManager.getConnection(url,user,password);
            //将自动提交设置为false
           connection.setAutoCommit(false);
            String sql="update user set userName='王五' where id=?";
            ps1=connection.prepareStatement(sql);
            ps1.setInt(1,1);
            ps1.executeUpdate();
            ps2=connection.prepareStatement(sql);
            ps2.setInt(1,2);
            ps2.executeUpdate();
            //所有操作完成后提交事务
            connection.commit();
        } catch (SQLException throwables) {
            //打印堆栈信息
            throwables.printStackTrace();
            try {
                //回滚事务
                connection.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }finally {
            try {
                ps1.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
                ps2.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
                connection.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }



三、PreparedStatements原理

1.Sql注入

什么是sql注入?


SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。


举个例子,比如我后端有这么一句sql语句


delete form user where id=XXX

其中XXX是前端传过来的数据,正常情况下前端传过来需要删除的id,后端就会删除相应的用户记录。


但是,总有那么些人盯着你的系统想搞些破坏,他们在前端输入字符串"1 or 1=1",查这么一看你是不是觉得很奇怪,但当你将它拼入你的sql语句中时,你会发现语句变成了delete form user where id=1 or 1=1,这时你恍然大悟,这不就是删除所有用户了吗?一拍脑门,可发现为时已晚,数据库user表被删的一干二净(虽然一般可以用备份恢复)。


2.为什么PreparedStatement能防止sql注入?

为什么PreparedStatement能防止sql注入呢?

查看源码,我们可以在ClientPreparedQueryBindings这个类中发现缘由


@Override
    public void setString(int parameterIndex, String x) {
        if (x == null) {
            setNull(parameterIndex);
        } else {
            int stringLength = x.length();
            if (this.session.getServerSession().isNoBackslashEscapesSet()) {
                // Scan for any nasty chars
                boolean needsHexEscape = isEscapeNeededForString(x, stringLength);
                if (!needsHexEscape) {
                    StringBuilder quotedString = new StringBuilder(x.length() + 2);
                    quotedString.append('\'');
                    quotedString.append(x);
                    quotedString.append('\'');
                    byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(quotedString.toString())
                            : StringUtils.getBytes(quotedString.toString(), this.charEncoding);
                    setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
                } else {
                    byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(x) : StringUtils.getBytes(x, this.charEncoding);
                    setBytes(parameterIndex, parameterAsBytes);
                }
                return;
            }
            String parameterAsString = x;
            boolean needsQuoted = true;
            if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
                needsQuoted = false; // saves an allocation later
                StringBuilder buf = new StringBuilder((int) (x.length() * 1.1));
                buf.append('\'');
                //
                // Note: buf.append(char) is _faster_ than appending in blocks, because the block append requires a System.arraycopy().... go figure...
                //
                for (int i = 0; i < stringLength; ++i) {
                    char c = x.charAt(i);
                    switch (c) {
                        case 0: /* Must be escaped for 'mysql' */
                            buf.append('\\');
                            buf.append('0');
                            break;
                        case '\n': /* Must be escaped for logs */
                            buf.append('\\');
                            buf.append('n');
                            break;
                        case '\r':
                            buf.append('\\');
                            buf.append('r');
                            break;
                        case '\\':
                            buf.append('\\');
                            buf.append('\\');
                            break;
                        case '\'':
                            buf.append('\'');
                            buf.append('\'');
                            break;
                        case '"': /* Better safe than sorry */
                            if (this.session.getServerSession().useAnsiQuotedIdentifiers()) {
                                buf.append('\\');
                            }
                            buf.append('"');
                            break;
                        case '\032': /* This gives problems on Win32 */
                            buf.append('\\');
                            buf.append('Z');
                            break;
                        case '\u00a5':
                        case '\u20a9':
                            // escape characters interpreted as backslash by mysql
                            if (this.charsetEncoder != null) {
                                CharBuffer cbuf = CharBuffer.allocate(1);
                                ByteBuffer bbuf = ByteBuffer.allocate(1);
                                cbuf.put(c);
                                cbuf.position(0);
                                this.charsetEncoder.encode(cbuf, bbuf, true);
                                if (bbuf.get(0) == '\\') {
                                    buf.append('\\');
                                }
                            }
                            buf.append(c);
                            break;
                        default:
                            buf.append(c);
                    }
                }
                buf.append('\'');
                parameterAsString = buf.toString();
            }
            byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
                    : (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
                            : StringUtils.getBytes(parameterAsString, this.charEncoding));
            setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
        }
    }

其实mysql驱动包里的操作便是将特定字符进行转义,防止sql注入的情况

三、实现一个简单的数据库连接池

虽然我们强调数据库操作需要在最后关闭数据库连接,但是在大多数情况下,频繁的开关数据库连接并不是一个明智的选择。


我们要明白——建立和关闭数据库连接是一个非常耗时的操作,如果我们仅仅为了增删改查一点数据就建立/关闭一次连接,这是一种非常大的浪费。


为了避免这种情况,我们就得对连接进行复用,而复用的方法之一便是编写一个数据库连接池,当然有很多开源的数据库连接池,比如阿里的druid。不过我们这里自己编写一个数据库连接池。


具体代码如下:


import java.sql.*;
import java.util.LinkedList;
import java.util.Queue;
public class JDBCUtil {
    private static Queue<Connection> pool=new LinkedList<>();
    private static Driver driver;
    //在加载这个类时会自动执行static代码块的代码,这块代码只会在加载这个类的时候执行(即只会执行一次)
    static {
        try {
            driver=new com.mysql.cj.jdbc.Driver();
            //注册驱动类
            DriverManager.registerDriver(driver);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    private static String url="jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true";
    private static String user="root";
    private static String password="jinhaolin";
    /**
     * 获取一个数据库连接池
     * @return 数据库连接
     */
    public static synchronized Connection getConnection(){
        if (pool.size()==0){
            try {
                pool.add(DriverManager.getConnection(url,user,password));
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        return pool.poll();
    }
    /**
     * 将用完的连接放回连接池
     * @param connection 要释放的连接
     */
    public static synchronized void release(Connection connection){
        pool.add(connection);
    }
}



以上只是一个粗浅的实现,是为了帮助大家理解数据库连接池的功能,上述实现只有最基础的功能。


为了避免线程安全问题,我在获取和释放连接的方法前加了synchronized 修饰。


总结

JDBC是sun公司为了方便Java操作各种数据库而制定的接口协议。各个数据库厂商如果要支持jdbc则需要实现jdbc制定的接口,提供相应的驱动包。


JDBC的使用很简单,但是繁琐,而且很多情况下我们需要重复性的劳动以实现数据库的操作。


在今后的学习中,你应该会遇到一些好用的框架(比如mybatis)来减少这些繁琐的操作,甚至有些自动化的工具可以一键生成相关代码(因为太有规律了)。

但是不要忘记这些Java持久层框架是基于jdbc的,所以了解其原理,掌握其使用,这对于一名Java后端开发工程师来说是一门必修课。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
Java 关系型数据库 数据库连接
JDBC:Java与数据库的“黄金搭档”,为何它如此重要?
JDBC:Java与数据库的“黄金搭档”,为何它如此重要?
48 8
|
3月前
|
Java 数据库连接 API
JDBC:Java数据库连接的“黑科技”大揭秘
JDBC:Java数据库连接的“黑科技”大揭秘
38 7
|
1月前
|
缓存 算法 关系型数据库
Mysql(3)—数据库相关概念及工作原理
数据库是一个以某种有组织的方式存储的数据集合。它通常包括一个或多个不同的主题领域或用途的数据表。
56 5
Mysql(3)—数据库相关概念及工作原理
|
21天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
40 2
|
22天前
|
SQL 数据采集 监控
局域网监控电脑屏幕软件:PL/SQL 实现的数据库关联监控
在当今网络环境中,基于PL/SQL的局域网监控系统对于企业和机构的信息安全至关重要。该系统包括屏幕数据采集、数据处理与分析、数据库关联与存储三个核心模块,能够提供全面而准确的监控信息,帮助管理者有效监督局域网内的电脑使用情况。
16 2
|
1月前
|
SQL 关系型数据库 数据库
SQL数据库:核心原理与应用实践
随着信息技术的飞速发展,数据库管理系统已成为各类组织和企业中不可或缺的核心组件。在众多数据库管理系统中,SQL(结构化查询语言)数据库以其强大的数据管理能力和灵活性,广泛应用于各类业务场景。本文将深入探讨SQL数据库的基本原理、核心特性以及实际应用。一、SQL数据库概述SQL数据库是一种关系型数据库
62 5
|
1月前
|
SQL Java 数据库
Springboot+spring-boot-starter-data-jdbc实现数据库的操作
本文介绍了如何使用Spring Boot的spring-boot-starter-data-jdbc依赖来操作数据库,包括添加依赖、配置数据库信息和编写基于JdbcTemplate的数据访问代码。
62 2
|
1月前
|
SQL 关系型数据库 MySQL
sql注入原理与实战(三)数据库操作
sql注入原理与实战(三)数据库操作
sql注入原理与实战(三)数据库操作
|
1月前
|
SQL druid Java
JDBC和数据库连接池-两个工具类-JDBCUtilsByDruid和BasicDAO
JDBC和数据库连接池-两个工具类-JDBCUtilsByDruid和BasicDAO
32 0
|
1月前
|
SQL 存储 Java
sql注入原理与实战(二)数据库原理
sql注入原理与实战(二)数据库原理
下一篇
无影云桌面