Categories
程式開發

多端消息推送的設計思考


前言

在實際的項目中,很多時候都需要用到推送的場景,而有時候推送的終端不止一個,比如:一個訂單下單後,需要同時推送給手機和APP應用內。如果按照常規的做法,我們肯定就是按如下的方式來做推送:

// 调用手机推送方法
pushMobileMsg(T t);
// 调用APP应用推送方法
pushAPPMsg(T t);
// ...更多推送

但是我覺得這樣的寫法不是很優雅,同時在開發過程中,也會讓人很關注過度關注這個推送的過程,有沒有一種更好更優雅的方式,只需讓開發關注推送本身,而無需關注平台的做法呢?

於是,我想到了設計模式中的建造者模式,這樣的話,推送的代碼就變得非常簡潔了,而且開發只需要關注自己業務本身即可,項目採用SpringBoot,使用Lombok簡化代碼,代碼如下:

Message.builder().setApp(params).setSms(params).push();

如上的方式,只需要組好各自推送的參數即可,推送部分交給Message類去做,還可以結合MQ或者其他的來實現異步化推送。整理設計圖如下:

多端消息推送的設計思考 1

具體設計

首先定義一個推送平台的枚舉

/**
* 推送平台枚举
* @author Nil
* @date 2020/9/23 9:42
*/
@Getter
@RequiredArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MessagePushTypeEnum {

APP("app", "APP"),
SMS("sms", "短信"),
;

private final String code;
private final String desc;

@JsonCreator
public static MessagePushTypeEnum convert(@JsonProperty("code") String code) {
return Arrays.stream(MessagePushTypeEnum.values()).filter(e -> e.getCode().equals(code))
.findFirst().orElse(null);
}
}

然後定義一個消息基類,這個類只有一個對象集合,因為不管哪種推送方式,都應該有一個推送對象,因為這個對象可能一個,也可能多個,我這裡就直接定義成一個List來接收。所有推送類直接繼承該類即可。

/**
* 消息基类
* @author Nil
* @date 2020/9/23 9:42
*/
public class BaseMessage implements Serializable {

private static final long serialVersionUID = -1846052540919826933L;

/**
* 推送对象
*/
@Getter
protected List clientList;
}

所有推送類都是基於Builder模式來做的,沒有直接使用Lombok提供的@Builder註解,而是自己封裝的Builder,不使用這個註解的原因是:這樣做可能給使用者多寬的限度,比如說他無法很好的知道哪些參數是必填的,哪些是非必填的,在寫法上過於自由,自己封裝主要是為了能夠按照規範來使用。參數非空校驗直接使用了Lombok提供的@NonNull,如果您的項目是Spring5以上,也可以採用Spring提供的 jsr-305 相關註解。

/**
* APP推送类
* @author Nil
* @date 2020/9/23 9:42
*/
@ToString
public final class AppMessage extends BaseMessage implements Serializable {

private static final long serialVersionUID = 1854471077996480719L;

/**
* 消息标题
*/
@Getter
private final String title;

/**
* 推送内容
*/
@Getter
private final String content;

/**
* 构造器
* @param title 标题
* @return builder
*/
public static Builder builder(String title) {
return new Builder(title);
}

/**
* 构造器
* @param title 标题
* @param content 内容
* @return builder
*/
public static Builder builder(String title,String content) {
return new Builder(title, content);
}

public AppMessage(Builder builder) {
this.title = builder.title;
this.content = builder.content;
this.clientList = builder.clientList;
}

@NoArgsConstructor
public static class Builder {
private String title;
private String content;
private List clientList;

/**
* 构造器
* @param title 标题
*/
public Builder(@NonNull String title) {
this.title = title;
}

/**
* 构造器
* @param title 标题
* @param content 内容
*/
public Builder(@NonNull String title, String content) {
this.title = title;
this.content = content;
}

/**
* 设置推送对象
* @param client 推送对象
* @return builder
*/
public Builder setClient(String client) {
this.clientList = Collections.singletonList(client);
return this;
}

/**
* 设置推送对象集合
* @param clientList 推送对象集合
* @return builder
*/
public Builder setClientList(List clientList) {
this.clientList = clientList;
return this;
}

public AppMessage build() {
return new AppMessage(this);
}
}
}

/**
* 短信推送类
* @author Nil
* @date 2020/9/23 9:42
*/
@ToString
public final class SmsMessage extends BaseMessage implements Serializable {

private static final long serialVersionUID = -3005703042180596644L;

/**
* 模板参数
*/
@Getter
private final Map params;

/**
* 模板
*/
@Getter
private final String templateName;

/**
* 构造器
* @param client 推送对象
* @param params 参数
* @return builder
*/
public static Builder builder(String client, Map params) {
return new Builder(client, params);
}

/**
* 构造器
* @param client 推送对象
* @param params 参数
* @param templateName 模板
* @return builder
*/
public static Builder builder(String client, Map params, String templateName) { return new Builder(client, params, templateName); }

/**
* 构造器
* @param clientList 推送对象集合
* @param params 参数
* @return builder
*/
public static Builder builder(List clientList, Map params) {
return new Builder(clientList, params);
}

/**
* 构造器
* @param clientList 推送对象集合
* @param params 参数
* @param templateName 模板
* @return builder
*/
public static Builder builder(List clientList, Map params, String templateName) { return new Builder(clientList, params, templateName); }

public SmsMessage(Builder builder) {
this.params = builder.params;
this.clientList = builder.clientList;
this.templateName = builder.templateName;
}

@NoArgsConstructor
public static class Builder {
private Map params;
private String templateName;
private List clientList;

/**
* 构造器
* @param client 推送对象
* @param params 参数
*/
public Builder(@NonNull String client, @NonNull Map params) {
Assert.state(CollUtil.isEmpty(params), "params is not empty");
this.clientList = Collections.singletonList(client);
this.params = params;
}

/**
* 构造器
* @param clientList 推送对象集合
* @param params 参数
* @param templateName
*/
public Builder(@NonNull List clientList, @NonNull Map params, @NonNull String templateName) {
Assert.state(CollUtil.isEmpty(params), "params is not empty");
Assert.state(CollUtil.isEmpty(clientList), "clientList is not empty");
this.clientList = clientList;
this.params = params;
this.templateName = templateName;
}

/**
* 推送对象集合
* @param clientList 推送对象集合
* @return builder
*/
public Builder setClientList(@NonNull List clientList) {
Assert.state(CollUtil.isEmpty(clientList), "clientList is not empty");
this.clientList = clientList;
return this;
}

/**
* 设置模板名称
* @param templateName 模板名称{@link SmsChannelTemplateEnum}
* @return builder
*/
public Builder setTemplateName(@NonNull String templateName) {
this.templateName = templateName;
return this;
}

public SmsMessage build() {
return new SmsMessage(this);
}
}

}

然後Message主要由一個Map集合構成,這個Map的key為平台類型,value為就是上面的推送類,裡面也實現了java8 Function的寫法,這樣可以更好的使推送代碼和業務代碼解耦,這樣就可以走策略模式,從而尋找各自的實現邏輯。

/**
* 消息推送实体
* @author Nil
* @date 2020/9/23 9:42
*/
public class Message implements Serializable {

private static final long serialVersionUID = 452899906849843857L;

/**
* 负责推送的逻辑,静态注入Bean
*/
private static final PushMsgFactory pushMsgFactory = SpringContextHolder.getBean(PushMsgFactory.class);

/**
* 消息数据
*/
@Getter
private final Map msgMap;

public static Builder builder() {
return new Builder();
}

public Message(Builder builder) {
this.msgMap = builder.msgMap;
}

@NoArgsConstructor
public static class Builder {
private final Map msgMap = new HashMap(16);

/**
* APP推送
* @param appMessage 推送参数
* @return builder
*/
public Builder setApp(AppMessage appMessage) {
msgMap.put(MessagePushTypeEnum.APP.getCode(), appMessage);
return this;
}

/**
* APP推送(复杂逻辑建议使用该方法解耦)
* @param function 执行方法
* @param t 推送数据
* @return builder
*/
public Builder setApp(Function function, T t) {
msgMap.put(MessagePushTypeEnum.APP.getCode(), function.apply
return this;
}

/**
* APP推送
* @param appMessageList 推送参数
* @return builder
*/
public Builder setAppList(List appMessageList) {
msgMap.put(MessagePushTypeEnum.APP.getCode(), appMessageList);
return this;
}

/**
* APP推送(复杂逻辑建议使用该方法解耦)
* @param function 执行方法
* @param t 推送数据
* @return builder
*/
public Builder setAppList(Function function, T t) {
msgMap.put(MessagePushTypeEnum.APP.getCode(), function.apply
return this;
}

/**
* 短信
* @param smsMessage 推送数据
* @return builder
*/
public Builder setSms(SmsMessage smsMessage) {
msgMap.put(MessagePushTypeEnum.SMS.getCode(), smsMessage);
return this;
}

/**
* 短信(复杂逻辑建议使用该方法解耦)
* @param function 执行方法
* @param t 推送数据
* @return builder
*/
public Builder setSms(Function function, T t) {
msgMap.put(MessagePushTypeEnum.SMS.getCode(), function.apply
return this;
}

/**
* 推送消息
*/
public void push() {
new Message(this).getMsgMap().forEach((k, v) -> pushMsgFactory.getService(k).pushMessage(v));
}
}
}

PushMsgFactory的作用是用來分發消息,因為項目採用的是MQ,不同平台的消息走不同的隊列,為了避免過多的if-else的操作,使用了策略模式來做分發,如果您的項目沒有使用MQ等中間件,也可以利用Spring的事件機制來實現異步化操作。

下面先看下基於MQ的異步化分發,先定義一個分發接口,用於走不同策略,因為不同推送類型的對象可能是不同的類,所以這裡使用Object來接收參數。

/**
* 消息分发处理
* @author Nil
* @date 2020/9/23 9:42
*/
public interface IPushMessage {

String BEAN_NAME = "PushMessageHandler";

/**
* 推送消息
* @param object 推送信息
*/
default void pushMessage(Object object) {}
}

然後定義PushMsgFactory工廠,用來實現策略模式

/**
* 消息分发工厂
* @author Nil
* @date 2020/9/23 9:42
*/
@Component
@RequiredArgsConstructor
public final class PushMsgFactory {

@Autowired(required = false)
private final Map beanMap;

public IPushMessage getService(String messageType) {
if (StringUtils.isNotBlank(messageType)) {
MessagePushTypeEnum messagePushTypeEnum = MessagePushTypeEnum.convert(messageType);
if (ObjectUtil.isNotNull(messagePushTypeEnum)) {
return beanMap.get(messagePushTypeEnum.getCode() + IPushMessage.BEAN_NAME);
}
}
return new IPushMessage() {};
}
}

不同的推送實現IPushMessage接口即可

/**
* 短信推送处理
* @author Nil
* @date 2020/9/23 9:42
*/
@Service
@RequiredArgsConstructor
public class SmsPushMessageHandler implements IPushMessage {

private final RabbitTemplate rabbitTemplate;

@Override
public void pushMessage(Object object) {
if (ObjectUtil.isNotNull(object) && object instanceof SmsMessage) {
rabbitTemplate.convertAndSend("短信队列名", object);
}
}

}

APP推送的實現也是類似,這裡就貼代碼了,然後在對應的MQ處理handler中實現推送邏輯就可以了。

如果項目中沒有使用中間件,則可以通過Spring事件機制來實現異步這一操作,思想都是差不多的。

上面的都處理完以後,使用就變得非常簡單了,代碼如下:

如果是簡單邏輯的代碼,比如

AppMessage message = AppMessage.builder("这是测试", "test").setClient("id").build();
Message.builder().setApp(message).push();

兩行代碼就可以直接搞定了,如果業務代碼非常多,則可以使用Function來處理


public AppMessage pushMsg(Object object) {
// ....推送参数组装
}

// 直接使用java8的Function
Message.builder().setApp(this::pushMsg, object).push();

以上就是關於多端消息推送設計的全部內容,如果您覺得這篇文章有用的話可以點個贊,有什麼疑問者有更好的解決方法也可以在評論區留言大家一起討論。