-
数据持久化
2019-06-22 22:46:19数据持久化介绍 1、持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化“,而持久化的实现过程大多通过...数据持久化介绍
1、持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化“,而持久化的实现过程大多通过各种关系数据库来完成。
2、持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、xml数据文件中。在java中,数据库存取技术可以分为如下几类
1、JDBC直接访问数据库。
2、JDO技术。
3、第三方O/R工具,如Hibernate、ibatis等。JDBC是java访问数据库的基石,JDO,Hibernate等只是封装了JDBC。
JDBC基础
1、JDBC(Java Database Connectivity)是一个独立于特定数据库管理系统、通用的SQL数据库存取和操作的公共接口(一组API),定义了用来访问数据库的标准Java类库,使用这个类库可以以一种标准的方法、方便地访问数据库资源。
2、JDBC为访问不同的数据库提供了一种同一的途径,为开发者屏蔽了一些细节问题。
3、JDBC的目标是使Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就使的程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。
Driver接口
1、Java.sql.Driver接口是所有JDBC驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不同的实现。
2、在程序中不需要直接去访问实现了Driver接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。加载与注册JDBC驱动
1、加载JDBC驱动需要调用Class类的静态方法forName(),向其传递要加载JDBC驱动的类名。
2、DriverManager类是驱动程序管理器类,负责管理驱动程序。
3、通常不用显示调用DriverManager类的registerDriver()方法来注册驱动程序类的实例,因为Driver接口的驱动程序类都包含了静态代码块,在这个静态代码块中,会调用DriverManager.registerDriver()方法来注册自身的一个实例。建立连接
1、可以调用DriverManager类的getConnection()方法建立到数据库的连接。
2、JDBC URL用于标识一个被注册的驱动程序,驱动程序管理器通过这个URL选择正确的驱动程序,从而建立到数据库的连接。
3、JDBC URL的标准由三个部分组成,各部分间用冒号分隔。- jdbc:<子协议>:<子名称>。
- 协议:JDBC URL中的协议总是jdbc。
- 子协议:子协议用于标识一个数据库驱动程序。
- 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库提供足够的信息。
JDBC数据库连接池的必要性
1、使用开发基于数据库的web程序时,传统的模式基本是按一下步骤:
-
在主程序(如servlet、beans)中建立数据库连接。
-
进行sql操作。
-
断开书库连接。
2、这种模式开发,存在的问题 -
普通的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接的时候都要将Connection加载到内存中,再验证用户名和密码(带花费0.05s~1s的时间)。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有的到很好的重复利用。若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。
-
对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄露,最终将导致重启数据库。
-
这种开发不能控制被创建的连接对象,系统资源会被毫无顾忌的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。
数据库连接池
1、为了解决传统开发中的数据库连接问题,可以采用数据库连接池技术。
2、数据库连接池的基本思想就是为数据库连接建立一个”缓冲池“。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从”缓冲池“中取出一个,使用完毕之后再放回去。
3、数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
4、数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。数据库连接池的工作原理
数据库连接池技术的优点
1、资源重用
- 由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。
2、更快的系统反应速度
- 数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间。
3、新的资源分配手段
- 对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,是实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源。
4、统一的连接管理,避免数据库连接泄露
- 在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄露。
两种开源的数据库连接池
1、JDBC的数据库连接池使用java.sql.DataSource来表示,DataSource只是一个接口,该接口通常由服务器(Weblogic,WebSphere,Tomact)提供实现,也有一些开源组织提供实现:
- DBCP数据库连接池。
- C3P0数据库连接池。
2、DataSource通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把DataSource称为连接池。
DBCP数据源
1、DBCP是Apache软件基金组织下的开源连接池实现,该连接池依赖组织下的另一个开源系统:Common-pool。如需使用该连接池实现,应在系统中增加如下两个jar文件:
- Commons-dbcp.jar:连接池的实现。
- Commons-pools.jar:连接池实现的依赖库。
2、Tomact的连接池正式采用该链接池来实现的。改数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。
C3P0
C3P0开源免费的连接池!目前使用它的开源项目有:Spring、Hibernate等。使用第三方工具需要导入jar包,C3P0使用时还需要添加配置文件c3p0-config.xml。
需要导入的包:c3p0-0.9.2-pre5.jar、mchange-commons-java-0.2.3.jar。DBUtils增删改查的操作
如果只使用JDBC进行开发,我们会发现冗余代码过多,为了简化JDBC开发,本案例我们将采用apache commons组件一个成员:DBUtils。
DBUtils就是JDBC的简化开发工具包。需要使用技术:连接池(获得连接),SQL语句都没有多少。DBUtils概述
DBUtils是java编程中的数据库操作使用工具,小巧简单实用。DBUtils封装了对JDBC的操作,简化了JDBC操作,可以少写代码。DBUtils三个核心功能介绍。
- QueryRunner中提供对sql语句操作的API。
- ResultSetHandler接口,用于定义select操作后,怎样封装结果集。
- DBUtils类,它就是一个工具类,定义了关闭资源与事务处理的方法。
QueryRunner和核心类
- QueryRunner(DataSource ds),提供数据源(连接池),DBUtils底层自动维护连接connection。
- update(String sql,Object …params),执行更新数据。
- query(String sql,ResultSetHandler rsh,Object …params),执行查询。
-
数据持久化概念和Java数据持久化技术
2016-02-19 16:21:09什么是数据持久化 数据持久化是把程序中的数据以某种形式保存到某种存储介质中,达到程序重启时不丢失的作用。 Java持久化技术 序列化(Serialization) 序列化(Serialization)也叫串行化,是Java内置的持久化...什么是数据持久化
数据持久化是把程序中的数据以某种形式保存到某种存储介质中,达到程序重启时不丢失的作用。Java持久化技术
序列化(Serialization)
序列化(Serialization)也叫串行化,是Java内置的持久化Java对象机制。只要某个类实现了java.io.Serializable接口,就能够使用java.io.ObjectOutputStream将该类对象以二进制字节码的形式写到硬盘上,并能使用java.io.ObjectInputStream将该对象从硬盘上还原。这样,即使重启硬盘,数据也能从硬盘上恢复。使一个普通Java类可序列化,要注意一下几方面:1、类一定要实现Seriablizable标记接口,并声明一个private static final 的long类型的serialVersionUID属性。2、类所有属性也要是可序列化的。3、这样,可序列化的类可通过ObjectOutput类中的writeObject方法和readObject方法保存和读取。如 以下是Java bean类:
一下是操作类PersonTest:package webDemo; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class Person implements Serializable { private static final long serialVersionUID = 5805021228954269035L; private String name; private List<String> hobys = new ArrayList<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public List<String> getHobys() { return hobys; } public void setHobys(List<String> hobys) { this.hobys = hobys; } @Override public String toString() { return "姓名:"+name+",爱好:"+ hobys; } }
package webDemo; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.util.Arrays; public class PersonTest { // 当前文件夹 public static File directory = new File(System.getProperty("user.dir")); // 控制台输入 public static String systemRead() throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); return br.readLine(); } // 将person对象持久化到硬盘上 public static void persist(Person person) throws IOException { File file = new File(directory, person.getName() + ".dat"); ObjectOutput output = new ObjectOutputStream(new FileOutputStream(file)); // 保存对象 output.writeObject(person); output.close(); System.out.println("文件保存在:" + file); } // 从硬盘上还原person对象 public static Person load(String name) throws IOException, ClassNotFoundException { File file = new File(directory, name + ".dat"); if (!file.exists()) return null; ObjectInput ins = new ObjectInputStream(new FileInputStream(file)); // 读取对象 return (Person) ins.readObject(); } // main方法 public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("人员信息管理系统."); System.out.println("1、输入'save'开始录入人员信息."); System.out.println("2、输入'load'开始查询人员信息."); System.out.println("3、输入'exit'退出系统."); System.out.println("请输入(回车结束输入):"); System.out.println("人员信息管理系统."); System.out.println("人员信息管理系统."); // 获取用户输入信息 String menu = systemRead(); // 如果输入为'save',则继续输入人员姓名、爱好 if ("save".equals(menu)) { System.out.println("请输入要录入的人员姓名:"); // 获取输入人员的姓名 String name = systemRead(); System.out.println("请输入'" + name + "'的爱好,用‘'’隔开:"); // 获得输入人员的爱好 String hobbies = systemRead(); // 创建人员对象 Person person = new Person(); person.setName(name); person.setHobys(Arrays.asList(hobbies.split(","))); // 持久化人员对象 persist(person); // 如果输入的为load,则继续查找人员的姓名,然后根据姓名查询该人员的信息 } else if ("load".equals(menu)) { System.out.println("请输入要查找的人员的姓名:"); // 获得输入人员的姓名 String name = systemRead(); // 根据姓名查询人员,并返回人员相关信息 Person person = load(name); if (person == null) { System.out.println("没有此人信息。"); } else { System.out.println("查询结果:" + person); } // 如果输入的为exit,则退出程序 } else if ("exit".equals(menu)) { System.out.println("再见"); System.exit(0); // 否则,提示错误信息 } else { System.out.println("无效输入:" + menu); } System.out.println(); // 继续执行main函数 main(null); } }
运行效果如下:人员信息管理系统.
1、输入'save'开始录入人员信息.
2、输入'load'开始查询人员信息.
3、输入'exit'退出系统.
请输入(回车结束输入):
人员信息管理系统.
人员信息管理系统.
chunqiao
无效输入:chunqiao
人员信息管理系统.
1、输入'save'开始录入人员信息.
2、输入'load'开始查询人员信息.
3、输入'exit'退出系统.
请输入(回车结束输入):
人员信息管理系统.
人员信息管理系统.
save
请输入要录入的人员姓名:
chunqiao
请输入'chunqiao'的爱好,用‘'’隔开:
打球,旅行,打架
文件保存在:E:\workspace-sts\webDemo\chunqiao.dat
人员信息管理系统.
1、输入'save'开始录入人员信息.
2、输入'load'开始查询人员信息.
3、输入'exit'退出系统.
请输入(回车结束输入):
人员信息管理系统.
人员信息管理系统.
load
请输入要查找的人员的姓名:
chunqiao
查询结果:姓名:chunqiao,爱好:[打球,旅行,打架]
人员信息管理系统.
1、输入'save'开始录入人员信息.
2、输入'load'开始查询人员信息.
3、输入'exit'退出系统.
请输入(回车结束输入):
人员信息管理系统.
人员信息管理系统.注,如果把实现序列化去掉,运行load会出错:load
请输入要查找的人员的姓名:
chunqiao
Exception in thread "main" java.io.InvalidClassException: webDemo.Person; class invalid for deserialization
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at webDemo.PersonTest.load(PersonTest.java:43)
at webDemo.PersonTest.main(PersonTest.java:77)
at webDemo.PersonTest.main(PersonTest.java:94)
at webDemo.PersonTest.main(PersonTest.java:94)
at webDemo.PersonTest.main(PersonTest.java:94)补充说明:在实际运用中,tomcat的会话(Session)中使用的就是序列化技术。当tomcat关闭时候,tomcat会把Session中的Java对象通过序列化技术保存到硬盘上,这样,重启tomcat时候就会把硬盘上的Java对象还原并放回到session里。因此,保存在session中的对象最好实现Seriaizable。
JDBC
JDBC(Java Database Connectivity)是最常用的持久化技术,它能够把数据保存进关系型数据库,并且高效地从数据库中查询出来。但是,JDBC并不直接支持对Java对象的持久化。使用JDBC技术,持久化Java对象时候可能需要用到sql代码,将数据保存到数据库中,如:package webDemo; import com.mysql.jdbc.Connection; import com.mysql.jdbc.PreparedStatement; import jdk.internal.org.objectweb.asm.tree.TryCatchBlockNode; public class PersonDao { public void persist(Person person) throws Exception { // 获取数据库连接 Connection conn = DBManager.getConnection(); java.sql.PreparedStatement preStmt = null; try { // 创建预编译对象 preStmt = conn.prepareStatement("INSERT INTO tb_person (name) values (?) "); preStmt.setString(1, person.getName()); // 插入一条人员数据 preStmt.executeUpdate(); // 分别插入爱好数据 for (String hobby : person.getHobys()) { preStmt = conn.prepareStatement(" INSERT INTO tb_boby ( person_name, hoby ) values (?, ?)"); preStmt.setString(1, person.getName()); preStmt.setString(2, hobby); // 执行插入操作 preStmt.executeUpdate(); } } catch (Exception e) { e.printStackTrace(); } finally { preStmt.close(); conn.close(); } } }
通过以上代码可以看出,使用JDBC持久化Java对象需要编写大量sql语句,当数据表结构和Java对象属性发生变化时候,JDBC的缺点就更加明显了。关系对象映射(ORM)
ORM是针对JDBC不能直接持久化Java对象的解决方案。ORM框架能够将Java对象映射到关系数据库,能够直接持久化复杂的Java对象。目前比较流行的ORM框架有hibernate、mybatis、TopLink。对象数据库(ODB)
Java数据对象(JDO)
EJB2.X
-
redis数据持久化
2014-10-31 21:17:10redis数据持久化 redis数据持久化分为两种方式:RDBCHAOFredis数据持久化
redis数据持久化分为两种方式:RDB持久化和AOF持久化。
RDB持久化
rdb持久化的基本原理是用.rdb文件保存当前数据库中的所有键值,redis服务重启的时候如果发现.rdb文件则自动加载文件数据到内存,回复数据库数据。创建edb文件的方式有两种:手动和自动1. 手动创建rdb文件在客户端使用save命令或者bgsave命令都能够将当前数据库中数据持久化到硬盘,保存为rdb文件(如下图所示)。不同的是save命令使用主进程做持久化操作,所以执行期间redis服务器将不能接收来着客户端的任何请求,拒绝服务。bgsave命令是在当前进程的基础上派生一个子进程来执行写操作,不会阻塞服务。bgsave命令使用子进程执行写操作,而大多数操作系统都是用写时复制技术来优化子进程的效率,所以这个时候服务器的内存会比较紧张,这就是为什么服务器rehash的时候如果正在执行bgsave或者bgrewriteaof命令,rehash因为会提高到5的原因。2. 自动创建rdb文件当redis服务启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:save 900 1save 300 10
save 60 10000
表示的意思是在距离上一次成功执行save操作后的900s内数据被修改1次以上,300s内被修改10次以上,60s内被修改10000次以上,只要有其中一个条件满足就自动保存执行rdb持久化。
AOF持久化
aof持久化的基本原理是使用.aof文件保存数据库从启动到现在执行的所有写操作,redis服务重启的时候通过重新执行.aof文件中的写操作回复数据库数据。当aof持久化功能处于打开状态时,服务器在执行完一个写命令之后,都会将命令按照协议格式追加到服务器的aof_buf缓冲区末尾。redis服务其实际上就是一个事件循环(loop),每次结束一个时间循环之后,都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到aof文件里面。flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定。各个值的具体含义如下:appendfsync值含义 appendfsync值 flushAppendOnlyFile函数行为 always 将aof_buf缓冲区中的所有内容写入病同步到AOF文件 everysec 将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过一秒钟,
那么再次对AOF文件进行同步,并且这个同步由一个专门的线程负责。no 将aof_buf缓冲区中的所有内容写到AOF文件,但并不对AOF文件进行同步,
何时同步由操作系统来决定。
通过AOF文件还原数据库的操作很简单,创建一个不带网络连接的伪客户端,一次从AOF文件中取出写指令以此执行即可。AOF重写
随着数据的增长,AOF文件可能会不断变大,如果不加以控制的话可能会对redis服务造成影响,为了解决这个问题,redis提供AOF文件重写功能,创建一个新的AOF文件替换现有的AOF文件。重写功能的实现其实是通过读取数据库数据来实现的,每读到一条数据,则将其转换成一条等价的些操作指令存到AOF文件,这样新的AOF文件将省去很多不必要的操作,节省空间。 -
Java 数据持久化系列之JDBC
2019-11-26 20:39:56前段时间小冰在工作中遇到了一系列关于数据持久化的问题,在排查问题时发现自己对 Java 后端的数据持久化框架的原理都不太了解,只有不断试错,因此走了很多弯路。于是下定决心,集中精力学习了持久化相关框架的原理...前段时间小冰在工作中遇到了一系列关于数据持久化的问题,在排查问题时发现自己对 Java 后端的数据持久化框架的原理都不太了解,只有不断试错,因此走了很多弯路。于是下定决心,集中精力学习了持久化相关框架的原理和实现,总结出这个系列。
上图是我根据相关源码和网上资料总结的有关 Java 数据持久化的架构图(只代表本人想法,如有问题,欢迎留言指出)。最下层就是今天要讲的 JDBC,上一层是数据库连接池层,包括 HikariCP 和 Druid等;再上一层是分库分表中间件,比如说 ShardingJDBC;再向上是对象关系映射层,也就是 ORM,包括 Mybatis 和 JPA;最上边是 Spring 的事务管理。
本系列的文章会依次讲解图中各个开源框架的基础使用,然后描述其原理和代码实现,最后会着重分析它们之间是如何相互集成和配合的。
废话不多说,我们先来看 JDBC。
JDBC 定义
JDBC是Java Database Connectivity的简称,它定义了一套访问数据库的规范和接口。但它自身不参与数据库访问的实现。因此对于目前存在的数据库(譬如Mysql、Oracle)来说,要么数据库制造商本身提供这些规范与接口的实现,要么社区提供这些实现。
如上图所示,Java 程序只依赖于 JDBC API,通过 DriverManager 来获取驱动,并且针对不同的数据库可以使用不同的驱动。这是典型的桥接的设计模式,把抽象 Abstraction 与行为实现Implementation 分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展。
JDBC 基础代码示例
单纯使用 JDBC 的代码逻辑十分简单,我们就以最为常用的MySQL 为例,展示一下使用 JDBC 来建立数据库连接、执行查询语句和遍历结果的过程。
public static void connectionTest(){ Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { // 1. 加载并注册 MySQL 的驱动 Class.forName("com.mysql.cj.jdbc.Driver").newInstance(); // 2. 根据特定的数据库连接URL,返回与此URL的所匹配的数据库驱动对象 Driver driver = DriverManager.getDriver(URL); // 3. 传入参数,比如说用户名和密码 Properties props = new Properties(); props.put("user", USER_NAME); props.put("password", PASSWORD); // 4. 使用数据库驱动创建数据库连接 Connection connection = driver.connect(URL, props); // 5. 从数据库连接 connection 中获得 Statement 对象 statement = connection.createStatement(); // 6. 执行 sql 语句,返回结果 resultSet = statement.executeQuery("select * from activity"); // 7. 处理结果,取出数据 while(resultSet.next()) { System.out.println(resultSet.getString(2)); } ..... }finally{ // 8.关闭链接,释放资源 按照JDBC的规范,使用完成后管理链接, // 释放资源,释放顺序应该是: ResultSet ->Statement ->Connection resultSet.close(); statement.close(); connection.close(); } }
代码中有详细的注释描述每一步的过程,相信大家也都对这段代码十分熟悉。
唯一要提醒的是使用完之后的资源释放顺序。按照 JDBC 规范,应该依次释放 ResultSet,Statement 和 Connection。当然这只是规范,很多开源框架都没有严格的执行,但是 HikariCP却严格准守了,它可以带来很多优势,这些会在之后的文章中讲解。
上图是 JDBC 中核心的 5 个类或者接口的关系,它们分别是 DriverManager、Driver、Connection、Statement 和 ResultSet。
DriverManager 负责管理数据库驱动程序,根据 URL 获取与之匹配的 Driver 具体实现。Driver 则负责处理与具体数据库的通信细节,根据 URL 创建数据库连接 Connection。
Connection 表示与数据库的一个连接会话,可以和数据库进行数据交互。Statement 是需要执行的 SQL 语句或者存储过程语句对应的实体,可以执行对应的 SQL 语句。ResultSet 则是 Statement 执行后获得的结果集对象,可以使用迭代器从中遍历数据。
不同数据库的驱动都会实现各自的 Driver、Connection、Statement 和 ResultSet。而更为重要的是,众多数据库连接池和分库分表框架也都是实现了自己的 Connection、Statement 和 ResultSet,比如说 HikariCP、Druid 和 ShardingJDBC。我们接下来会经常看到它们的身影。
接下来,我们依次看一下这几个类及其涉及的操作的原理和源码实现。
载入 Driver 实现
可以直接使用 Class#forName的方式来载入驱动实现,或者在 JDBC 4.0 后则基于 SPI 机制来导入驱动实现,通过在 META-INF/services/java.sql.Driver 文件中指定实现类的方式来导入驱动实现,下面我们就来看一下两种方式的实现原理。
Class#forName 作用是要求 JVM 查找并加载指定的类,如果在类中有静态初始化器的话,JVM 会执行该类的静态代码段。加载具体 Driver 实现时,就会执行 Driver 中的静态代码段,将该 Driver 实现注册到 DriverManager 中。我们来看一下 MySQL 对应 Driver 的具体代码。它就是直接调用了 DriverManager的 registerDriver 方法将自己注册到其维护的驱动列表中。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { // 直接调用 DriverManager的 registerDriver 将自己注册到其中 DriverManager.registerDriver(new Driver()); } }
SPI 机制使用 ServiceLoader 类来提供服务发现机制,动态地为某个接口寻找服务实现。当服务的提供者提供了服务接口的一种实现之后,必须根据 SPI 约定在 META-INF/services 目录下创建一个以服务接口命名的文件,在该文件中写入实现该服务接口的具体实现类。当服务调用 ServiceLoader 的 load 方法的时候,ServiceLoader 能够通过约定的目录找到指定的文件,并装载实例化,完成服务的发现。
DriverManager 中的 loadInitialDrivers 方法会使用 ServiceLoader 的 load 方法加载目前项目路径下的所有 Driver 实现。
public class DriverManager { // 程序中已经注册的Driver具体实现信息列表。registerDriver类就是将Driver加入到这个列表 private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); // 使用ServiceLoader 加载具体的jdbc driver实现 static { loadInitialDrivers(); } private static void loadInitialDrivers() { // 省略了异常处理 // 获得系统属性 jdbc.drivers 配置的值 String drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); // 通过 ServiceLoader 获取到Driver的具体实现类,然后加载这些类,会调用其静态代码块 while(driversIterator.hasNext()) { driversIterator.next(); } return null; } }); String[] driversList = drivers.split(":"); // for 循环加载系统属性中的Driver类。 for (String aDriver : driversList) { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } } }
比如说,项目引用了 MySQL 的 jar包 mysql-connector-java,在这个 jar 包的 META-INF/services 文件夹下有一个叫 java.sql.Driver 的文件,文件的内容为 com.mysql.cj.jdbc.Driver。而 ServiceLoader 的 load 方法找到这个文件夹下的文件,读取文件的内容,然后加载出文件内容所指定的 Driver 实现。而正如之前所分析的,这个 Driver 类被加载时,会调用 DriverManager 的 registerDriver 方法,从而完成了驱动的加载。
Connection、Statement 和 ResultSet
当程序加载完具体驱动实现后,接下来就是建立与数据库的连接,执行 SQL 语句并且处理返回结果了,其过程如下图所示。
建立 Connection
创建 Connection 连接对象,可以使用 Driver 的 connect 方法,也可以使用 DriverManager 提供的 getConnection 方法,此方法通过 url 自动匹配对应的驱动 Driver 实例,然后还是调用对应的 connect 方法返回 Connection 对象实例。
建立 Connection 会涉及到与数据库进行网络请求等大量费时的操作,为了提升性能,往往都会引入数据库连接池,也就是说复用 Connection,免去每次都创建 Connection 所消耗的时间和系统资源。
Connection 默认情况下,对于创建的 Statement 执行的 SQL 语句都是自动提交事务的,即在 Statement 语句执行完后,自动执行 commit 操作,将事务提交,结果影响到物理数据库。为了满足更好地事务控制需求,我们也可以手动地控制事务,手动地在Statement 的 SQL 语句执行后进行 commit 或者rollback。
connection = driver.connect(URL, props); // 将自动提交关闭 connection.setAutoCommit(false); statement = connection.createStatement(); statement.execute("INSERT INTO activity (activity_id, activity_name, product_id, start_time, end_time, total, status, sec_speed, buy_limit, buy_rate) VALUES (1, '香蕉大甩卖', 1, 530871061, 530872061, 20, 0, 1, 1, 0.20);"); // 执行后手动 commit statement.getConnection().commit();
Statement
Statement 的功能在于根据传入的 SQL 语句,将传入 SQL 经过整理组合成数据库能够识别的执行语句(对于静态的 SQL 语句,不需要整理组合;而对于预编译SQL 语句和批量语句,则需要整理),然后传递 SQL 请求,之后会得到返回的结果。对于查询 SQL,结果会以 ResultSet 的形式返回。
当你创建了一个 Statement 对象之后,你可以用它的三个执行方法的任一方法来执行 SQL 语句。
- boolean execute(String SQL) : 如果 ResultSet 对象可以被检索,则返回的布尔值为 true ,否则返回 false 。当你需要使用真正的动态 SQL 时,可以使用这个方法来执行 SQL DDL 语句。
- int executeUpdate(String SQL) : 返回执行 SQL 语句影响的行的数目。使用该方法来执行 SQL 语句,是希望得到一些受影响的行的数目,例如,INSERT,UPDATE 或 DELETE 语句。
- ResultSet executeQuery(String SQL) : 返回一个 ResultSet 对象。当你希望得到一个结果集时使用该方法,就像你使用一个 SELECT 语句。
对于不同类型的 SQL 语句,Statement 有不同的接口与其对应。
接口 介绍 Statement 适合运行静态 SQL 语句,不接受动态参数 PreparedStatement 计划多次使用并且预先编译的 SQL 语句,接口需要传入额外的参数 CallableStatement 用于访问数据库存储过程 Statement 主要用于执行静态SQL语句,即内容固定不变的SQL语句。Statement每执行一次都要对传入的SQL语句编译一次,效率较差。而 PreparedStatement则解决了这个问题,它会对 SQL 进行预编译,提高了执行效率。
PreparedStatement pstmt = null; try { String SQL = "Update activity SET activity_name = ? WHERE activity_id = ?"; pstmt = connection.prepareStatement(SQL); pstmt.setString(1, "测试"); pstmt.setInt(2, 1); pstmt.executeUpdate(); } catch (SQLException e) { } finally { pstmt.close(); } }
除此之外, PreparedStatement 还可以预防 SQL 注入,因为 PreparedStatement 不允许在插入参数时改变 SQL 语句的逻辑结构。
PreparedStatement 传入任何数据不会和原 SQL 语句发生匹配关系,无需对输入的数据做过滤。如果用户将”or 1 = 1”传入赋值给占位符,下述SQL 语句将无法执行:select * from t where username = ? and password = ?。
ResultSet
当 Statement 查询 SQL 执行后,会得到 ResultSet 对象,ResultSet 对象是 SQL语句查询的结果集合。ResultSet 对从数据库返回的结果进行了封装,使用迭代器的模式可以逐条取出结果集中的记录。
while(resultSet.next()) { System.out.println(resultSet.getString(2)); }
ResultSet 一般也建议使用完毕直接 close 掉,但是需要注意的是关闭 ResultSet 对象不关闭其持有的 Blob、Clob 或 NClob 对象。 Blob、Clob 或 NClob 对象在它们被创建的的事务期间会一直持有效,除非其 free 函数被调用。
参考
- https://blog.csdn.net/wl044090432/article/details/60768342
- https://blog.csdn.net/luanlouis/article/details/29850811
-
什么是数据持久化?为什么要持久化?
2019-03-12 21:16:59什么是数据持久化?为什么要持久化? 一直听人说数据库持久化,也想学这方面的技术,但是什么是数据库持久化呢? 持久数据其实就是将数据保存到数据库。 数据持久化就是将内存中的数据模型转换为存储模型,以及将...
-
利用Java实现的简单本地群聊系统
-
GPS时间同步服务器,NTP时间同步服务器,——数据采集系统应用
-
js创建数据库
-
2-29-闭包
-
ORA-00904 WMSYS.WM_CONCAT标识符无效解决方案
-
Spring Boot专栏九:最简单的商城系统项目的第一步——规划项目
-
CENTOS8配置VSFTPD实例(FTP Server)
-
概率统计23——假设检验理论(2)
-
2021最新Kubernetes(k8s)集群实战精讲
-
(新)备战2021软考网络工程师培训学习套餐
-
云计算基础-Linux系统管理员
-
jQuery监听事件和解绑事件,非常详细
-
数据结构李春葆版期末试卷,复习资料,ppt
-
微信支付2021系列之扫码支付一学就会java版
-
python数据分析基础
-
thinkphp5.1博客后台实战视频
-
【2021】Python3+Selenium3自动化测试(不含框架)
-
Java星选一卡通
-
智联万物,京东IoT技术创新与实践
-
IPSec IKEv1实验(华为设备)