一.需求场景
最近我们系统要开发个对接新邮箱(敏感邮件,不可截图不可转发)的发送功能,我们需求业务场景是这样的(已上线,同事都夸设计牛逼):
- 入口是点击某个人信息进去。
- 接口逻辑查询该邮件模板的默认信息(模板主题、发件人为系统公邮账号、收件人为点击的这个人信息、正文为产品制定的默认模板内容,里面有很多变量根据某些条件动态替换变量值),然后全部回显到我们的邮件编辑界面。
- 然后富文本框,用户可以修改主题、收件人、抄送人、邮件正文等等信息。
- 用户确认无误了,则调用我们的发送邮件接口,异步发送邮件,及时返回"邮件正在发送中,请留意新邮箱"。
二.可能面临的痛点
好啦,上面业务场景大概介绍完毕。
- 我先说下普通开发需求(你做你的他做他的)的痛点:我们系统因为邮件入口之后肯定会比较多(他们很喜欢在系统发邮件然后做相关功能),这个敏感加密邮件是我第一次对接的,然后我开发的这个发送邮件入口是第一版,依我对我们业务的了解,之后肯定还会有12345678…个入口,不同的默认邮件模板、如果遇到他们觉得给领导汇报发出去的邮件文案不合适需要调整默认模板内容等等,肯定得找我们开发及时修改文案,紧急版本等等(之前就经历过),然后涉及各个大领导审批,甚是麻烦。如果各个入口不设计,邮件正文甚至可能都在业务层自己处理了(接触过邮件发送的小伙伴们肯定见识过),要改正文真的就只能发版本解决了。
- 如果不统一下设计方案,可能这个同事开发一个查询接口另一个同事开发一个查询接口,不同的VO数据结构返回,不同的请求入参QO,也是增加了前端同事的联调工作量。
- 明明一个很简单的功能让系统显得很臃肿,甚至到处都是邮件发送的方法,QO,VO等等
咱们是优雅+代码洁癖人士,肯定的好好设计一番~ ok,废话不多说,开始演示我们的设计思路吧~
三.Spring事件监听机制结合策略+模板方法完成邮件发送设计
上一篇文章给大家详细说了下Spring的事件侦听机制,这里就是利用这机制生产实践了,上demo
1.定义查询QO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MailInfoQO {
@ApiModelProperty("模板编号")
private String templateCode;
}
2.定义返回VO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class MailInfoVO {
@ApiModelProperty("发件人")
private String senderAddress;
@ApiModelProperty("邮件标题")
private String title;
@ApiModelProperty("邮件正文")
private String content;
@ApiModelProperty("收件人列表")
private List<MailReceiverVO> receiverAddressList;
@ApiModelProperty("抄送人列表")
private List<MailReceiverVO> ccReceiverAddressList;
}
3.Contoller
@ApiOperation("测试获取邮件信息")
@RequestMapping(value = "/getMailInfo", method = RequestMethod.POST)
public CommonResult<MailInfoVO> getMailInfo(@RequestBody MailInfoQO mailInfoQO) {
return CommonResult.success(testCaseService.getMailInfo(mailInfoQO));
}
4.Service实现
- 利用Spring的事件侦听机制,侦听Spring的容器刷新事件ContextRefreshedEvent
上一篇文章已经简单分析过Spring事件侦听机制的源码了(会找到所有加了@EventListener注解的方法,然后给方法添加侦听器,只要有这个事件触发就会调用我们的侦听器执行)
- 把注解bean都添加到成员变量mailTemplateMap中
// 所有的<邮件模板编号,具体模板实现> map集合
private static Map<String, AbstractMailTemplate> mailTemplateMap = new HashMap<>(16);
@EventListener
public void initMailTemplateMap(ContextRefreshedEvent contextRefreshedEvent){
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
// 拿到我们的MailTemplate注解bean
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(MailTemplate.class);
if(CollectionUtils.isEmpty(beansWithAnnotation)){
return;
}
// 注解bean都添加到成员变量mailTemplateMap中,key为邮件模板编号,value为具体的模板实现
beansWithAnnotation.forEach((key,value)->{
String bizType = value.getClass().getAnnotation(MailTemplate.class).value();
mailTemplateMap.put(bizType , (AbstractMailTemplate) value);
});
}
@Override
public MailInfoVO getMailInfo(MailInfoQO mailInfoQO) {
// 基于策略模式:根据模板编号拿到对应的模板实现
AbstractMailTemplate abstractMailTemplate = mailTemplateMap.get(mailInfoQO.getTemplateCode());
Optional.ofNullable(abstractMailTemplate).orElseThrow(()->new TulingMallException(ResultCode.FAILED.getCode(),"模板编号有误"));
abstractMailTemplate.mailInfoQO = mailInfoQO;
// 模板方法设计模式:组装当前邮件模板
return abstractMailTemplate.mailTemplateMethod();
}
前端请求该接口,入参为邮件的模板编号(我们业务场景还带个人员id),这样就直接把这个邮件模板的完整邮箱信息发出去了,怎么做到的呢?好的,接着看具体的策略+模板方法。
5.定义邮件抽象模板
- 因为我们的邮件信息都是固定格式,我们需要找出可变的部分(各自的邮件模板信息不一样)和固定的部分(拿到邮件模板内容,对内容变量进行替换)
- 可变的部分则定义成抽象方法,公共的逻辑则定义成私有方法,在抽象模板里面处理公共逻辑(所有具体实现模板都要处理的部分,例如对邮件模板内容变量进行替换)
public abstract class AbstractMailTemplate <T>{
@ApiModelProperty("邮件信息查询QO")
public MailInfoQO mailInfoQO;
/**
* 查询邮件发送人
* @return
*/
protected abstract String getMailSenderAddress();
/**
* 查询邮件模板
* @return
*/
protected abstract THrMailTemplate getMailTemplate();
/**
* 查询邮件变量
* @return
*/
protected abstract<T> T getMailContentVar();
/**
* 查询邮件收件人
* @return
*/
protected abstract List<MailReceiverVO> getMailReceiverList();
/**
* 查询邮件抄送人
* @return
*/
protected abstract List<MailReceiverVO> getMailCCReceiverList();
/**
* 替换邮件模板变量
* @return
*/
private String replaceMailTemplateVar(){
Map<String, Object> mailTemplateVarMap = BeanUtil.beanToMap(getMailContentVar());
StringBuilder mailContentResult = new StringBuilder(getMailTemplate().getTemplateContent());
Set<Map.Entry<String, Object>> entries = mailTemplateVarMap.entrySet();
for(Map.Entry<String, Object> entry: mailTemplateVarMap.entrySet()){
replaceMailContentByEntry(mailContentResult,entry);
}
return mailContentResult.toString();
}
/**
* 根据键值对替换邮件内容、多个相同变量则递归处理
* @param mailContentResult
* @param entry
*/
private void replaceMailContentByEntry(StringBuilder mailContentResult , Map.Entry<String, Object> entry){
int startIndex = mailContentResult.indexOf(entry.getKey());
if(startIndex == -1){
return;
}
int endIndex = startIndex + entry.getKey().length();
mailContentResult.replace(startIndex,endIndex, StringUtils.isEmpty(entry.getKey()) ?
"/" :entry.getValue().toString());
replaceMailContentByEntry(mailContentResult,entry);
}
/**
* 封装邮件模板:为了防止恶意的操作,一般模板方法前面会加上final关键字,不允许被覆写
* @return
*/
public final MailInfoVO mailTemplateMethod(){
return new MailInfoVO().setSenderAddress(getMailSenderAddress())
.setTitle(getMailTemplate().getTemplateName())
.setContent(replaceMailTemplateVar())
.setReceiverAddressList(getMailReceiverList())
.setCcReceiverAddressList(getMailCCReceiverList());
}
}
6.自定义邮件模板注解
用于标识我们的注解bean,侦听到容器刷新事件发布则把所有注解bean都添加到我们的成员变量mailTemplateMap 中
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MailTemplate {
String value();
String name();
}
7.具体的邮件模板、实现抽象模板
@Component
@MailTemplate(value = "WATER_PLAN_MAIL_TEMPLATE" , name="活水计划邮件模板")
@Scope("prototype")
public class WaterPlanMailTemplate<T> extends AbstractMailTemplate<T>{
@Override
protected String getMailSenderAddress() {
return "rise@163.com";
}
@Override
protected THrMailTemplate getMailTemplate() {
return new THrMailTemplate().setTemplateName("活水计划邮件发送主题").setTemplateContent("name总您好!您的年龄:age,您的邮箱:mail");
}
@Override
protected <T> T getMailContentVar() {
return (T) new WaterMailTemplateDTO().setName("jinbiao").setAge(25).setMail("jinbiao666@163.com");
}
@Override
protected List<MailReceiverVO> getMailReceiverList() {
return Collections.singletonList(new MailReceiverVO(1,"张三"));
}
@Override
protected List<MailReceiverVO> getMailCCReceiverList() {
return Collections.singletonList(new MailReceiverVO(2,"李四"));
}
}
8.开始测试
传入我们的邮件模板编号,直接返回业务处理完的整个邮件信息。
四.解决了哪些痛点?
1.模板修改无需发版解决
假如我们业务对模板要改内容,我们只需通过dba执行一个dml脚本即可生效。
我们是设计了一张邮件模板表的,针对每一个邮件发送入口,维护了一个模板编号,主题,模板内容(带富文本标签的整个内容,然后可变的地方用变量对象例如WaterMailTemplateDTO里面的属性代替)。
2.约定组装邮件模板的步骤,代码清晰简洁
任何一个新接触这块业务的同事,都见名之意知道每个步骤的方法是干嘛的,代码清晰简洁,照着已有模板,新增对应的模板实现即可。
3.减少了前端同事的在邮件功能上的联调工作量,传入一个不同的模板编号即可,双方都很愉快~
五.小结
好了,到了这里相信小伙伴们已经理解这种设计思路了:
- 模板方法:固有的步骤,然后找到可变的部分定义为抽象方法交给子类实现,公共的地方由抽象模板统一处理。
- Spring事件发布与侦听机制结合策略模式也很是好用
休息了,明天又是早起打工的一天,各位小伙伴们晚安。。。