SpringJdbcTemplate解析

2 篇文章 0 订阅
2 篇文章 0 订阅

JdbcTemplate

​ JdbcTemplate是Spring Framework中提供的一组数据库操作组件,包含许多针对数据库的操作方法,用于简化代码中对数据库的操作。本文针对JdbcTemplate源代码,结合其中的设计结构、功能模块,以及涉及到的类加载器、SPI等知识做相关解析。

​ 先来看下JdbcTemplate的基本使用方法,以查询为例

// 加载驱动并创建数据源
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource(URL, USER_NAME, PASSWORD);
driverManagerDataSource.setDriverClassName(DRIVE_NAME);

// 创建JdbcTemplate对象
JdbcTemplate jdbcTemplate = new JdbcTemplate(driverManagerDataSource);

// 执行查询操作
RowMapper<SysUserInfo> rowMapper = new BeanPropertyRowMapper<>(SysUserInfo.class);
List<SysUserInfo> objectList = jdbcTemplate.query(QUERY, rowMapper);
objectList.forEach(t-> System.out.println(t.toString()));

​ 是不是非常简单,短短几行代码就完成了数据库的查询操作。不需要做繁琐的驱动加载、数据源管理、结果集处理等操作。

​ JdbcTemplate大致上可以分为基础支持层、核心处理层、对外接口层。基础支持层依赖JDK和Jdbc驱动的支持。其中使用JDK的类加载机制加载Jdbc驱动。核心处理层包括数据源、事务、ORM框架的参数映射和结果集映射、执行模板等模块。对外接口层提供了一系列增删改查以及数据源参数设置的方法。接下来对各层做详细讲解。

JdbcTemplate-架构图

基础支持层

​ 基础支持层主要用于加载Jdbc驱动,以提供原生的、不经任何封装的Jdbc方法。

JDBC

原生JDBC的流程为

// 1、通过反射加载驱动
// 1.1、即使注释这行依然能够运行,这是因为Java SPI机制会自动加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 2、创建连接Collection
Connection connection = DriverManager.getConnection(URL, USER_NAME, PASSWORD);

// 3、创建Sql执行对象Statement
Statement statement = connection.createStatement();

// 4、执行Sql语句,并取得ResultSet
ResultSet resultSet = statement.executeQuery("select * from sys_user");

// 5、处理结果集ResultSet
while (resultSet.next()){
	System.out.println(resultSet.getString("username"));
}

// 6、释放连接资源
resultSet.close();
statement.close();
connection.close();

JdbcTemplate-JDK及JDBC整体UML

​ 1、通过Class.forName加载驱动时,会执行com.mysql.cj.jdbc.Driver的静态代码块,其会向DriverManager的CopyOnWriteArrayList属性注册一个自身的实例。

​ 2、调用DriverManager.getConnection()时,会从CopyOnWriteArrayList属性中查找已注册的驱动,并将创建工作委托给java.sql.Driver的实现类NonRegisteringDriver去执行。

​ 3、NonRegisteringDriver根据URL的前缀,创建不同的连接。常用的jdbc:mysql会创建ConnectionImpl。

​ 4、ConnectionImpl会创建StatementImpl,Sql执行完成后会创建ResultSetImpl。这些步骤由驱动完成,这里不深入探究。

​ 从以上步骤可以看出原生Jdbc流程大致可以分为:加载驱动–>创建数据库连接–>执行Sql语句并获取结果集–>结果集处理–>关闭连接。JdbcTemplate将这些步骤封装在了数据源管理、参数映射、结果映射中。

JDK

SPI

​ 在Jdbc驱动加载的过程中,调用了java.sql.DriverManager.registerDriver方法,方法签名如下。从中可以看出其参数为接口,即JDK是实现的是面向接口的方法,而把方法的具体实现交由第三方来做。这就是开发中一个非常重要的理念,即SPI,可以减少程序的侵入性,提高扩展性。

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException

​ SPI的全名为Service Provider Interface(服务提供接口),主要用于第三方厂商完成服务实现。在面向对象编程中,我们提倡面向接口编程,更关注编程的抽象概念。在JDK中,有很多方法都是基于接口编写,比如sql包中的数据库驱动管理、数据源、数据库连接、执行语句、结果集处理等,JDK提供了这些顶层设计,将具体的实现交由第三方,这样很大的提高了程序的扩展性。

​ 在驱动加载中,即使没有通过Class.forName(“com.mysql.cj.jdbc.Driver”)显式地加载驱动,驱动依然能后被正常加载,这是为什么呢,SPI是如何实现的呢?

​ DriverManager的静态代码块中,会调用ServiceLoader遍历目录下的驱动,最终通过Class.forName()的方式加载。

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");    
}

private static void loadInitialDrivers() {
  	ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  	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);
        }
    }
}

​ ServiceLoader的内部类会加载资源(即CLASSPATH下的包)目录"META-INF/services/"下的配置文件,进而加载驱动。

private static final String PREFIX = "META-INF/services/";

String fullName = PREFIX + service.getName();
if (loader == null)
		configs = ClassLoader.getSystemResources(fullName);
else
		configs = loader.getResources(fullName);

​ SPI作为一种服务发现机制,本质上是用来解耦。在模块设计中,将程序的控制和模块的加载交由主模块来做,子模块可以使用多种实现,这样很好的体现了可插拔和替换。

ClassLoad

​ 类加载器负责加载所有的类,加载.class字节码文件到内存中,其为所有被载入内存中的类生成一个java.lang.Class实例对象。在加JVM中存在三种类加载器:

1、启动类加载器(bootstrap class loader):它用来加载 Java 的核心类,即jre/lib/rt.jar等包中的类。

2、扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。其父加载器为根类加载器。

3、系统类加载器(system class loader):也被成为应用加载器,用于加载CLASSPATH和java.class.path系统属性。其父加载器为扩展类加载器。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

​ 对于类加载机制,存在双亲委派模型,即:类加载器会先将加载任务委托给其父类加载器,当父类加载器无法加载时,才会自己加载。可以想象为行政流程中的工作流,当有任务要处理时,必须要先上报给上级,当上级无法处理或无需处理时才自己处理。这样的好处体现在一下两个方面:1、避免类的重复加载。类的加载必须先委托给父类加载,当父类已加载后会直接返回,不会重新加载。2、避免核心类被篡改。当加载核心类时,其会被委托到启动类加载器,如果启动类加载器已加载,则会直接返回。如果存在包权限问题,则会直接报错。这样就保证了核心类只会在启动类加载器中加载一次。

​ ServiceLoader类位于rt.jar包下的java.util目录下,由双亲委派模型可知,是由启动类加载器加载,其无法委托系统类加载器加载第三方驱动,因为类的加载无法向下委托。由此引出了线程上下文类加载器,它打破了双亲委派模型的链式加载限制。

核心处理层

执行模板

​ 从原生Jdbc的流程可以看出,除了第4步执行Sql语句有不同以外(增删改查),其他步骤是非常相似的。由此可以将这一流程封装为模板,再结合回调模式,将执行对象封装在回调方法中。基于Statement的模板核心代码为

@Override
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 {
		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());
	}
}

​ 实际的执行语句则是封装在回调方法中,以查询为例:

JdbcTemplate-回调UML

class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
	@Override
	@Nullable
	public T doInStatement(Statement stmt) throws SQLException {
		ResultSet rs = null;
		try {
			rs = stmt.executeQuery(sql);
			return rse.extractData(rs);
		}
		finally {
			JdbcUtils.closeResultSet(rs);
		}
	}
	@Override
	public String getSql() {
		return sql;
	}
}

参数映射

​ 参数映射比较简单,就是将参数数据填充到PrepareStatement中。

结果集映射

JdbcTemplate-结果集映射UML

​ 原生的Jdbc执行语句后返回的是ResultSet,这个在数据处理上是非常不方便的,我们希望能提供一种ORM框架的对象–管理映射模型,将查询的结果直接返回成一个对象。JdbcTemplate提供了结果集映射以实现这一功能。其本质上非常简单,就是通过反射的方式循环遍历ResultSet中的数据构造一个对象。

​ 结果集映射由RowMapper接口提供,我们来看一下其中一个实现类BeanPropertyRowMapper的核心代码。

@Override
public T mapRow(ResultSet rs, int rowNumber) throws SQLException {
	Assert.state(this.mappedClass != null, "Mapped class was not specified");
	T mappedObject = BeanUtils.instantiateClass(this.mappedClass);
	BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
	initBeanWrapper(bw);

	ResultSetMetaData rsmd = rs.getMetaData();
	int columnCount = rsmd.getColumnCount();
	Set<String> populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null);
	
    for (int index = 1; index <= columnCount; index++) {
		String column = JdbcUtils.lookupColumnName(rsmd, index);
		String field = lowerCaseName(StringUtils.delete(column, " "));
		PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null);
		bw.setPropertyValue(pd.getName(), value);
	}
	return mappedObject;
}

​ 其流程就是通过反射实例化一个对象,再通过循环遍历结果集中的列,将值填充到对象中。在这之上还封装了一层ResultSetExtractor,用来调用RowMapper,来看下其中一个实现类RowMapperResultSetExtractor的核心代码,个人认为,如果业务不复杂,这个类是可以省略的,可以将所有的功能放在RowMapper中实现。

@Override
public List<T> extractData(ResultSet rs) throws SQLException {
	List<T> results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>());
	int rowNum = 0;
	while (rs.next()) {
		results.add(this.rowMapper.mapRow(rs, rowNum++));
	}
	return results;
}

数据源管理

数据源初始化

JdbcTemplate-数据源初始化UML

​ 数据源初始化主要是基于JDK提供的数据源相关类,可以去看之前关于动态数据源的文章,这里不再赘述。

创建连接

JdbcTemplate数据源连接创建UML

​ 在执行模板中,并不是通过驱动或者数据源来直接创建数据库连接,而是委托给DataSourceUtils来完成的。这是因为事务只有在同一个线程/数据库连接中执行才有效,里面添加了对事务的支持。它首先查看当前是否存在事务管理上下文,并尝试从事务管理上下文获取连接,如果获取失败,直接从数据源中获取连接。在获取连接后,如果当前拥有事务上下文,则将连接绑定到事务上下文中。来看下核心代码:

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())) {
		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.
			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;
}

private static Connection fetchConnection(DataSource dataSource) throws SQLException {
	Connection con = dataSource.getConnection();
	if (con == null) {
		throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
	}
	return con;
}

​ 对于事务相关的知识,在之后进行分析。

总结

​ 作为Spring Framework官方提供的工具类,JdbcTemplate十分短小精悍。其中使用的SPI思想、模板、结果集映射都十分值得借鉴,从其基本原理出发也能衍生出许多其他的知识点。另外Spring中提供的其他Template工具类,例如RedisTemplate、RestTemplate、MongoTemplates等也十分优秀,以后有机会也会进行分析,多多对比才能看到其中的特点及优劣。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值