我来为你详细讲解如何在Spring Boot 2.5.10 + JDK8项目中重写Logback获取error信息并发送邮件通知。
一、项目依赖配置
1. pom.xml 添加依赖
xml
<dependencies>
<!-- Spring Boot Starter Web (已包含) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Mail 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Lombok (可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. QQ邮箱配置(推荐,配置简单)
application.yml 配置:
yaml
spring:
# QQ邮箱配置(推荐)
mail:
host: smtp.qq.com
port: 465 # SSL端口
username: 123456789@qq.com # 你的QQ邮箱
password: xxxxxxxxxxxxxxxx # QQ邮箱授权码(不是登录密码!)
protocol: smtps
default-encoding: UTF-8
properties:
mail:
smtp:
ssl:
enable: true
auth: true
starttls:
enable: true
required: true
connectiontimeout: 5000
timeout: 3000
writetimeout: 5000
# 自定义邮件配置
error:
email:
enabled: true # 是否启用邮件通知
from: 123456789@qq.com # 发件人邮箱
to: # 收件人列表
- your-manager@qq.com
- ops-team@company.com
cc: # 抄送列表(可选)
- backup@qq.com
bcc: # 密送列表(可选)
- monitor@qq.com
subject-prefix: "[系统告警] " # 邮件主题前缀
# 邮件发送频率控制
interval: 60000 # 发送间隔(毫秒),避免频繁发送
batch-size: 10 # 批量发送条数
3. 163邮箱配置(备用方案)
如果你使用163邮箱,修改配置如下:
yaml
spring:
mail:
host: smtp.163.com
port: 465
username: your-email@163.com
password: xxxxxxxxxxxx # 163邮箱授权码
protocol: smtps
# ... 其他配置同上
注意:无论是QQ邮箱还是163邮箱,都需要:
-
开启SMTP服务
-
获取授权码(不是登录密码)
二、创建自定义Logback Appender
1. ErrorLogAppender.java - 自定义Appender
java
package com.example.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.core.AppenderBase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 自定义Logback Appender,捕获ERROR日志并发送邮件
*/
@Slf4j
@Component
public class ErrorLogAppender extends AppenderBase<ILoggingEvent> {
// 错误日志队列(线程安全)
private final BlockingQueue<ILoggingEvent> errorQueue = new ArrayBlockingQueue<>(1000);
// 邮件发送服务
@Autowired(required = false)
private ErrorEmailService errorEmailService;
// 配置参数
@Value("${error.email.enabled:true}")
private boolean emailEnabled;
@Value("${error.email.interval:60000}")
private long emailInterval;
// 处理线程
private Thread processorThread;
private volatile boolean running = true;
@PostConstruct
public void init() {
if (emailEnabled && errorEmailService != null) {
startProcessor();
log.info("ErrorLogAppender 初始化完成,邮件通知已启用");
} else {
log.warn("ErrorLogAppender 初始化完成,邮件通知未启用");
}
}
@PreDestroy
public void destroy() {
running = false;
if (processorThread != null) {
processorThread.interrupt();
try {
processorThread.join(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* 启动处理线程
*/
private void startProcessor() {
processorThread = new Thread(() -> {
log.info("错误日志处理器线程启动");
while (running) {
try {
// 从队列中取出错误日志
ILoggingEvent event = errorQueue.poll(1, TimeUnit.SECONDS);
if (event != null) {
processErrorEvent(event);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("处理错误日志时发生异常", e);
}
}
log.info("错误日志处理器线程停止");
});
processorThread.setName("Error-Log-Processor");
processorThread.setDaemon(true);
processorThread.start();
}
/**
* Logback Appender的核心方法
*/
@Override
protected void append(ILoggingEvent event) {
// 只处理ERROR级别的日志
if (event.getLevel().isGreaterOrEqual(Level.ERROR)) {
// 添加到队列
boolean success = errorQueue.offer(event);
if (!success) {
log.warn("错误日志队列已满,丢弃日志: {}", event.getMessage());
}
}
}
/**
* 处理错误日志事件
*/
private void processErrorEvent(ILoggingEvent event) {
try {
// 构建错误信息
String errorMessage = buildErrorMessage(event);
// 发送邮件
if (emailEnabled && errorEmailService != null) {
errorEmailService.sendErrorEmail(errorMessage, event);
}
// 也可以在这里添加其他处理逻辑
// 如:保存到数据库、发送到消息队列等
} catch (Exception e) {
log.error("处理错误日志事件失败", e);
}
}
/**
* 构建详细的错误信息
*/
private String buildErrorMessage(ILoggingEvent event) {
StringBuilder sb = new StringBuilder();
sb.append("======= 系统错误告警 =======\n\n");
sb.append("【时间】: ").append(formatTimestamp(event.getTimeStamp())).append("\n");
sb.append("【级别】: ").append(event.getLevel()).append("\n");
sb.append("【类名】: ").append(event.getLoggerName()).append("\n");
sb.append("【线程】: ").append(event.getThreadName()).append("\n");
sb.append("【消息】: ").append(event.getFormattedMessage()).append("\n\n");
// MDC上下文信息(如果有)
if (event.getMDCPropertyMap() != null && !event.getMDCPropertyMap().isEmpty()) {
sb.append("【上下文信息】:\n");
event.getMDCPropertyMap().forEach((key, value) ->
sb.append(" ").append(key).append(": ").append(value).append("\n"));
sb.append("\n");
}
// 异常堆栈
IThrowableProxy throwableProxy = event.getThrowableProxy();
if (throwableProxy != null) {
sb.append("【异常堆栈】:\n");
sb.append(getStackTrace(throwableProxy));
}
sb.append("===========================");
return sb.toString();
}
/**
* 格式化时间戳
*/
private String formatTimestamp(long timestamp) {
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
.format(new java.util.Date(timestamp));
}
/**
* 获取堆栈跟踪信息
*/
private String getStackTrace(IThrowableProxy throwableProxy) {
StringBuilder sb = new StringBuilder();
int depth = 0;
while (throwableProxy != null && depth < 10) { // 限制深度,避免过长
sb.append(throwableProxy.getClassName())
.append(": ")
.append(throwableProxy.getMessage())
.append("\n");
StackTraceElementProxy[] stackTraceElements = throwableProxy.getStackTraceElementProxyArray();
if (stackTraceElements != null) {
// 只取前20行堆栈
int limit = Math.min(stackTraceElements.length, 20);
for (int i = 0; i < limit; i++) {
sb.append(" at ").append(stackTraceElements[i].getStackTraceElement()).append("\n");
}
if (stackTraceElements.length > 20) {
sb.append(" ... ").append(stackTraceElements.length - 20).append(" more\n");
}
}
throwableProxy = throwableProxy.getCause();
if (throwableProxy != null) {
sb.append("Caused by: ");
depth++;
}
}
return sb.toString();
}
}
三、邮件发送服务
1. ErrorEmailService.java - 邮件服务
java
package com.example.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Date;
import java.util.List;
/**
* 错误邮件发送服务
*/
@Slf4j
@Service
public class ErrorEmailService {
@Autowired
private JavaMailSender mailSender;
@Value("${error.email.from:}")
private String fromEmail;
@Value("#{'${error.email.to:}'.split(',')}")
private List<String> toEmails;
@Value("#{'${error.email.cc:}'.split(',')}")
private List<String> ccEmails;
@Value("#{'${error.email.bcc:}'.split(',')}")
private List<String> bccEmails;
@Value("${error.email.subject-prefix:[系统告警] }")
private String subjectPrefix;
@Value("${spring.mail.username:}")
private String defaultFromEmail;
// 上次发送时间,用于控制频率
private long lastSendTime = 0;
/**
* 发送错误邮件
*/
@Async("emailTaskExecutor")
public void sendErrorEmail(String errorContent, ILoggingEvent event) {
if (!isEmailConfigured()) {
log.warn("邮件配置不完整,无法发送错误邮件");
return;
}
// 频率控制(避免频繁发送)
long currentTime = System.currentTimeMillis();
if (currentTime - lastSendTime < 60000) { // 1分钟内不重复发送
log.debug("邮件发送频率控制,跳过本次发送");
return;
}
try {
// 使用HTML格式邮件
sendHtmlEmail(errorContent, event);
lastSendTime = currentTime;
log.info("错误邮件发送成功");
} catch (Exception e) {
log.error("发送错误邮件失败", e);
}
}
/**
* 发送HTML格式邮件
*/
private void sendHtmlEmail(String errorContent, ILoggingEvent event) throws MessagingException {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
// 设置邮件基本信息
String from = fromEmail != null && !fromEmail.isEmpty() ? fromEmail : defaultFromEmail;
helper.setFrom(from);
helper.setTo(toEmails.toArray(new String[0]));
// 设置抄送
if (!CollectionUtils.isEmpty(ccEmails) && !(ccEmails.size() == 1 && ccEmails.get(0).isEmpty())) {
helper.setCc(ccEmails.toArray(new String[0]));
}
// 设置密送
if (!CollectionUtils.isEmpty(bccEmails) && !(bccEmails.size() == 1 && bccEmails.get(0).isEmpty())) {
helper.setBcc(bccEmails.toArray(new String[0]));
}
// 设置主题
String subject = subjectPrefix + "系统发生错误 - " +
event.getLoggerName() + " - " +
new java.text.SimpleDateFormat("MM-dd HH:mm").format(new Date());
helper.setSubject(subject);
// 构建HTML内容
String htmlContent = buildHtmlContent(errorContent, event);
helper.setText(htmlContent, true);
// 发送邮件
mailSender.send(mimeMessage);
}
/**
* 发送简单文本邮件(备用方案)
*/
private void sendSimpleEmail(String errorContent, ILoggingEvent event) {
SimpleMailMessage message = new SimpleMailMessage();
String from = fromEmail != null && !fromEmail.isEmpty() ? fromEmail : defaultFromEmail;
message.setFrom(from);
message.setTo(toEmails.toArray(new String[0]));
if (!CollectionUtils.isEmpty(ccEmails) && !(ccEmails.size() == 1 && ccEmails.get(0).isEmpty())) {
message.setCc(ccEmails.toArray(new String[0]));
}
String subject = subjectPrefix + "系统发生错误 - " +
event.getLoggerName() + " - " +
new java.text.SimpleDateFormat("MM-dd HH:mm").format(new Date());
message.setSubject(subject);
message.setText(errorContent);
mailSender.send(message);
}
/**
* 构建HTML邮件内容
*/
private String buildHtmlContent(String errorContent, ILoggingEvent event) {
String html = "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <style>\n" +
" body { font-family: Arial, sans-serif; margin: 20px; }\n" +
" .header { background-color: #f44336; color: white; padding: 10px; border-radius: 5px; }\n" +
" .content { background-color: #f9f9f9; padding: 15px; border: 1px solid #ddd; border-radius: 5px; margin-top: 10px; }\n" +
" .info-item { margin: 5px 0; }\n" +
" .stacktrace { background-color: #fff; padding: 10px; border: 1px solid #ccc; border-radius: 3px; font-family: monospace; white-space: pre-wrap; }\n" +
" .footer { margin-top: 20px; color: #666; font-size: 12px; }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"header\">\n" +
" <h2>⚠️ 系统错误告警</h2>\n" +
" </div>\n" +
" \n" +
" <div class=\"content\">\n" +
" <div class=\"info-item\"><strong>时间:</strong>" + formatTimestamp(event.getTimeStamp()) + "</div>\n" +
" <div class=\"info-item\"><strong>级别:</strong><span style=\"color: red; font-weight: bold;\">" + event.getLevel() + "</span></div>\n" +
" <div class=\"info-item\"><strong>类名:</strong>" + event.getLoggerName() + "</div>\n" +
" <div class=\"info-item\"><strong>线程:</strong>" + event.getThreadName() + "</div>\n" +
" <div class=\"info-item\"><strong>消息:</strong>" + event.getFormattedMessage() + "</div>\n";
// 添加上下文信息
if (event.getMDCPropertyMap() != null && !event.getMDCPropertyMap().isEmpty()) {
html += " <div class=\"info-item\"><strong>上下文信息:</strong></div>\n";
html += " <div style=\"margin-left: 20px;\">\n";
for (var entry : event.getMDCPropertyMap().entrySet()) {
html += " <div>" + entry.getKey() + ": " + entry.getValue() + "</div>\n";
}
html += " </div>\n";
}
// 添加堆栈信息
if (event.getThrowableProxy() != null) {
html += " <div class=\"info-item\"><strong>异常堆栈:</strong></div>\n";
html += " <div class=\"stacktrace\">" +
getStackTraceHtml(event.getThrowableProxy()) +
"</div>\n";
}
html += " </div>\n" +
" \n" +
" <div class=\"footer\">\n" +
" <p>此邮件由系统自动发送,请勿回复。</p>\n" +
" <p>发送时间:" + new Date() + "</p>\n" +
" </div>\n" +
"</body>\n" +
"</html>";
return html;
}
private String formatTimestamp(long timestamp) {
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
.format(new java.util.Date(timestamp));
}
private String getStackTraceHtml(IThrowableProxy throwableProxy) {
StringBuilder sb = new StringBuilder();
int depth = 0;
while (throwableProxy != null && depth < 5) {
sb.append("<div style=\"color: #d32f2f;\">")
.append(throwableProxy.getClassName())
.append(": ")
.append(throwableProxy.getMessage())
.append("</div>");
StackTraceElementProxy[] elements = throwableProxy.getStackTraceElementProxyArray();
if (elements != null) {
int limit = Math.min(elements.length, 15);
for (int i = 0; i < limit; i++) {
sb.append("<div style=\"margin-left: 20px; color: #666;\">")
.append("at ")
.append(elements[i].getStackTraceElement())
.append("</div>");
}
if (elements.length > 15) {
sb.append("<div style=\"margin-left: 20px; color: #999;\">")
.append("... ")
.append(elements.length - 15)
.append(" more lines")
.append("</div>");
}
}
throwableProxy = throwableProxy.getCause();
if (throwableProxy != null) {
sb.append("<div style=\"margin-top: 5px;\"><strong>Caused by:</strong></div>");
depth++;
}
}
return sb.toString();
}
/**
* 检查邮件配置是否完整
*/
private boolean isEmailConfigured() {
if (mailSender == null) {
log.error("mailSender 未注入");
return false;
}
if (CollectionUtils.isEmpty(toEmails) || (toEmails.size() == 1 && toEmails.get(0).isEmpty())) {
log.error("收件人邮箱未配置");
return false;
}
if ((fromEmail == null || fromEmail.isEmpty()) &&
(defaultFromEmail == null || defaultFromEmail.isEmpty())) {
log.error("发件人邮箱未配置");
return false;
}
return true;
}
}
四、配置Logback使用自定义Appender
1. logback-spring.xml 配置文件
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<!-- 定义常量 -->
<property name="LOG_HOME" value="./logs" />
<property name="APP_NAME" value="my-application" />
<!-- 引入Spring Boot默认配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<!-- 注册自定义Appender -->
<appender name="ERROR_CAPTURE" class="com.example.logging.ErrorLogAppender">
<!-- 只处理ERROR级别 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- ERROR级别文件输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%ex</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!-- 异步Appender,提高性能 -->
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<neverBlock>true</neverBlock>
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="ERROR_CAPTURE" />
</appender>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 根日志配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_ERROR" />
</root>
<!-- 应用特定包日志 -->
<logger name="com.example" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_ERROR" />
</logger>
</configuration>
五、配置异步任务执行器
1. AsyncConfig.java - 异步配置
java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* 邮件发送线程池
*/
@Bean("emailTaskExecutor")
public Executor emailTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(2);
// 最大线程数
executor.setMaxPoolSize(5);
// 队列大小
executor.setQueueCapacity(100);
// 线程名前缀
executor.setThreadNamePrefix("email-task-");
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间
executor.setAwaitTerminationSeconds(60);
// 初始化
executor.initialize();
return executor;
}
}
六、测试使用
1. 测试Controller
java
package com.example.controller;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class TestController {
@GetMapping("/test/error")
public String testError(@RequestParam(defaultValue = "test") String message) {
// 设置MDC上下文信息(会在邮件中显示)
MDC.put("userId", "user123");
MDC.put("requestId", "req-" + System.currentTimeMillis());
MDC.put("ip", "127.0.0.1");
try {
// 模拟业务逻辑
log.info("处理请求,参数: {}", message);
// 模拟一个错误
if ("error".equals(message)) {
throw new RuntimeException("测试异常:" + message);
}
// 手动记录ERROR日志
if ("logerror".equals(message)) {
log.error("手动记录错误日志,参数: {}", message,
new IllegalArgumentException("非法参数"));
return "错误日志已记录";
}
return "请求成功: " + message;
} catch (Exception e) {
log.error("处理请求时发生异常", e);
return "发生错误: " + e.getMessage();
} finally {
// 清除MDC
MDC.clear();
}
}
}
2. 启动类
java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
七、邮箱配置指南
QQ邮箱配置步骤:
-
登录QQ邮箱 → 设置 → 账户
-
开启服务:找到"POP3/SMTP服务"和"IMAP/SMTP服务"
-
开启服务(需要短信验证)
-
生成授权码:会生成一个16位的授权码
-
在application.yml中使用授权码作为password
163邮箱配置步骤:
-
登录163邮箱 → 设置 → POP3/SMTP/IMAP
-
开启服务:开启SMTP服务
-
获取授权码(需要短信验证)
-
使用授权码作为password
八、运行测试
-
启动应用
-
访问测试接口:
-
http://localhost:8080/test/error?message=logerror记录错误日志 -
http://localhost:8080/test/error?message=error触发异常
-
-
查看控制台日志,确认错误被捕获
-
检查邮箱,查看是否收到错误通知邮件
九、故障排查
如果邮件发送失败,检查:
-
邮箱配置是否正确
-
确认使用授权码而非登录密码
-
确认邮箱已开启SMTP服务
-
-
网络连接
-
检查是否能连接到SMTP服务器
-
检查防火墙是否屏蔽了465端口
-
-
查看应用日志
-
查看ErrorLogAppender的启动日志
-
查看邮件发送失败的异常堆栈
-
十、优化建议
-
频率控制:避免同一错误频繁发送邮件
-
错误分类:根据不同错误类型发送到不同负责人
-
邮件模板:可以提取邮件模板到外部文件
-
重试机制:邮件发送失败时重试
-
开关控制:提供API动态开启/关闭邮件通知
这样配置后,系统一旦发生ERROR级别的错误,就会自动发送邮件通知到指定邮箱,便于及时处理问题。