前一阵学了SpringBoot发送邮件、SpringBoot制作Starter和自定义业务异常等知识,我突发奇想,我可以制作一个能捕获项目所有异常,通过邮件形式把异常信息发送给开发者的Starter,开发者实现只需要配置相关信息,就可以使用。实现无侵入性编程,支持热拔插使用。
SpringBoot中Starter知识、SpringBoot驱动配置文件知识(处理配置属性值多个,分割逗号处理方式)、SpringBoot发送邮件知识、SpringMVC处理全局异常(HandlerExceptionResolver)、SpringMVC配置(WebMvcConfigurer)
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-mail
org.springframework.boot
spring-boot-autoconfigure
注:Spring 官方 Starter通常命名为spring-boot-starter-{name}
, 非官方Starter命名应遵循{name}-spring-boot-starter
的格式。
package com.hanxiaozhang.exceptionmail.autocinfigure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
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.io.File;
/**
* 〈一句话功能简述〉
* 〈Mail服务〉
*
* @author hanxinghua
* @create 2019/8/24
* @since 1.0.0
*/
public class MailService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private JavaMailSender sender;
/**
* 发送纯文本的简单邮件
*
* @param from
* @param to
* @param subject
* @param content
*/
public void sendSimpleMail(String from,String to, String subject, String content){
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
try {
sender.send(message);
logger.info("简单邮件已经发送!");
} catch (Exception e) {
logger.error("发送简单邮件时发生异常!", e);
}
}
/**
* 发送html格式的邮件
*
* @param from
* @param to
* @param subject
* @param content
*/
public void sendHtmlMail(String from,String to, String subject, String content){
MimeMessage message = sender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
sender.send(message);
logger.info("html邮件已经发送。");
} catch (MessagingException e) {
logger.error("发送html邮件时发生异常!", e);
}
}
/**
* 发送带附件的邮件
*
* @param from
* @param to
* @param subject
* @param content
* @param filePath
*/
public void sendAttachmentsMail(String from,String to, String subject, String content, String filePath){
MimeMessage message = sender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
FileSystemResource file = new FileSystemResource(new File(filePath));
String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
helper.addAttachment(fileName, file);
sender.send(message);
logger.info("带附件的邮件已经发送。");
} catch (MessagingException e) {
logger.error("发送带附件的邮件时发生异常!", e);
}
}
/**
* 发送嵌入静态资源(一般是图片)的邮件
*
*
* @param from
* @param subject
* @param content 邮件内容,需要包括一个静态资源的id,比如:
* @param rscPath 静态资源路径和文件名
* @param rscId 静态资源id
*/
public void sendInlineResourceMail(String from,String to, String subject, String content, String rscPath, String rscId){
MimeMessage message = sender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
FileSystemResource res = new FileSystemResource(new File(rscPath));
helper.addInline(rscId, res);
sender.send(message);
logger.info("嵌入静态资源的邮件已经发送。");
} catch (MessagingException e) {
logger.error("发送嵌入静态资源的邮件时发生异常!", e);
}
}
}
package com.hanxiaozhang.exceptionmail.autocinfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 〈一句话功能简述〉
* 〈读取异常信息邮件配置信息〉
*
* @author hanxinghua
* @create 2019/8/24
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "hanxiaozhang.mail")
public class ExceptionMailProperties {
/**
* 字符集
*/
private static final Charset DEFAULT_CHARSET;
/**
* 邮箱服务器地址
*/
private String host;
/**
* 端口号
*/
private Integer port;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 邮箱协议,默认"smtp"
*/
private String protocol = "smtp";
/**
* 默认编码
*/
private Charset defaultEncoding;
/**
* 配置Map
*/
private Map properties;
/**
* JNDI名称
*/
private String jndiName;
/**
* 测试连接
*/
private boolean testConnection;
/**
* 邮件发送者,默认配置邮箱的用户,见getSender()
*/
private String sender;
/**
* 邮件接收者,默认配置邮箱的用户,见getReceiver()
*/
private String receiver;
/**
* 发送邮件主题
*/
private String sendMailTitle="异常信息";
/**
* 是否打印日志
*/
private boolean isPrintLog=false;
public ExceptionMailProperties() {
this.defaultEncoding = DEFAULT_CHARSET;
this.properties = new HashMap();
}
public String getHost() {
return this.host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return this.port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public String getProtocol() {
return this.protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public Charset getDefaultEncoding() {
return this.defaultEncoding;
}
public void setDefaultEncoding(Charset defaultEncoding) {
this.defaultEncoding = defaultEncoding;
}
public Map getProperties() {
return this.properties;
}
public void setJndiName(String jndiName) {
this.jndiName = jndiName;
}
public String getJndiName() {
return this.jndiName;
}
public boolean isTestConnection() {
return this.testConnection;
}
public void setTestConnection(boolean testConnection) {
this.testConnection = testConnection;
}
public String getSender() {
if(null==sender||"".equals(sender)){
return getUsername();
}
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getReceiver() {
if(null==receiver||"".equals(receiver)){
return getUsername();
}
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getSendMailTitle() {
return sendMailTitle;
}
public void setSendMailTitle(String sendMailTitle) {
this.sendMailTitle = sendMailTitle;
}
public boolean isPrintLog() {
return this.isPrintLog;
}
public void setPrintLog(boolean printLog) {
isPrintLog = printLog;
}
static {
DEFAULT_CHARSET = StandardCharsets.UTF_8;
}
}
package com.hanxiaozhang.exceptionmail.autocinfigure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 〈一句话功能简述〉
* 〈SpringMvc异常处理〉
*
*
* @author hanxinghua
* @create 2019/8/31
* @since 1.0.0
*/
public class GlobalExceptionResolver implements HandlerExceptionResolver {
/**
* 日志
*/
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 发送邮件服务
*/
private MailService mailService;
/**
* 异常邮件配置文件
*/
private ExceptionMailProperties exceptionMailProperties;
/**
* 发送者集合(Spring源码写法)
* 参考源码:org.springframework.core.env.AbstractEnvironment
* 目的:处理配置文件多参数(逗号截取),例,spring.profiles.active=home,mail 写法
*
*/
private final Set receiverProfiles = new LinkedHashSet();
/**
* 构造函数
*
* @param mailService
* @param exceptionMailProperties
*/
public GlobalExceptionResolver(MailService mailService,ExceptionMailProperties exceptionMailProperties) {
this.mailService = mailService;
this.exceptionMailProperties=exceptionMailProperties;
}
@Nullable
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
//打印日志
if(exceptionMailProperties.isPrintLog()){
logger.info("exception-mail-starter-print-log:\n"+sw.toString());
}
//发送邮件
sendExceptionMail(sw.toString());
return null;
}
/**
* 发送异常邮件
*
* @param content
*/
private void sendExceptionMail(String content){
this.doGetReceiverProfiles().forEach(x->{
mailService.sendSimpleMail(exceptionMailProperties.getSender(),x, exceptionMailProperties.getSendMailTitle(), content);
});
}
/**
* 获取发送者集合(Spring源码写法)
*
* @return
*/
private Set doGetReceiverProfiles() {
synchronized(this.receiverProfiles) {
if (this.receiverProfiles.isEmpty()) {
String receivers = exceptionMailProperties.getReceiver();
if (StringUtils.hasText(receivers)) {
this.setReceiverProfiles(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(receivers)));
}
}
return this.receiverProfiles;
}
}
/**
* 赋值发送者集合(Spring源码写法)
*
* @param receivers
*/
private void setReceiverProfiles(String... receivers) {
Assert.notNull(receivers, "receiver array must not be null");
synchronized(this.receiverProfiles) {
this.receiverProfiles.clear();
String[] var3 =receivers;
int var4 = var3.length;
//Java中i++语句是需要一个临时变量取存储返回自增前的值,而++i不需要。
//https://blog.csdn.net/u010188178/article/details/83996538
for(int var5 = 0; var5 < var4; ++var5) {
String receiver = var3[var5];
this.validateReceiver(receiver);
this.receiverProfiles.add(receiver);
}
}
}
/**
* 验证发送者字符串(Spring源码写法)
*
* @param receiver
*/
private void validateReceiver(String receiver) {
// 如果字符串里面的值为null, "", " ",那么返回值为false;否则为true
if (!StringUtils.hasText(receiver)) {
throw new IllegalArgumentException("Invalid hanxiaozhang.mail.receiver [" + receiver + "]: must contain text");
// 验证邮件格式
} else if (!verifyEmail(receiver)) {
throw new IllegalArgumentException("Invalid hanxiaozhang.mail.receiver [" + receiver + "]: must mail format");
}
}
/**
* 检查邮箱是否合法
*
* @param mail
* @return
*/
public static boolean verifyEmail(String mail){
String regExp = "^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\\.[a-zA-Z0-9_-]{2,3}){1,2})$";
Pattern p = Pattern.compile(regExp);
Matcher m = p.matcher(mail);
return m.matches();
}
}
注:我这个Starter支持配置多个发送者,此功能的代码借鉴参考了SpringBoot中[ spring.profiles.active=home,mail ]处理方法。
package com.hanxiaozhang.exceptionmail.autocinfigure;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* 〈一句话功能简述〉
* 〈ExceptionMailAutoConfigure〉
*
* @author hanxinghua
* @create 2019/8/24
* @since 1.0.0
*/
@Configuration
//判断当前classpath下是否存在指定类,若是存在则将当前的配置装载入spring容器
@ConditionalOnClass(value={MailService.class,GlobalExceptionResolver.class})
//使,使用 @ConfigurationProperties的类生效。在配置类只配置@ConfigurationProperties,没有使用@Component时使用
@EnableConfigurationProperties(value={ExceptionMailProperties.class,MailProperties.class})
//当配置文件中 hanxiaozhang.mail.enable 值为true时,实例化此类
@ConditionalOnProperty(prefix = "hanxiaozhang.mail",value = "enabled",havingValue = "true")
public class ExceptionMailAutoConfigure implements WebMvcConfigurer {
@Autowired
private ExceptionMailProperties exceptionMailProperties;
@Autowired
private MailProperties mailProperties;
@Bean
@Primary
//判断是否是当前spring context已经处理的此bean,若是不存在则将当前的配置装载入spring容器
@ConditionalOnMissingBean
MailService mailService(){
BeanUtils.copyProperties(exceptionMailProperties,mailProperties);
return new MailService();
}
@Override
public void extendHandlerExceptionResolvers(List resolvers) {
resolvers.add(new GlobalExceptionResolver(mailService(),exceptionMailProperties));
}
}
resources/META-INF/
下新建spring.factories
文件:org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hanxiaozhang.exceptionmail.autocinfigure.ExceptionMailAutoConfigure
com.hanxiaozhang
spring-boot-starter-exception-mail
0.0.1-SNAPSHOT
hanxiaozhang.mail:
#是否启动
enabled: true
#邮箱服务器地址
host: smtp.sina.cn
#邮箱用户名
username: XXXXXX@sina.com
#邮箱密码(注意:QQ邮箱应该使用授权码)
password: XXXXX
#邮件发送者,默认username
#sender:
#邮件接收者,默认username
receiver: XXXXX@qq.com,XXXXX@163.com
#邮件主题,默认“项目异常”
sendMailTitle: XXX项目异常
#打印日志,默认false
printLog: true
#编码格式
default-encoding: UTF-8
@RequestMapping("/test")
public String test(){
int i=2;
i=i/0;
//throw new DataNotFoundException("测试");
return null;
}
springBootStarterExceptionMail