Bootstrap

多端消息推送的设计思考

前言

在实际的项目中,很多时候都需要用到推送的场景,而有时候推送的终端不止一个,比如:一个订单下单后,需要同时推送给手机和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();

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