多端消息推送的设计思考
前言
在实际的项目中,很多时候都需要用到推送的场景,而有时候推送的终端不止一个,比如:一个订单下单后,需要同时推送给手机和APP应用内。如果按照常规的做法,我们肯定就是按如下的方式来做推送:
// 调用手机推送方法
pushMobileMsg(T t);
// 调用APP应用推送方法
pushAPPMsg(T t);
// ...更多推送
但是我觉得这样的写法不是很优雅,同时在开发过程中,也会让人很关注过度关注这个推送的过程,有没有一种更好更优雅的方式,只需让开发关注推送本身,而无需关注平台的做法呢?
于是,我想到了设计模式中的建造者模式,这样的话,推送的代码就变得非常简洁了,而且开发只需要关注自己业务本身即可,项目采用SpringBoot,使用Lombok简化代码,代码如下:
Message.builder().setApp(params).setSms(params).push();
如上的方式,只需要组好各自推送的参数即可,推送部分交给Message类去做,还可以结合MQ或者其他的来实现异步化推送。整理设计图如下:

具体设计
首先定义一个推送平台的枚举
/**
* 推送平台枚举
* @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提供的 相关注解。
/**
* 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(t));
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(t));
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(t));
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();
以上就是关于多端消息推送设计的全部内容,如果您觉得这篇文章有用的话可以点个赞,有什么疑问者有更好的解决方法也可以在评论区留言大家一起讨论。