
上面这张图,是服务器模块的具体模块划分
在上一篇文章中,我们已经实现了内存管理模块了,今天这一篇文章,我们 来实现硬盘上面的数据库管理这一部分内容:

管理上述核心概念,需要两个部分:
- 一个是在内存上进行管理
- 一个是在硬盘上进行管理
我们来看硬盘这一块的管理:
硬盘上面的数据的保存一共分为两个部分:
第一个部分是数据库(主要保存的数据是交换机,队列,绑定)
第二个部分是文件(主要保存的数据是消息)
我们先来看数据库中如何去保存好交换机,队列,绑定这些核心的概念
为什么使用MySQL数据库不合适,因为MySQL数据库本身就比较重量
所以此处我们为了使用更加方便,我们就采取一个更加轻量的数据库:SQLite
这个也是一个关系型数据库,和MySQL非常相似,也支持同样的SQL语句的增删改查
但是这个数据库的特点就是:轻量
这一个完整的SQLite数据库只有一个单独的可执行文件(内存大小不到1MB)
同时MySQL是客户端服务器结构的程序
而SQLite只是一个本地的数据库,不涉及网络,相当于是直接操作本地的硬盘文件
大家放心好了,这个SQLite不是什么小玩意,它的名气非常大,咖位也是很大的,应用非常广泛
在一些性能不高的设备上面,使用的数据库首先选择的就是这个SQLite
尤其是移动端和嵌入式设备(空调,冰箱,洗衣机)
Android系统就是内置了SQLite
配置文件
在JAVA 中想要使用SQLite,直接使用Maven,把SQLite的依赖都给引入进来,就可以了
引入依赖之后,会自动加载jar包和动态库文件
所以我们就在官网上面下载它的依赖,然后将依赖导入到pom.xml文件,点击刷新即可:
java
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.0.0</version>
</dependency>
我们接下来去yml配置文件中去设置数据源即可:
java
url: jdbc:sqlite:./data/meta.db
注意我们的SQLite数据库在组织数据的时候,是把数据存储在当前硬盘的某个指定的文件中:
谈到相对路径,要明确"基准路径","工作路径"
如果是在IDEA中直接运行程序,此时的工作路径就是当前项目所在的路径,在我们这个当前项目下,会出现一个文件夹目录(data),在这个data目录下,会出现一个meta.db的文件
如果是通过java -jar方式运行程序,此时你在哪个目录下执行的命令,哪个目录就是工作路径
java
username:
password:
对于MySQL来说,之所以要设置用户名和密码,是因为MySQL是一个客户端服务器程序,一个数据库服务器对应着多个客户端的访问,每个客户端的权限不同,所以要设置用户名和密码去将多个客户端区别开
而我们的SQLite则不是客户端服务器程序,是把数据存储在了文件上面,既然是一个本地文件,和网络无关,就只有本地主机才能访问,只有自己一个人才可以访问
所以SQLite是不需要用户名和密码的
下面这个配置的含义是:我们使用哪个类作为我们数据库驱动的类?(org.sqlite.JDBC)(固定写法)
java
driver-class-name: org.sqlite.JDBC
总结,我们的配置文件中的SQLite的大致写法如下所示:
java
spring:
datasource:
url: jdbc:sqlite:./data/meta.db
username:
password:
driver-class-name: org.sqlite.JDBC
SQLite和MySQL一样,都可以通过Mybatis这样 的框架来使用,后续我们操作数据库,都是基于Mybatis这样的框架来操作
在resources文件夹下面,建立一个新的目录mapper,将Myabtis的各种XML文件都放置其中:
下面我们继续去写Mybatis的配置文件:
java
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
最后总结我们的配置文件中的内容如下所示:
java
spring:
datasource:
url: jdbc:sqlite:./data/meta.db
username:
password:
driver-class-name: org.sqlite.JDBC
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
数据库建表
数据库管理里面一共管理三个核心概念:
- 交换机存储
- 队列存储
- 绑定存储
我们需要去建库建表
我们目前是使用SQLite,就不需要去建数据库了,因为我们的数据库文件(meda.db)就是数据库
当我们把刚刚的配置文件和依赖都导入了之后,程序自动之后,就会自动建数据库了
创建表:
下面我们开始建表:
我们就围绕着刚刚的三个核心概念来设计数据库表
- 交换机
- 队列
- 绑定
需要设计三张表,
但是这个建表的操作,具体什么时候来开始执行呢?
以前我们写的程序,比如博客系统之类的,都是需要先把数据库的表都先给创建好,然后再启动服务器,
之前是先把建库建表的SQL语句都写到一个.sql文件中去
需要建表的时候,直接复制这个sql文件到MySQL的客户端中去执行就可以了
以前这个建表的操作是在 部署阶段完成的
之前大概部署一次就可以了,不需要反复操作,但是后续接触到的很多程序可能会涉及到多次反复部署,所以能够简化部署的步骤也很重要,能够自动完成的尽量都自动完成
接下来我们尝试去简化一下这个部署的过程:
Mybatis的基本使用流程
- 创建一个interface,描述有哪些方法要给JAVA代码使用
- 创建对应的XML,通过XML来实现上述interface中的抽象方法
Mybatis的本质就是通过XML来描述具体要做的工作,然后根据我们写出来的XML文件自动生成一些java代码,把要完成的方法写好就可以了
下面是第一步:
创建一个叫做MetaMapper的接口,描述有哪些方法要给JAVA代码使用

如下所示:
java
package org.example.mq.mqserver.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MetaMapper {
//提供三个核心的建表方法:
void createExchangeTable(); //创建交换机的表
void createQueueTable(); //创建队列的表
void createBindingTable(); //创建绑定的表
}

第二步:
在resources中的mapper目录下面,创建对应的MetaMapper.xml文件,通过XML来实现上述接口中的抽象方法
在交换机这个类里面有一个属性比较特殊:
java
//为了存到数据库,要把map转换为json格式的字符串
private Map<String,Object> arguments = new HashMap<>();
arguments这个属性本身是一个键值对的格式,而在数据库中都是不支持哈希表,键值对这样的格式的,所以我们需要把这个键值对格式的arguments进行序列化,借助于json,把这个键值对格式转化为json格式的字符串,然后存储到数据库中去:
所以,我们可以直接根据交换机的类中的属性去创建交换机这个表:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mq.mqserver.mapper.MetaMapper">
<update id="createExchangeTable">
create table if not exists exchange(
name varchar(50) primary key,
type int,
durable boolean,
autoDelete boolean,
arguments varchar(1024),
);
</update>
</mapper>
下面是根据MSGQueue这个类创建queue这个表:
xml
<update id="createQueueTable">
create table if not exists queue(
name varchar(50) primary key,
durable boolean,
exclusive boolean,
autoDelete boolean,
arguments varchar(1024),
);
</update>
下面是根据Binding这个类来创建binding表:
xml
<update id="createBindingTable">
create table if not exists binding(
exchangeName varchar(50),
queueName varchar(50),
bindingKey varchar(50)
);
</update>
下面来实现arguments的转换
转换的思路如下所示:
后续我们针对Exchange这个对象在数据库中插入数据的时候,Mybatis会自动调用当前这个Exchange的类中的Getter和Setter方法,拿到属性,进一步把属性填写到我们的数据库记录中
如何去完成属性的获取和设置呢?
如何去实现把arguments这个键值对和数据库中的字符串类型相互转换呢,
关键要点在于:Mybatis在完成数据库操作的时候,会自动地调用到对象的getter和setter方法
1: Myabtis在往数据库中写入数据的时候,会调用getter方法,拿到属性的值,再往数据库中写入数据
如果在这个过程中,让getArguments方法(arguments属性的getter方法)得到的结果是String类型的结果,那么此时就可以直接把这个String类型的结果作为数据写入到数据库中了
如果Getter方法得到的结果是键值对的结果,那么是不可以写入到数据库中的
2: Mybatis数据库从数据库中读取数据的时候,就会调用对象的setter方法,把数据库中读到的结果设置到对象的属性中,如果这个过程中,让setArguments方法内部针对数据库拿过来的字符串进行解析,解析成一个Map对象
重新编写Getter和Setter方法
第一个Getter方法,把键值对格式转换为String(JSON格式):
java
public String getArguments() throws JsonProcessingException {
//是把我们当前的arguments参数从Map转换为了String(JSON)
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(arguments);
}
第二个Setter方法:把数据库返回的一个字符串的参数,解析为一个键值对的格式,然后通过键值对的格式设置arguments的属性:
java
//这个方法是从数据库中读数据之后,构造Exchange对象,会自动调用到:
public void setArguments(String argumentsJson){
//把参数中的argumentsJson按照JSON格式解析,转成Map对象:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.readValue(argumentsJson, new TypeReference<HashMap<String, Object>>() {});
}
其中的代码:
java
objectMapper.readValue(argumentsJson, new TypeReference<HashMap<String, Object>>() {});
第二个参数,用来描述当前JSON字符串,要转成的JAVA对象是啥类型的
如果是一个简单类型的,那就直接使用对应类型的类对象即可
如果是集合这样的复杂类型的,可以使用TypeReference匿名内部类对象,来描述复杂类型的具体信息(通过泛型参数来描述)
然后把这个转换之后的结果交给this.arguments即可,最后抛出一个异常即可:
java
//这个方法是从数据库中读数据之后,构造Exchange对象,会自动调用到:
public void setArguments(String argumentsJson) throws JsonProcessingException {
//把参数中的argumentsJson按照JSON格式解析,转成Map对象:
ObjectMapper objectMapper = new ObjectMapper();
this.arguments = objectMapper.readValue(argumentsJson, new TypeReference<HashMap<String, Object>>() {});
}
刚刚我们在Exchange类中的将arguments给转换完毕了,接下来我们就去将MSGQueue类中的arguments也给转换完毕:
下面是arguemnts的Getter方法:
java
public String getArguments() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(arguments);
}
public void setArguments(String argumentsJson) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
this.arguments = objectMapper.readValue(argumentsJson, new TypeReference<Map<String, Object>>() {});
}
实现插入删除:
三张核心功能的表创建完毕之后,我们就开始针对这三张表实现插入和删除的操作了:
也就是在MetaMapper这个接口里面编写分别编写交换机,队列,绑定的插入和删除的抽象方法:
java
//针对上述的三个基本概念,进行插入,删除,修改:
//插入和删除交换机:
void insertExchange(Exchange exchange);
void deleteExchange(String exchangeName);
//插入 和删除队列:
void insertQueue(MSGQueue msgQueue);
void deleteQueue(String queueName);
//插入和删除绑定:
void insertBinding(Binding binding);
void deleteBinding(Binding binding);
对于交换机和队列这两个表,由于使用name作为主键,直接按照name进行删除即可:
但是在deleteBinding方法中参数之所以设置为Binding对象是因为:对于绑定来说,是没有主见的,删除操作其实是针对exchangeName和queueName两个维度进行筛选的,所以Binding的删除方法就以Binding对象作为参数了
接下来去MetaMapper.xml文件中去实现交换机的插入和删除:也就是编写insertExchange和deleteExchange方法的实现:
xml
<insert id="insertExchange" parameterType="org.example.mq.mqserver.core.Exchange">
insert into exchange
values (#{name}, #{type}, #{durable}, #{autoDelete}, #{arguments});
</insert>
<delete id="deleteExchange" parameterType="java.lang.String">
delete from exchange
where name = #{exchangeName};
</delete>
然后下面是去实现队列的插入和删除,也就是实现insertQueue和deleteQueue这两个方法:
xml
<insert id="insertQueue" parameterType="org.example.mq.mqserver.core.MSGQueue">
insert into queue
values (#{name}, #{durable}, #{exclusive}, #{autoDelete}, #{arguments});</insert>
<delete id="deleteQueue" parameterType="java.lang.String">
delete from queue
where name = #{queueName};
</delete>
注意Mybatis在看到#{arguments}这个参数之后,就会按照之前我们写好的getArguments方法来获取到这个参数的内容,此处数据库中期望的类型是String,此处也就需要让getArguments方法能够得到String
下面去实现绑定的插入和删除:也就是实现insertBinding和deleteBinding这两个方法:
xml
<insert id="insertBinding" parameterType="org.example.mq.mqserver.core.Binding">
insert into binding
values (#{exchangeName}, #{queueName}, #{bindingKey});
</insert>
<delete id="deleteBinding" parameterType="org.example.mq.mqserver.core.Binding">
delete from binding
where exchangeName = #{exchangeName} and queueName = #{queueName};
</delete>
目前为止,数据库的大致操作已经编写完毕了,我们下面写一个类来整合上述的数据库操作:
这个类叫做DataBaseManager:
我们先去mqserver下创建一个datacenter的包:

然后再这个包下面创建一个DataBaseManager的类:
java
package org.example.mq.mqserver.datacenter;
/*
通过这个DataBaseManager类来整合上述的操作:
*/
public class DataBaseManager {
}
在这个类中编写初始化方法:
java
//针对数据库进行初始化:
public void init(){
}
这个初始化方法不是构造方法,而是当拎出来的一个普通方法
构造方法一般是初始化类的属性,不是涉及到太多业务逻辑
我们现在写的init()方法带有业务逻辑,于是就单独拿出来,手动调用这个初始化方法更好一些
那么这个init()方法,也就是这个初始化方法是要干些什么呢?
数据库的初始化 = 建库建表 + 插入默认数据
如下图所示:
上面是初始化方法的逻辑,下面开始写代码:
我们先来写一个判断数据库是否存在的方法,这个方法的逻辑也就是判断meta.db文件是否存在:
java
//判读数据库是否存在:就是判断meta.db文件是否存在
private boolean checkDBExists(){
File file = new File("./data/meta.db");
if(file.exists()){
return true;
}
return false;
}
接着我们编写建库建表的方法:
这个方法中我们是不需要手动创建数据库的,因为Mybatis会自动创建出meta.db文件
所以这个方法中只需要去实现创建表的操作即可:
java
//创建表也就是创建交换机,队列,绑定这三张表:
//创建数据库和表:
private void createTable(){
medaMapper.createExchangeTable();
medaMapper.createQueueTable();
medaMapper.createBindingTable();
System.out.println("[DataBaseManager] 创建表完成!");
}
接着我们去实现一个添加默认数据的方法:
此处其实也就是添加一个默认 的交换机:
因为在RabbitMQ中有一个这样的设定:带有一个匿名的交换机,类型是DIRECT:
java
//创建一个添加默认数据的方法:
private void createDefaultData(){
//构造一个默认的交换机:
Exchange exchange = new Exchange();
exchange.setName("");
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
medaMapper.insertExchange(exchange);
System.out.println("[DataBaseManager] 创建初始化数据完成!");
}
接下来,我们去编写初始化方法:
java
//针对数据库进行初始化:
public void init(){
if(!checkDBExists()){
//如果数据库存在,就开始建表操作:
//创建数据表:
createTable();
//然后插入默认数据:
createDefaultData();
System.out.println("[DataBaseManager] 数据库初始化完成!");
}else{
//如果数据库已经存在了,就什么都不用做:
System.out.println("[DataBaseManager] 数据库已经存在了");
}
}
如下所示,总的代码如下:
java
package org.example.mq.mqserver.datacenter;
import org.example.mq.mqserver.core.Exchange;
import org.example.mq.mqserver.core.ExchangeType;
import org.example.mq.mqserver.mapper.MetaMapper;
import java.io.File;
/*
通过这个DataBaseManager类来整合上述的操作:
*/
public class DataBaseManager {
private MetaMapper medaMapper;
//针对数据库进行初始化:
public void init(){
if(!checkDBExists()){
//如果数据库存在,就开始建表操作:
//创建数据表:
createTable();
//然后插入默认数据:
createDefaultData();
System.out.println("[DataBaseManager] 数据库初始化完成!");
}else{
//如果数据库已经存在了,就什么都不用做:
System.out.println("[DataBaseManager] 数据库已经存在了");
}
}
//判读数据库是否存在:就是判断meta.db文件是否存在
private boolean checkDBExists(){
File file = new File("./data/meta.db");
if(file.exists()){
return true;
}
return false;
}
//创建数据库和表:
private void createTable(){
//创建一个交换机表:
medaMapper.createExchangeTable();
//创建一个队列表:
medaMapper.createQueueTable();
//创建一个绑定表:
medaMapper.createBindingTable();
System.out.println("[DataBaseManager] 创建表完成!");
}
//创建一个添加默认数据的方法:
private void createDefaultData(){
//构造一个默认的交换机:
Exchange exchange = new Exchange();
exchange.setName("");
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
medaMapper.insertExchange(exchange);
System.out.println("[DataVaseManageer] 中添加默认数据交换机成功了!");
}
}
表面上看,这个代码没有问题,但是实际上这个medaMapper的相关调用的时候,没有medaMapper进行构造,所以上面的代码一定会在createDefaultData方法和createTable方法中出现空指针异常,所以需要对下面这个代码进行实例化:
java
private MetaMapper medaMapper;
这个MedaMapper类已经被注入到Spring中了,但是我们这里不打算使用@Autowried注入,换一种方式:
如何从Spring中拿到这个现成的对象?
可以在MqApplication启动类中加一个静态成员:
java
public static ConfigurableApplicationContext context;
然后将run方法的执行结果返回给这个context:

之后就可以在init方法中编写下面的方法通过context去手动获取到MetaMapper对象:
java
medaMapper = MqApplication.context.getBean(MetaMapper.class);
初始化操作完成之后,我们再去把数据库的其他操作也进行一次封装:
java
//把数据库的其他操作也这个类中进行封装:
public void insertExchange(Exchange exchange){
medaMapper.insertExchange(exchange);
}
public void deleteExchange(String exchangeName){
medaMapper.deleteExchange(exchangeName);
}
public void insertQueue(MSGQueue msgQueue){
medaMapper.insertQueue(msgQueue);
}
public void deleteQueue(String queueName){
medaMapper.deleteQueue(queueName);
}
public void insertBinding(Binding binding){
medaMapper.insertBinding(binding);
}
public void deleteBinding(Binding binding){
medaMapper.deleteBinding(binding);
}
最后我我们给补上一个查找方法吧:
在MetaMapper类中编写如下代码:
java
//查找所有交换机:
List<Exchange> selectAllExchanges();
//查找所有队列:
List<MSGQueue> selectAllQueues();
//查找所有绑定:
List<Binding> selectAllBindings();
然后在XML文件中的编写内容如下所示:
查找所有交换机:
xml
<select id="selectAllExchanges" resultType="org.example.mq.mqserver.core.Exchange">
select * from exchange;
</select>
查找所有队列:
xml
<select id="selectAllQueues" resultType="org.example.mq.mqserver.core.MSGQueue">
select * from queue;
</select>
查找所有绑定:
xml
<select id="selectAllBindings" resultType="org.example.mq.mqserver.core.Binding">
select * from binding;
</select>
最后在DataBaseManager类中补上查找方法即可:
java
//查找所有交换机:
public List<Exchange> selectAllExchanges(){
return medaMapper.selectAllExchanges();
}
//查找所有队列:
public List<MSGQueue> selectAllQueues(){
return medaMapper.selectAllQueues();
}
//查找所有绑定:
public List<Binding> selectAllBinding(){
return medaMapper.selectAllBindings();
}
如上所示,数据库的操作就都封装好了,封装到了DataBaseManager这个类里面了,后面要是需要使用到数据库操作,就直接使用这个类和这个类中的方法即可:
DataBaseManager这个类的所有代码如下所示:
java
package org.example.mq.mqserver.datacenter;
import org.example.mq.MqApplication;
import org.example.mq.mqserver.core.Binding;
import org.example.mq.mqserver.core.Exchange;
import org.example.mq.mqserver.core.ExchangeType;
import org.example.mq.mqserver.core.MSGQueue;
import org.example.mq.mqserver.mapper.MetaMapper;
import java.io.File;
import java.util.List;
/*
通过这个DataBaseManager类来整合上述的操作:
*/
public class DataBaseManager {
private MetaMapper medaMapper;
//针对数据库进行初始化:
public void init(){
//手动从Spring中获取到medaMapper对象
medaMapper = MqApplication.context.getBean(MetaMapper.class);
if(!checkDBExists()){
//如果数据库存在,就开始建表操作:
//创建数据表:
createTable();
//然后插入默认数据:
createDefaultData();
System.out.println("[DataBaseManager] 数据库初始化完成!");
}else{
//如果数据库已经存在了,就什么都不用做:
System.out.println("[DataBaseManager] 数据库已经存在了");
}
}
//判读数据库是否存在:就是判断meta.db文件是否存在
private boolean checkDBExists(){
File file = new File("./data/meta.db");
if(file.exists()){
return true;
}
return false;
}
//创建数据库和表:
private void createTable(){
//创建一个交换机表:
medaMapper.createExchangeTable();
//创建一个队列表:
medaMapper.createQueueTable();
//创建一个绑定表:
medaMapper.createBindingTable();
System.out.println("[DataBaseManager] 创建表完成!");
}
//创建一个添加默认数据的方法:
private void createDefaultData(){
//构造一个默认的交换机:
Exchange exchange = new Exchange();
exchange.setName("");
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
medaMapper.insertExchange(exchange);
System.out.println("[DataVaseManageer] 中添加默认数据交换机成功了!");
}
//把数据库的其他操作也这个类中进行封装:
public void insertExchange(Exchange exchange){
medaMapper.insertExchange(exchange);
}
public void deleteExchange(String exchangeName){
medaMapper.deleteExchange(exchangeName);
}
//查找所有交换机:
public List<Exchange> selectAllExchanges(){
return medaMapper.selectAllExchanges();
}
public void insertQueue(MSGQueue msgQueue){
medaMapper.insertQueue(msgQueue);
}
public void deleteQueue(String queueName){
medaMapper.deleteQueue(queueName);
}
//查找所有队列:
public List<MSGQueue> selectAllQueues(){
return medaMapper.selectAllQueues();
}
public void insertBinding(Binding binding){
medaMapper.insertBinding(binding);
}
public void deleteBinding(Binding binding){
medaMapper.deleteBinding(binding);
}
//查找所有绑定:
public List<Binding> selectAllBinding(){
return medaMapper.selectAllBindings();
}
}
针对DataBaseManager单元测试
进行单元测试,要求,单元测试用例和单元测试用例之间,是需要相互独立的,不会干扰的
如下所示,就是一个错误的测试用例:
所以良好的测试用例应该是保持用例之间的相互独立:
那么如何保证每个测试用例之间相互独立呢:
方法如下所示:
每一个测试用例执行之前,先执行一段逻辑,搭建测试的环境,准备好测试需要用到的一些东西
每个测试用例执行之后,再去执行一段逻辑,把测试用例执行过程中产生的中间结果和影响都给消除掉
比如测试用例在在测试的时候,在数据库中插入了一些数据,那么在测试用例执行结束之后,就要把刚刚插入的数据给删除掉
所以编写一个setUp方法来执行准备工作,每个用例执行之前,都要调用这个setUp方法去做好准备工作,同时加上一个注解@BeforeEach,确保方法在用例执行前就自动执行:
java
//使用这个方法在测试用例执行之前去执行:
@BeforeEach
public void setUp(){
//由于在init方法中,需要通过context对象拿到metaMapper实例,所以就要把context对象搞出来:
MqApplication.context = SpringApplication.run(MqApplication.class);
dataBaseManager.init();
}
然后也要编写一个tearDown方法来执行收尾工作,每个用例执行之后,都要调用这个tearDown方法去清除中间结果和影响,同时在这个方法上加上注解@AfterEach,确保在用例执行之后自动执行这个方法:
这个方法要执行的逻辑是用例执行完毕之后,直接删除数据库,也就是直接删除meda.db文件:
我们就先去DataBaseManager类中编写一个直接删除数据库的方法:
java
//直接删除数据库的方法:
//删除meda.db文件:
public void deleteDB(){
File file = new File("./data/meta.db");
boolean ret = file.delete();
if(ret == true){
System.out.println("[DataBaseManager] 删除数据库成功!");
}else{
System.out.println("[DataBaseManager] 删除数据库失败!");
}
}
然后我们回到这个测试用例中的tearDown方法里面,直接调用刚刚的删除数据库的方法即可:
java
//使用这个方法在测试用例执行之后去自动执行:
@AfterEach
public void tearDown(){
//清空数据库,直接删除meta.db文件:
dataBaseManager.deleteDB();
}
注意,如果我们直接在tearDown方法中就进行了删除操作,大概率会出错,不能先直接删除,
在删除之前要先去关闭上述的context对象,
因为此处的context对象,持有了MetaMapper的实例,MetaMapper实例又打开了meta.db数据库文件
如果meta.db文件被别人打开了,此时直接删除文件会失败(Windows的限制,Linux不会有)
只有先关闭了context对象之后,此时MetaMapper实例就会被销毁,实例被销毁之后,meta.db数据库文件就没有人持有了,此时才可以进行删除meta.db数据库文件
同时获取context操作会占用8080端口,此处的close也是为了释放出8080端口
所以tearDown方法的正确代码如下所示:
java
//使用这个方法在测试用例执行之后去自动执行:
@AfterEach
public void tearDown(){
//清空数据库,直接删除meta.db文件:
// 在删除之前要先去关闭上述的context对象,
MqApplication.context.close();
dataBaseManager.deleteDB();
}

测试创建表
好了,刚刚的测试用例的准备工作做好了,下面我们开始进行测试了
先来测试创建表的操作:
测试init初始化方法
在测试用例中编写如下代码:
由于我们的init方法已经在上面的setUp方法中调用过了,所以此处的测试方法就直接检查数据库状态,查看是否创建成功即可:
这个测试方法的检查核心逻辑如下:
直接从数据库中查询,看看是否符合预期,
查询交换机表(里面应该有一个数据:匿名的exchange),查询队列表(没有数据),查询绑定表(没有数据)
java
public void testInitable(){
//查询所有的交换机:
List<Exchange> exchangeList = dataBaseManager.selectAllExchanges();
//查询所有的队列:
List<MSGQueue> queueList = dataBaseManager.selectAllQueues();
//查询所有的绑定:
List<Binding> bindingList = dataBaseManager.selectAllBinding();
//验证结果是否符合要求:
//使用断言
//assertEquals用来判定结果是不是相等:
//查看交换机列表的个数是否和1相等:
Assertions.assertEquals(1,exchangeList.size());
//查看队列列表的个数是否和0相等:
Assertions.assertEquals(0,queueList.size());
//查看绑定列表的个数是否和0相等:
Assertions.assertEquals(0,bindingList.size());
}
在这个断言中为什么把1放在前面呢:
因为这个assertEquals方法的形参的第一个参数叫做excepted(预期的),第二个形参叫做actual(实际的)
接着继续去查看这个交换机列表 的第一个交换机是不是匿名的DIRECT直接交换机:
java
//查看交换机列表的0号元素是不是匿名的:
Assertions.assertEquals("",exchangeList.get(0).getName());
//查看交换机列表的0号元素是不是交换机类型:
Assertions.assertEquals(ExchangeType.DIRECT,exchangeList.get(0).getType());
代码逻辑编写完毕,最后在这个测试方法的上面加上一个@Test注解就可以测试这个方法了:
所以这个初始化方法的测试方法总的代码如下所示:
java
//直接从数据库中查询,看看是否符合预期,
//查询交换机表(里面应该有一个数据:匿名的exchange),查询队列表(没有数据),查询绑定表(没有数据)
@Test
public void testInitable(){
//查询所有的交换机:
List<Exchange> exchangeList = dataBaseManager.selectAllExchanges();
//查询所有的队列:
List<MSGQueue> queueList = dataBaseManager.selectAllQueues();
//查询所有的绑定:
List<Binding> bindingList = dataBaseManager.selectAllBinding();
//验证结果是否符合要求:
//使用断言
//assertEquals用来判定结果是不是相等:
//查看交换机列表的个数是否和1相等:
Assertions.assertEquals(1,exchangeList.size());
//查看队列列表的个数是否和0相等:
Assertions.assertEquals(0,queueList.size());
//查看绑定列表的个数是否和0相等:
Assertions.assertEquals(0,bindingList.size());
//查看交换机列表的0号元素是不是匿名的:
Assertions.assertEquals("",exchangeList.get(0).getName());
//查看交换机列表的0号元素是不是交换机类型:
Assertions.assertEquals(ExchangeType.DIRECT,exchangeList.get(0).getType());
}
我们运行代码发现出错啦:
错误显示data这个目录并不存在:

原因是我们在使用Mybatis来创建数据表的时候,第一次操作时,会先创建出数据库出来,也就是会自动创建出一个叫做meta.db文件出来,但是我们写的是/data/meta.db
所以这个meta.db文件会被创建在data目录下,但是我们还没有这个data目录呢,那当然就会报错啦
由于data目录不存在所以创建meta.db文件的操作也就会失败:
所以我们在创建数据库之前要先去创建data目录:
回到DataBaseManager类中的初始化方法里面,先去创建一个data目录:
具体创建data目录的代码:
java
//先创建一个data目录:
File dataDir = new File("./data");
dataDir.mkdir();
代码添加位置如图所示:

创建好了之后,再次运行测试用例即可:
测试插入交换机的方法
初始化方法测试完毕之后,我们下面来测试插入交换机的方法:
这个方法的执行逻辑如下所示:
直接构造一个Exchange对象,把对象插入到数据库中,再去查询,看是否插入成功了:
我们就直接将这个创建交换机的过程直接封装为了一个方法:
java
//把创建交换机封装为一个方法:
public Exchange createTestsExchange(String exchangeName){
Exchange exchange = new Exchange();
exchange.setName(exchangeName);
exchange.setType(ExchangeType.FANOUT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
exchange.setArguments("aa",1);
exchange.setArguments("bb",2);
return exchange;
}
同时由于原本的setArguments和getArguments方法中的参数是一个JSON的字符串,是为了和数据库交互使用的,不方便获取到key和value,所以我们就打算在Exchange类中再去写一对setArguments和getArguments方法,为了更加方便地获取和设置这里的键值对:
java
//在这里的Exchange类中针对arguments。再提供一组getter和setter方法,用来去更方便获取/设置这里的键值对
//在这里针对arguments。再提供一组getter和setter方法,用来去更方便获取/设置这里的键值对
public Object getArguments(String key){
return arguments.get(key);
}
public void setArguments(String key, Object value){
arguments.put(key, value);
}
最后我们来编写测试插入交换机的方法:
java
@Test
public void testInsertExchange(){
//我们直接构造一个Exchange对象,把对象插入到数据库中,再去查询,看是否插入成功了:
Exchange newExchange = createTestsExchange("testExchange");
//把对象插入到交换机中:
dataBaseManager.insertExchange(newExchange);
//插入完毕之后,查询结果:
List<Exchange> exchangeList = dataBaseManager.selectAllExchanges();
//查看是不是两个交换机:第一个是默认的,第二个是插入的:
Assertions.assertEquals(2,exchangeList.size());
//获取到第二个插入的交换机:
Exchange testExchange = exchangeList.get(1);
//查询名字:
Assertions.assertEquals("testExchange",testExchange.getName());
//查询类型:
Assertions.assertEquals(ExchangeType.FANOUT,testExchange.getType());
//查询交换机内容是否一致:
Assertions.assertEquals(1,testExchange.getArguments("aa"));
Assertions.assertEquals(2,testExchange.getArguments("bb"));
}
运行测试用例,结果没有问题,测试成功:

测试删除交换机的方法
测试删除交换机的方法的大致执行逻辑如下所示:
先构造一个交换机,然后把这个交换机插入到数据库中,最后按照交换机的名字删除即可
如下代码所示:
java
@Test
public void testDeleteExchange(){
//先创建一个交换机,插入到数据库中,最后根据名字删除:
Exchange exchange = createTestsExchange("testExchange");
//把交换机插入到数据库中:
dataBaseManager.insertExchange(exchange);
//获取到插入到数据库中的交换机:
List<Exchange> exchangeList = dataBaseManager.selectAllExchanges();
//测试交换机是否插入成功:
Assertions.assertEquals(2,exchangeList.size());
Assertions.assertEquals("testExchange",exchangeList.get(1).getName());
//在数据库中根据名字删除交换机:
dataBaseManager.deleteExchange("testExchange");
//再次查询,看这个testExchange是否还存在:
//查询是否只剩下一个默认的匿名交换机了:
exchangeList = dataBaseManager.selectAllExchanges();
Assertions.assertEquals(1,exchangeList.size());
Assertions.assertEquals("",exchangeList.get(0).getName());
}
运行这个测试用例,结果删除成功了:
当前我们只是简单的设计了测试用例,实际上如果是站在更加严谨的角度,还需要设计更加丰富的测试用例,
俺以后要去应聘高贵的开发岗,不管测试不行吗?
不行,因为单元测试本身就是开发搞的
测试和开发都找,我们去找工作一定是要广撒网多捞鱼...
测试插入队列的方法
这个测试插入队列的方法和刚刚的交换机的逻辑基本都是一样的
第一步:先去在MSGQueue这个类中编写两个新的关于arguments的getter和setter方法:
java
public Object getArguments(String key){
return arguments.get(key);
}
public void setArguments(String key, Object value){
arguments.put(key,value);
}
第二步:把创建一个队列的操作封装为一个方法:
java
//创建一个队列封装为一个方法:
private MSGQueue createTestQueue(String queueName){
MSGQueue queue = new MSGQueue();
queue.setName(queueName);
queue.setDurable(true);
queue.setAutoDelete(false);
queue.setExclusive(false);
queue.setArguments("aa",1);
queue.setArguments("bb",2);
return queue;
}
第三步:
把创建好的队列的对象插入到数据库中,再去查询,看是否插入成功了:
java
@Test
public void testInsertQueue(){
//创建一个队列对象:
MSGQueue queue = createTestQueue("testQueue");
//把对象插入到数据库中:
dataBaseManager.insertQueue(queue);
//查询是否插入成功:
List<MSGQueue> queueList = dataBaseManager.selectAllQueues();
//测试是否只有一个队列
Assertions.assertEquals(1,queueList.size());
//获取到这个数据库中的队列:
MSGQueue testQueue = queueList.get(0);
//测试队列的名字,属性是否都相同:
Assertions.assertEquals("testQueue",testQueue.getName());
Assertions.assertEquals(true,testQueue.isDurable());
Assertions.assertEquals(false,testQueue.isAutoDelete());
Assertions.assertEquals(false,testQueue.isExclusive());
Assertions.assertEquals(1,testQueue.getArguments("aa"));
Assertions.assertEquals(2,testQueue.getArguments("bb"));
}
运行如下,测试用例没有报错,结果运行成功了:
测试删除队列的方法
这个测试删除队列的方法和刚刚的交换机的逻辑基本都是一样的
先构造一个队列,然后把这个队列插入到数据库中,最后按照队列的名字删除即可
java
@Test
public void testDeleteQueue(){
//先构造了一个叫做testQueue的队列出来:
MSGQueue queue = createTestQueue("testQueue");
//把这个队列插入到数据库中去:
dataBaseManager.insertQueue(queue);
//获取到数据库中的所有队列:
List<MSGQueue> queueList = dataBaseManager.selectAllQueues();
//查看是否只有一个队列:
Assertions.assertEquals(1,queueList.size());
//下面进行删除队列操作:
dataBaseManager.deleteQueue("testQueue");
//删除之后,再次查询数据库中是否还有队列:
queueList = dataBaseManager.selectAllQueues();
//查看数据库中的队列元素是否为0:
Assertions.assertEquals(0,queueList.size());
}
运行测试用例,结果执行成功:
测试插入绑定的方法
先去封装好一个插入绑定的方法
java
//创建一个绑定:
private Binding createTestBinding(String exchangeName, String queueName){
Binding binding = new Binding();
binding.setExchangeName(exchangeName);
binding.setQueueName(queueName);
binding.setBindingKey("testBindingKey");
return binding;
}
然后在测试方法中,创建一个绑定,将这个绑定插入数据库中去,插入完毕之后,查询数据库中的绑定是否插入成功,查询数据库中的绑定的个数,属性是否和刚刚插入的绑定都一致:
java
//测试插入绑定的方法:
@Test
public void testInsertBiding(){
//创建一个绑定,把这个绑定插入到数据库中:
Binding binding = createTestBinding("testExchange","testQueue");
//把绑定插入到数据库中:
dataBaseManager.insertBinding(binding);
//插入完毕之后,查看数据库中的绑定:
List<Binding> bindingList = dataBaseManager.selectAllBinding();
//测试数据库中的绑定是否只有一个:
Assertions.assertEquals(1,bindingList.size());
//测试数据库中的绑定的内容是否和插入的一致:
Binding testBinding = bindingList.get(0);
//测试交换机名字是否一致:
Assertions.assertEquals("testExchange",testBinding.getExchangeName());
//测试队列名字是否一致:
Assertions.assertEquals("testQueue",testBinding.getQueueName());
//测试BindingKey是否一致:
Assertions.assertEquals("testBindingKey",testBinding.getBindingKey());
}
运行测试用例,运行成功:
测试删除绑定的方法
先构造一个绑定,然后把这个绑定插入到数据库中,最后按照绑定的名字删除即可:
java
//测试删除绑定的方法:
//先构造一个绑定,然后把这个绑定插入到数据库中,最后在数据库中删除这个绑定即可:
@Test
public void testDeleteBinding(){
Binding binding = createTestBinding("testExchange","testQueue");
//把绑定插入到数据库中:
dataBaseManager.insertBinding(binding);
//插入完毕之后,查询数据库中是否只有这一个绑定:
List<Binding> bindingList = dataBaseManager.selectAllBinding();
Assertions.assertEquals(1,bindingList.size());
//在数据库中删除这个绑定:
Binding testDeleteBinding = createTestBinding("testExchange","testQueue");
dataBaseManager.deleteBinding(testDeleteBinding);
//删除完毕之后,查询一下数据库中是否还有这个绑定:
bindingList = dataBaseManager.selectAllBinding();
Assertions.assertEquals(0,bindingList.size());
}
运行测试用例,结果如下所示:
到此为止,我们的数据库操作以及数据库操作的测试用例就都编写完毕了~
虽然这个测试用例的编写是比较无聊的,但是测试用例的编写是非常重要的,
因为以后在开发中,你写代码时是一定会出现bug的,这个是客观的事实,所以为了不让自己写的bug影响到我们的年终奖,就需要对代码进行周密的测试,这个才是应对bug的有效手段,
前期花了一些时间去写测试代码,后期出现了bug之后,就可以快速地排查出bug解决掉了
单元测试只是正对着这一小块代码测试,出现了问题,就可以立马排查出来解决掉了,
这些单元测试会大大提高了整个项目的开发效率
当前项目的小结
当前已经完成了下列的工作:
1:对于当前项目要做什么,要完成哪些功能和模块有了清晰的认识
2: 设计了核心类,Exchange,MSGQueue,Binding,Message
3: 设计了数据库的设计:主要是针对了Exchange,MSGQueue,Binding进行了操作和管理:
使用了SQLite+Mybatis来完成相关的数据库操作
4:针对数据库代码进行了单元测试,
下一个环节会设计Message对象如何在文件中进行存储:会涉及到字节级别的操作和陌生的文件操作方法。