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代码示例,接下来我们要用更优雅的方式来实现发送模板消息!
模板模式是一种行为型设计模式,它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
在模板模式中,我们定义一个抽象类,其中包含一个模板方法,该方法定义了算法的骨架,以及一些抽象的方法,这些方法需要子类去实现。子类可以继承该抽象类,并实现其中的抽象方法,从而完成具体的业务逻辑。
我们的业务就是发送模板消息,最开始我使用了switch分支来判断不同的模板消息,起初它是好用的,但是随着消息类型的增加,慢慢的代码开始冗余了起来,所以我想到了利用模板模式来彻底优化这个业务。
这次设计的模板架构分为五个模块:
1.IMessage为模板通用定义,实现了两个方法,getMessageName用于获取模板自己的名称,getWxMaTemplateData用于获取模板的参数配置
2.LoginSuccessTemplateImpl为登录成功模板消息的实例定义,实现了IMessage接口,并且重写getMessageName和getWxMaTemplateData方法,即能提供自己的名称和参数列表到接口定义中。
1.WechatMessageConfig为模板配置,用于获取所有模板映射的集合,方便我们通过名称获取模板配置实例,摒弃了ifelse的判断模式。
2.WechatMessageSupport为基础数据支撑,用于提供基础的微信模板发送消息的方法,以及模板消息注入方法的实现。
3.IWechatMessageExec为发送模板的接口,用于实现发送模板消息的规范。
4.AbstractWechatMessageBase为抽象的微信模板消息,用于抽象化管理所有模板消息,在这里做参数的校验,以及unionId的获取。
5.IWechatMessageExecImpl为发送消息的实现,在抽象层面获取到fromOpenId后即可通过抽象层继承的Support层的微信模板发送消息的方法来发送微信模板消息。
预览了结构之后,可以更好的理解下面的代码实现
现在开始实现代码的部分,首先我们来定义一个消息体的抽象接口,用来表示统一的模板
/**
* @author: Brath
* @date: 2023-06-5 18:10
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 消息体的抽象接口
*/
public interface IMessage {
/**
* 获取消息名称
*
* @return
*/
String getMessageName();
/**
* 获取模板消息集合
*
* @return
*/
List<WxMaTemplateData> getWxMaTemplateData();
}
/**
* @author: Brath
* @date: 2023-06-5 18:13
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 登录成功模板消息
*/
@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表示消息的类型,你可以自己定义常量来代替这段代码
/**
* @author: Brath
* @date: 2023-06-5 19:11
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: WechatMessageSupport
*/
@Service
public class WechatMessageSupport extends WechatMessageConfig {
private static final Logger logger = LoggerFactory.getLogger(WechatMessageSupport.class);
/**
* Information service interface
*/
protected static NotifyService notifyService;
/**
* 注入消息服务接口
*
* @param notifyService
*/
@Resource
public void setNotifyService(NotifyService notifyService) {
WechatMessageSupport.notifyService = notifyService;
}
/**
* 注入消息体
*
* @param paramMap
* @param iMessage
*/
public static IMessage injectMessage(HashMap<String, Object> paramMap, IMessage iMessage) {
// 获取class模板
Class<?> clazz = iMessage.getClass();
// 缓存setter方法
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;
}
/**
* 生成setter方法名称
*
* @param propertyName
* @return
*/
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);
}
}
/**
* @author: Brath
* @date: 2023-06-5 19:39
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: WechatMessageConfig
*/
@Service
public class WechatMessageConfig {
/**
* 消息策略组映射表
* 疑:为什么初始值会设置成8?而不是6呢?
* 答:因为 HashMap 的实现可能会自动将 initcp 调整为接近 2 的幂次方的值,以便更好地处理哈希冲突
*/
public static Map<String, IMessage> messageStrategyGroup = new ConcurrentHashMap<>(8);
/**
* 初始化消息组
*
* @param properties
*/
@Resource
private void initService(IMessage[] properties) {
Arrays.stream(properties).forEach(
property -> messageStrategyGroup.put(property.getMessageName(), property)
);
}
}
这两段代码我们实现了数据的基础支撑,以及类型模式配置。
/**
* @author: Brath
* @date: 2023-06-5 20:23
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: IWechatMessageExec
*/
public interface IWechatMessageExec {
/**
* 发送模板消息
*/
Object sendMessage(IMessage message, String unionId);
}
/**
* @author: Brath
* @date: 2023-06-6 07:12
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: AbstractWechatMessage
*/
public abstract class AbstractWechatMessageBase extends WechatMessageSupport implements IWechatMessageExec {
private static final Logger logger = LoggerFactory.getLogger(AbstractWechatMessageBase.class);
/**
* 发送抽象消息,准备消息体,转换FromOpenID
*/
@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());
}
//这段代码原本在WechatMessageSupport层实现,被我剔除掉了,这个需要大家去自己实现收集公众号关注人的fromOpenId,不是微信的openId,是关注了公众号后,产生的openId。
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());
}
//总之fromOpenId就是关注了公众号后产生的唯一ID
return this.sendMessage(message, fromOpenId, message.getMessageName());
}
/**
* 发送抽象消息
*
* @param message
* @param openId
* @param messageType
* @return
*/
protected abstract Object sendMessage(IMessage message, String openId, String messageType);
}
/**
* @author: Brath
* @date: 2023-06-6 07:40
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 模板消息推送实现
*/
@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();
}
}
<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>
system:
notify:
wx:
enable: true #启动模板消息
template:
- name: loginSuccessTemplateMessage #登录成功模板消息
templateId: qDHo-UXXXXXXXXXXXXXXXXXXXXX #公众平台模板ID
/**
* @author: Brath
* @date: 2023-06-6 07:40
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: NotifyProperties
*/
@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<>();
}
}
/**
* @author: Brath
* @date: 2023-06-6 07:40
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 挂载NotifyProperties类,获取配置信息并自动加载
*/
@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;
}
}
/**
* @author: Brath
* @date: 2023-05-25 08:49
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: NotifyType
*/
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;
}
}
/**
* @author: Brath
* @date: 2023-06-6 07:40
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 公众号模板消息服务
*/
@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;
}
/**
* 微信模版消息通知,不跳转
*
* 该方法会尝试从数据库获取缓存的FormId去发送消息
*
* @param touser 接收者openId
* @param notifyType 通知类别,通过该枚举值在配置文件中获取相应的模版ID
* @param params 通知模版内容里的参数,类似"您的验证码为{1}"中{1}的值
*/
@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);
}
/**
* 微信模版消息通知,带跳转
*
* 该方法会尝试从数据库获取缓存的FormId去发送消息
*
* @param touser 接收者openId
* @param notifyType 通知类别,通过该枚举值在配置文件中获取相应的模版ID
* @param params 通知模版内容里的参数,类似"您的验证码为{1}"中{1}的值
* @param page 点击消息跳转的页面
*/
@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);
}
/**
* 获取模板ID
*
* @param notifyType
* @param values
* @return
*/
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;
}
}
/**
* @author: Brath
* @date: 2023-06-6 07:40
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 微信模版消息通知
*/
public class WxTemplateSender {
private final Log logger = LogFactory.getLog(WxTemplateSender.class);
@Autowired
private WxMaService wxMaService;
/**
* 发送微信消息(模板消息),不带跳转
*
* @param touser
* 用户 OpenID
* @param templatId
* 模板消息ID
* @param parms
* 详细内容
*/
public void sendWechatMsg(String touser, String templatId, String[] parms) {
sendMsg(touser, templatId, parms, "", "", "");
}
/**
* 发送微信消息(模板消息),不带跳转
*
* @param touser
* 用户 OpenID
* @param templatId
* 模板消息ID
* @param parms
* 详细内容
*/
public void sendWechatMsg(String touser, String templatId, List<WxMaTemplateData> parms) {
sendMsg(touser, templatId, parms, "", "", "");
}
/**
* 发送微信消息(模板消息),带跳转
*
* @param touser
* 用户 OpenID
* @param templatId
* 模板消息ID
* @param parms
* 详细内容
* @param page
* 跳转页面
*/
public void sendWechatMsg(String touser, String templatId, List<WxMaTemplateData> parms, String page) {
sendMsg(touser, templatId, parms, page, "", "");
}
/**
* 发送消息基类
*
* @param touser
* @param templatId
* @param parms
* @param page
* @param color
* @param emphasisKeyword
*/
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;
}
}
单元测试发送消息:
/**
* @author: Brath
* @date: 2023-06-6 08:21
* @github: https://github.com/Guoqing815
* @Copyright: 公众号:InterviewCoder | 博客:https://brath.top - 为了更好的你,也为了更好的世界。
* @description: 单测
*/
@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");
}
}
Brath是一个热爱技术的Java程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!