RabbitMQ 封裝
程式碼
https://gitee.com/wosperry/wosperry-rabbit-mqtest/tree/master
參考Abp事件匯流排的用法,對拷貝的Demo進行簡單封裝
定義 RabbitMQOptions
用於配置
{
"MyRabbitMQOptions": {
"UserName": "admin",
"Password": "admin",
"Host": "192.168.124.220",
"Port": 5672,
"ExchangeName": "PerryExchange"
}
}
public class MyRabbitMQOptions
{
public string UserName { get; set; }
public string Password { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public string ExchangeName { get; set; } = "";
}
定義 QueueNameAttribute
控制佇列名字
/// <summary>
/// 定義佇列名字,優先順序高於類完整名
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class QueueNameAttribute : Attribute
{
public string QueueName { get; }
public QueueNameAttribute(string queueName)
{
QueueName = queueName;
}
}
定義 IMyPublisher<T>
,通過注入某個型別的 IMyPublisher<T>
,自動序列化物件併發布到配置好的MQ裡
/// <summary>
/// 用於注入使用
/// </summary>
public interface IMyPublisher<T> where T : class
{
Task PublishAsync(T data, Encoding encoding = null);
}
public class MyPublisher<T> : IMyPublisher<T>, IDisposable where T : class
{
private readonly MyRabbitMQOptions _myOptions;
private readonly IConnection _connection;
private readonly IModel _channel;
private readonly string _queueName;
/// <summary>
/// 非注入時使用此構造方法
/// </summary>
public MyPublisher(IConnection connection)
{
_connection = connection;
}
/// <summary>
/// 依賴注入自動走這個構造方法
/// </summary>
/// <param name="optionsMonitor"></param>
/// <param name="factory"></param>
public MyPublisher(IOptionsMonitor<MyRabbitMQOptions> optionsMonitor, ConnectionFactory factory)
{
_myOptions = optionsMonitor.CurrentValue;
_connection = factory.CreateConnection();
// 建立通道
_channel = _connection.CreateModel();
// 宣告一個Exchange
_channel.ExchangeDeclare(_myOptions.ExchangeName, ExchangeType.Direct, false, false, null);
var type = typeof(T);
// 獲取類上的QueueNameAttribute特性,如果不存在則使用類的完整名
var attr = type.GetCustomAttribute<QueueNameAttribute>();
_queueName = string.IsNullOrWhiteSpace(attr?.QueueName) ? type.FullName : attr.QueueName;
// 宣告一個佇列
_channel.QueueDeclare(_queueName, false, false, false, null);
//將佇列繫結到交換機
_channel.QueueBind(_queueName, _myOptions.ExchangeName, _queueName, null);
}
/// <summary>
/// 釋出訊息
/// </summary>
public Task PublishAsync(T data, Encoding encoding = null)
{
// 物件轉 object[] 傳送
var msg = JsonConvert.SerializeObject(data);
byte[] bytes = (encoding ?? Encoding.UTF8).GetBytes(msg);
_channel.BasicPublish(_myOptions.ExchangeName, _queueName, null, bytes);
return Task.CompletedTask;
}
public void Dispose()
{
// 結束
_channel.Close();
_connection.Close();
}
}
定義 IMyEventHandler<T>
,供 NetCore 專案注入使用,配置後,可以在程式啟動的時候,找到該介面所有的實現類,並開啟消費者
/// <summary>
/// Handler的配置
/// </summary>
public class MyEventHandlerOptions
{
/// <summary>
/// 禁用 byte[] 解析
/// </summary>
public bool DisableDeserializeObject { get; set; } = false;
/// <summary>
/// 配置Encoding
/// </summary>
public Encoding Encoding { get; set; } = Encoding.UTF8;
}
public abstract class MyEventHandler<T> : IMyEventHandler<T> where T : class
{
private IModel _channel;
private string _queueName;
private EventingBasicConsumer _consumer;
public MyEventHandlerOptions Options = new()
{
DisableDeserializeObject = false
};
public void Begin(IConnection connection)
{
var type = typeof(T);
// 獲取類上的QueueNameAttribute特性,如果不存在則使用類的完整名
var attr = type.GetCustomAttribute<QueueNameAttribute>();
_queueName = string.IsNullOrWhiteSpace(attr?.QueueName) ? type.FullName : attr.QueueName;
//建立通道
_channel = connection.CreateModel();
_consumer = new EventingBasicConsumer(_channel);
_consumer.Received += MyReceivedHandler;
//消費者
_channel.BasicConsume(_queueName, false, _consumer);
}
// 收到訊息後
private void MyReceivedHandler(object sender, BasicDeliverEventArgs e)
{
try
{
// 如果未配置禁用則不解析,後面抽象方法的data引數會始終為空
if (!Options.DisableDeserializeObject)
{
T data = null;
// 反序列化為物件
var message = Options.Encoding.GetString(e.Body);
data = JsonConvert.DeserializeObject<T>(message);
OnReceivedAsync(data, message).Wait();
// 確認該訊息已被消費
_channel?.BasicAck(e.DeliveryTag, false);
}
}
catch (Exception ex)
{
OnConsumerException(ex);
}
}
/// <summary>
/// 收到訊息
/// </summary>
/// <param name="data">解析後的物件</param>
/// <param name="message">訊息原文</param>
/// <remarks>Options.DisableDeserializeObject為true時,data始終為null</remarks>
public abstract Task OnReceivedAsync(T data, string message);
/// <summary>
/// 異常
/// </summary>
/// <param name="ex">派生類不重寫的話,異常被隱藏</param>
public virtual void OnConsumerException(Exception ex)
{
}
}
給依賴注入寫一些擴充方法
public static class MyRabbiteMQExtensions
{
/// <summary>
/// 初始化訊息佇列,並新增Publisher到IoC容器
/// </summary>
/// <remarks>從Configuration讀取"MyRabbbitMQOptions配置項"</remarks>
public static IServiceCollection AddMyRabbitMQ(this IServiceCollection services, IConfiguration configuration)
{
#region 配置項
// 從Configuration讀取"MyRabbbitMQOptions配置項
var optionSection = configuration.GetSection("MyRabbitMQOptions");
// 這個myOptions是當前方法使用
MyRabbitMQOptions myOptions = new();
optionSection.Bind(myOptions);
// 加了這行,才可以注入IOptions<MyRabbitMQOptions>或者IOptionsMonitor<MyRabbitMQOptions>
services.Configure<MyRabbitMQOptions>(optionSection);
#endregion
// 加了這行,才可以注入任意型別引數的 IMyPublisher<> 使用
services.AddTransient(typeof(IMyPublisher<>), typeof(MyPublisher<>));
// 建立一個工廠物件,並配置單例注入
services.AddSingleton(new ConnectionFactory
{
UserName = myOptions.UserName,
Password = myOptions.Password,
HostName = myOptions.Host,
Port = myOptions.Port
});
return services;
}
/// <summary>
/// IServiceCollection的擴充方法,用於發現自定義的EventHandler並新增到服務容器
/// </summary>
/// <param name="types">包含了自定義Handler的類集合,可以使用assembly.GetTypes()</param>
/// <remarks>遍歷所有types,將繼承自IMyEventHandler的類註冊到容器</remarks>
public static IServiceCollection AddMyRabbitMQEventHandlers(this IServiceCollection services, Type[] types)
{
var baseType = typeof(IMyEventHandler);
foreach (var type in types)
{
// baseType可以放type,並且type不是baseType
if (baseType.IsAssignableFrom(type) && baseType != type)
{
// 瞬態注入配置
services.AddTransient(typeof(IMyEventHandler), type);
}
}
return services;
}
/// <summary>
/// 給app擴充方法
/// </summary>
/// <remarks>
/// 在IoC容器裡獲取到所有繼承自IMyEvetnHandler的實現類,並開啟消費者
/// </remarks>
public static IApplicationBuilder UseMyEventHandler(this IApplicationBuilder app)
{
var handlers = app.ApplicationServices.GetServices(typeof(IMyEventHandler));
var factory = app.ApplicationServices.GetService<ConnectionFactory>();
// 遍歷呼叫自定義的Begin方法
foreach (var h in handlers)
{
var handler = h as IMyEventHandler;
handler?.Begin(factory.CreateConnection());
}
return app;
}
}
在Net6 WebApi中使用
program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 新增MyRabbitMQ到services
builder.Services.AddMyRabbitMQ(builder.Configuration);
builder.Services.AddMyRabbitMQEventHandlers(typeof(PerryTest).Assembly.GetTypes());
var app = builder.Build();
// 使用 MyEventHandler
app.UseMyEventHandler();
app.MapControllers();
app.Run();
定義ETO
[QueueName("perry.test")]
public class PerryTest
{
public Guid Id { get; set; }
public string? Name { get; set; }
public int Count { get; set; }
public string? Remark { get; set; }
}
控制器中簡答檢查是否可以正常使用
[Route("api")]
[ApiController]
public class TestController : ControllerBase
{
public IMyPublisher<PerryTest> TestPublisher { get; }
public TestController(IMyPublisher<PerryTest> testPublisher)
{
TestPublisher = testPublisher;
}
[HttpGet("test")]
public async Task<string> TestAsync()
{
var data = new PerryTest()
{
Id = Guid.NewGuid(),
Name = "AAA",
Count = 123,
Remark = "哈哈哈"
};
await TestPublisher.PublishAsync(data);
return "傳送了一個訊息";
}
}
執行截圖
參考 .NET Core 使用RabbitMQ,拷貝了一些Demo
文章裡的生產者Demo
//建立連線工廠
ConnectionFactory factory = new ConnectionFactory
{
UserName = "admin",//使用者名稱
Password = "admin",//密碼
HostName = "192.168.157.130"//rabbitmq ip
};
//建立連線
var connection = factory.CreateConnection();
//建立通道
var channel = connection.CreateModel();
//宣告一個佇列
channel.QueueDeclare("hello", false, false, false, null);
Console.WriteLine("\nRabbitMQ連線成功,請輸入訊息,輸入exit退出!");
string input;
do
{
input = Console.ReadLine();
var sendBytes = Encoding.UTF8.GetBytes(input);
//釋出訊息
channel.BasicPublish("", "hello", null, sendBytes);
} while (input.Trim().ToLower()!="exit");
channel.Close();
connection.Close();
文章裡的消費者Demo
//建立連線工廠
ConnectionFactory factory = new ConnectionFactory
{
UserName = "admin",//使用者名稱
Password = "admin",//密碼
HostName = "192.168.157.130"//rabbitmq ip
};
//建立連線
var connection = factory.CreateConnection();
//建立通道
var channel = connection.CreateModel();
//事件基本消費者
EventingBasicConsumer consumer = new EventingBasicConsumer(channel);
//接收到訊息事件
consumer.Received += (ch, ea) =>
{
var message = Encoding.UTF8.GetString(ea.Body);
Console.WriteLine($"收到訊息: {message}");
//確認該訊息已被消費
channel.BasicAck(ea.DeliveryTag, false);
};
//啟動消費者 設定為手動應答訊息
channel.BasicConsume("hello", false, consumer);
Console.WriteLine("消費者已啟動");
Console.ReadKey();
channel.Dispose();
connection.Close();
這次封裝的總結
- 上網找個Demo
- 先按最簡單的寫法,寫完能正常使用的,功能獨立的程式碼。其實是提醒自己,不要陷入到雜七雜八的各項優化中去,先寫完實現再考慮怎麼去改成更加好的。
- 腦袋裡想著,我要怎麼樣使用這個功能,怎麼樣才能讓用的時候寫的程式碼少一些,配置簡單一些
- 檢查哪些內容應該分離到配置項,然後抽離出去
- 考慮要支援哪些型別的專案,如果想要支援低版本,可能需要降級一些依賴包
- 支援建構函式注入的話,要注意最多引數的建構函式給依賴注入使用,依賴注入用的建構函式不好被非注入時使用的話,考慮多提供一個給Framework用。