我的Java Web之路 - Spring JDBC初步使用

本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多码农和想成为码农的人。
本文转发自头条号【普通的码农】的文章,大家可以关注一下,直接在今日头条的移动端APP中阅读。因为平台不同,会出现有些格式、图片、链接无效方面的问题,我尽量保持一致。
原文链接:https://www.toutiao.com/i6763115239123714574/

介绍

上篇文章使用了Java异常对租房网应用的HouseService进行了改造,虽然有了一定的异常处理,但还是没有解决核心的几个问题(参考这篇文章最后的总结部分):
• 有很多代码重复的地方,比如每次访问数据库都需要建立连接、创建Statement对象、访问结果集每次都要编写循环迭代和转换成对象的代码、最后每次都要释放各种资源(即下一个问题),加入的异常处理代码也有相当的重复;
• 访问数据库的一些资源没有释放,比如连接、Statement、结果集。
本篇文章使用Spring框架的其中一个重要模块(就称为Spring JDBC吧)来化解这些难题。事实上,Spring的主要宗旨就是简化Java EE(包括了Web)应用的开发,基本上可以集成所有技术,比如前端控制器层的Servet有Spring MVC,JDBC有Spring JDBC,ORM有Spring ORM,还有消息队列、缓存、定时任务等等都有相应的解决方案。
闲话少絮,下面我们就用Spring JDBC来解决这些问题。

添加Spring JDBC的依赖

现在,想必读者朋友已经比较熟悉Maven的作用和用法了(不清楚的可以参考这篇文章和这篇文章),一般使用一个第三方库的第一步就是在Maven的POM文件中添加该依赖。
首先,我们可以在POM文件中查看一下Spring JDBC的依赖是否已经存在,这个就可以用到IDE中查看POM文件的各种视图了,我用的是Eclipse,POM文件的Dependency Hierarchy视图,如下图:
在这里插入图片描述
我们可以在过滤器中输入jdbc来快速确定是否有Spring JDBC的依赖,如果没有,则需要在Maven的中央仓库(https://mvnrepository.com/)中查找是否有该依赖:
在这里插入图片描述
果然不出所料,还真的就有spring-jdbc这个依赖,后面就无需再介绍了,只要在POM文件中添加如下配置即可,但要注意自己使用的是哪个Spring版本:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-jdbc</artifactId>
 <version>5.2.1.RELEASE</version>
</dependency>

Spring JDBC概述

添加Spring JDBC依赖之后,我们就可以很方便的使用IDE(我用的是Eclipse)来查看该依赖的总体结构了:
在这里插入图片描述
它主要包括四个包:
• core:正如名字所示,是Spring JDBC的核心包(不要与Spring框架的Core模块搞混了哦),它主要包含JdbcTemplate类及其各种回调接口和相关类,JdbcTemplate类是最经典、最流行、最低级的Spring JDBC方案,其他方案都是基于它实现的。
• datasource:这个包里面是一系列简化DataSource(DataSource接口实际上属于JDBC规范的一部分,大家可以查看JDBC规范,不再赘述)访问的工具,以及用于测试或JavaEE容器外运行JDBC代码的DataSource接口的各种简单实现。
• object:此包中的类将RDBMS(即关系数据库)的查询、更新、存储过程描述成线程安全的、可复用的对象,它使用了JDO(即Java Data Object,是Java对象持久化的新的规范)建模。
• support:包含了SQLException的转换功能和一些工具类。
这次我们主要使用JdbcTemplate来简化我们的数据库访问代码,至于Spring JDBC的其他内容暂不讨论,读者朋友也可以查阅Spring官方文档(https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/data-access.html#jdbc)。

使用JdbcTemplate

JdbcTemplate是线程安全(至于什么是线程,后续讨论)的,这意味着我们在应用中将它实例化(生成它的一个对象)一次之后,各个组件都可以使用它。如果我们使用Spring IoC来管理对象的依赖关系的话,就是一次配置,到处使用。
JdbcTemplate需要一个DataSource对象来初始化自己的对象,DataSource是一个接口,就类似于java.sql.Driver接口,各个JDBC厂商都会提供自己的实现,所以我们的应用在使用DataSource接口时需要配置其具体的实现。
因此,JdbcTemplate的使用主要包括以下步骤:
• 生成一个DataSource对象;
• 使用DataSource实例来生成一个JdbcTemplate对象;
• 将JdbcTemplate对象注入到各个需要访问数据库的组件中;
• 各个需要访问数据库的组件中使用JdbcTemplate对象访问数据库。
使用JdbcTemplate的相关组件之间的关系如下图所示:
在这里插入图片描述
即需要访问数据库的各个组件依赖于JdbcTemplate,JdbcTemplate依赖于DataSource接口,DataSource接口由各个数据库厂商提供的JDBC驱动实现(也有可能是由应用服务器实现,比如Tomcat)。

生成DataSource对象

因为我们的租房网应用现在是使用Spring IoC来管理各个组件的实例,所以可以在Spring IoC的配置元数据中生成DataSource的Bean,有三种方式提供Spring IoC的配置元数据:
• 基于XML,此种方式主要使用<bean>标签,可以参考这篇文章;
• 基于Java,此种方式主要使用@Configuration和@Bean注解,可以参考这篇文章;
• 基于自动扫描,可以参考这篇文章,此种方式用于自己开发的组件,而现在我们打算使用H2提供的DataSource,故此方式不适用。
我这里直接采用基于XML的方式提供Spring IoC的配置元数据,还记得我们之前是如何使用Spring MVC和Spring IoC改造租房网应用的吗?大家可以参考这篇文章。改造后我们已经有了一个Spring IoC的配置文件,即 WebContent/WEB-INF/dispatcher.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans.xsd 
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">
	
	<context:component-scan base-package="houserenter"/>
</beans>

我们只需要在这个文件中使用<bean>标签即可生成DataSource对象。现在的问题是我该用哪个类来生成DataSource对象呢。答案显然是实现 javax.sql.DataSource 接口的某个具体类啊,那么H2的JDBC驱动里是否提供了这么一个具体类呢?该具体类又如何实例化呢?
我们可以直接到H2的官方教程中搜索datasource关键字:
在这里插入图片描述
可以得知,H2的JDBC驱动包里还真的有 org.h2.jdbcx.JdbcDataSource 这么一个类,我们可以进一步在IDE中(我用的是Eclipse)找到该类的源码:
在这里插入图片描述
这个类还真实现了javax.sql.DataSource 接口,当然还实现了很多其他接口,比如与连接池和分布式事务相关的接口,我们暂且忽略。还有,这个类具有访问数据库的几个重要属性,比如url、用户名和密码等。这样,我们大概也就清楚了该如何实例化这个具体类了:

现在,我们可以重新发布一下租房网应用并启动Tomcat,进行验证,结果却出现如下异常:

org.springframework.beans.factory.BeanCreationException: Error
creating bean with name ‘dataSource’ defined in ServletContext
resource [/WEB-INF/dispatcher.xml]: Error setting property values;
nested exception is
org.springframework.beans.NotWritablePropertyException: Invalid
property ‘userName’ of bean class [org.h2.jdbcx.JdbcDataSource]: Bean
property ‘userName’ is not writable or has an invalid setter method.
Does the parameter type of the setter match the return type of the
getter?

从最后的信息中我们可以看到生成DataSource对象失败的原因是属性 userName 不可或 setter 方法无效。我们明明可以看到属性 userName 是存在的,那就是没有合适的 setter 方法了,根据Spring IoC默认的规则,属性 userName 的 setter 方法应该是 setUserName() ,结果还真没有该方法,而是有一个类似的 setUser() 方法,那我们把配置文件中的属性名改为 user 试试,结果Spring IoC可以正常实例化org.h2.jdbcx.JdbcDataSource 了。
属性 passwordChars 是有一个 setPasswordChars() 方法的,所以不用修改,不过还有一个 setPassword() 方法也是设置该属性的,因此将<property>标签的属性名改为 password 也是可以的。
由此我们得出一个结论是,Spring IoC在通过<property>标签实例化一个Bean时,主要看的是有没有对应的 setter 方法的名字,而非类中的属性名。
现在,我们的 dispatcher.xml 文件变成这样了:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans.xsd 
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">
	
	<context:component-scan base-package="houserenter"/>

	<bean id="dataSource" class="org.h2.jdbcx.JdbcDataSource">
		<property name="url" value="jdbc:h2:~/h2db/houserenter" />
		<property name="user" value="sa" />
		<property name="password" value="" />
	</bean>
</beans>

生成JdbcTemplate对象

我们完全可以使用类似上面生成DataSource对象的方法来生成JdbcTemplate对象:

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
	<constructor-arg ref="dataSource" />
</bean>

这里使用了构造器注入的方式,将上面创建的 dataSource 这个Bean注入到 jdbcTemplate 这个Bean,那是因为JdbcTemplate类有个 JdbcTemplate(DataSource dataSource) 构造方法,大家可以自行看看其源码。
现在,我们的 dispatcher.xml 文件变成这样了:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans.xsd 
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">
	
	<context:component-scan base-package="houserenter"/>

	<bean id="dataSource" class="org.h2.jdbcx.JdbcDataSource">
		<property name="url" value="jdbc:h2:~/h2db/houserenter" />
		<property name="user" value="sa" />
		<property name="password" value="" />
	</bean>
	
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<constructor-arg ref="dataSource" />
	</bean>
</beans>

将JdbcTemplate对象注入到其他组件

我们的租房网暂时只有HouseService这个组件需要访问数据库,因此只需将JdbcTemplate对象注入到这个组件的Bean中即可。
因为HouseService这个组件使用了@Service注解生成Bean,即开启了组件扫描,且上面已经配置了JdbcTemplate对象的生成,所以完全可以使用@Autowired来自动装配。
即我们可以在HouseService类中添加:

@Autowired
private JdbcTemplate jdbcTemplate;

使用JdbcTemplate对象访问数据库

JdbcTemplate类提供了众多方法供我们选择使用,很多方法都有很多重载版本,常用的有:
• execute,通常用来执行DDL(即建库、建表之类的SQL);
• update,通常用来执行update、insert、delete等写操作的SQL;
• query,通常用来查询返回多条记录的 select SQL语句;
• queryForObject,通常用来查询返回单条记录的 select SQL语句,返回结果可以是一个数值,可以是一个字符串,也可以是一个领域对象(比如我们的House类)。
更具体的使用大家可以参考官方文档(https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/data-access.html#jdbc),或者JdbcTemplate的源码或Javadoc。
下面还是直接上HouseService的代码吧:

package houserenter.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import houserenter.entity.House;
@Service
public class HouseService {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	@PostConstruct
	public void generateMockHouses() {
		jdbcTemplate.execute("drop table if exists house");
		jdbcTemplate.execute("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))");
		
		jdbcTemplate.update("insert into house values(?, ?, ?)", "1", "金科嘉苑3-2-1201", "详细信息");
		jdbcTemplate.update("insert into house values(?, ?, ?)", "2", "万科橙9-1-501", "详细信息");
	}
	
	public List<House> findHousesInterested(String userName) {
		// 这里查找该用户感兴趣的房源,省略,改为用模拟数据
		return jdbcTemplate.query(
				"select id,name,detail from house", 
				new RowMapper<House>() {
					@Override
					public House mapRow(ResultSet rs, int rowNum) throws SQLException {
						return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"));
					}
		});
	}
	public House findHouseById(String houseId) {
		return jdbcTemplate.queryForObject(
				"select id,name,detail from house where id = ?",
				new Object[]{houseId},
				new RowMapper<House>() {
					@Override
					public House mapRow(ResultSet rs, int rowNum) throws SQLException {
						return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"));
					}
				});
	}
	public void updateHouseById(House house) {
		jdbcTemplate.update(
				"update house set id=?, name=?, detail=? where id=?",
				house.getId(), house.getName(), house.getDetail(), house.getId());
	}
}

需要关注一下几点:
• 原来在HouseService构造函数中生成模拟房源数据的方案不再可行,因为现在生成模拟房源数据使用的是注入的jdbcTemplate对象,而Spring IoC在构造HouseService这个Bean时会调用构造函数,此时jdbcTemplate对象并不一定已经生成并注入到HouseService这个Bean(正在构造中),所以将生成模拟数据的代码移到一个带有注解@PostConstruct(此注解好像在JDK11中已经不属于Java的核心模块,需要使用Maven单独添加 javax.annotation-api 依赖,大家可以参考官方文档https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/core.html#beans-postconstruct-and-predestroy-annotations)的方法中,这样Spring IoC在装配完HouseService这个Bean后就会执行此方法生成模拟数据。
• JdbcTemplate在执行SQL语句时可以使用问号 ? 来代表需要传入的参数,这远比使用String来拼接SQL语句要好看且安全,而且还可以使用命名参数,暂不赘述。
• 在执行 select SQL语句时需要传入 RowMapper 接口的实现类的一个实例,它是一个泛型接口(以后再讨论Java泛型),主要就是用来将查询结果中每一行记录的各列的值转换为我们的领域对象的各个属性,上面代码中使用的是匿名类的方式(以后再讨论匿名类),很明显,两处使用功能匿名类的地方又是重复的,而且匿名类的功能还是比较通用的,就是将查询结果的行记录转换为房源这个领域对象,所以应该独立出来消除重复:

	private static final class HouseMapper implements RowMapper<House> {

		@Override
		public House mapRow(ResultSet rs, int rowNum) throws SQLException {
			return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"));
		}
		
	}

于是,上面调用JdbcTemplate的query()方法或queryForObject()方法传入 RowMapper 接口的实现类的一个实例时,可以直接使用:

new HouseMapper()

大家可以自行修改试试。
修改之后,我们可以进行验证,没有什么问题。

总结

通过采用Spring JDBC的改造后,HouseService的代码明显清爽了很多,并且我们只修改了这一个类,并不涉及控制器层面的代码,这就是分层给我们带来的好处!
• JdbcTemplate是线程安全的;
• JdbcTemplate依赖于一个DataSource对象;
• DataSource是JDBC规范中的一个接口,通常由数据库厂商提供的驱动来实现;
• JdbcTemplate的query()方法执行返回多记录的查询,queryForObject()执行返回单记录的查询;需要传入 RowMapper 接口的实现类的一个实例;
• JdbcTemplate的update()方法执行写操作的SQL语句;
• JdbcTemplate的execute()方法执行DDL;
• 使用Spring IoC来管理DataSource、JdbcTemplate的实例化和依赖注入很方便。
虽然我们消除了很多重复代码,也不必关心资源释放的问题,但是仍然存在以下问题:
• 每次访问都要建立数据库连接,性能低下;
• 与数据库设计耦合严重(SQL语句直接写在代码中);
• 访问数据库的代码没有独立出来(事实上,应该从Service层独立出来形成DAO层);
• 等等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值