【项目篇】仿照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对象如何在文件中进行存储:会涉及到字节级别的操作和陌生的文件操作方法。

相关推荐
Kyrie_Li15 分钟前
Kafka常见问题及解决方案
分布式·kafka
hoho不爱喝酒1 小时前
微服务 RabbitMQ 组件的介绍、安装与使用详解
微服务·rabbitmq·ruby
掘金-我是哪吒2 小时前
分布式微服务系统架构第117集:Kafka发送工具,标准ASCII
分布式·微服务·kafka·系统架构·linq
方二华2 小时前
分布式唯一ID设计
分布式
CopyLower2 小时前
设计与实现分布式锁:基于Redis的Java解决方案
java·redis·分布式
DemonAvenger3 小时前
Go 并发利器:深入剖析 errgroup 的错误处理与最佳实践
分布式·架构·go
麻芝汤圆3 小时前
Spark 集群搭建:Standalone 模式详解
大数据·开发语言·hadoop·分布式·spark·mapreduce
冼紫菜4 小时前
[特殊字符] 分布式事务中,@GlobalTransactional 与 @Transactional 到底怎么配合用?
java·数据库·分布式·后端·mysql
王五周八4 小时前
基于RabbitMQ实现订单超时自动处理
分布式·rabbitmq
我爱拉臭臭5 小时前
分布式之CAP原则:理解分布式系统的核心设计哲学
linux·运维·服务器·分布式