JDBC是一套链接和操做数据库的标准、规范。经过提供DriverManager
、Connection
、Statement
、ResultSet
等接口将开发人员与数据库提供商隔离,开发人员只须要面对JDBC接口,无需关心怎么跟数据库交互。java
类名 | 做用 |
---|---|
DriverManager |
驱动管理器,用于注册驱动,是获取 Connection 对象的入口 |
Driver |
数据库驱动,用于获取Connection 对象 |
Connection |
数据库链接,用于获取Statement 对象、管理事务 |
Statement |
sql执行器,用于执行sql |
ResultSet |
结果集,用于封装和操做查询结果 |
prepareCall |
用于调用存储过程 |
记得释放资源。另外,ResultSet
和Statement
的关闭都不会致使Connection
的关闭。mysql
maven要引入oracle的驱动包,要把jar包安装在本地仓库或私服才行。git
使用PreparedStatement
而不是Statement
。能够避免SQL注入,而且利用预编译的特色能够提升效率。github
使用JDBC对mysql数据库的用户表进行增删改查。sql
JDK:1.8数据库
maven:3.6.1编程
IDE:sts4浏览器
mysql driver:8.0.15安全
mysql:5.7服务器
一个完整的JDBC保存操做主要包括如下步骤:
注册驱动(JDK6后会自动注册,可忽略该步骤);
经过DriverManager
得到Connection
对象;
开启事务;
经过Connection
得到PreparedStatement
对象;
设置PreparedStatement
的参数;
执行保存操做;
保存成功提交事务,保存失败回滚事务;
释放资源,包括Connection
、PreparedStatement
。
CREATE TABLE `demo_user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id', `name` varchar(16) COLLATE utf8_unicode_ci NOT NULL COMMENT '用户名', `age` int(3) unsigned DEFAULT NULL COMMENT '用户年龄', `gmt_create` datetime DEFAULT NULL COMMENT '记录建立时间', `gmt_modified` datetime DEFAULT NULL COMMENT '记录最近修改时间', `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`), KEY `index_age` (`age`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
项目类型Maven Project,打包方式jar
<!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- mysql驱动的jar包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> <!-- oracle驱动的jar包 --> <!-- <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>11.2.0.2.0</version> </dependency> -->
注意:因为oracle商业版权问题,maven并不提供Oracle JDBC driver
,须要将驱动包手动添加到本地仓库或私服。
下面的url拼接了好几个参数,主要为了不乱码和时区报错的异常。
路径:resources目录下
driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true #这里指定了字符编码和解码格式,时区,是否加密传输 username=root password=root #注意,xml配置是&采用&替代
若是是oracle数据库,配置以下:
driver=oracle.jdbc.driver.OracleDriver url=jdbc:oracle:thin:@//localhost:1521/xe username=system password=root
private static Connection createConnection() throws Exception { // 导入配置文件 Properties pro = new Properties(); InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "jdbc.properties" ); Connection conn = null; pro.load( in ); // 获取配置文件的信息 String driver = pro.getProperty( "driver" ); String url = pro.getProperty( "url" ); String username = pro.getProperty( "username" ); String password = pro.getProperty( "password" ); // 注册驱动,JDK6后不须要再手动注册,DirverManager的静态代码块会帮咱们注册 // Class.forName(driver); // 得到链接 conn = DriverManager.getConnection( url, username, password ); return conn; }
这里简单地模拟实际业务层调用持久层,并开启事务。另外,获取链接、开启事务、提交回滚、释放资源都经过自定义的工具类 JDBCUtil
来实现,具体见源码。
@Test public void save() { UserDao userDao = new UserDaoImpl(); // 建立用户 User user = new User( "zzf002", 18, new Date(), new Date() ); try { // 开启事务 JDBCUtil.startTrasaction(); // 保存用户 userDao.insert( user ); // 提交事务 JDBCUtil.commit(); } catch( Exception e ) { // 回滚事务 JDBCUtil.rollback(); e.printStackTrace(); } finally { // 释放资源 JDBCUtil.release(); } }
接下来看看具体的保存操做,即DAO层方法。
public void insert( User user ) throws Exception { String sql = "insert into demo_user (name,age,gmt_create,gmt_modified) values(?,?,?,?)"; Connection connection = JDBCUtil.getConnection(); //获取PreparedStatement对象 PreparedStatement prepareStatement = connection.prepareStatement( sql ); //设置参数 prepareStatement.setString( 1, user.getName() ); prepareStatement.setInt( 2, user.getAge() ); prepareStatement.setDate( 3, new java.sql.Date( user.getGmt_create().getTime() ) ); prepareStatement.setDate( 4, new java.sql.Date( user.getGmt_modified().getTime() ) ); //执行保存 prepareStatement.executeUpdate(); //释放资源 JDBCUtil.release( prepareStatement, null ); }
DriverManager
主要用于管理数据库驱动,并为咱们提供了获取链接对象的接口。其中,它有一个重要的成员属性registeredDrivers
,是一个CopyOnWriteArrayList
集合(经过ReentrantLock
实现线程安全),存放的是元素是DriverInfo
对象。
//存放数据库驱动包装类的集合(线程安全) private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { //调用重载方法,传入的DriverAction对象为null registerDriver(driver, null); } public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException { if(driver != null) { //当列表中没有这个DriverInfo对象时,加入列表。 //注意,这里判断对象是否已经存在,最终比较的是driver地址是否相等。 registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); } else { throw new NullPointerException(); } println("registerDriver: " + driver); }
为何集合存放的是Driver
的包装类DriverInfo
对象,而不是Driver
对象呢?
经过DriverInfo
的源码可知,当咱们调用equals
方法比较两个DriverInfo
对象是否相等时,实际上比较的是Driver
对象的地址,也就是说,我能够在DriverManager
中注册多个MYSQL驱动。而若是直接存放的是Driver
对象,就不能达到这种效果(由于没有遇到须要注册多个同类驱动的场景,因此我暂时理解不了这样作的好处)。
DriverInfo
中还包含了另外一个成员属性DriverAction
,当咱们注销驱动时,必须调用它的deregister
方法后才能将驱动从注册列表中移除,该方法决定注销驱动时应该如何处理活动链接等(其实通常在构造DriverInfo
进行注册时,传入的DriverAction
对象为空,根本不会去使用到这个对象,除非一开始注册就传入非空DriverAction
对象)。
综上,集合中元素不是Driver
对象而DriverInfo
对象,主要考虑的是扩展某些功能,虽然这些功能几乎不会用到。
注意:考虑篇幅,如下代码通过修改,仅保留所需部分。
class DriverInfo { final Driver driver; DriverAction da; DriverInfo(Driver driver, DriverAction action) { this.driver = driver; da = action; } @Override public boolean equals(Object other) { //这里对比的是地址 return (other instanceof DriverInfo) && this.driver == ((DriverInfo) other).driver; } }
当加载com.mysql.cj.jdbc.Driver
这个类时,静态代码块中会执行注册驱动的方法。
static { try { //静态代码块中注册当前驱动 java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } }
由于从JDK6开始,DriverManager
增长了如下静态代码块,当类被加载时会执行static代码块的loadInitialDrivers
方法。
而这个方法会经过查询系统参数(jdbc.drivers)和SPI机制两种方式去加载数据库驱动。
注意:考虑篇幅,如下代码通过修改,仅保留所需部分。
static { loadInitialDrivers(); } //这个方法经过两个渠道加载全部数据库驱动: //1. 查询系统参数jdbc.drivers得到数据驱动类名 //2. SPI机制 private static void loadInitialDrivers() { //经过系统参数jdbc.drivers读取数据库驱动的全路径名。该参数能够经过启动参数来设置,其实引入SPI机制后这一步好像没什么意义了。 String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } //使用SPI机制加载驱动 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { //读取META-INF/services/java.sql.Driver文件的类全路径名。 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; } }); if (drivers == null || drivers.equals("")) { return; } //加载jdbc.drivers参数配置的实现类 String[] driversList = drivers.split(":"); for (String aDriver : driversList) { try { Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
补充:SPI机制本质上提供了一种服务发现机制,经过配置文件的方式,实现服务的自动装载,有利于解耦和面向接口编程。具体实现过程为:在项目的META-INF/services
文件夹下放入以接口全路径名命名的文件,并在文件中加入实现类的全限定名,接着就能够经过ServiceLoder
动态地加载实现类。
打开mysql的驱动包就能够看到一个java.sql.Driver
文件,里面就是mysql驱动的全路径名。
获取链接对象的入口是DriverManager.getConnection
,调用时须要传入url、username和password。
获取链接对象须要调用java.sql.Driver
实现类(即数据库驱动)的方法,而具体调用哪一个实现类呢?
正如前面讲到的,注册的数据库驱动被存放在registeredDrivers
中,因此只有从这个集合中获取就能够了。
注意:考虑篇幅,如下代码通过修改,仅保留所需部分。
public static Connection getConnection(String url, String user, String password) throws SQLException { java.util.Properties info = new java.util.Properties(); if (user != null) { info.put("user", user); } if (password != null) { info.put("password", password); } //传入url、包含username和password的信息类、当前调用类 return (getConnection(url, info, Reflection.getCallerClass())); } private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; //遍历全部注册的数据库驱动 for(DriverInfo aDriver : registeredDrivers) { //先检查这当前类加载器是否有权限加载这个驱动,若是是才进入 if(isDriverAllowed(aDriver.driver, callerCL)) { //这一步是关键,会去调用Driver的connect方法 Connection con = aDriver.driver.connect(url, info); if (con != null) { return con; } } else { println(" skipping: " + aDriver.getClass().getName()); } } }
因为使用的是mysql的数据驱动,这里实际调用的是com.mysql.cj.jdbc.Driver
的方法。
从如下代码能够看出,mysql支持支持多节点部署的策略,本文仅对单机版进行扩展。
注意:考虑篇幅,如下代码通过修改,仅保留所需部分。
//mysql支持多节点部署的策略,根据架构不一样,url格式也有所区别。 private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://"; private static final String URL_PREFIX = "jdbc:mysql://"; private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://"; public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://"; public java.sql.Connection connect(String url, Properties info) throws SQLException { //根据url的类型来返回不一样的链接对象,这里仅考虑单机版 ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info); switch (conStr.getType()) { case SINGLE_CONNECTION: //调用ConnectionImpl.getInstance获取链接对象 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; } }
这个类有个比较重要的字段session
,能够把它当作一个会话,和咱们平时浏览器访问服务器的会话差很少,后续咱们进行数据库操做就是基于这个会话来实现的。
注意:考虑篇幅,如下代码通过修改,仅保留所需部分。
private NativeSession session = null; public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException { //调用构造 return new ConnectionImpl(hostInfo); } public ConnectionImpl(HostInfo hostInfo) throws SQLException { //先根据hostInfo初始化成员属性,包括数据库主机名、端口、用户名、密码、数据库及其余参数设置等等,这里省略不放入。 //最主要看下这句代码 createNewIO(false); } public void createNewIO(boolean isForReconnect) { if (!this.autoReconnect.getValue()) { //这里只看不重试的方法 connectOneTryOnly(isForReconnect); return; } connectWithRetries(isForReconnect); } private void connectOneTryOnly(boolean isForReconnect) throws SQLException { JdbcConnection c = getProxy(); //调用NativeSession对象的connect方法创建和数据库的链接 this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c); return; }
接下来的代码主要是创建会话的过程,首先时创建物理链接,而后根据协议创建会话。
注意:考虑篇幅,如下代码通过修改,仅保留所需部分。
public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager) throws IOException { //首先得到TCP/IP链接 SocketConnection socketConnection = new NativeSocketConnection(); socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout); // 对TCP/IP链接进行协议包装 if (this.protocol == null) { this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager); } else { this.protocol.init(this, socketConnection, this.propertySet, transactionManager); } // 经过用户名和密码链接指定数据库,并建立会话 this.protocol.connect(user, password, database); }
针对数据库的链接,暂时点到为止,另外还有涉及数据库操做的源码分析,后续再完善补充。
本文为原创文章,转载请附上原文出处连接:https://github.com/ZhangZiSheng001/jdbc-demo