UP | HOME

JDBC 连接数据库

Table of Contents

1 JDBC 使用方法

1.1 JDBC 连接数据库流程

Java 中定义了 JDBC 的连接操作数据库的基本流程

  1. JDK 的实现包在 java.sql.*
  2. 实现包括如下四个步骤
  3. 注意 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) 实际上是基于接口的编程, 策略模式和配置文件组 合实现的动态加载机制

  1. 首先需要在运行路径中包含待加载的驱动 mysql-connector-java-5.1.47.jar, 三 方驱动在 META-INF/services/java.sql.Driver 中添加驱动配置信息
  2. 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");
    }
    
  3. loadInitialDrivers(); 方法中, ServiceLoader.load(Driver.class) 通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类 加载到 JVM

    AccessController.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;
        }
      });
    
  4. 使用 DriverManager 中定义了 registeredDrivers 来存储项目中引用的确定类 信息

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
  5. 调用 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
  1. JdbcAccessor 类实现了 InitializingBean 接口
    • 该类中包含数据库的连接 DataSource
    • 该类实现了对数据库连接的操作
      • getDataSource() 获取 DataSource
      • setDataSource() 设置 DataSource
  2. 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());
  }
}
  1. applyStatementSettings(...) 用户设置输入参数
    • fectchSize 减少网络交互次数
    • maxRows 获取结果集的最大行数
  2. action.doInStatement(...) 是调用 StatementCallback 类型的回调函数

    @FunctionalInterface
    public interface StatementCallback<T> {
      @Nullable
      T doInStatement(Statement stmt) throws SQLException, DataAccessException;
    }
    
  3. handleWarnings(...) 进行告警处理
    • 这里的告警默认输出异常信息
    • 可以通过配置抛出 SQLWarningException 异常
  4. 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();
          }
        }
      }));
}

Last Updated 2021-07-24 Sat 00:57. Created by Jinghui Hu at 2021-07-23 Fri 11:43.