【项目篇】仿照RabbitMQ模拟实现消息队列

上面这张图,是服务器模块的具体模块划分

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

管理上述核心概念,需要两个部分:

  1. 一个是在内存上进行管理
  2. 一个是在硬盘上进行管理

我们来看硬盘这一块的管理:

硬盘上面的数据的保存一共分为两个部分:

第一个部分是数据库(主要保存的数据是交换机,队列,绑定)

第二个部分是文件(主要保存的数据是消息)

我们先来看数据库中如何去保存好交换机,队列,绑定这些核心的概念

为什么使用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

数据库建表

数据库管理里面一共管理三个核心概念:

  1. 交换机存储
  2. 队列存储
  3. 绑定存储

我们需要去建库建表

我们目前是使用SQLite,就不需要去建数据库了,因为我们的数据库文件(meda.db)就是数据库

当我们把刚刚的配置文件和依赖都导入了之后,程序自动之后,就会自动建数据库了

创建表:

下面我们开始建表:

我们就围绕着刚刚的三个核心概念来设计数据库表

  1. 交换机
  2. 队列
  3. 绑定
    需要设计三张表,
    但是这个建表的操作,具体什么时候来开始执行呢?

以前我们写的程序,比如博客系统之类的,都是需要先把数据库的表都先给创建好,然后再启动服务器,

之前是先把建库建表的SQL语句都写到一个.sql文件中去

需要建表的时候,直接复制这个sql文件到MySQL的客户端中去执行就可以了

以前这个建表的操作是在 部署阶段完成的

之前大概部署一次就可以了,不需要反复操作,但是后续接触到的很多程序可能会涉及到多次反复部署,所以能够简化部署的步骤也很重要,能够自动完成的尽量都自动完成

接下来我们尝试去简化一下这个部署的过程:

Mybatis的基本使用流程

  1. 创建一个interface,描述有哪些方法要给JAVA代码使用
  2. 创建对应的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对象如何在文件中进行存储:会涉及到字节级别的操作和陌生的文件操作方法。

相关推荐
茶杯梦轩1 天前
从零起步学习RabbitMQ || 第三章:RabbitMQ的生产者、Broker、消费者如何保证消息不丢失(可靠性)详解
分布式·后端·面试
回家路上绕了弯3 天前
深入解析Agent Subagent架构:原理、协同逻辑与实战落地指南
分布式·后端
用户8307196840823 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
用户8307196840825 天前
RabbitMQ vs RocketMQ 事务大对决:一个在“裸奔”,一个在“开挂”?
后端·rabbitmq·rocketmq
初次攀爬者6 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者8 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧9 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖9 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农9 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者9 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端