SpringBoot工程中JDBC应用实践
目录
HikariCP应用实践
背景分析
目开发过程中应用程序与数据库交互时,“获得连接”或“释放连接”是非常消耗系统资源的两个过程,频繁地进行数据库连接的建立和关闭会极大影响系统的性能,若多线程并发量很大,这样耗时的数据库连接就可能让系统变得卡顿。因为TCP连接的创建开支十分昂贵,并且数据库所能承载的TCP并发连接数也有限制,针对这种场景,数据库连接池应运而生。如下图所示:
池化思想分析
池化思想是我们项目开发过程中的一种非常重要的思想,如整数池,字符串池,对象池、连接池、线程池等都是池化思想的一种应用,都是通过复用对象,以减少因创建和释放对象所带来的资源消耗,进而来提升系统性能。例如Integer对象的内部池应用,代码如下:
package com.cy.java.pool;
public class TestInteger01 {
public static void main(String[] args) {
Integer n1=100;//Integer.valueOf(100) 编译时优化
Integer n2=100;
Integer n3=200;
Integer n4=200;//池中没有则new Integer(200)
System.out.println(n1==n2);//true
System.out.println(n3==n4);//false
}
}
连接池原理分析
在系统初始化的时候,在内存中开辟一片空间,将一定数量的数据库连接作为对象存储在对象池里,并对外提供数据库连接的获取和归还方法。用户访问数据库时,并不是建立一个新的连接,而是从数据库连接池中取出一个已有的空闲连接对象;使用完毕归还后的连接也不会马上关闭,而是由数据库连接池统一管理回收,为下一次借用做好准备。如果由于高并发请求导致数据库连接池中的连接被借用完毕,其他线程就会等待,直到有连接被归还。整个过程中,连接并不会关闭,而是源源不断地循环使用,有借有还。数据库连接池还可以通过设置其参数来控制连接池中的初始连接数、连接的上下限数,以及每个连接的最大使用次数、最大空闲时间等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。
Java中的连接池
Java官方,为了在应用程序中更好的应用连接池技术,定义了一套数据源规范,例如javax.sql.DataSource接口,基于这个接口,很多团队或个人创建了不同的连接池对象。然后我们的应用程序中通过耦合与DataSource接口,便可以方便的切换不同厂商的连接池。Java项目中通过连接池获取连接的一个基本过程,如下图所示:
在上图中,用户通过DataSource对象的getConnection()方法,获取一个连接。假如池中有连接,则直接将连接返回给用户。假如池中没有连接,则会调用Dirver(驱动,由数据库厂商进行实现)对象的connect方法从数据库获取,拿到连接以后,可以将连接在池中放一份,然后将连接返回给调用方。连接需求方再次需要连接时,可以从池中获取,用完以后再还给池对象。
数据库连接池在Java数据库相关中间件产品群中,应该算是底层最基础的一类产品,作为企业应用开发必不可少的组件,无数天才们为我们贡献了一个又一个的优秀产品,它们有的随时代发展,功成身退,有的则还在不断迭代,老而弥坚,更有新生代产品,或性能无敌,或功能全面。目前市场上常见的连接池有DBCP、C3P0,DRUID,HikariCP等。
HikariCP入门实践
数据初始化
打开mysql控制台,然后按如下步骤执行sql文件。
第一步:登录mysql。
mysql –uroot –proot
第二步:设置控制台编码方式。
set names utf8;
第三步:执行db-cloud-admin.sql文件(切记不要打开文件复制到mysql客户端运行)。
source d:/db-cloud-admin.sql
其中db-cloud-admin.sql文件中的公告表设计如下:
drop table if exists sys_notices;
create table sys_notices (
id int(4) auto_increment comment 'ID',
title varchar(50) not null comment '标题',
type char(1) not null comment '类型(1通知 2公告)',
content varchar(500) default null comment '公告内容',
status char(1) default '0' comment '状态(0正常 1关闭)',
createdUser varchar(64) default '' comment '创建者',
createdTime datetime comment '创建时间',
modifiedUser varchar(64) default '' comment '更新者',
modifiedTime datetime comment '更新时间',
remark varchar(255) comment '备注',
primary key (id)
) engine=innodb auto_increment=1 comment = '通知公告表';
创建项目模块
基于IDEA创建项目Module,名字为02-jdbc,初始pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<groupId>com.cy</groupId>
<artifactId>02-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>02-jdbc</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加项目依赖
- mysql数据库驱动依赖。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
spring对象jdbc支持(此时会默认帮我们下载HiKariCP连接池)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
配置HikariCP连接池
打开application.properties配置文件,添加如下内容(必写)。
spring.datasource.url=jdbc:mysql:///db-system?serverTimezone=GMT%2B8&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
hikariCP 其它额外配置(可选),代码如下(具体配置不清晰的可自行百度):
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
创建项目启动类
package com.cy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JdbcApplication {//Application.class
public static void main(String[] args) {//Main Thread
SpringApplication.run(JdbcApplication .class, args);
}
}
HikariCP 连接池测试
第一步:在项目中添加单元测试类及测试方法,代码如下:
package com.cy.pj.common.datasource;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class DataSourceTests {
@Autowired
private DataSource dataSource;
@Test
public void testConnection() throws Exception{
System.out.println(dataSource.getConnection());
}
}
在当前测试类中我们需要:
掌握单元测试类、测试方法编写规范。
理解DataSource的设计规范及规范的实现。
分析在测试类中dataSource属性指向的对象是谁?
分析在测试类中DataSource的实现类对象由谁创建和管理?
思考基于DataSource接口获取连接的基本过程是怎样的?
第二步:API调用过程分析,如图所示:
测试BUG分析
- 类编译错误,DataSource为javax.sql包中的类型,如图所示:
- 连接错误:数据库连接不上,如图所示:
JDBC基本操作实践
业务分析
基于HikariCP,借助JDBC技术访问公告表中的数据。
业务原型设计
公告列表页面,如图所示:
代码设计及实现
基于JDBC技术操作表中数据,并进行单元测试。
第一步:定义单元测试类,关键代码如下:
package com.cy.pj.sys.dao;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.sql.*;
/**
* 通过此单元测试类获取数据源对象,并且通过数据对象获取数据库连接
* @SpringBootTest 注解描述的类
* 为springboot中的单元测试类
* 说明:
* 1)springboot中的单元测试类必须放在启动类所在包
* 或子包中
* 2)springboot中的单元测试类必须使用@SpringBootTest注解描述
*/
@SpringBootTest
public class JdbcTests {//is a Object
@Autowired
private DataSource dataSource;//HikariDataSource (类)
}
第二步:在单元测试类中添加向表中写入数据的方法,关键代码如下:
@Test
void testSaveNotice01()throws SQLException{
//1.建立连接 (负责与数据库进行通讯)
Connection conn= dataSource.getConnection();
//2.创建statement(sql传送器->负责与将sql发送到数据库端)
String sql="insert into sys_notices " +
" (title,content,type,status,createdTime,createdUser,modifiedTime,modifiedUser) " +
" values ('维护通知','2020-02-02 19:00 系统维护','1','0',now(),'tony',now(),'tony') ";
//这种方式Statement的创建,适合sql中不需要动态传入值的方式。
Statement stmt=conn.createStatement();
//3.发送sql
stmt.execute(sql);
//4.处理结果
//5.释放资源(后续释放资源要写到finally代码块中)
stmt.close();
conn.close();//将连接返回池中
}
第三步:在类中定义向表中写入数据的另一种方式,关键代码如下:
@Test
void testSaveNotice02()throws SQLException{
//1.建立连接 (负责与数据库进行通讯)
Connection conn= dataSource.getConnection();
//2.创建statement(sql传送器->负责与将sql发送到数据库端)
String sql="insert into sys_notices " +
" (title,content,type,status,createdTime,createdUser,modifiedTime,modifiedUser) " +
" values (?,?,?,?,?,?,?,?) ";//?表示占位符
PreparedStatement stmt=conn.prepareStatement(sql);//预编译方式创建Statement对象
//3.发送sql
//3.1为sql中的?号赋值
stmt.setString(1,"升级通知");
stmt.setString(2,"2021年3月18号 系统升级");
stmt.setString(3,"1");
stmt.setString(4,"0");
stmt.setTimestamp(5,new Timestamp(System.currentTimeMillis()));
stmt.setString(6,"jason");
stmt.setTimestamp(7,new Timestamp(System.currentTimeMillis()));
stmt.setString(8,"tony");
//3.2 发送sql
stmt.execute();
//4.处理结果
//5.释放资源(后续释放资源要写到finally代码块中)
stmt.close();
conn.close();//将连接返回池中
}
}
第四步,添加查询通知的单元测试方法,关键代码如下:
@Test
void testSelectNotices01()throws SQLException{
//1.建立连接
Connection conn=dataSource.getConnection();
//2.创建Statement
String sql="select id,title,content,status,type,createdTime from sys_notices where id>=?";
PreparedStatement pstmt=conn.prepareStatement(sql);
//3.发送sql(发送到数据库)
pstmt.setInt(1,2);
boolean flag=pstmt.execute();
//4.处理结果
ResultSet rs=null;
if(flag){//true表示查询,有结果集
//获取结果集(二维表结构)
rs=pstmt.getResultSet();
List<Map<String,Object>> list=new ArrayList<>();
while(rs.next()){//一行记录应为一个map对象 (行映射)
//构建map,用于存储当前行记录
Map<String,Object> map=new HashMap();
//将取出类的数据存储到map (key为字段名,值为字段value)
map.put("id",rs.getInt("id"));
map.put("title",rs.getString("title"));
map.put("content",rs.getString("content"));
map.put("status",rs.getString("status"));
map.put("type",rs.getString("type"));
map.put("createdTime",rs.getTimestamp("createdTime"));
//...
//将每行记录对应的map对象存储到list集合
System.out.println(map);
list.add(map);
}
}
//5.释放资源
rs.close();
pstmt.close();
conn.close();
}
第五步,通过元数据让查询映射更加灵活,关键代码如下:
@Test
void testSelectNotices02()throws SQLException{
//1.建立连接
Connection conn=dataSource.getConnection();
//2.创建Statement
String sql="select id,title,content,status,type,createdTime from sys_notices where id>=?";
PreparedStatement pstmt=conn.prepareStatement(sql);
//3.发送sql(发送到数据库)
pstmt.setInt(1,2);
boolean flag=pstmt.execute();
//4.处理结果
ResultSet rs=null;
if(flag){//true表示查询,有结果集
//获取结果集(二維表結構)
rs=pstmt.getResultSet();
List<Map<String,Object>> list=new ArrayList<>();
//获取结果集中的元shuju (表名,字段名)
ResultSetMetaData rsmd=rs.getMetaData();
while(rs.next()){//一行记录应为一个map对象 (行映射)
Map<String,Object> map=new HashMap();
//将取出类的数据存储到map (key为字段名,值为字段value)
for(int i=1;i<=rsmd.getColumnCount();i++){
//getColumnCount();獲取列的數量
map.put(rsmd.getColumnLabel(i),rs.getObject(rsmd.getColumnLabel(i)));
//rsmd.getColumnLabel(i) 获取第i列的名字
}
//将每行记录对应的map对象存储到list集合
System.out.println(map);
list.add(map);
}
}
//5.释放资源
rs.close();
pstmt.close();
conn.close();
}
JdbcTemplate操作实践(拓展)
Dao逻辑设计及实现
定义一个数据访问对象,由Spring为其注入JdbcTemplate对象,并基于此对象实现数据访问,例如:
package com.cy.dao;
@Repository
public class JdbcTemplateDaoImpl {
@Autowired
private JdbcTemplate jdbcTemplate;//spring
public Map<String,Object> selectById(Long id){
String sql="select * from sys_notices where id=?";
return jdbcTemplate.queryForMap(sql,id);
}
}
单元测试逻辑设计及实现
package com.cy;
import java.util.Map;
@SpringBootTest
public class JdbcTemplateDaoTests {
@Autowired
private JdbcTemplateDaoImpl jdbcTemplateDao;
@Test
void testSelectById(){
Map<String,Object> map=jdbcTemplateDao.selectById(40L);
System.out.println(map);
}
}