Spring 的 JDBC Templet 是 Spring 对 JDBC 使用的一个基本的封装。他主要是帮助程序员实现了数据库连接的管理,其余的使用方式和直接使用 JDBC 没有什么大的区别。
JDBC 的使用大家都比较熟悉了。这里主要为了演示在 SpringBoot 中使用 Spring JDBC Templet 的步骤,所以我们就设计一个简单的需求。一个用户对象的 CURD 的操作。对象有两个属性,一个属性是id,一个属性是名称。存储在 MySQL 的 auth_user 表里面。
在 Intellij IDEA 里面新建一个空的 SpringBoot 项目。具体步骤参考 SpringBoot 的第一次邂逅 。根据本样例的需求,我们要添加下面三个依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.6</version> </dependency>
因为要发布 Http Rest 的服务,所以添加 spring-boot-starter-web 依赖,这里我们要使用 JDBC Tempet 方法来访问数据库,所以添加了 spring-boot-starter-jdbc 依赖,要访问 MySQL 数据库,所以添加了 MySQL 最新版本的 JDBC 驱动程序。
假定在 Linux 操作系统上已经安装了 MySQL 5.7。以下操作都是在操作系统的命令行中,通过 root 用户登录到 MySQL 的命令行客户端中执行的。
create database springboot_jdbc; create table auth_user (uuid bigint not null,name varchar(32), primary key (uuid)) default charset=utf8mb4;
grant all privileges on springboot_jdbc.* to 'springboot'@'%' identified by 'springboot'; flush privileges;
SpringBoot 的数据源是自动配置的。在 SpringBoot 2.0 中,有几种数据源配置可选,他们按照 HikariCP -> Tomcat pooling -> Commons DBCP2 优先顺序来选择最后实际使用哪个数据源。
在项目加入 spring-boot-starter-jdbc 依赖的时候,就已经包括了 HikariCP 数据源的依赖,所以这里自动配置 HikariCP 连接池数据源。
在 appplications.properties 中增加如下的配置
#通用数据源配置 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://10.110.2.5:3306/spring-boot-jdbc?charset=utf8mb4&useSSL=false spring.datasource.username=springboot spring.datasource.password=springboot # Hikari 数据源专用配置 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5
其中 Hikari 数据源的大部分配置如下图。每个配置代表的含义可以自行查询一下
根据需求,对应的用户数据实体有两个属性,一个是 id ,一个是 name 。这是一个纯 POJO 对象。
package com.yanggaochao.springboot.learn.springbootjdbclearn.domain.dao; /** * 用户实体对象 * * @author 杨高超 * @since 2018-03-09 */ public class UserDO { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
通常在 Http Rest 接口中,我们不仅想直接返回业务对象的内容,还要返回一些通用的信息,例如接口调用的结果,调用失败的时候返回的自定义文本消息等。那么我们就需要建立两个通用的 rest 返回对象,除了返回通用的接口调用结果和文本消息,一个包括一个单独的业务内容,一个包含一个持有多个业务内容的集合。具体定义如下
package com.yanggaochao.springboot.learn.springbootjdbclearn.domain.bo; /** * 单个对象返回结果 * * @author 杨高超 * @since 2018-03-09 */ public class RestItemResult<T> { private String result; private String message; private T item; public String getResult() { return result; } public void setResult(String result) { this.result = result; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
package com.yanggaochao.springboot.learn.springbootjdbclearn.domain.bo; import java.util.Collection; /** * 集合对象返回结果 * * @author 杨高超 * @since 2018-03-09 */ public class RestCollectionResult<T> { private String result; private String message; private Collection<T> items; public String getResult() { return result; } public void setResult(String result) { this.result = result; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Collection<T> getItems() { return items; } public void setItems(Collection<T> items) { this.items = items; } }
package com.yanggaochao.springboot.learn.springbootjdbclearn.dao; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.dao.UserDO; import java.util.List; /** * 用户数据层接口 * * @author 杨高超 * @since 2018-03-09 */ public interface UserDao { /** * 向数据库中保存一个新用户 * * @param user 要保存的用户对象 * @return 是否增肌成功 */ Boolean add(UserDO user); /** * 更新数据库中的一个用户 * * @param user 要更新的用户对象 * @return 是否更新成功 */ Boolean update(UserDO user); /** * 删除一个指定的用户 * * @param id 要删除的用户的标识 * @return 是否删除成功 */ boolean delete(Long id); /** * 精确查询一个指定的用户 * * @param id 要查询的用户的标识 * @return 如果能够查询到,返回用户信息,否则返回 null */ UserDO locate(Long id); /** * 通过名称模糊查询用户 * * @param name 要模糊查询的名称 * @return 查询到的用户列表 */ List<UserDO> matchName(String name); }
用户数据持久层实现
package com.yanggaochao.springboot.learn.springbootjdbclearn.dao.impl; import com.yanggaochao.springboot.learn.springbootjdbclearn.dao.UserDao; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.dao.UserDO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.stereotype.Repository; import java.util.ArrayList; import java.util.List; /** * 用户对象数据库访问实现类 * * @author 杨高超 * @since 2018-03-09 */ @Repository public class UserDaoJDBCTempletImpl implements UserDao { private final JdbcTemplate jdbcTemplate; @Autowired public UserDaoJDBCTempletImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public Boolean add(UserDO user) { String sql = "INSERT INTO AUTH_USER(UUID,NAME) VALUES(?,?)"; return jdbcTemplate.update(sql, user.getId(), user.getName()) > 0; } @Override public Boolean update(UserDO user) { String sql = "UPDATE AUTH_USER SET NAME = ? WHERE UUID = ?"; return jdbcTemplate.update(sql, user.getName(), user.getId()) > 0; } @Override public boolean delete(Long id) { String sql = "DELETE FROM AUTH_USER WHERE UUID = ?"; return jdbcTemplate.update(sql, id) > 0; } @Override public UserDO locate(Long id) { String sql = "SELECT * FROM AUTH_USER WHERE UUID=?"; SqlRowSet rs = jdbcTemplate.queryForRowSet(sql, id); if (rs.next()) { return generateEntity(rs); } return null; } @Override public List<UserDO> matchName(String name) { String sql = "SELECT * FROM AUTH_USER WHERE NAME LIKE ?"; SqlRowSet rs = jdbcTemplate.queryForRowSet(sql, "%" + name + "%"); List<UserDO> users = new ArrayList<>(); while (rs.next()) { users.add(generateEntity(rs)); } return users; } private UserDO generateEntity(SqlRowSet rs) { UserDO weChatPay = new UserDO(); weChatPay.setId(rs.getLong("UUID")); weChatPay.setName(rs.getString("NAME")); return weChatPay; } }
这里首先用一个注解 @Repository 表示这是一个数据持久层的类,SpringBoot 将自动将这个类实例化。然后在构造函数上增加一个 @Autowired ,SpringBoot 在实例化这个类的时候,会自动将 JDBCTemplet 实例注入到这个类里面。这里 JDBCTemplet 实例是 SpringBoot 根据 applications.properties 中数据源相关的配置自动配置出来的。按照 SpringBoot 自动配置数据源的算法,这里将会配置的数据源是 HikariCP。
剩下的则和普通的 Spring JDBCTemplet 开发一样,通过程序员手动在对象和数据库 SQL 之间进行转换,实现了用户的增加、修改、删除、模糊匹配、精确查询等功能。
package com.yanggaochao.springboot.learn.springbootjdbclearn.service; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.dao.UserDO; import java.util.List; /** * 用户服务层接口 * * @author 杨高超 * @since 2018-03-09 */ public interface UserService { UserDO add(UserDO user); UserDO update(UserDO user); boolean delete(Long id); UserDO locate(Long id); List<UserDO> matchName(String name); }
package com.yanggaochao.springboot.learn.springbootjdbclearn.service.impl; import com.yanggaochao.springboot.learn.springbootjdbclearn.dao.UserDao; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.dao.UserDO; import com.yanggaochao.springboot.learn.springbootjdbclearn.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; /** * 用户业务层实现类 * * @author 杨高超 * @since 2018-03-09 */ @Service public class UserServiceImpl implements UserService { private final UserDao userDao; @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } @Override public UserDO add(UserDO user) { user.setId(new Date().getTime()); if (userDao.add(user)) { return user; } return null; } @Override public UserDO update(UserDO user) { if (userDao.update(user)) { return locate(user.getId()); } return null; } @Override public boolean delete(Long id) { return userDao.delete(id); } @Override public UserDO locate(Long id) { return userDao.locate(id); } @Override public List<UserDO> matchName(String name) { return userDao.matchName(name); } }
这里通过一个 @Service 注解声明这个实现类是一个业务层的类。持久层的 UserDao 通过 @Autowired 让 SpringBoot 实例化这个业务层类的时候,自动将对应的持久层类注入到这个业务类中。
这里在增加用户对象的时候,给用户设定标识的时候,简单的用了一个当前时间的毫秒数作为标识。实际开发的过程中,这个地方需要用一个保证全局唯一的机制来保证这个标识不能重复。
package com.yanggaochao.springboot.learn.springbootjdbclearn.web; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.bo.RestCollectionResult; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.bo.RestItemResult; import com.yanggaochao.springboot.learn.springbootjdbclearn.domain.dao.UserDO; import com.yanggaochao.springboot.learn.springbootjdbclearn.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 用户 Http Rest 接口 * * @author 杨高超 * @since 2018-03-09 */ @RestController @RequestMapping("api/v1/user") public class UserApi { @Autowired private UserService userService; @RequestMapping(value = "/add", method = RequestMethod.POST) public RestItemResult<UserDO> add(@RequestBody UserDO user) { RestItemResult<UserDO> result = new RestItemResult<>(); user = userService.add(user); if (user != null) { result.setItem(user); result.setResult("success"); } else { result.setMessage("新增用户失败"); result.setResult("failure"); } return result; } @RequestMapping(value = "/update", method = RequestMethod.POST) public RestItemResult<UserDO> update(@RequestBody UserDO user) { RestItemResult<UserDO> result = new RestItemResult<>(); user = userService.update(user); if (user != null) { result.setItem(user); result.setResult("success"); } else { result.setMessage("修改用户失败"); result.setResult("failure"); } return result; } @RequestMapping(value = "/delete/{uuid}", method = RequestMethod.GET) public RestItemResult<UserDO> delete(@PathVariable Long uuid) { RestItemResult<UserDO> result = new RestItemResult<>(); if (userService.delete(uuid)) { result.setResult("success"); } else { result.setMessage("删除用户失败"); result.setResult("failure"); } return result; } @RequestMapping(value = "/locate/{uuid}", method = RequestMethod.GET) public RestItemResult<UserDO> locate(@PathVariable Long uuid) { RestItemResult<UserDO> result = new RestItemResult<>(); UserDO user = userService.locate(uuid); if (user != null) { result.setItem(user); result.setResult("success"); } else { result.setMessage("查询用户失败"); result.setResult("failure"); } return result; } @RequestMapping(value = "/match/{name}", method = RequestMethod.GET) public RestCollectionResult<UserDO> match(@PathVariable String name) { RestCollectionResult<UserDO> result = new RestCollectionResult<>(); List<UserDO> users = userService.matchName(name); result.setItems(users); result.setResult("success"); return result; } }
这里 @RestController 用来声明这是一个 Http Rest 接口类。通过类上的 @RequestMapping 和方法上的 @RequestMapping组合形成每个接口的调用路由。方法上的 @RequestMapping 中的 method 属性声明了 http 调用的方法。 @RequestBody 注解自动将 post 数据中的 json 对象转成 POJO 对象。@PathVariable 将 http url 路径中的数据自动转换成为服务方法的参数。
测试通过 Apache commons的 HttpClient 来调用 Http Rest 服务。
package com.yanggaochao.springboot.learn.springbootjdbclearn; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.params.HttpMethodParams; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.Map; /** * @author 杨高超 * @since 2018-03-09 */ public class HttpClientHelper { /** * 用 get 方法发起一个http请求 * * @param url 要访问的 http 的 url * @return 访问 http 后得到的回应文本 */ public String httpGetRequest(String url, Map<String, String> headers) { try { HttpClient httpclient = new HttpClient(); GetMethod method = new GetMethod(url); method.setRequestHeader("Content-Type", "application/json; charset=utf-8"); method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false)); if (headers != null) { for (String key : headers.keySet()) { method.setRequestHeader(key, headers.get(key)); } } int statusCode = httpclient.executeMethod(method); if (statusCode == 200) { return parseInputStream(method.getResponseBodyAsStream()); } else { System.out.println(url + " status = " + statusCode); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 用 post 方法发起一个 http 请求 * * @param url 要访问的 http 的 url * @param data post 请求中的 data 数据 * @return 访问 http 后得到的回应文本 */ public String httpPostRequest(String url, String data, Map<String, String> headers) { try { HttpClient httpclient = new HttpClient(); PostMethod method = new PostMethod(url); method.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); method.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36"); if (headers != null) { for (String key : headers.keySet()) { method.setRequestHeader(key, headers.get(key)); } } method.setRequestEntity(new StringRequestEntity(data, "json", "utf-8")); int statusCode = httpclient.executeMethod(method); if (statusCode == 200) { return parseInputStream(method.getResponseBodyAsStream()); } else { System.out.println(url + " status = " + statusCode + parseInputStream(method.getResponseBodyAsStream())); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 从 java.io.Reader 中解析文本数据 * * @param rd java.io.Reader 对象 * @throws Exception 发生错误时抛出异常 */ private String parseReader(Reader rd) throws Exception { BufferedReader brd = new BufferedReader(rd); String line; StringBuilder respongseContext = new StringBuilder(); while ((line = brd.readLine()) != null) { respongseContext.append(line).append("/n"); } //rd.close(); if (respongseContext.length() > 0) { respongseContext.deleteCharAt(respongseContext.length() - 1); } return respongseContext.toString(); } /** * 从输入流中解析文本数据 * * @param is 输入流 * @throws Exception 发生错误时抛出异常 */ private String parseInputStream(InputStream is) throws Exception { return parseReader(new BufferedReader(new InputStreamReader(is))); } }
这里主要是实现了用 GET 和 POST 方法调用 Http Rest 服务的方法。
采用 JUnit 来执行测试用例。为了实现测试,我们额外增加了下面的 maven 依赖
<dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>3.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.codehaus.jettison</groupId> <artifactId>jettison</artifactId> <version>1.3.3</version> <scope>test</scope> </dependency>
package com.yanggaochao.springboot.learn.springbootjdbclearn; import org.codehaus.jettison.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; /** * Description: * * @author 杨高超 * @since 2018-03-09 */ public class UserApiTest { private String userAddUrl = "http://localhost:3030/security/api/v1/user/add"; private String userLocateUrl = "http://localhost:3030/security/api/v1/user/locate/"; private String userDeleteUrl = "http://localhost:3030/security/api/v1/user/delete/"; private String userUpdateUrl = "http://localhost:3030/security/api/v1/user/update"; private String userMatchUrl = "http://localhost:3030/security/api/v1/user/match/"; JSONObject addUser = new JSONObject(); Long addUserId = null; List<Long> userIds = new ArrayList<>(); @Before public void before() throws Exception { addUser.put("name", "美羊羊"); JSONObject addResultJson = new JSONObject(new HttpClientHelper().httpPostRequest(userAddUrl, addUser.toString(), null)); assert ("success".equals(addResultJson.getString("result"))); addUserId = addResultJson.getJSONObject("item").getLong("id"); JSONObject user = new JSONObject(); user.put("name", "喜羊羊"); addResultJson = new JSONObject(new HttpClientHelper().httpPostRequest(userAddUrl, user.toString(), null)); assert ("success".equals(addResultJson.getString("result"))); userIds.add(addResultJson.getJSONObject("item").getLong("id")); user.put("name", "灰太狼"); addResultJson = new JSONObject(new HttpClientHelper().httpPostRequest(userAddUrl, user.toString(), null)); assert ("success".equals(addResultJson.getString("result"))); userIds.add(addResultJson.getJSONObject("item").getLong("id")); } @Test public void testUpdateUser() throws Exception { JSONObject user = new JSONObject(); user.put("name", "霉羊羊"); user.put("id", addUserId); new HttpClientHelper().httpPostRequest(userUpdateUrl, user.toString(), null); JSONObject locateResultJson = new JSONObject(new HttpClientHelper().httpGetRequest(userLocateUrl + addUserId, null)); assert (user.getString("name").equals(locateResultJson.getJSONObject("item").getString("name"))); } @Test public void testMatchUser() throws Exception { JSONObject matchResultJson = new JSONObject(new HttpClientHelper().httpGetRequest(userMatchUrl + URLEncoder.encode("羊","UTF-8"), null)); assert (matchResultJson.has("items") && matchResultJson.getJSONArray("items").length() == 2); matchResultJson = new JSONObject(new HttpClientHelper().httpGetRequest(userMatchUrl + URLEncoder.encode("狼","UTF-8"), null)); assert (matchResultJson.has("items") && matchResultJson.getJSONArray("items").length() == 1); } @After public void after() throws Exception { if (addUserId != null) { JSONObject deleteResultJson = new JSONObject(new HttpClientHelper().httpGetRequest(userDeleteUrl + addUserId, null)); assert ("success".equals(deleteResultJson.getString("result"))); } for (Long userId : userIds) { JSONObject deleteResultJson = new JSONObject(new HttpClientHelper().httpGetRequest(userDeleteUrl + userId, null)); assert ("success".equals(deleteResultJson.getString("result"))); } } }
这里在 @Test 声明了两个测试用例,一个测试了用户修改功能,一个测试了用户模糊查询功能。 @Before 声明了在执行每个测试用例之前要做的准备工作。这里首先往数据库中插入三条数据,同时也测试了数据的增加功能、精确查询的功能。@After 声明了执行每个测试用例后的清理工作。这里主要是将之前插入的数据给删除了。这里同步测试了用户删除的功能。