Velocity可配置消息模版封装
3.实现:
3.1.涉及到的包:
<dependency> <groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
3.2.取模版数据并重写ResourceLoader
@Servicepublic class VelocityTemplateServiceImpl implements IVelocityTemplateService {
private static final Logger logger = LoggerFactory.getLogger(VelocityTemplateServiceImpl.class);
...
/**
* 模版实体
*/
private static NotifyTemplateDto notifyTemplateDto;
/**
* 获取消息内容,这里涉及邮件和钉钉两种消息格式
*
* @param scene 场景
* @param attrMap 属性map
* @return 消息实体
*/
@Override
public NotifyTemplateDto fetchContent(String scene, Map<String, Object> attrMap) {
notifyTemplateDto = new NotifyTemplateDto();
if (StringUtils.isBlank(scene)) {
return notifyTemplateDto;
}
//通过场景查出场景下配置的模版取最新
...
notifyTemplateDto = selectNewestOne();
//整合邮件模版内容
String mailContent = mergeContent(CustomMailTemplateLoader.class.getName(), TemplateTypeEnums.MAIL.name(), attrMap);
notifyTemplateDto.setMailTemplate(mailContent);
//整合钉钉模版内容
String ddContent = mergeContent(CustomDDTemplateLoader.class.getName(), TemplateTypeEnums.DD.name(), attrMap);
notifyTemplateDto.setDDTemplate(ddContent);
return notifyTemplateDto;
}
/**
* 整合消息内容
*
* @param resourceLoaderClass 自定义资源加载类
* @param templateType 模版类型(邮件/钉钉)
* @param attrMap 参数map
* @return 消息内容
*/
private String mergeContent(String resourceLoaderClass, String templateType, Map<String, Object> attrMap) {
Properties p = new Properties();
//自定义模版资源加载器
p.setProperty(Velocity.RESOURCE_LOADER, "CustomResourceLoader");
//注意这里会创建resourceLoaderClass的实例
p.setProperty("CustomResourceLoader.resource.loader.class", resourceLoaderClass);
p.setProperty(Velocity.INPUT_ENCODING, "utf-8");
p.setProperty(Velocity.OUTPUT_ENCODING, "utf-8");
VelocityEngine ve = new VelocityEngine();
//初始化模版引擎
ve.init(p);
Template template = ve.getTemplate(templateType, "UTF-8");
VelocityContext context = new VelocityContext();
//动态参数处理
for (Map.Entry<String, Object> entry : attrMap.entrySet()) {
context.put(entry.getKey(), entry.getValue());
}
StringWriter writer = new StringWriter();
//模版内容整合
template.merge(context, writer);
return writer.toString();
}
/**
* 重写资源加载方法
*
* @param
* @return
*/
public static class CustomMailTemplateLoader extends ResourceLoader {
@Override
public void init(ExtendedProperties configuration) {
}
@Override
public InputStream getResourceStream(String source) throws ResourceNotFoundException {
try {
return new ByteArrayInputStream(notifyTemplateDto.getMailTemplate().getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isSourceModified(Resource resource) {
return false;
}
@Override
public long getLastModified(Resource resource) {
return 0;
}
}
/**
* 重写资源加载方法
*
* @param
* @return
*/
public static class CustomDDTemplateLoader extends ResourceLoader {
@Override
public void init(ExtendedProperties configuration) {
}
@Override
public InputStream getResourceStream(String source) throws ResourceNotFoundException {
try {
return new ByteArrayInputStream(notifyTemplateDto.getDDTemplate().getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isSourceModified(Resource resource) {
return false;
}
@Override
public long getLastModified(Resource resource) {
return 0;
}
}
}
解读:在外部类中持有一个静态NotifyTemplateDto对象,用于存放从数据库中查询到的模版内容,这个实体中包含各种格式的消息模版(邮件、钉钉etc...),而查询数据库的操作放在外部类的一个统一方法中。然后根据消息格式的不同,定义多个静态内部类CustomXXXTemplateLoader继承ResourceLoader重写其getResourceStream的方法,实现就很简单直接从NotifyTemplateDto对象里取相应格式模版返回即可。
为什么使用内部类?
也可以不使用内部类。将查询库的逻辑写在重写的getResourceStream方法中,只不过这样的话多种消息类型就要写多个查询的逻辑,比较冗余。使用内部类是为了能够将查库逻辑抽离出来,结果放在一个全局变量中,这样getResourceStream方法只需要从全局变量中取值。
为什么内部类得是静态?
在初始化模版引擎时会生成自定义资源加载器的实例,而普通内部类的实例生成需要外部类的实例引用,而这时并没有外部类的实例引用,而静态内部类通过外部类的类名就可初始化。当没有声明为static时,模版引擎初始化会报如下错误:
org.apache.velocity.exception.VelocityException: Problem instantiating the template loader: com.xxx.xxx.xxx.service.impl.tools.VelocityTemplateServiceImpl$CustomMailTemplateLoader.
Look at your properties file and make sure the
name of the template loader is correct.
at org.apache.velocity.runtime.resource.loader.ResourceLoaderFactory.getLoader(ResourceLoaderFactory.java:60)
at org.apache.velocity.runtime.resource.ResourceManagerImpl.initialize(ResourceManagerImpl.java:130)
at org.apache.velocity.runtime.RuntimeInstance.initializeResourceManager(RuntimeInstance.java:730)
at org.apache.velocity.runtime.RuntimeInstance.init(RuntimeInstance.java:263)
at org.apache.velocity.runtime.RuntimeInstance.init(RuntimeInstance.java:646)
at org.apache.velocity.app.VelocityEngine.init(VelocityEngine.java:116)
...
Caused by: java.lang.InstantiationException: com.xxx.xxx.xxx.service.impl.tools.VelocityTemplateServiceImpl$CustomMailTemplateLoader
at java.lang.Class.newInstance(Class.java:427)
at org.apache.velocity.util.ClassUtils.getNewInstance(ClassUtils.java:105)
at org.apache.velocity.runtime.resource.loader.ResourceLoaderFactory.getLoader(ResourceLoaderFactory.java:46)
... 64 common frames omitted
Caused by: java.lang.NoSuchMethodException: com.xxx.xxx.xxx.service.impl.tools.VelocityTemplateServiceImpl$CustomMailTemplateLoader.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 66 common frames omitted
当然了,既然想要被静态内部类访问,全局的NotifyTemplateDto对象就也得声明为静态。
3.3.自定义注解
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})
public @interface NeedNotify {
/**
* 消息通知场景
*/
String scene() default "";
}
需要发送消息的方法使用注解标注:
/** * 需要发送消息的场景返回结果为消息模版所需的填充参数
*
* @param
* @return
*/
@NeedNotify(scene = "apply_success")//指定消息场景
@Override
public Map<String, Object> handleBusiness(BusinessParamsDto dto) {
...//业务逻辑处理
//消息所需的动态参数
Map<String, Object> notifyMap = Maps.newHashMap();
notifyMap.put(TemplateAttrEnums.url.name(), EnvUtils.pageUrl);
//如果动态参数较多可封装为一个实体传入,模版中通过 $obj.属性名 进行配置
notifyMap.put(TemplateAttrEnums.obj.name(), dto);
notifyMap.put(TemplateAttrEnums.title.name(), "测试");
//放入动态的收件人
notifyMap.put("recipients", "xxx");
return notifyMap;
}
@NeedNotify注解需要指定了消息场景
如果项目中的动态参数比较统一,比如项目首页地址等,可以提炼出一个属性的枚举,在配置模版时从这个枚举中选择属性,这样模版的配置和动态参数的绑定比较容易统一
3.4.设置切面
统一在业务方法执行完成之后发送消息
@Aspect@Component
public class NotificationAspect {
private static final Logger logger = LoggerFactory.getLogger(NotificationAspect.class);
private static final String DEFAULT_TITLE = "消息通知";
@Autowired
private IVelocityTemplateService velocityTemplateService;
/**
* 统一消息通知
*
* @param
* @return
*/
@AfterReturning(pointcut = "execution(* com.xxx.xxx.xxx.service.impl.*.*(..)) && @annotation(needNotify)", returning = "result")
public void after(JoinPoint joinPoint, NeedNotify needNotify, Object result) {
try {
Map<String, Object> resultMap = (Map<String, Object>) result;
if (resultMap.get(TemplateAttrEnums.obj.name()) == null) {
//目标方法的入参可用于模版填充
resultMap.put(TemplateAttrEnums.obj.name(), joinPoint.getArgs());
}
//根据注解中的指定的场景获取到消息模版
NotifyTemplateDto notifyTemplateDto = velocityTemplateService.fetchContent(needNotify.scene(), resultMap);
//模版配置校验
if (notifyTemplateDto == null || (StringUtils.isBlank(notifyTemplateDto.getRecipients()) && resultMap.get(TemplateAttrEnums.recipients.name()) == null) || (StringUtils.isBlank(notifyTemplateDto.getMailTemplate()) && StringUtils.isBlank(notifyTemplateDto.getDDTemplate()))) {
logger.warn("场景:{} 下消息模版配置异常", needNotify.scene());
return;
}
...
if (StringUtils.isNotBlank(notifyTemplateDto.getMailTemplate())) {
sedMail(...);//具体发送邮件消息的封装实现
}
if (StringUtils.isNotBlank(notifyTemplateDto.getDDTemplate())) {
sedDD(...);//具体发送钉钉消息的封装实现
}
} catch (Exception e) {
logger.error("消息通知异常:", e);
}
}
@AfterReturning注解的切面在目标方法后执行,且能获取到目标方法的返回值。其属性pointcut定义了切面起作用的范围,returning定义返回的接收名,可用该定义取到方法的返回值。方法中第一个参数刚开始用成了ProceedJoinPoint结果报错,注意ProceedJoinPoint只能用于around切面。
测试成功,over。
以上是 Velocity可配置消息模版封装 的全部内容, 来源链接: utcz.com/z/515115.html