框架使用:Spring Boot,Mybatis
软件:IDEA 2022.3.3社区版
使用技术:Java、SQLite
详情查看链接: 消息队列的模拟实现(一)
首先,实现消息队列这里主要分为三个模块:
- 公共模块
- 主要存储公共类
- 服务器模块
- 主要存储服务器方法,比如读取请求返回响应数据
- 客户端模块
- 主要存储客户端方法,比如请求连接方法和构造请求
根据上面的描述,我们创建一个Spring boot
来搭建我们的项目。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency> <groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starter-testartifactId>
<version>2.3.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.xerialgroupId>
<artifactId>sqlite-jdbcartifactId>
<version>3.41.2.1version>
dependency>
dependencies>
添加依赖后创建文件夹目录:
在主目录下创建三个文件夹
- common :公共类模块
- mqclient :客户端模块
- mqserver 服务器模块
因为公共模块是创建总和其他模块公共方法,不宜作为第一选项实现模块,所以这里我们选择服务器的实现。
对照核心类设置表,并理解属性的使用位置和属性含义。
交换机表需要的属性也是根据我们需要的功能进行展开:
exchangeName
:用于识别交换机type
:上面我们提到了交换机有三种类型、direct、fanout和topicdurable
:持久化,数据的持久化autodelete
: 无人使用是否删除,节约内存空间argument
: argument中是创建一些交换机指定的一些额外的参数选项,后续代码中并没有对应的参数,相对于一个扩展参数
在mqServer
创建一个文件夹用于存储核心类
,该核心主要是一些实体类。
分析完交换机的字段,创建一个交换机类:
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data//添加setter和getter 方法
public class Exchange {
private String name;
private ExchangeType type = ExchangeType.DIRECT;
private boolean durable = false;//持久化存储
private boolean autoDelete = false;//自动删除
//扩展属性
private Map<String,Object> arguments = new HashMap<>();
}
在创建交换机时,我们将设定交换机类型,这里建议使用枚举,创建一个交换机类型枚举。取名为ExechangeType
同样是放在核心文件夹中。
//这个类用于存储三种类型
public enum ExchangeType {
DIRECT(0),
FANOUT(1),
TOPIC(2);
private final int type;
ExchangeType(int type) {
this.type = type;
}
}
接下来分析队列需要的字段名称:
queueName
:队列名,用于识别队列durable
:数据是否需要持久化设置exclusive
:独占功能,开启独占只能给一个消费者使用autoDelete
:无人使用是否删除,节约内存空间argument
:扩展参数,先不实现
同样的在核心文件夹创建一个queue类,这里需要注意这个名字与Java中的集合类名字相同,这里因为是存储消息的队列,所以改名为MSGQueue
。
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class MSGQueue {
private String name;//队列名称
private boolean durable;//队列持久化设置
private boolean exclusive;//对列只能属于一个消费者
private boolean autoDelete;//对不使用的队列自动删除
//扩展功能
private Map<String,Object> argument = new HashMap<>();
}
要分析绑定,就应该知道绑定的作用,绑定的作用是用来将交换机和队列进行绑定,更好的传输数据。
- exchangeName:交换机名称
- queueName:队列名称
- bindingKey :当交换机为topic时使用匹配时的机制
import lombok.Data;
@Data
public class Binding {
private String exchangeName;//绑定的交换机名称
private String queueName;//绑定的队列名称
private String bindingKey;//当交换机是topic类型时需要进行匹配时使用的字段
}
Message 中需要实现Serializable
接⼝.
这个接口是用来管理序列化和反序列化。对象的序列化是指将对象转换成字节流的过程,可以将序列化的对象存储到硬盘中或通过网络传输。而反序列化是将字节流转化为对象的过程。
后续需要把 Message? 写⼊⽂件以及进⾏⽹络传输
- BasicProperties :消息属性,需要额外写一个类表示
- byte[] : 消息体,消息的具体内容
- offsetBeg :消息体开始的地方在文件中相当于一个下标
- offsetEnd:消息结束的地方
- isValid:标记消息的有效性,0x1 是有效 0x0是无效
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.xml.validation.ValidatorHandler;
import java.io.Serializable;
import java.util.UUID;
@Data
public class Message implements Serializable {
//验证开发者版本号
private static final long serialVersionUID = 1L;
//Message中核心部分
private BasicProperties basicProperties = new BasicProperties();
private byte[] body;//正文部分
/*
使用两个偏移量来表示某个消息存储在文件中的位置
使用前闭后开模式[offsetBeg,offsetEnd]
*/
private long offsetBeg = 0;//开始
private long offsetEnd = 0;//结尾
//使用一个变量标记文件是否有效
// 0x1 -> 有效 0x0 -> 无效
private byte isValid = 0x1;
//创建一个工厂方法,让工厂方法帮我们封装一个 Message 对象
//在这个方法中获取一个UUID 以 M- 作为消息前缀
public static Message createMessage(String routingKey,BasicProperties basicProperties,byte[] body){
Message message = new Message();
if(basicProperties != null){
message.setBasicProperties(basicProperties);
}
//UUID 的生成
message.basicProperties.setMessageId("M-"+ UUID.randomUUID());
message.basicProperties.setRoutingKey(routingKey);
message.setBody(body);
//只设置两个属性,一个body ,一个 basicProperties
return message;
}
}
里面包含了信息ID、消息匹配令牌和持久化数据,注意因为是由message引用所有也需要添加Serializable接口。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class BasicProperties implements Serializable {
//为了保障消息的唯一性
private String messageId;
//用于匹配政策
private String routingKey;
//持久化设置 1-》不持久化 2-》持久化
private int deliverMode = 1;
}
Sqlite
中建表在Sqlite
中建表,在资源文件中创建一个mateMapper.xml
文件用编写创建表格的代码或者修改表格的语句。
这里是将每一个建表语句都分开使用
update
来建表,能否改为一个update
来创建多个表?借助一个方法创建多个表创建?答:不可以,原因是因为 SQLite 和 MySQL是不一样的,只能执行第一条语句,后续语句直接忽略,所以每执行一次,都需要额外再写一次SQL语句。
根据字段进行创建,以下分别是交换机、队列和绑定表的创建。
<update id="createExchangeTable">
create table if not exists exchange(
name varchar(50) primary key,
type int,
durable boolean,
autoDelete boolean,
argument varchar(1024),
)
update>
<update id="createQueueTable">
create table if not exists queue(
name varchar(50) primary key,
durable boolean,
exclusive boolean,
AutoDelete boolean,
argument varchar(1024)
)
update>
<update id="createBindingTable">
create table if not exists binding(
exchangeName varchar(50),
queueName varchar(50),
bindingKey varchar(256)
)
update>
当添加了sql
语句后,我们应该在代码实现一个接口来调用文件中的方法。
创建一个专门存放映射接口的文件夹叫mapper,创建一个mateMapper
类作为映射关系来调用MateMapper.xml
中的方法。
import com.example.mq.mqserver.core.Binding;
import com.example.mq.mqserver.core.Exchange;
import com.example.mq.mqserver.core.MSGQueue;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.concurrent.Executors;
@Mapper
public interface MateMapper {
//创建三个表
void createExchangeTable();
void createQueueTable();
void createBindingTable();
}
Map
数据结构的序列化Mybatis
在操作数据库时,读取数据库数据时会调用Getter
方法,我们只需要在这个读取数据的过程中,使得数据是字符串即可,同样的,在设置这个数据时,会调用Setter
方法,以同样的方法去修改即可,总而言之在设置Map时将传递数据的格式改为字符串,修改类中的getter和setter
方法。
将map参数转换为字符串存储,再将字符串数据转化为Json
数据格式使用读取。修改getter
方法和setter
方法。将序列化代码添加到Exchange、MSGQueue等存在 Map 数据结构的类中。
ObjectMapper
所以需要添加依赖<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.15.2version>
dependency>
ObjectMapper
中转换字符串的方法。相对于一个序列化和反序列化的过程。 public String getArguments() {
//将当前的argument参数修改成String(Json)
ObjectMapper objectMapper =new ObjectMapper();
try {
//转化为json
return objectMapper.writeValueAsString(arguments);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//如果出现异常,则返回一个空字符串即可
return "{}";
}
//这个方法,是从数据库中读取数据后,构造Exchange对象,会自动调用的方法
public void setArguments(String argumentsJson) {
//把参数中的argumentJson解析,转换成一个map对象
ObjectMapper objectMapper = new ObjectMapper();
try {
objectMapper.readValue(argumentsJson, new TypeReference<HashMap< String, Object >>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
new TypeReference
参数含义:
用于描述当前的json字符串转化为字符串类型,使用方法:
- 简单类型:直接使用对应类型即可
- 集合这种的复杂类型:使用TypeReference匿名内部类来表示复杂类型的具体信息。
添加增加和删除方法并实现SQL
语句。
TypeReference
类的意义:
是Java的一种泛型类,主要用于获取泛型中具体类型的信息,并在需要时恢复泛型的信息。
这里提供了两种方法来连接数据库。这里我将数据库名称取名为mate.db
。
编写一个yml文件:
spring:
datasource:
url: jdbc:sqlite:./MessageData/meta.db #相对路径的写法
username:
password:
driver-class-name: org.sqlite.JDBC
# sqlite不需要写用户名和密码,严密性没有MySql强
编写一个properties文件
#连接 sqlite 数据库
spring.datasource.url=jdbc:sqlite:./data/mate.db
spring.datasource.username=
spring.datasource.password=
spring.datasource.driver-class-name=org.sqlite.JDBC
#这里不需要填写用户名和密码,因为是单用户数据库
#设置mybatis的接口文件
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
可以随意使用,所以这里两种文件的区别也可以一眼看出。
在之前的代码中,进行了建表操作,接下来我们对接口中数据进行一些功能添加。
- 添加一些初始化数据
- 在程序启动时,做出逻辑判断
- 数据库存在,无操作动作
- 数据库不存在,创建库创建表(如何判断?查询数据库
mate.db
文件存在性即可)
首先是对表中添加数据操作,可以先在接口中实现API
,然后再添加sql语句。
//添加数据操作
void addExchange(Exchange exchange);
void addQueue(MSGQueue queue);
void addBinding(Binding binding);
//删除数据操作
void deleteExchange(String exchangeName);//交换机根据交换机名称删除
void deleteQueue(String queueName);//队列根据队列名称删除
void deleteBinding(String ExchangeName, String queueName);//绑定根据交换机和队列的名称删除
//查询操作分为查询一个或者查询所有
//先实现查询所有数据
List<Exchange> selectAllExchange();
List<MSGQueue> selectAllQueue();
List<Binding> selectAllBinding();
//查询一个
Exchange selectExchange(String exchangeName);
MSGQueue selectQueue(String queueName);
Binding selectBinding(Binding binding);
MateMapper.xml
中实现操作sqlite语句Mybatis字段名含义
id
:方法名称对应到接口中的方法名parameterType
:对调用mapper接口的使用的参数类型resultType
:查询结果的参数类型
<insert id="addExchange" parameterType="com.example.mq.mqserver.core.Exchange">
insert into exchange value(#{name},#{type},#{durable},#{autoDelete},#{argument})
insert>
<insert id="addQueue" parameterType="com.example.mq.mqserver.core.MSGQueue">
insert into queue value(#{name},#{durable},#{exclusive},#{autoDelete},#{argument})
insert>
<insert id="addBinding" parameterType="com.example.mq.mqserver.core.Binding">
insert into binding value(#{exchangeName},#{queueName),#{BindingKey});
insert>
<delete id="deleteExchange" parameterType="com.example.mq.mqserver.core.Exchange">
delete from exchange where name = #{exchangeName};
delete>
<delete id="deleteQueue" parameterType="com.example.mq.mqserver.core.MSGQueue">
delete from queue where name = #{queueName};
delete>
<delete id="deleteBinding" parameterType="com.example.mq.mqserver.core.Binding">
delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName};
delete>
<select id="selectAllExchange" resultType="com.example.mq.mqserver.core.Exchange">
select * from exchange
select>
<select id="selectAllQueue" resultType="com.example.mq.mqserver.core.MSGQueue">
select * from queue;
select>
<select id="selectAllBinding" resultType="com.example.mq.mqserver.core.Binding">
select * from binding
select>
<select id="selectExchange" resultType="com.example.mq.mqserver.core.Exchange">
select * from exchange where name = #{exchangeName};
select>
<select id="selectQueue" resultType="com.example.mq.mqserver.core.MSGQueue">
select * from queue where name = #{queueNAme};
select>
<select id="selectBinding" resultType="com.example.mq.mqserver.core.Binding">
select * from binding where exchangeName = #{exchangeName} and queueName = #{queueName};
select>
以上就是我们第一步的创建工作,接下来使用DataBaseManger来总和上述功能。
在mqserver
中创建一个文件夹名为datacenter(数据中心)
,文件夹创建一个类,类名为DataBaseManger
,然后使用这个类来调用我们刚刚创建的方法,创建数据库文件、删除数据库文件和建表等操作。
整合步骤
- 创建一个
MateMapper
- 创建初始化方法,
- 检查数据库可靠性,不存在就创建一个,
- 添加默认交换机
- 添加数据库文件
- 建表操作
- 删除数据库文件(这里为了方便后面代码的添加,提前实现)
import com.example.mq.MqApplication;
import com.example.mq.mqserver.mapper.MateMapper;
import java.io.File;
public class DataBaseManger {
private MateMapper mateMapper;
//初始化
public void init(){
//获取bean 对象
mateMapper = MqApplication.context.getBean(MateMapper.class);
//检查数据库的存在性
if(! checkDBExists()){
//数据库不存在,进行以下操作
// 1. 创建目录
File dataFile = new File("./data");
dataFile.mkdirs();
// 2. 创建数据库
cretaeTable();//将三个表对封装在里面
// 3. 插入默认数据
creataDefaultData();//添加一个默认的交换机,这是因为 RabbitMq中有一个
System.out.println("[DataBAseManger]第一阶段 数据库初始化完成!");
}else {
System.out.println("[DataBaseManger]第一阶段 数据库已经存在!");
}
}
private boolean checkDBExists() {
File file = new File("/data/mate.db");
return file.exists();
}
//删除数据库文件
public void deleteDataBase(){
//1. 删除文件
//2. 删除目录
File file = new File("./data/mte.db");
boolean delete = file.delete();
if(delete){
System.out.println("[DataBaseManger] 删除数据库成功!");
}else {
System.out.println("[DataBaseManger] 删除数据库失败!");
}
File deleteDir = new File("./data");
delete = deleteDir.delete();
if(delete){
System.out.println("[DataBaseMAnger] 删除数据库目录成功!");
}else {
System.out.println("[DataBaseMAnger] 删除数据库目录失败!");
}
}
}
这里我们添加两个方法给上面代码使用
创建表工作:
private void createTable() {
mateMapper.createExchangeTable();
mateMapper.createQueueTable();
mateMapper.createExchangeTable();
System.out.println("[DataBaseMange]第一阶段 建表完成!");
}
添加默认交换机数据
private void createDefaultData() {
Exchange exchange = new Exchange();
exchange.setName("");
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(false);
exchange.setAutoDelete(false);
System.out.println("[DataBaseManger] 第一阶段 添加默认数据成功 ");
}
最后将系统中的后续添加的方法写入代码中。
//添加程序后续方法
public void addExchange(Exchange exchange){
mateMapper.addExchange(exchange);
}
public void addQueue(MSGQueue queue){
mateMapper.addQueue(queue);
}
public void addBinding(Binding binding){
mateMapper.addBinding(binding);
}
public void deleteExchange(String exchangeName){
mateMapper.deleteExchange(exchangeName);
}
public void deleteQueue(String queueName){
mateMapper.deleteQueue(queueName);
}
public void deleteBinding(Binding binding){
mateMapper.deleteBinding(binding);
}
public List<Exchange> selectAllExchange(){
return mateMapper.selectAllExchange();
}
public List<MSGQueue> selectAllQueue(){
return mateMapper.selectAllQueue();
}
public List<Binding> selectAllBinding(){
return mateMapper.selectAllBinding();
}
public Exchange selectExchange(String exchangeName){
return mateMapper.selectExchange(exchangeName);
}
public MSGQueue selectQueue(String queueName){
return mateMapper.selectQueue(queueName);
}
public Binding selectBinding(Binding binding){
return mateMapper.selectBinding(binding);
}
接下来我们将对这个整合类进行单元测试。
注释解读:
@BeforEach
:每执行一个测试方法之前执行一次(一般用于初始化一个对象)
@AfterEach
:每执行完一个测试方法后执行一次(一般用于关闭资源)
针对DataBaseMAnger
进行一次单元测试。
单元测试用例和用例是互不干扰的,相互独立。
测试目标:
- 将三个模块的增删改查进行测试和修改
- 达到预期目标
首先我们创建一个 dataBaseManger
类以便后续调用,添加两个方法来创建数据库对象和删除数据库对象。
import com.example.mq.mqserver.datacenter.DataBaseManger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.SpringApplication;
@SpringBootTest
public class DataBaseMangerTest {
private DataBaseManger dataBaseManger;
@BeforeEach
public void setUp(){
//通过 SpringApplication 得到 mataMapper 实例
MqApplication.context = SpringApplication.run(MqApplication.class);
//初始化dataBaseManger对象
dataBaseManger.init();
}
@AfterEach
public void tearDown(){
//1.关闭 context 对象
//2. 删除数据库文件
MqApplication.context.close();
dataBaseManger.deleteDataBase();
}
}
在使用了初始化方法后,我们需要测试在初始化方法中的建表功能和添加默认数据是否成功。
@Test
public void testInit(){
//检查数据库状态,查询数据库中存在该表
List<Exchange> exchanges = dataBaseManger.selectAllExchange();
List<MSGQueue> msgQueues = dataBaseManger.selectAllQueue();
List<Binding> bindings = dataBaseManger.selectAllBinding();
//使用断言来判断表中数据
Assertions.assertEquals(1,exchanges.size());
//如果查询到存在数据,需要判断数据是不是我们默认添加的那一条
Assertions.assertEquals("",exchanges.get(0).getName());
Assertions.assertEquals(ExchangeType.DIRECT,exchanges.get(0).getType());
Assertions.assertTrue(exchanges.get(0).isDurable());
Assertions.assertEquals(0,msgQueues.size());
Assertions.assertEquals(0,bindings.size());
}
结果:
- 设置一个方法对一个交换机进行实例化
- 编写测试代码
- 将刚刚的交换机引入,然后进行添加到数据库操作
- 查询数据库中交换机是否和本地交换机相同
- 删除交换机,再次查询数据库中的交换机,是否为空
代码实现:
public Exchange createExchangeTest(String exchangeName){
Exchange exchange = new Exchange();
exchange.setName(exchangeName);
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
return exchange;
}
@Test
public void testExchange(){
//1.构造一个交换机并插入
Exchange expectExchange = createExchangeTest(exchangeName);
dataBaseManger.addExchange(expectExchange);
//2.查询这个交换机,比较结果比较是否一致
Exchange actualExchange = dataBaseManger.selectExchange(exchangeName);
Assertions.assertEquals(expectExchange,actualExchange);
//3.删除这个交换机
dataBaseManger.deleteExchange(exchangeName);
//4.测试交换机是否存在
actualExchange = dataBaseManger.selectExchange(exchangeName);
Assertions.assertNull(actualExchange);
}
代码实现:
public MSGQueue createQueueTest(String queueName){
MSGQueue msgQueue = new MSGQueue();
msgQueue.setName(queueName);
msgQueue.setDurable(true);
msgQueue.setExclusive(false);
msgQueue.setAutoDelete(false);
return msgQueue;
}
@Test
public void testQueue(){
//1. 创建一个队列并添加到数据库中
MSGQueue expectQueue = createQueueTest(queueName);
dataBaseManger.addQueue(expectQueue);
//2.查询数据库中姓名为 queueName 的队列
MSGQueue actualQueue = dataBaseManger.selectQueue(queueName);
//3.创建的队列和数据库中进行对比
Assertions.assertEquals(expectQueue,actualQueue);
//4.删除这个队列
dataBaseManger.deleteQueue(queueName);
//5.再次查询同样队列名称,是否为 null
actualQueue = dataBaseManger.selectQueue(queueName);
Assertions.assertNull(actualQueue);
}
代码实现:
//创建绑定需要一个交换机和一个队列
public Binding creatTestBinding(String exchangeName,String queueName){
Binding binding = new Binding();
binding.setExchangeName(exchangeName);
binding.setQueueName(queueName);
binding.setBindingKey("testBindKeys");
return binding;
}
@Test
public void bindTest(){
//1.添加绑定到数据库中
Binding expectBind = creatTestBinding(exchangeName, queueName);
dataBaseManger.addBinding(expectBind);
//2. 查询绑定数据然后对比数据
Binding actualBind = dataBaseManger.selectBinding(expectBind);
//3. 对比数据是否相同
Assertions.assertEquals(expectBind,actualBind);
//4. 删除绑定
dataBaseManger.deleteBinding(actualBind);
//5.查询绑定并比较数据
actualBind = dataBaseManger.selectBinding(expectBind);
Assertions.assertNull(actualBind);
}