# 【公众号开发】公众号技术分享第二期 - 公众号模板消息 - 模板模式实战分享
# 打一波广告…
# 面试记 APP
Github:https://github.com/Guoqing815/interview
安卓 APP 下载:https://www.pgyer.com/interview_app_release
Brath 的个人博客:https://brath.top
面试记官方公众号,定期分享有趣的编程知识:https://mp.weixin.qq.com/s/jWs6lLHl5L-atXJhHc4YvA
# 前言
大家好啊,我是 Brath,一名激进的全栈开发者,从这期专栏开始,我会逐渐分享面试记项目中的部分优质源码给到大家。还希望大家多多关注!
上期我们介绍了微信模板消息的配置方式,以及 Java 代码示例,接下来我们要用更优雅的方式来实现发送模板消息!
# 注意:这是实战代码,不是 Demo!
# 模板模式介绍:
模板模式是一种行为型设计模式,它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
在模板模式中,我们定义一个抽象类,其中包含一个模板方法,该方法定义了算法的骨架,以及一些抽象的方法,这些方法需要子类去实现。子类可以继承该抽象类,并实现其中的抽象方法,从而完成具体的业务逻辑。
# 如何使用模板设计模式优雅的实现发送微信公众号模板消息?
# 一、设计模板架构
我们的业务就是发送模板消息,最开始我使用了 switch 分支来判断不同的模板消息,起初它是好用的,但是随着消息类型的增加,慢慢的代码开始冗余了起来,所以我想到了利用模板模式来彻底优化这个业务。
这次设计的模板架构分为五个模块:
# 1. 模板:定义每个模板的规范,抽取共性方法,凝练重要功能
# 2. 消息类型:分类管理,为不同的消息类型赋能
# 3. 类型配置:便携管理消息类型,摒弃了 if 与 switch 判断
# 4. 数据支撑:为模板消息提供基础数据支撑,例如公众号消息推送业务
# 5. 统一校验:在抽象的层面去校验模板合法性,校验通过发送消息
# 6. 消息注入:接口层面通过反射实例化消息,并将参数集合注入消息体
# 二、结构预览
# messageData 包,定义消息体,规范消息格式
1.IMessage 为模板通用定义,实现了两个方法,getMessageName 用于获取模板自己的名称,getWxMaTemplateData 用于获取模板的参数配置
2.LoginSuccessTemplateImpl 为登录成功模板消息的实例定义,实现了 IMessage 接口,并且重写 getMessageName 和 getWxMaTemplateData 方法,即能提供自己的名称和参数列表到接口定义中。
# messageModule 包,定义模板配置,发送流程
1.WechatMessageConfig 为模板配置,用于获取所有模板映射的集合,方便我们通过名称获取模板配置实例,摒弃了 ifelse 的判断模式。
2.WechatMessageSupport 为基础数据支撑,用于提供基础的微信模板发送消息的方法,以及模板消息注入方法的实现。
3.IWechatMessageExec 为发送模板的接口,用于实现发送模板消息的规范。
4.AbstractWechatMessageBase 为抽象的微信模板消息,用于抽象化管理所有模板消息,在这里做参数的校验,以及 unionId 的获取。
5.IWechatMessageExecImpl 为发送消息的实现,在抽象层面获取到 fromOpenId 后即可通过抽象层继承的 Support 层的微信模板发送消息的方法来发送微信模板消息。
预览了结构之后,可以更好的理解下面的代码实现
# 三、模板消息以及实现类型
现在开始实现代码的部分,首先我们来定义一个消息体的抽象接口,用来表示统一的模板
# 抽象接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface IMessage { String getMessageName () ; List<WxMaTemplateData> getWxMaTemplateData () ; }
# 实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Component("loginSuccessTemplateMessage") @ApiModel(value = "登录成功模板消息") @Data @Accessors(chain = true) public class LoginSuccessTemplateImpl implements IMessage { @NotBlank(message = "登录用户不能为空") private String loginUser; @NotBlank(message = "登录地址不能为空") private String loginAddr; @NotBlank(message = "登录IP不能为空") private String loginIp; @Override public String getMessageName () { return NotifyType.LOGIN_SUCCESS_TEMPLATE_MESSAGE.getType(); } @Override public List<WxMaTemplateData> getWxMaTemplateData () { List<WxMaTemplateData> params = new ArrayList <>(); params.add(new WxMaTemplateData ("keyword1" , loginUser + ",您的账号刚刚在" + loginAddr + "登录。请关注本次登录时间和地点,若是您本人登录,请忽略本提醒,若非本人登录,请点击修改密码,并检查账号最近登录和操作行为是否有问题。" , "#173177" )); params.add(new WxMaTemplateData ("keyword2" , new SimpleDateFormat ("yyyy-MM-dd HH:mm" ).format(new Date ()), "#173177" )); params.add(new WxMaTemplateData ("keyword3" , loginIp, "#173177" )); return params; } }
注意:NotifyType 表示消息的类型,你可以自己定义常量来代替这段代码
# 这两段代码实现了一个抽象的消息模板接口,并创建了登录成功模板消息类型,实现了对应的方法
# 四、数据支撑以及数据配置
# 数据支撑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 @Service public class WechatMessageSupport extends WechatMessageConfig { private static final Logger logger = LoggerFactory.getLogger(WechatMessageSupport.class); protected static NotifyService notifyService; @Resource public void setNotifyService (NotifyService notifyService) { WechatMessageSupport.notifyService = notifyService; } public static IMessage injectMessage (HashMap<String, Object> paramMap, IMessage iMessage) { Class<?> clazz = iMessage.getClass(); Map<String, Method> setterMap = new HashMap <>(16 ); for (Method method : clazz.getMethods()) { if (method.getName().startsWith(Consts.SET)) setterMap.put(method.getName(), method); } for (Map.Entry<String, Object> entry : paramMap.entrySet()) { String paramName = entry.getKey(); Object paramValue = entry.getValue(); String setterName = generateSetterName(paramName); Method method = setterMap.get(setterName); if (method != null ) { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length != 1 || !parameterTypes[0 ].isInstance(paramValue)) { logger.warn("无法为消息对象设置参数 {} = {}, 参数类型不匹配" , paramName, paramValue); continue ; } if (!method.isAccessible()) { method.setAccessible(true ); } try { method.invoke(iMessage, paramValue); } catch (IllegalAccessException | InvocationTargetException e) { logger.error("无法为消息对象设置参数 {} = {}" , paramName, paramValue, e); } } else { logger.warn("无法为消息对象设置参数 {} = {}" , paramName, paramValue); } } return iMessage; } private static String generateSetterName (String propertyName) { return Optional.ofNullable(propertyName).filter(name -> !name.isEmpty()).map(name -> "set" + Character.toUpperCase(name.charAt(0 )) + name.substring(1 )).orElse(null ); } }
# 数据配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Service public class WechatMessageConfig { public static Map<String, IMessage> messageStrategyGroup = new ConcurrentHashMap <>(8 ); @Resource private void initService (IMessage[] properties) { Arrays.stream(properties).forEach( property -> messageStrategyGroup.put(property.getMessageName(), property) ); } }
这两段代码我们实现了数据的基础支撑,以及类型模式配置。
# 五、注入抽象消息到模板,并且实现发送流程
# 抽象消息接口:定义发送模板消息的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface IWechatMessageExec { Object sendMessage (IMessage message, String unionId) ; }
# 抽象消息实现:在抽象消息层面去判断消息体的参数合法性,并且将 unionId 转换为 fromOpenId
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public abstract class AbstractWechatMessageBase extends WechatMessageSupport implements IWechatMessageExec { private static final Logger logger = LoggerFactory.getLogger(AbstractWechatMessageBase.class); @Override public Object sendMessage (IMessage message, String unionId) { ValidatorUtils.validateEntity(message); if (AssertUtil.isEmpty(unionId)) { logger.error(DEFALUT_FAIL, ResponseCode.DATA_DOES_NOT_EXIST.desc()); return ResponseUtil.fail(ResponseCode.DATA_DOES_NOT_EXIST.desc()); } String fromOpenId = wxservFollowService.getFromOpenIdByUnionId(unionId); if (AssertUtil.isEmpty(fromOpenId)) { logger.error(DEFALUT_FAIL, ResponseCode.DATA_DOES_NOT_EXIST.desc()); return ResponseUtil.fail(ResponseCode.DATA_DOES_NOT_EXIST.desc()); } return this .sendMessage(message, fromOpenId, message.getMessageName()); } protected abstract Object sendMessage (IMessage message, String openId, String messageType) ; }
# 具体消息实现:在抽象层确保消息体完整性后,调用抽象层面的具体实现类,调用数据支撑层的 notifyService 服务完成发送模板消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Component("wechatMessageExecImpl") public class IWechatMessageExecImpl extends AbstractWechatMessageBase { private static final Logger logger = LoggerFactory.getLogger(IWechatMessageExecImpl.class); @Override protected Object sendMessage (IMessage message, String openId, String messageType) { logger.info("【公众号模板消息推送】:开始 -- messageType:{},openId:{}" , messageType, openId); try { notifyService.notifyWxTemplate( openId, NotifyType.of(messageType), message.getWxMaTemplateData() ); logger.info("【公众号模板消息推送】:结束 -- messageType:{},openId:{}" , messageType, openId); } catch (Exception e) { e.printStackTrace(); logger.error("【公众号模板消息推送】:异常 -- {}" , e.getMessage()); return ResponseUtil.fail(e.getMessage()); } return ResponseUtil.ok(); } }
# SpringBoot 配置文件、消息类型、NotifyService 实现:
# pom 文件配置依赖:这里我们使用大神 binarywang 的 miniapp 库来实现微信支持
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.3 .0 </version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>3.3 .0 </version> </dependency>
# yaml 配置:实现自定义配置类,配置模板集合,以及是否开启
1 2 3 4 5 6 7 system: notify: wx: enable: true template: - name: loginSuccessTemplateMessage templateId: qDHo-UXXXXXXXXXXXXXXXXXXXXX
# 配置信息类:使用静态内部类实现子配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data @ConfigurationProperties(prefix = "system.notify") public class NotifyProperties { private Wx wx; @Data public static class Wx { private boolean enable; private List<Map<String, String>> template = new ArrayList <>(); } }
# 自动配置类:使用 EnableConfigurationProperties 注解注入 NotifyProperties 配置并实现 notifyService 配置注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Configuration @EnableConfigurationProperties(NotifyProperties.class) public class NotifyAutoConfiguration { private final NotifyProperties properties; public NotifyAutoConfiguration (NotifyProperties properties) { this .properties = properties; } @Bean(name = "NotifyService") public NotifyService notifyService () { NotifyService notifyService = new NotifyService (); NotifyProperties.Wx wxConfig = properties.getWx(); if (wxConfig.isEnable()) { notifyService.setWxTemplateSender(wxTemplateSender()); NotifyService.wxTemplate = wxConfig.getTemplate(); } return notifyService; } @Bean public WxTemplateSender wxTemplateSender () { WxTemplateSender wxTemplateSender = new WxTemplateSender (); return wxTemplateSender; } }
# 消息类型:配置模板消息类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public enum NotifyType { LOGIN_SUCCESS_TEMPLATE_MESSAGE("loginSuccessTemplateMessage" ); private static final EnumMap<NotifyType, String> typeMap = new EnumMap <>(NotifyType.class); static { for (NotifyType notifyType : NotifyType.values()) { typeMap.put(notifyType, notifyType.type); } } private String type; NotifyType(String type) { this .type = type; } public static NotifyType of (String messageType) { for (NotifyType notifyType : typeMap.keySet()) { if (typeMap.get(notifyType).equals(messageType)) { return notifyType; } } throw new IllegalArgumentException ("No such enum object for the given messageType" ); } public String getType () { return this .type; } }
# NotifyService 服务实现:实现发送消息的服务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @Data @NoArgsConstructor public class NotifyService { private MailSender mailSender; private String sendFrom; private String sendTo; private WxTemplateSender wxTemplateSender; public static List<Map<String, String>> wxTemplate = new ArrayList <>(); public boolean isWxEnable () { return wxTemplateSender != null ; } @Async public void notifyWxTemplate (String touser, NotifyType notifyType, List<WxMaTemplateData> params) { if (wxTemplateSender == null ) return ; String templateId = getTemplateId(notifyType, wxTemplate); wxTemplateSender.sendWechatMsg(touser, templateId, params); } @Async public void notifyWxTemplate (String touser, NotifyType notifyType, List<WxMaTemplateData> params, String page) { if (wxTemplateSender == null ) return ; String templateId = getTemplateId(notifyType, wxTemplate); wxTemplateSender.sendWechatMsg(touser, templateId, params, page); } private String getTemplateId (NotifyType notifyType, List<Map<String, String>> values) { for (Map<String, String> item : values) { String notifyTypeStr = notifyType.getType(); if (item.get("name" ).equals(notifyTypeStr)) return item.get("templateId" ); } return null ; } }
# WxTemplateSender 发送者实现:实现发送消息的基础类,使用 WxMaService 库发送消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 public class WxTemplateSender { private final Log logger = LogFactory.getLog(WxTemplateSender.class); @Autowired private WxMaService wxMaService; public void sendWechatMsg (String touser, String templatId, String[] parms) { sendMsg(touser, templatId, parms, "" , "" , "" ); } public void sendWechatMsg (String touser, String templatId, List<WxMaTemplateData> parms) { sendMsg(touser, templatId, parms, "" , "" , "" ); } public void sendWechatMsg (String touser, String templatId, List<WxMaTemplateData> parms, String page) { sendMsg(touser, templatId, parms, page, "" , "" ); } private void sendMsg (String touser, String templatId, String[] parms, String page, String color, String emphasisKeyword) { if (touser == null ) return ; WxMaTemplateMessage msg = new WxMaTemplateMessage (); msg.setTemplateId(templatId); msg.setToUser(touser); msg.setFormId(touser); msg.setPage(page); msg.setColor(color); msg.setEmphasisKeyword(emphasisKeyword); msg.setData(createMsgData(parms)); try { wxMaService.getMsgService().sendTemplateMsg(msg); } catch (Exception e) { e.printStackTrace(); } } private static final JsonParser JSON_PARSER = new JsonParser (); private static final String SEND_MSG_API = "https://api.weixin.qq.com/cgi-bin/message/template/send" ; private void sendMsg (String touser, String templatId, List<WxMaTemplateData> parms, String page, String color, String emphasisKeyword) { if (touser == null ) return ; WxMaTemplateMessage msg = new WxMaTemplateMessage (); msg.setTemplateId(templatId); msg.setToUser(touser); msg.setFormId(touser); msg.setPage(page); msg.setColor(color); msg.setEmphasisKeyword(emphasisKeyword); msg.setData(parms); try { String responseContent = this .wxMaService.post(SEND_MSG_API, msg.toJson()); JsonObject jsonObject = JSON_PARSER.parse(responseContent).getAsJsonObject(); if (jsonObject.get(WxMaConstants.ERRCODE).getAsInt() != 0 ) { throw new WxErrorException (WxError.fromJson(responseContent)); } } catch (WxErrorException e) { logger.error("【微信消息模板】:服务端异常,原因可能是:{}" , e); e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } private List<WxMaTemplateData> createMsgData (String[] parms) { List<WxMaTemplateData> dataList = new ArrayList <WxMaTemplateData>(); for (int i = 1 ; i <= parms.length; i++) { dataList.add(new WxMaTemplateData ("keyword" + i, parms[i - 1 ])); } return dataList; } }
# 在配置完如上代码后,我们的代码就完成了,接下来即可通过单元测试来测试模板消息!
# 六、使用抽象模板发送消息
单元测试发送消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SpringRunnerTest { private Logger logger = LoggerFactory.getLogger(SpringRunnerTest.class); @Resource private IWechatMessageExec messageExec; @Test public void test_sendMessage () { ProductTestTemplateImpl productTestTemplate = new ProductTestTemplateImpl () .setCompanyName("测试公司" ) .setUserName("Brath" ) .setVersion("2.0.6" ); RegistTemplateImpl registTemplate = new RegistTemplateImpl (); LoginSuccessTemplateImpl loginSuccessTemplate = new LoginSuccessTemplateImpl () .setLoginUser("Brath" ) .setLoginAddr("JVM" ) .setLoginIp("0.0.0.0" ); messageExec.sendMessage(loginSuccessTemplate, "o-KKr6Qwsxxxxxxxxxxxxxxx" ); } }
# 测试成功~
# tips:如果你觉得 Brath 分享的代码还可以的话,请将我分享给更多需要帮助的人~
# 到此为止,公众号发送模板消息的知识分享就结束啦,还请同学们多多关注 InterviewCoder,做一个激进的开发者,为了更好的你,也为了更好的世界!
# 完结撒花❀
# 关于我
Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!