【Spring boot】一个简单的logback邮件appender

logback本身就提供了SMTPAppender,在和spring boot中使用时,我希望采用spring boot配置的邮件服务来发送,还有一点,我并不希望邮件发送的太频繁,过几秒就来一封。

获取ApplicationContext

这里通过ApplicationContext来获取Spring Boot中配置的邮件服务,因为logback的appender并不是由spring管理,所以无法直接注入,在以前使用Spring mvc的时候,可以使用ContextLoader来获取,但现在发现通过ContextLoader获取的始终为null,所以这里通过启动类来获取:

通过jar启动

ApplicationContext ctx = SpringApplication.run(Blog.class, args);

通过war启动

启动类继承了SpringBootServletInitializer,然后通过

@Override
protected WebApplicationContext run(SpringApplication application) {
    WebApplicationContext webApplicationContext = super.run(application);
    ctx = webApplicationContext;
    return webApplicationContext;
}

获取

创建邮件记录

package me.qyh.blog.support.logback;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import org.springframework.context.ApplicationContext;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.thymeleaf.util.StringUtils;

import ch.qos.logback.core.UnsynchronizedAppenderBase;
import ch.qos.logback.core.encoder.Encoder;
import me.qyh.blog.Blog;
import me.qyh.blog.entity.BlogConfig;
import me.qyh.blog.service.BlogConfigService;
import me.qyh.blog.utils.FileUtils;

public class EmailAppender<E> extends UnsynchronizedAppenderBase<E> {

	private JavaMailSender javaMailSender;
	private BlogConfigService configService;
	private ReentrantLock lock = new ReentrantLock();
	private Path current;

	private Encoder<E> encoder;
	private int seconds = 600;

	private ScheduledFuture<?> future;

	private int maxMByte = 10;// when error log reached this size,send email immediately
	private long maxByte;

	@Override
	public void start() {
		if (seconds < 0) {
			System.err.println("seconds must be positive!");
			return;
		}
		if (maxMByte < 0) {
			System.err.println("maxMByte must be positive!");
			return;
		}
		maxByte = maxMByte * 1024 * 1024L;
		super.start();
		future = getContext().getScheduledExecutorService().scheduleAtFixedRate(this::sendEmail, seconds, seconds,
				TimeUnit.SECONDS);
	}

	@Override
	protected void append(E event) {
		if (!isStarted()) {
			return;
		}
		if (javaMailSender == null) {
			ApplicationContext context = getApplicationContext();
			if (context == null) {
				return;
			}
			javaMailSender = context.getBeanProvider(JavaMailSender.class).getIfAvailable();
			if (javaMailSender == null) {
				this.stop();
				return;
			}
			configService = context.getBean(BlogConfigService.class);
		}
		String error = new String(this.encoder.encode(event), StandardCharsets.UTF_8);
		lock.lock();
		try {
			if (current == null || !Files.exists(current)) {
				// create temp file
				try {
					current = Files.createTempFile(null, ".log");
				} catch (IOException e) {
					this.stop();
					return;
				}
			}

			try {
				Files.writeString(current, error, StandardOpenOption.APPEND);
				if (Files.size(current) > maxByte) {
					sendAsync();
				}
			} catch (IOException e) {

			}
		} finally {
			lock.unlock();
		}
	}

	private void sendEmail() {
		if (configService == null || javaMailSender == null) {
			return;
		}
		if (current == null || !Files.exists(current)) {
			return;
		}
		final BlogConfig config = configService.getConfig();
		if (StringUtils.isEmptyOrWhitespace(config.getEmail())) {
			return;
		}
		lock.lock();
		try {
			sendAsync();
		} finally {
			lock.unlock();
		}
	}

	private void sendAsync() {
		try {
			Path copy;
			try {
				copy = Files.createTempFile(null, ".log");
				Files.move(current, copy, StandardCopyOption.REPLACE_EXISTING);
			} catch (IOException e) {
				return;
			}
			super.getContext().getScheduledExecutorService().execute(() -> {
				try {
					javaMailSender.send(mm -> {
						MimeMessageHelper helper = new MimeMessageHelper(mm, true, StandardCharsets.UTF_8.name());
						helper.setText("error.log", false);
						helper.setTo(configService.getConfig().getEmail());
						helper.setSubject("error.log");
						helper.addAttachment("error_log.txt", copy.toFile());
						mm.setFrom();
					});
				} finally {
					FileUtils.deleteQuietly(copy);
				}
			});
		} finally {
			FileUtils.deleteQuietly(current);
			current = null;
		}
	}

	@Override
	public void stop() {
		super.stop();
		sendEmail();
		if (future != null) {
			future.cancel(true);
		}
	}

	public void setEncoder(Encoder<E> encoder) {
		this.encoder = encoder;
	}

	public void setSeconds(int seconds) {
		this.seconds = seconds;
	}

	public void setMaxMByte(int maxMByte) {
		this.maxMByte = maxMByte;
	}

	private ApplicationContext getApplicationContext() {
		return Blog.getApplicationContext();
	}
}

配置

<appender name="emailAppender"
    class="me.qyh.blog.support.logback.EmailAppender">
    <encoder>
        <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n
        </Pattern>
        <charset>UTF-8</charset>
    </encoder>
</appender>
<root level="ERROR">
    <appender-ref ref="emailAppender" />
</root>