公司的一个项目上线之后,领导提出需求:将线上的异常发送到开发运维人员的邮箱,及时处理bug。这种情况下必须用AOP啊,搞起
切面类的定义
@Component
@EnableAspectJAutoProxy
@Aspect
@Slf4j
public class SendExceptionMailAOPHandler {
@Resource
private EmailSendConfig emailSendConfig;
@Resource
private NacosConfig Config;
//创建切入点,在service层切入
@Pointcut(value = "execution(* com.qoros.cvrm.provider.service.impl.*.*(..))")
public void servicePointCut() {
}
@AfterThrowing(value = "servicePointCut()", throwing = "e")
public void sendExceptionByMail(Exception e) {
//获取异常信息
InputStream inputStream = StreamUtils.getExceptionStream(e);
try {
String line = StreamUtils.getInputStreamFirstLine(inputStream);
sendExceptionMail(inputStream, line);
} finally {
CloseUtils.closeQuietly(inputStream);
}
//发送邮件
public void sendExceptionMail(InputStream inputStream, String firstLine) {
try {
//收件人列表
String to[] = emailSendConfig.getExceptionInfoTo().split(",");
//抄送人列表
String cc[] = emailSendConfig.getExceptionInfoCC().split(",");
//保存信息
Map<String, InputStream> attachmentMap = new HashMap<>(1);
//NacosConfig.getEnvType()获取系统的环境,dev ? uat? prd,在配置文件中定义的
attachmentMap.put(NacosConfig.getEnvType() + "环境:CVRM异常文本.txt", inputStream);
StringBuffer content = new StringBuffer();
try {
//由于项目部署在两台服务器上,需要拿到当前服务器IP
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
NetworkInterface networkInterface;
Enumeration<InetAddress> inetAddresses;
InetAddress inetAddress;
String ip;
while (networkInterfaces.hasMoreElements()) {
networkInterface = networkInterfaces.nextElement();
inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
inetAddress = inetAddresses.nextElement();
if (inetAddress != null && inetAddress instanceof Inet4Address) { // IPV4
ip = inetAddress.getHostAddress();
if(ip.indexOf(".")!=-1&&ip.indexOf("127.0.0.1")==-1) {
content.append("ip地址:").append(ip).append("<br/>");
}
}
}
}
content.append("具体异常信息,请参考附件文本内容");
} catch (SocketException ex) {
log.error("", ex);
}
//调用邮件配置类中的发送邮件方法
emailSendConfig.sendMail(emailSendConfig.getEmailFrom(), to, cc, nacosConfig.getEnvType() + "环境:" + "系统异常提醒" + firstLine, content.toString(),
attachmentMap, "text/html;charset=UTF-8");
} finally {
CloseUtil.closeQuietly(inputStream);
}
}
}
邮箱配置类的定义
@Configuration
@RefreshScope
@Slf4j
@Data
public class EmailSendConfig {
//邮箱发送人信息,这个项目用nacos配置文件
@Value("${mailbox.userName:}")
private String emailAccount;
@Value("${mailbox.password:}")
private String emailPassword;
@Value("${mailbox.host:}")
private String emailHost;
@Value("${mailbox.from:}")
private String emailFrom;
//异常接收人
@Value("${exceptionInfo.to:}")
private String exceptionInfoTo;
//抄送人
@Value("${exceptionInfo.cc:}")
private String exceptionInfoCC;
private JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
private void initMailAccount() {
javaMailSender.setHost(emailHost);
javaMailSender.setUsername(emailAccount);
javaMailSender.setPassword(emailPassword);
javaMailSender.setDefaultEncoding("UTF-8");
}
public void sendMail(String emailFrom, String to[], String cc[], String subject, String text, Map<String, InputStream> attachmentMap, String attachmentMimeType) {
log.info("--------------[mail/mailSend] start------------------");
try {
initMailAccount();
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(emailFrom, "XXXX系统");
helper.setTo(to);
helper.setCc(cc);
helper.setSubject(subject);
helper.setText(text, true);
MimeMultipart mm = new MimeMultipart();
message.setContent(mm);
// 6. 创建文本"节点"
MimeBodyPart textPart = new MimeBodyPart();
textPart.setContent(text, "text/html;charset=UTF-8");
mm.addBodyPart(textPart);
if (MapUtils.isNotEmpty(attachmentMap)) {
for (String attName : attachmentMap.keySet()) {
// 9. 创建附件"节点"
MimeBodyPart attachment = new MimeBodyPart();
InputStream inputStream = attachmentMap.get(attName);
// 读取本地文件
DataHandler dh2 = new DataHandler(new ByteArrayDataSource(inputStream, attachmentMimeType));
// 将附件数据添加到"节点"
attachment.setDataHandler(dh2);
// 设置附件的文件名(需要编码)
attachment.setFileName(attName);
mm.addBodyPart(attachment); // 如果有多个附件,可以创建多个多次添加
inputStream.close();
}
}
javaMailSender.send(message);
} catch (Exception e) {
log.error("邮件发送失败", e);
}
log.info("--------------[mail/mailSend] end------------------");
}
}
StreamUtils类的定义,处理异常文件流
@Slf4j
public class StreamUtils {
/**
* 异常对象转为输入流返回
* @param e 异常
* @return inputStream 一定要确保使用完后关闭,否则会造成内存泄漏
*/
public static InputStream getExceptionStream(Exception e){
//发送邮件
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024*2);
InputStream inputStream = null;
PrintStream printStream =null;
try {
printStream = new PrintStream(outputStream);
e.printStackTrace(printStream);
printStream.flush();
outputStream.flush();
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
inputStream.reset();
} catch (IOException ex) {
log.error("邮件发送异常日志", ex);
} finally {
closeQuietly(outputStream, printStream);
}
return inputStream;
}
//关闭资源的类
public static void closeQuietly(Closeable... closeable) {
for (Closeable closeAb : closeable) {
if (closeAb == null)
continue;
try {
closeAb.close();
} catch (IOException ex) {
assert true;
}
}
}
}
写完之后测试ok,但在测试的过程中发现,遇到N次异常会发N封邮件,邮件999+,清理要花很长时间,得想办法继续优化一下。
在 SendExceptionMailAOPHandler 类中加一个缓存,发邮件的时候判断一下
//用于存放异常类型 限制长度为1000,缓存清理时间为1小时
Cache<String, Exception> cacheException = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder()
.limit(1000).expireAfterWrite(1, TimeUnit.HOURS).buildCache();
@AfterThrowing(value = "servicePointCut()", throwing = "e")
public void sendExceptionByMail(Exception e) {
//发送邮件
InputStream inputStream = StreamUtils.getExceptionStream(e);
try {
String line = StreamUtils.getInputStreamFirstLine(inputStream);
//如果该异常类是BusinessException(自定义业务异常)则每次都发送异常邮件
//如果是其他异常类,则存放进cache缓存中,一小时之内不重复发送异常邮件
if (e instanceof BusinessException || cacheException.get(line) == null) {
//如果cacheException没有该异常则存放该异常
cacheException.put(line, e);
sendExceptionMail(inputStream, line);
} else {
return;
}
} finally {
CloseUtils.closeQuietly(inputStream);
}
}