多端消息推送的设计思考
前言
在实际的项目中,很多时候都需要用到推送的场景,而有时候推送的终端不止一个,比如:一个订单下单后,需要同时推送给手机和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 */@ToStringpublic 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 */@ToStringpublic 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@RequiredArgsConstructorpublic 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@RequiredArgsConstructorpublic 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的FunctionMessage.builder().setApp(this::pushMsg, object).push();
以上就是关于多端消息推送设计的全部内容,如果您觉得这篇文章有用的话可以点个赞,有什么疑问者有更好的解决方法也可以在评论区留言大家一起讨论。
版权声明: 本文为 InfoQ 作者【Nil】的原创文章。
原文链接:【http://xie.infoq.cn/article/81c0ff8440d71fb52cb54a2ec】。文章转载请联系作者。
Nil
非宁静无以致远 2020.09.17 加入
还未添加个人简介
评论 (6 条评论)