描述
在我们的项目中,总是有一些我们不可控制的异常,比如数据库连接不上,redis挂掉,以及一些代码上未可知的异常爆发,不能在项目上线时就可以统计出来,并且修复,所以当我们这些bug抛出异常时,或者在某些可控的严重异常需要推送邮件或者短信或者其他的通讯工具比如 钉钉或者飞书等,我们就需要这样的功能,这里提供一个邮件通知方法,当有未知异常或者被定义为严重异常的,就会给运维人员发送一个邮件进行通知,方便计时应对和问题定位。
解决方案
在springboot中的全局异常捕获处,对不可控异常拿到异常栈信息,进行异常msg的组装和通过freemarker模板进行渲染html文本,然后再把这个异常msg的html进行qq模式的email的发送,freemarker模板可以支撑字符串模板渲染,即渲染的模板字符串可以保存到数据库,也可以直接定义好xxx.ftl模板,都行,这里需要强调的是 渲染的模板字符串可以保存到数据库就更加灵活,可以设计一套freemarker模板的管理系统,比如,对自定义的freemarker模板配置后,保存到数据库,然后根据不同的用户或者企业或者业务,就可以从库中获取对应的freemarker模板,进行数据渲染html,再通过短信或者邮件或者钉钉,这样就实现了类似阿里或者腾讯等第三方的模板配置后进行消息推送的功能。
代码
代码组成包含有自定义的模板的工具jar包和邮件的springboot-starter,以及在微服务中对异常的处理,和调用邮件的消息封装以及模板的创建。
模板自定springboot-starter
详细代码:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wlc-spring-boot-tools</artifactId>
<groupId>com.wlc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wlc-template</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<--freemarker依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<--spring-webmvc依赖,这里可以换成spring的ioc包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.9</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
BeanUtils,处理bean转map。
import org.springframework.cglib.beans.BeanMap;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 描述 实体工具类 <br>
* 作者:IT学习道场 <br>
* 时间:2018/10/26 13:37
*/
public class BeanUtils extends org.springframework.beans.BeanUtils {
public BeanUtils() {
}
/**
* 实例化对象:传入类对类进行实例化对象
*
* @param clazz 类
* @return 对象
* @author Lius
* @date 2018/10/26 13:49
*/
public static <T> T newInstance(Class<?> clazz) {
return (T) instantiateClass(clazz);
}
/**
* 实例化对象,传入类名对该类进行实例化对象
*
* @param clazzStr 类名,传入是必须传入全路径,com...
* @return 对象
* @author Lius
* @date 2018/10/26 13:54
*/
public static <T> T newInstance(String clazzStr) {
try {
Class<?> clazz = Class.forName(clazzStr);
return newInstance(clazz);
} catch (ClassNotFoundException e) {
throw new RuntimeException();
}
}
/**
* 把对象封装成Map形式
*
* @param src 需要封装的实体对象
* @author Lius
* @date 2018/10/26 14:08
*/
public static Map toMap(Object src) {
return BeanMap.create(src);
}
/**
* 把Map转换成bean对象
*
* @author Lius
* @date 2018/10/26 14:09
*/
public static <T> T toBean(Map<String, Object> beanMap, Class<T> valueType) {
// 对象实例化
T bean = BeanUtils.newInstance(valueType);
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(valueType);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
String properName = propertyDescriptor.getName();
// 过滤class属性
if ("class".equals(properName)) {
continue;
}
if (beanMap.containsKey(properName)) {
Method writeMethod = propertyDescriptor.getWriteMethod();
if (null == writeMethod) {
continue;
}
Object value = beanMap.get(properName);
if (!writeMethod.isAccessible()) {
writeMethod.setAccessible(true);
}
try {
writeMethod.invoke(bean, value);
} catch (Throwable throwable) {
throw new RuntimeException("Could not set property '" + properName + " ' to bean" + throwable);
}
}
}
return bean;
}
}
FreemarkerUtil,来处理模板的封装。
import com.wlc.template.util.BeanUtils;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import java.io.IOException;
import java.util.Map;
/**
* 描述: FreemarkerUtil辅助类 <br>
* 时间: 2022-07-01 9:44 <br>
* 作者:IT学习道场
*/
public class FreemarkerUtil {
private FreeMarkerConfigurer freeMarkerConfigurer;
/**
* bean转map
* @param bean 转换bean
* @return Map<String, Object> - map对象
*/
public Map<String, Object> beanToMap(Object bean){
Map<String, Object> map = BeanUtils.toMap(bean);
return map;
}
/**
* 根据模板路径获取模板渲染数据
* @param templatePath 模板路径 ,ex:templatePath = "notice.ftl",意思是 resources/templates/下的notice.ftl文件
* @param data 渲染数据对象
* @return String- 渲染后的html文本
*/
public String freeMarkerRenderHtml(String templatePath, Map<String, Object> data ){
//获取模板信息
Template template = null;
String html= "";
try {
template = freeMarkerConfigurer.getConfiguration().getTemplate(templatePath);
html = FreeMarkerTemplateUtils.processTemplateIntoString(template, data);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
return html;
}
/**
* 字符文本渲染成html文本
* @param templateName 模板名
* @param templateText 模板文本
* @param data 渲染数据map
* @return String - 渲染后的html文本
*/
public String textRenderFreemarkerHtml(String templateName, String templateText, Map<String, Object> data){
String html = textRenderFreemarkerHtml(templateName, templateText, "utf-8", data);
return html;
}
/**
* 字符文本渲染成html文本
* @param templateName 模板名
* @param templateText 模板文本
* @param charEncode 模板编码
* @param data 渲染数据map
* @return String - 渲染后的html文本
*/
public String textRenderFreemarkerHtml(String templateName, String templateText, String charEncode, Map<String, Object> data){
Template template = textToFreemarkerTemplate(templateName, templateText, charEncode);
String html = freemarkerTemplateRenderHtml(template, data);
return html;
}
/**
* 根据模板对象和数据map渲染html文本
* @param template 模板对象
* @param data 渲染数据map
* @return String - html文本
*/
public String freemarkerTemplateRenderHtml(Template template, Map<String, Object> data){
String html = "";
try {
//渲染data数据到模板中
html = FreeMarkerTemplateUtils.processTemplateIntoString(template, data);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
return html;
}
/**
* 文本转freemarker模板
* @param templateName 模板名字
* @param templateText 模板文本
* @param charEncode 模板编码
* @return Template- 返回freemarkerTemplate对象
*/
public Template textToFreemarkerTemplate(String templateName, String templateText, String charEncode){
//获取配置文件
Configuration cfg = freeMarkerConfigurer.getConfiguration();
//创建freeMarker字符串模板加载器
StringTemplateLoader stringLoader = new StringTemplateLoader();
//加载模板名字和模板文本
stringLoader.putTemplate(templateName, templateText);
//配置文件设置模板加载器
cfg.setTemplateLoader(stringLoader);
freemarker.template.Template template = null;
try {
//从配置文件中获取模板对象
template = cfg.getTemplate(templateName, charEncode);
} catch (IOException e) {
e.printStackTrace();
}
return template;
}
}
这样,模板的渲染jar就封装好了。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wlc-spring-boot-tools</artifactId>
<groupId>com.wlc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wlc-email-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 模板jar -->
<dependency>
<groupId>com.wlc</groupId>
<artifactId>wlc-template</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
EmailUtil-> 邮件发送工具类。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Date;
/**
* 描述: email辅助类 <br>
* 时间: 2022-07-01 10:20 <br>
* 作者:IT学习道场
*/
public class EmailUtil {
private JavaMailSender javaMailSender;
private MailProperties mailProperties;
/**
* 发送普通邮件
* @param subject 主题
* @param simpleText 内容
* @param toEamils 邮件接受邮箱数组
*/
public void simpleEmailSend(String subject, String simpleText, String... toEamils) {
SimpleMailMessage message = new SimpleMailMessage();
message.setSentDate(new Date());
message.setFrom(mailProperties.getUsername());
message.setTo(toEamils);
message.setSubject(subject);
message.setText(simpleText);
//发送邮件
javaMailSender.send(message);
}
/**
* 发送html邮件
* @param subject 发送主题
* @param html 发送的html
* @param toEamils 邮件接收人数组
*/
public void emailSendHtml(String subject, String html, String... toEamils) {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper message = null;
try {
message = new MimeMessageHelper(mimeMessage, true);
message.setSentDate(new Date());
message.setFrom(mailProperties.getUsername());
message.setTo(toEamils);
message.setSubject(subject);
message.setText(html, true);
//发送邮件
javaMailSender.send(mimeMessage);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
EmailService -> email服务类的辅助类。
import com.wlc.email.util.EmailUtil;
import com.wlc.template.freemarker.FreemarkerUtil;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;
/**
* 描述: email服务类 <br>
* 时间: 2022-07-01 10:33 <br>
* 作者:IT学习道场
*/
public class EmailService {
private EmailUtil emailUtil;
private FreemarkerUtil freemarkerUtil;
/**
* 根据模板路径给邮件发送html模板
* @param subject 邮件主题
* @param templatePath html模板路径
* @param data 渲染html模板里的数据map
* @param toEamils 邮件接受者数组
*/
public void emailSendHtmlByTemplatePath(String subject, String templatePath, Map<String, Object> data, String... toEamils) {
String html = freemarkerUtil.freeMarkerRenderHtml(templatePath, data);
emailUtil.emailSendHtml(subject, html, toEamils);
}
/**
* 根据字符模板文本给邮件发送html模板
* @param subject 邮件主题
* @param templateName html模板名字
* @param templateText html模板text文本(就是html的模板字符串,使用html模板易字符串的形式保存到数据库,
* 然后从数据库中读取模板字符串,在转换成模板对象,把data进行渲染成html,来发送邮件)
* @param data 渲染html模板里的数据map
* @param toEamils 邮件接受者数组
*/
public void emailSendHtmlByTemplateText(String subject, String templateName, String templateText, Map<String, Object> data, String... toEamils) {
String html = freemarkerUtil.textRenderFreemarkerHtml(templateName, templateText, data);
emailUtil.emailSendHtml(subject, html, toEamils);
}
/**
* 根据字符模板文本给邮件发送html模板
* @param subject 邮件主题
* @param templateName html模板名字
* @param templateText html模板text文本(就是html的模板字符串,使用html模板易字符串的形式保存到数据库,
* 然后从数据库中读取模板字符串,在转换成模板对象,把data进行渲染成html,来发送邮件)
* @param data 渲染html模板里的数据map
* @param charEncode 编码格式 ex: "utf-8"
* @param toEamils 邮件接受者数组
*/
public void emailSendHtmlByTemplateText(String subject, String templateName, String templateText, String charEncode, Map<String, Object> data, String... toEamils) {
String html = freemarkerUtil.textRenderFreemarkerHtml(templateName, templateText, charEncode, data);
emailUtil.emailSendHtml(subject, html, toEamils);
}
}
EmailAutoConfiguration --> 自定义邮件自动化配置类。
import com.wlc.email.service.EmailService;
import com.wlc.email.util.EmailUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 描述: 自定义邮件自动化配置类 <br>
* 时间: 2022-07-01 11:00 <br>
* 作者:IT学习道场
*/
public class EmailAutoConfiguration {
public EmailService emailService(){
EmailService emailService = new EmailService();
return emailService;
}
EmailUtil emailUtil(){
EmailUtil emailUtil = new EmailUtil();
return emailUtil;
}
}
resources/META-INF下的spring.factories文件。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wlc.email.config.EmailAutoConfiguration
这样一个邮件的自定义springboot-starter就封装好了。
下面就是在各个微服务中的全局异常捕获中进行msg的freemarker的组装和邮件的发送。
演示的服务代码,全局异常处理。
ftl代码,有类似需求可以copy。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"></meta>
<title>邮件内容</title>
</head>
<body>
<p style="text-align:center "><img width="400px" height="200px" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimgsa.baidu.com%2Fexp%2Fw%3D500%2Fsign%3Da5c70c27a1efce1bea2bc8ca9f50f3e8%2Fa9d3fd1f4134970a05665ffe93cad1c8a6865dcd.jpg&refer=http%3A%2F%2Fimgsa.baidu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659171243&t=17d3f0b344e9cb402e8ec4311207fa5a"></p>
<h4>系统管理员</h4>
<h4> 您好!</h4>
<p>系统异常了!日志如下:</p>
<p>服务名:<span style="color:red">${appName}</span></p>
<p>服务ip:<span style="color:red">${ipAddr}</span></p>
<p>类路径:<span style="color:red">${className}</span></p>
<p>方法名字:<span style="color:red">${methodName}</span></p>
<p>异常发生行号:<span style="color:red">${lineNumber}</span></p>
<p style="color:red">${content}</p>
<p style="width: 100%;text-align: right">IT学习道场系统</p>
</body>
</html>
EmailExtendContent -> email的异常邮件信息组装类,这里是给邮件哪个模板里需要的数据组装。
import com.utils.IPUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.servlet.http.HttpServletRequest;
/**
* 描述: email的异常邮件信息组装类 <br>
* 时间: 2022-06-30 17:03 <br>
* 作者:IT学习道场
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EmailExtendContent {
//异常
public Exception e;
//请求request
public HttpServletRequest request;
//ip地址
public String ipAddr;
//类命
public String className;
//方法名
public String methodName;
//发生异常的行号
public int lineNumber;
public EmailExtendContent(Exception e, HttpServletRequest request) {
this.e = e;
this.request = request;
handler();
}
void handler(){
StackTraceElement stackTraceElement = e.getStackTrace()[0];
// 获取类名
ipAddr = IPUtil.getIpAddr(request);
className = stackTraceElement.getClassName();
methodName = stackTraceElement.getMethodName();
lineNumber = stackTraceElement.getLineNumber();
}
}
EmailHandler --> 微服务中email处理类定制化处理类。
import com.wlc.email.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 描述: email处理类 <br>
* 时间: 2022-07-01 11:11 <br>
* 作者:IT学习道场
*/
@Component
public class EmailHandler {
@Autowired
private EmailService emailService; // 自定义邮件starter里的 EmailService
@Value("${spring.application.name:no-service}") //服务应用没名字
private String appName;
@Value("#{'${spring.mail.toEamils:}'.split(',')}") //邮件接收者的邮箱数组
private String[] toEamils;
/**
* 异步发送邮件
* @param stackExceptionMsg 栈异常信息 str
* @param e 异常对象
* @param request request
*/
@Async
public void sendExceptionEmail(String stackExceptionMsg,Exception e, HttpServletRequest request){
//根据异常对象和request组装EmailExtendContent
EmailExtendContent emailExtendContent = new EmailExtendContent(e, request);
//组装渲染数据data
Map<String, Object> data = builderData(appName, emailExtendContent, stackExceptionMsg);
//发送邮件
emailService.emailSendHtmlByTemplatePath("美术传媒系统异常报告", "notice.ftl", data, toEamils);
}
private Map builderData(String appName, EmailExtendContent extendContent, String content){
Map<String, Object> data = new HashMap<>();
data.put("appName", appName);
data.put("ipAddr", extendContent.ipAddr);
data.put("className", extendContent.className);
data.put("methodName", extendContent.methodName);
data.put("lineNumber", extendContent.lineNumber);
data.put("content", content);
return data;
}
}
然后就可以在全局异常里调用一下 EmailHandler.sendExceptionEmail即可。
import cn.hutool.core.exceptions.ExceptionUtil;
import com.email.EmailHandler;
import com.http.constant.HttpCode;
import com.http.exception.BusinessException;
import com.utils.JsonUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* 全局结果响应处理,全局异常处理 <br>
* 作者:IT学习道场 <br>
* 时间:2019-01-24 10:33
*/
public class ResultAdvice implements ResponseBodyAdvice<Object> {
private EmailHandler emailHandler;
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
/**
* 先捕获异常 然后再把数据返回到ResponseBody中,
* 然后在Body中要返回数据的时候调用上面的拦截方法beforeBodyWrite()
*/
(value = Exception.class)
public Object handleException(Object o, Exception e, HttpServletRequest request) {
//此处返回json数据
//捕捉到的异常如果是自定义异常类,那么就返回自定义异常类中的错误码和错误信息
String stackExceptionMsg = ExceptionUtil.stacktraceToString(e);
//异常输出到日志
log.error(stackExceptionMsg);
//自定义基础异常
if (e instanceof BusinessException) {
return new ResultException(((BusinessException) e).getCode(), false, ((BusinessException) e).getMessage(), request.getRequestURL().toString());
//非法参数异常
} else if (e instanceof IllegalArgumentException) {
return new ResultException(HttpCode.BAD_REQUEST.code, false, "参数异常,请稍候再试", request.getRequestURL().toString());
//绑定异常
} else if (e instanceof BindException) {
return new ResultException(HttpCode.BAD_REQUEST.code, false, ((BindException) e).getBindingResult().getFieldError().getDefaultMessage(), request.getRequestURL().toString());
//方法参数异常验证异常
} else if (e instanceof MethodArgumentNotValidException) {
return new ResultException(HttpCode.BAD_REQUEST.code, false, ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage(), request.getRequestURL().toString());
}
//这里是除了自定义异常的其他异常信息
else {
// 这里是未知不可控异常,发送异常邮件即可
emailHandler.sendExceptionEmail(stackExceptionMsg, e, request);
return new ResultException(HttpCode.SERVER_ERROR.code, false, "系统异常请联系管理员", request.getRequestURL().toString());
}
}
}
下面是application.yml的邮件配置。
erver:
port: 8080
#启用undertow
undertow:
# CPU有几核,就填写几。
io-threads: 4
#阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
worker-threads: 32
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可
buffer-size: 1024
# 是否分配的直接内存(NIO直接分配的堆外内存)
direct-buffers: true
servlet:
context-path: /
spring:
application:
name: wlc
redis:
host: localhost
port: 6379
password:
lettuce:
pool:
# 连接池中的最大空闲连接 默认8
max-idle: 8
# 连接池中的最小空闲连接 默认0
min-idle: 0
# 连接池最大连接数 默认8 ,负数表示没有限制
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1
max-wait: -1
#, 1603610130 .com
mail:
host: smtp.qq.com
#你开通smtp的邮箱地址
username: xxxxx .com
#你的邮箱开通smtp时的授权码
password: xxxxxxxxxx
port: 465
toEamils: xxxx .com,yyyy .com
properties:
mail:
smtp:
auth: true
ssl:
enable: true
starttls:
enable: true
required: true
验证,已通知找个接口,搞个异常测试,我的是 int i= 1/0,简单测试。