多端消息推送的设计思考

用户头像
Nil
关注
发布于: 2020 年 09 月 24 日
多端消息推送的设计思考

前言

在实际的项目中,很多时候都需要用到推送的场景,而有时候推送的终端不止一个,比如:一个订单下单后,需要同时推送给手机和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<String>来接收。所有推送类直接继承该类即可。

/**
* 消息基类
* @author Nil
* @date 2020/9/23 9:42
*/
public class BaseMessage implements Serializable {
private static final long serialVersionUID = -1846052540919826933L;
/**
* 推送对象
*/
@Getter
protected List<String> 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<String> 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<String> 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<String, String> params;
/**
* 模板
*/
@Getter
private final String templateName;
/**
* 构造器
* @param client 推送对象
* @param params 参数
* @return builder
*/
public static Builder builder(String client, Map<String, String> params) {
return new Builder(client, params);
}
/**
* 构造器
* @param client 推送对象
* @param params 参数
* @param templateName 模板
* @return builder
*/
public static Builder builder(String client, Map<String, String> params, String templateName) { return new Builder(client, params, templateName); }
/**
* 构造器
* @param clientList 推送对象集合
* @param params 参数
* @return builder
*/
public static Builder builder(List<String> clientList, Map<String, String> params) {
return new Builder(clientList, params);
}
/**
* 构造器
* @param clientList 推送对象集合
* @param params 参数
* @param templateName 模板
* @return builder
*/
public static Builder builder(List<String> clientList, Map<String, String> 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<String, String> params;
private String templateName;
private List<String> clientList;
/**
* 构造器
* @param client 推送对象
* @param params 参数
*/
public Builder(@NonNull String client, @NonNull Map<String, String> 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<String> clientList, @NonNull Map<String, String> 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<String> 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<String, Object> msgMap;
public static Builder builder() {
return new Builder();
}
public Message(Builder builder) {
this.msgMap = builder.msgMap;
}
@NoArgsConstructor
public static class Builder {
private final Map<String, Object> 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 <T> Builder setApp(Function<T, AppMessage> function, T t) {
msgMap.put(MessagePushTypeEnum.APP.getCode(), function.apply(t));
return this;
}
/**
* APP推送
* @param appMessageList 推送参数
* @return builder
*/
public Builder setAppList(List<AppMessage> appMessageList) {
msgMap.put(MessagePushTypeEnum.APP.getCode(), appMessageList);
return this;
}
/**
* APP推送(复杂逻辑建议使用该方法解耦)
* @param function 执行方法
* @param t 推送数据
* @return builder
*/
public <T> Builder setAppList(Function<T, List<AppMessage>> 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 <T> Builder setSms(Function<T, SmsMessage> 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<String, IPushMessage> 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();

以上就是关于多端消息推送设计的全部内容,如果您觉得这篇文章有用的话可以点个赞,有什么疑问者有更好的解决方法也可以在评论区留言大家一起讨论。



发布于: 2020 年 09 月 24 日 阅读数: 1170
用户头像

Nil

关注

非宁静无以致远 2020.09.17 加入

还未添加个人简介

评论 (6 条评论)

发布
用户头像
能异步实现的代码,已经不在业务系统里同步实现了。
2020 年 09 月 27 日 06:55
回复
我这里发送的时候是异步的,但是生成消息是同步的。
2020 年 09 月 27 日 08:52
回复
用户头像
观察者模式(发布订阅)模式也可以实现
2020 年 09 月 27 日 01:44
回复
用户头像
技巧使用得不错,而且讲解很细致,赞一个!!SpringContextHolder是不是可以调整使用BeanFactory,不用再去定义一个Factory,当然接口的不同实现,serviceName规则可以是appPushMessage和smsPushMessage 。。java8的Function使用让代码变得更简练~~
2020 年 09 月 25 日 16:08
回复
嗯嗯,也是可以的,但是直接使用BeanFactory的ApplicationContext来获取Bean的话,如果Bean不存在会直接抛异常,我这里给了一个默认的实现。
2020 年 09 月 25 日 17:06
回复
对于报错应该不应该,个人看法是应该报错的就不往后延迟了,早死早投胎
2020 年 09 月 28 日 16:22
回复
没有更多了
多端消息推送的设计思考