JDBC 连接数据库
Table of Contents
1 JDBC 使用方法
1.1 JDBC 连接数据库流程
Java 中定义了 JDBC 的连接操作数据库的基本流程
- JDK 的实现包在
java.sql.*
下 - 实现包括如下四个步骤
- 注意
Class.forName(...)
是兼容性的写法- 低版本的 JDK 中没有加载数据库驱动的代码,需要显示加载驱动
- 高版本的 JDK 不需要手动调用
Class.forName(...)
方法,在DriverManager
的源码中可以看到一个静态块,通过 SPI 方式自动加载驱动
private static String url = "jdbc:mysql://localhost:3306/database?useSSL=false"; private static String username = "user01"; private static String password = "pass01"; public static void testMySQL() throws Exception { // 加载驱动 Class.forName("com.mysql.jdbc.Driver"); // 创建连接 Connection conn = DriverManager.getConnection(url, username, password); // 发送 SQL 语句 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT 'Hello'"); while (rs.next()) { // rs.getObject(1); String message = rs.getString(1); System.out.println(message); } // 关闭连接 conn.close(); }
1.2 SPI 连接数据库流程
SPI (Service Provider Interface) 实际上是基于接口的编程, 策略模式和配置文件组 合实现的动态加载机制
- 首先需要在运行路径中包含待加载的驱动
mysql-connector-java-5.1.47.jar
, 三 方驱动在META-INF/services/java.sql.Driver
中添加驱动配置信息 DriverManager
的静态代码块中通过调用loadInitialDrivers();
来读取使 用驱动信息/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
在
loadInitialDrivers();
方法中,ServiceLoader.load(Driver.class)
通过扫描META-INF/services
目录下的配置文件找到实现类的全限定名,把类 加载到 JVMAccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 这里加载通过 ServiceLoader 调用 AppClassLoader 加载 JDBC 驱动 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } });
使用
DriverManager
中定义了registeredDrivers
来存储项目中引用的确定类 信息// List of registered JDBC drivers private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
调用
getConnection
方法时通过变量所有注册的驱动来获取连接信息// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); } println("DriverManager.getConnection(\"" + url + "\")"); // Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. SQLException reason = null; for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); 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"); }
2 Spring 中的 JdbcTemplate
Spring 框架中对 JDBC 的操作进行了封装,提供可以方便操作数据库的 JdbcTemplate
类
2.1 数据库配置类
package io.github.jeanhwea.app09_jdbc.beans; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DriverManagerDataSource; @Configuration public class DataSourceConfig { @Bean public DataSource getDataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/db01"); dataSource.setUsername("user01"); dataSource.setPassword("pass01"); return dataSource; } }
2.2 添加实体及 Mapper
实体类
package io.github.jeanhwea.app09_jdbc.beans; public class Employee { private Long id; private String name; public Employee(Long id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Employee[" + this.id +"]"; } }
对象的 Mapper
package io.github.jeanhwea.app09_jdbc.beans; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.jdbc.core.RowMapper; public class EmployeeRowMapper implements RowMapper<Employee> { @Override public Employee mapRow(ResultSet rs, int rowNum) throws SQLException { Employee empl = new Employee(rs.getLong("G_ID"), rs.getString("EMPL_NAME")); return empl; } }
2.3 服务类 Service
//////////////////////////////////////////////////////////////////////////////// // 服务接口 //////////////////////////////////////////////////////////////////////////////// package io.github.jeanhwea.app09_jdbc.beans; import java.util.List; import org.springframework.stereotype.Component; @Component public interface EmployeeService { List<Employee> getAll(); } //////////////////////////////////////////////////////////////////////////////// // 实现类 //////////////////////////////////////////////////////////////////////////////// package io.github.jeanhwea.app09_jdbc.beans; import java.util.List; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component public class EmployeeServiceImpl implements EmployeeService { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public EmployeeServiceImpl(DataSource dataSource) { this.setDataSource(dataSource); } @Override @SuppressWarnings("unchecked") public List<Employee> getAll() { List<Employee> employees = jdbcTemplate.query("select * from TB_BASE_EMPL", new EmployeeRowMapper()); return employees; } }
2.4 主函数
package io.github.jeanhwea.app09_jdbc; import io.github.jeanhwea.app09_jdbc.beans.Employee; import io.github.jeanhwea.app09_jdbc.beans.EmployeeService; import java.util.List; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class MyApp09 { public static void main(String[] args) { String pkgname = MyApp09.class.getPackage().getName(); System.out.println(pkgname); AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(pkgname); EmployeeService employeeService = ctx.getBean("employeeServiceImpl", EmployeeService.class); List<Employee> employeeList = employeeService.getAll(); System.out.println(employeeList); } }
3 代码实现
3.1 JdbcTemplate
类
JdbcTemplate
是操作数据的主要类,它的继承关系如下
org.springframework.jdbc.core Class JdbcTemplate java.lang.Object org.springframework.jdbc.support.JdbcAccessor org.springframework.jdbc.core.JdbcTemplate All Implemented Interfaces: org.springframework.beans.factory.InitializingBean, JdbcOperations
JdbcAccessor
类实现了InitializingBean
接口- 该类中包含数据库的连接 DataSource
- 该类实现了对数据库连接的操作
getDataSource()
获取 DataSourcesetDataSource()
设置 DataSource
JdbcTemplate
类实现了操作数据方法,提供 SQL 交互方法execute(...)
系列方法update(...)
系列方法query(...)
,queryForMap(...)
,queryForObject(...)
方法
3.2 获取数据库连接
在数据库中获取连接的调用栈如下
doGetConnection:104, DataSourceUtils (org.springframework.jdbc.datasource) getConnection:79, DataSourceUtils (org.springframework.jdbc.datasource) execute:371, JdbcTemplate (org.springframework.jdbc.core) query:452, JdbcTemplate (org.springframework.jdbc.core) query:462, JdbcTemplate (org.springframework.jdbc.core) getAll:25, EmployeeServiceImpl (io.github.jeanhwea.app09_jdbc.beans) main:15, MyApp09 (io.github.jeanhwea.app09_jdbc)
其中的 doGetConnection(...)
方法完成对连接获取操作
/** * Actually obtain a JDBC Connection from the given DataSource. * Same as {@link #getConnection}, but throwing the original SQLException. * <p>Is aware of a corresponding Connection bound to the current thread, for example * when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread * if transaction synchronization is active (e.g. if in a JTA transaction). * <p>Directly accessed by {@link TransactionAwareDataSourceProxy}. * @param dataSource the DataSource to obtain Connections from * @return a JDBC Connection from the given DataSource * @throws SQLException if thrown by JDBC methods * @see #doReleaseConnection */ public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection(); } // Else we either got no holder or an empty thread-bound holder here. logger.debug("Fetching JDBC Connection from DataSource"); Connection con = fetchConnection(dataSource); // 当前线程是否支持同步 if (TransactionSynchronizationManager.isSynchronizationActive()) { try { // Use same Connection for further JDBC actions within the transaction. // Thread-bound object will get removed by synchronization at transaction completion. // 在同一事务中,JDBC 的连接需要保持一致 ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else { holderToUse.setConnection(con); } // 这里记录数据连接的引用数量 holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } catch (RuntimeException ex) { // Unexpected exception from external delegation call -> close Connection and rethrow. releaseConnection(con, dataSource); throw ex; } } return con; }
3.3 执行 SQL: execute(...)
方法
在数据库中执行 SQL 的逻辑如下
//------------------------------------------------------------------------- // Methods dealing with static SQL (java.sql.Statement) //------------------------------------------------------------------------- @Override @Nullable public <T> T execute(StatementCallback<T> action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Connection con = DataSourceUtils.getConnection(obtainDataSource()); Statement stmt = null; try { // 执行 SQL, 并返回结果 stmt = con.createStatement(); applyStatementSettings(stmt); T result = action.doInStatement(stmt); handleWarnings(stmt); return result; } catch (SQLException ex) { // Release Connection early, to avoid potential connection pool deadlock // in the case when the exception translator hasn't been initialized yet. String sql = getSql(action); JdbcUtils.closeStatement(stmt); stmt = null; DataSourceUtils.releaseConnection(con, getDataSource()); con = null; throw translateException("StatementCallback", sql, ex); } finally { JdbcUtils.closeStatement(stmt); DataSourceUtils.releaseConnection(con, getDataSource()); } }
applyStatementSettings(...)
用户设置输入参数- fectchSize 减少网络交互次数
- maxRows 获取结果集的最大行数
action.doInStatement(...)
是调用StatementCallback
类型的回调函数@FunctionalInterface public interface StatementCallback<T> { @Nullable T doInStatement(Statement stmt) throws SQLException, DataAccessException; }
handleWarnings(...)
进行告警处理- 这里的告警默认输出异常信息
- 可以通过配置抛出
SQLWarningException
异常
DataSourceUtils.releaseConnection(...)
释放连接
3.4 数据库更新: update(...)
方法
数据库更新和执行 SQL 类似,这里需要熟悉 PreparedStatement
接口,它是一预编
译的 SQL 语句
PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES SET SALARY = ? WHERE ID = ?"); pstmt.setBigDecimal(1, 153833.00); pstmt.setInt(2, 110592);
实际的 update(...)
方法实现如下
protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss) throws DataAccessException { logger.debug("Executing prepared SQL update"); return updateCount(execute(psc, ps -> { try { if (pss != null) { // 设置 PreparedStatement 的实际值 pss.setValues(ps); } // 调用更新语句 int rows = ps.executeUpdate(); if (logger.isTraceEnabled()) { logger.trace("SQL update affected " + rows + " rows"); } return rows; } finally { if (pss instanceof ParameterDisposer) { ((ParameterDisposer) pss).cleanupParameters(); } } })); }