实用指南:springboot项目中使用的Logback如何重写Logback获取error信息,获取到error信息后如何发送错误邮件

我来为你详细讲解如何在Spring Boot 2.5.10 + JDK8项目中重写Logback获取error信息并发送邮件通知。

一、项目依赖配置

1. pom.xml 添加依赖

xml


    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
    
    
        org.springframework.boot
        spring-boot-starter-mail
    
    
    
    
        org.projectlombok
        lombok
        true
    

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邮箱,都需要:

  1. 开启SMTP服务

  2. 获取授权码(不是登录密码)

二、创建自定义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 {
    
    // 错误日志队列(线程安全)
    private final BlockingQueue 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 toEmails;
    
    @Value("#{'${error.email.cc:}'.split(',')}")
    private List ccEmails;
    
    @Value("#{'${error.email.bcc:}'.split(',')}")
    private List 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 = "\n" +
                "\n" +
                "\n" +
                "    \n" +
                "    \n" +
                "\n" +
                "\n" +
                "    
\n" + "

⚠️ 系统错误告警

\n" + "
\n" + " \n" + "
\n" + "
时间:" + formatTimestamp(event.getTimeStamp()) + "
\n" + "
级别:" + event.getLevel() + "
\n" + "
类名:" + event.getLoggerName() + "
\n" + "
线程:" + event.getThreadName() + "
\n" + "
消息:" + event.getFormattedMessage() + "
\n"; // 添加上下文信息 if (event.getMDCPropertyMap() != null && !event.getMDCPropertyMap().isEmpty()) { html += "
上下文信息:
\n"; html += "
\n"; for (var entry : event.getMDCPropertyMap().entrySet()) { html += "
" + entry.getKey() + ": " + entry.getValue() + "
\n"; } html += "
\n"; } // 添加堆栈信息 if (event.getThrowableProxy() != null) { html += "
异常堆栈:
\n"; html += "
" + getStackTraceHtml(event.getThrowableProxy()) + "
\n"; } html += "
\n" + " \n" + "
\n" + "

此邮件由系统自动发送,请勿回复。

\n" + "

发送时间:" + new Date() + "

\n" + "
\n" + "\n" + ""; 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("
") .append(throwableProxy.getClassName()) .append(": ") .append(throwableProxy.getMessage()) .append("
"); StackTraceElementProxy[] elements = throwableProxy.getStackTraceElementProxyArray(); if (elements != null) { int limit = Math.min(elements.length, 15); for (int i = 0; i < limit; i++) { sb.append("
") .append("at ") .append(elements[i].getStackTraceElement()) .append("
"); } if (elements.length > 15) { sb.append("
") .append("... ") .append(elements.length - 15) .append(" more lines") .append("
"); } } throwableProxy = throwableProxy.getCause(); if (throwableProxy != null) { sb.append("
Caused by:
"); 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



    
    
    
    
    
    
    
    
    
    
    
        
        
            ERROR
            ACCEPT
            DENY
        
    
    
    
    
        ${LOG_HOME}/error.log
        
            ${LOG_HOME}/error.%d{yyyy-MM-dd}.log
            30
        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%ex
        
        
            ERROR
        
    
    
    
    
        0
        512
        true
        
        
    
    
    
    
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
        
    
    
    
    
        
        
    
    
    
    
        
        
    
    

五、配置异步任务执行器

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邮箱配置步骤

  1. 登录QQ邮箱 → 设置 → 账户

  2. 开启服务:找到"POP3/SMTP服务"和"IMAP/SMTP服务"

  3. 开启服务(需要短信验证)

  4. 生成授权码:会生成一个16位的授权码

  5. 在application.yml中使用授权码作为password

163邮箱配置步骤

  1. 登录163邮箱 → 设置 → POP3/SMTP/IMAP

  2. 开启服务:开启SMTP服务

  3. 获取授权码(需要短信验证)

  4. 使用授权码作为password

八、运行测试

  1. 启动应用

  2. 访问测试接口

    • http://localhost:8080/test/error?message=logerror 记录错误日志

    • http://localhost:8080/test/error?message=error 触发异常

  3. 查看控制台日志,确认错误被捕获

  4. 检查邮箱,查看是否收到错误通知邮件

九、故障排查

如果邮件发送失败,检查:

  1. 邮箱配置是否正确

    • 确认使用授权码而非登录密码

    • 确认邮箱已开启SMTP服务

  2. 网络连接

    • 检查是否能连接到SMTP服务器

    • 检查防火墙是否屏蔽了465端口

  3. 查看应用日志

    • 查看ErrorLogAppender的启动日志

    • 查看邮件发送失败的异常堆栈

十、优化建议

  1. 频率控制:避免同一错误频繁发送邮件

  2. 错误分类:根据不同错误类型发送到不同负责人

  3. 邮件模板:可以提取邮件模板到外部文件

  4. 重试机制:邮件发送失败时重试

  5. 开关控制:提供API动态开启/关闭邮件通知

这样配置后,系统一旦发生ERROR级别的错误,就会自动发送邮件通知到指定邮箱,便于及时处理问题。

posted @ 2026-03-08 10:14  clnchanpin  阅读(17)  评论(0)    收藏  举报