1. 什么是JDBC
JDBC:
Java Database Connectivity: SUN公司为了简化、统一对数据库的操作,定义了一套Java操作数据库的规范(接口),称之为JDBC。这套接口由数据库厂商去实现,这样,开发人员只需要学习jdbc接口,并通过jdbc加载具体的驱动,就可以操作数据库
驱动:
JDBC是Java制定的接口,数据库产商依照该接口编写与自家数据库配套的实现类。比如MySQL、Oracle、SqlServer等都有自己的不同实现,这些实现类的集合既是我们笼统意义上的驱动
这样也就体现了面向接口编程的思想,在代码中直接new具体的驱动类,会使程序高度耦合。比如,后期如果要切换数据库(虽然很少),就要临时调换驱动类,需要修改源码,不符合开闭原则。而面向接口编程,实际上就是一种“多态”。屏蔽具体的实现,只需调用接口方法,传入规定的参数即可得到预期的返回值。切换数据库驱动并不影响程序运行结果
2. 两种获取数据库连接的方法
以mysql为例
2.1 直接使用com.mysql.jdbc.Driver
的connect
方法
@Test
public void testDriver() throws SQLException {
Driver driver = new com.mysql.cj.jdbc.Driver();
String url ="jdbc:mysql://localhost:3306/xtom?serverTimezone=GMT&useSSL=false";
Properties info = new Properties();
info.put("user", "root");
info.put("password", "yzxdyr19990210");
//3. 调用 Driver 接口的 connect(url, info) 获取数据库连接
Connection connection = driver.connect(url, info);
System.out.println(connection);
}
我们来分析一下这个代码
**1. Driver **
Driver driver = new com.mysql.cj.jdbc.Driver();
.....
Connection connection = driver.connect(url, info);
也就是上面的面向接口编程,但是这种写法属于硬编码,所有的配置都在代码中完全写好了,之后如果我想更改一下实现类就必须更改代码;
先看左边的Driver接口,规定了几个接口中的方法,这里关注一下connect方法
再看实现类com.mysql.cj.jdbc.Driver
里面包括一个空的构造和一个静态代码块,但是没有实现Driver接口,再看他继承了NonRegisteringDriver
,Driver
的实现是在那里面进行的
@Override
public java.sql.Connection connect(String url, Properties info) throws SQLException {
try {
if (!ConnectionUrl.acceptsUrl(url)) {
/*
* According to JDBC spec:
* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
* JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
*/
return null;
}
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
case LOADBALANCE_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);
case FAILOVER_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);
case REPLICATION_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);
default:
return null;
}
} catch (UnsupportedConnectionStringException e) {
// when Connector/J can't handle this connection string the Driver must return null
return null;
} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}
核心代码是
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
就是根据我们给定的url和用户名密码,返回一个与数据库关联的Connection
这样我们调用driver.connect(url, info) 就可以获得数据库连接
但是在实现类com.mysql.cj.jdbc.Driver中的静态代码块是干什么的呢
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
这个下面再提
2.2 使用DriverManager获取数据库连接(5.1.6版本前)
@Test
public void testDriverManager() throws Exception{
//1. 驱动的全类名
String driverClass = "com.mysql.cj.jdbc.Driver";
//2. 准备连接数据库的基本信息: url, user, password
String url ="jdbc:mysql://localhost:3306/xtom?serverTimezone=GMT&useSSL=false";
String user = "root";
String password = "root";
//2. 注册驱动(对应的 Driver 实现类中有注册驱动的静态代码块)
Class.forName(driverClass);
//3. 通过 DriverManager 的 getConnection() 方法获取数据库连接
Connection connection =
DriverManager.getConnection(url, user, password);
System.out.println(connection);
}
首先关注加载数据库驱动程序的那一行代码
Class.forName(driverClass);
这里通过Class对象主动触发JVM加载com.mysql.jdbc.Driver
类,也就执行了静态代码块,com.mysql.jdbc.Driver
被注册进DriverManager
,也就是注册驱动,告诉程序使用哪一个数据库jar包
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
那什么是DriverManager呢
在DriverManager
内部维护一个类型为DriverInfo的容器,DriverInfo存储的是Driver
也就是说该容器存储所有已注册的Driver
,通过DriverManager
可以管理多个驱动程序,支持注册多个(多种)驱动,所以它叫“驱动管理器”(DriverManager
)
之后通过DriverManager
的getConnection
就可以拿到对应的连接
Connection connection =
DriverManager.getConnection(url, user, password);
getConnection方法在底层会循环遍历所有驱动,找到当前注册的驱动后调用driver.connect()获得Connection
//私有方法,只能内部调用:获取数据库连接
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//callerCl是类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
//同步代码块
synchronized(DriverManager.class) {
if (callerCL == null) {
//如果没有传入类加载器,使用当前线程的类加载器
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
SQLException reason = null;
//循环遍历容器中所有已注册的Driver
for(DriverInfo aDriver : registeredDrivers) {
//isDriverAllowed()方法会使用callerCl尝试加载每一个Driver,加载成功返回true
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//底层还是使用了Driver本身的connect方法,获取Connection
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
//加载驱动类,加载失败返回false
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
所以也就是说静态代码块注册驱动到DriverManager对于Driver方式获取Connection是多余的,但是对于DriverManager获取Connection是必须的
2.3 使用DriverManager获取数据库连接(5.1.6版本后,使用spi机制)
使用mysql-connector-java
连接数据库,在5.1.6之前的版本都需要加上Class.forName(“com.mysql.jdbc.Driver”)
; 但是从5.1.6版本以及后面的版本,这句代码就可以去掉了,使用spi机制就能加载具体的实现类
Connection connection = DriverManager.getConnection(url, user, password);
具体怎么做到的下面再说
3. JDBC与SPI机制
3.1 SPI机制
深入理解SPI机制
Java的SPI机制
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services
文件夹查找文件,自动加载文件里所定义的类;
这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制;
Java的SPI类似于IOC的功能,将装配的控制权移到了程序之外,实现在模块装配的时候不用在程序中动态指明。所以SPI的核心思想就是解耦,这在模块化设计中尤其重要,这样就可以解决我们上面说的硬编码的问题
3.2 使用
3.3 jdbc与spi机制
在DriverManager
中也有一段静态代码块
上面说到,使用mysql-connector-java连接数据库,在5.1.6之前的版本都需要加上Class.forName(“com.mysql.jdbc.Driver”); 但是从5.1.6版本以及后面的版本,这句代码就可以去掉了。这是为什么呢?
我们跟进loadInitialDrivers
看一下这个方法
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
使用时,我们直接调用DriverManager.getConn()方法自然会触发静态代码块的执行,开始加载驱动
主要需要关注的代码是
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
这块代码就是通过Java的SPI加载Driver接口的所有实例,并将实例初始化。mysql-connector-java 包中META-INF/services目录下有个名为java.sql.Driver的文件,内容就是Driver接口的实现类
当Driver实例化的时候,会先执行静态代码块,向DriverManager注册一个自己的实例,在DriverManager中注册的驱动信息都保存在registeredDrivers中
DriverManager.getConnection方法真正调用的时候,就是遍历registeredDrivers 中驱动信息,找到可以使用的驱动,拿到数据库连接。
这里通过SPI机制成功的进行解耦,代码中不再强制指定使用哪个驱动实现,而是将装配的控制权移到了程序外,成功的做到了业务代码和与第三方装配逻辑分离
3.4 优缺点
优点:使用Java SPI机制的优势是实现了解耦,使第三方模块的装配逻辑与业务代码分离。应用程序可以根据实际业务情况使用新的框架拓展或者替换原有组件。
缺点:ServiceLoader在加载实现类的时候会全部加载并实例化,假如不想使用某些实现类,它也会被加载示例化的,这就造成了浪费。另外获取某个实现类只能通过迭代器迭代获取,不能根据某个参数来获取,使用方式上不够灵活。
Dubbo框架中大量使用了SPI来进行框架扩展,但它是重新对SPI进行了实现,完美的解决上面提到的问题。
3.5 打破双亲委派机制
关于什么是双亲委派机制具体看JVM - 类加载子系统这篇文章
上面说到Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务。编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现;
但是便利的同时也带来了困扰。提供商提供的类不能放JDK里的lib目录下,所以也就没法用BootstrapClassLoader加载了;
就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。
没有SPI时,你可以现在classpath
里加一个mysql-connector-java.jar,然后这样写
Class clz = Class.forName("com.mysql.jdbc.Driver");
Driver d = (Driver) clz.newInstance();
这没问题了,这里用了Application Classloader
加载了mysql-connector-java.jar
的com.mysql.jdbc.Driver
。问题是你hard code
了一定要加载"com.mysql.jdbc.Driver"
,不是很优雅,不能实现“用接口编程,自动实例化真的实现“的这种编码形式
⚠️ 注意Class.forName()
加载用的是调用者的Classloader
使用SPI后,代码大致会这样
Connection connection = DriverManager.getConnection("jdbc:mysql://xxxxxx/xxx", "xxxx", "xxxxx");
DriverManager就根据"jdbc:mysql"这个提示去找具体实现去了,整个流程基本如下所示
- 从META-INF/services/java.sql.Driver文件得到实现类名字DriverA
- Class.forName(“xx.xx.DriverA”)来加载实现类
- Class.forName()方法默认使用当前类的ClassLoader,JDBC是在DriverManager类里调用Driver的,当前类也就是DriverManager,它的加载器是BootstrapClassLoader。
- 用BootstrapClassLoader去加载非rt.jar包里的类xx.xx.DriverA,就会找不到
- 要加载xx.xx.DriverA需要用到AppClassLoader或其他自定义ClassLoader
- 最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类
这样就出现了一个问题:如何在父加载器加载的类中,去调用子加载器去加载类?
jdk提供了两种方式
- Thread.currentThread().getContextClassLoader()
- ClassLoader.getSystemClassLoader()
他们一般都指向AppClassLoader,他们能加载classpath中的类,SPI使用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码
public static <S> ServiceLoader<S> load(Class<S> var0) {
ClassLoader var1 = Thread.currentThread().getContextClassLoader();
return load(var0, var1);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
4. Connection数据库连接对象
上面连接两种方法连接数据库后获得的都是数据库连接对象Connection,这个数据库连接对象有两个作用
- 获取执行SQL的对象
//Statement
Statement statement = connection.createStatement();
//PreparedStatement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
- 管理事务
JDBC默认为自动提交事务,也就是说,每一条SQL语句都是一个单独的事务
使用JDBC处理事务:
① 设置setAutoCommit(boolean)为false,让JDBC不自动提交事务,同时开启事务
② commit():提交结束事务
③ rollback():回滚结束事务
try{
connection.setAutoCommit(false);//开启事务
......
connection.commit();//try的最后提交事务
} catch() {
connection.rollback();//回滚事务
}
5. Statement对象
用于执行静态sql(里面给的参数都是给定值)并返回生成结果的对象
① executeUpdate(String sql)::执行DML(insert, update, delete)语句和DDL语句(create, alter, drop),返回的是影响的行数,可以通过行数判断是否执行成功
Statement st = connection.createStatement();
String sql = "insert into user(….) values(…..) ";
int num = st.executeUpdate(sql);
if(num>0){
System.out.println("插入成功");
}
②**executeQuery(String sql):**方法完成数据查询操作(DQL操作)返回值是结果集对象
Statement st = conn.createStatement();
String sql = "select * from user where id=1";
ResultSet rs = st.executeUpdate(sql);
while(rs.next()){
//根据获取列的数据类型,分别调用rs的相应方法映射到java对象中
}
6. PreparedStatement 对象
6.1 SQL注入问题
在拼接sql时,有一些sql关键字参与字符串的拼接,造成安全性问题
比如我们使用上面的Statement对象进行一个查询操作
//sql里字符串两边要有单引号
String sql = "select * from user where username='"+username+"' and password = '"+password+"'";
看起来很正常,但是我输入用户名和密码为
user//用户名
a' or 'a'='a//密码
这个时候sql语句会被拼接为
select * from user where username='user' and password = 'a' or 'a'='a'
注意看后面的or,这个sql语句变成了恒等式,永远都为true
6.2 解决SQL注入问题
PreparedStatement 是预编译sql,sql的参数使用占位符替代,不用拼接
//获得预处理对象
String sql = "select * from user where username=?and password = ?";
PreparedStatement stat = connection.prepareStatement(sql);
//SQL语句占位符设置实际参数
stat.setString(1, "ser");
stat.setString(2, "aaaaa");
//执行SQL语句
preparedStatement.executeQuery();
7. 结果集对象
对于statement.excuteUpdate(sql);执行结果返回的是int类型的值。此处的sql语句为非查询语句。
而如果要执行查询语句则需要使用Statement对象的excuteQuery(sql),返回结果集类型的ResultSet,下图查询出来的集合就是一个结果集,在ResultSet中有一个指向行的光标
ResultSet rs = statement.executeQuery(sql);
rs为结果对象集,通过**rs.next()**进行行遍历,就是让游标向下移动一行
然后再去使用getXXX方法获取当前光标行的指定列的值,这个方法有两种参数
① getXXX(i),i为列号和(XXX是你要获得的列的类型)
② getXXX(“a”),a为对应的列名,(XXX是你要获得的列的类型)
所以只要能够获得查询结果集的总列数。通过行列遍历就可以遍历整个查询的结果集。我们可以通过rs.getMetaData().getColumnCount();的方式获取到插叙结果集。这样便可以进行结果集的遍历。遍历核心代码如下:
int count = rs.getMetaData().getColumnCount();
while(rs.next()) {//遍历行,next()方法返回值为Boolean,当存在数据行返回true,反之false
for(int i = 1; i <= count; i++) {//遍历列
System.out.print(rs.getString(i));
}
System.out.println();
}
8. 数据库连接池
用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、拓机
也就是说每一次用户来都要申请连接,使用完在关闭,来了再申请,再关闭,这样消耗资源,且时间也较长,我们要做的是当系统初始化好了,容器就申请一些连接池对象,用户访问的时候直接从容器中获取连接对象,访问完把连接对象归还给容器给别人使用,而不是释放掉
这样节约了资源并且高效
最小连接数: 是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费
最大连接数: 是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作
如果最小连接数与最大连接数相差很大: 那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接.不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,他将被放到连接池中等待重复使用或是空间超时后被释放.
8.1 实现原理
是sun公司定义好的规则,也就是一个接口,一个连接到这个DataSource对象所代表的物理数据源的工厂,DataSource接口由驱动程序供应商实现
所以实现的连接池都有两个事
① 获取连接
getConnection()
② 归还连接
如果连接对象Connection是从连接池获取的,那么调用Connection.close()方法,则可以自动归还连接,而不是关闭,而不需要我们记忆新的方法