简单的spring发送邮件以及异常邮件通知

以前发送邮件功能和评论通知是混合在一起的,现在想把它独立开来。
spring的邮件发送功能非常的简单,引入jar,配置服务,到处注入……
  <dependency>
        <groupId>javax.mail</groupId>
        <artifactId>mail</artifactId>
        <version>1.4.3</version>
    </dependency>
<bean id="javaMailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host" value="smtp.gmail.com" />
    <property name="port" value="587" />
    <property name="username" value="username" />
    <property name="password" value="password" />

    <property name="javaMailProperties">
       <props>
                 <prop key="mail.smtp.auth">true</prop>
                 <prop key="mail.smtp.starttls.enable">true</prop>
              </props>
    </property>
</bean>

最后对邮件做一个限制,可以设置指定时间内最多发送多少封邮件:

package me.qyh.blog.service.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.mail.internet.MimeMessage;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SerializationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;

import me.qyh.blog.config.Constants;
import me.qyh.blog.config.Limit;

public class MailSender implements InitializingBean {

    @Autowired
    private JavaMailSender javaMailSender;

    private static final Executor executor = Executors.newCachedThreadPool();
    private ConcurrentLinkedQueue<MessageBean> queue = new ConcurrentLinkedQueue<MessageBean>();

    private static final Limit limit = new Limit(1, 100, TimeUnit.SECONDS);
    private static final Logger logger = LoggerFactory.getLogger(MailSender.class);

    private static final File sdfile = new File("message_shutdown.dat");

    private TimeCount timeCount;

    public void send(MessageBean mb) {
        doSend(mb, true);
    }

    private synchronized boolean doSend(MessageBean mb, boolean pull) {
        long current = System.currentTimeMillis();
        if (timeCount == null)
            timeCount = new TimeCount(current, 1);
        else {
            timeCount.count++;
            if (timeCount.exceed(current)) {
                if (pull) {
                    logger.debug("在" + (current - timeCount.start) + "毫秒内,发送邮件数量达到了" + limit.getLimit() + "封,放入队列中");
                    queue.add(mb);
                }
                return false;
            }
            if (timeCount.reset(current))
                this.timeCount = new TimeCount(current, 1);
        }
        sendMail(mb);
        return true;
    }

    protected void sendMail(final MessageBean mb) {
        executor.execute(new Runnable() {

            @Override
            public void run() {
                try {
                    javaMailSender.send(new MimeMessagePreparator() {

                        @Override
                        public void prepare(MimeMessage mimeMessage) throws Exception {
                            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, mb.html,
                                    Constants.CHARSET.name());
                            helper.setText(mb.text, mb.html);
                            helper.setTo(mb.to);
                            helper.setSubject(mb.subject);
                            mimeMessage.setFrom();
                        }
                    });
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
            }
        });
    }

    private final class TimeCount {
        private long start;
        private int count;

        public TimeCount(long start, int count) {
            super();
            this.start = start;
            this.count = count;
        }

        public boolean exceed(long current) {
            return (limit.toMill() + start) >= current && (count > limit.getLimit());
        }

        public boolean reset(long current) {
            return (limit.toMill() + start) < current;
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (sdfile.exists()) {
            logger.debug("发现序列化文件,执行反序列化操作");
            queue = SerializationUtils.deserialize(new FileInputStream(sdfile));
            if (!FileUtils.deleteQuietly(sdfile))
                logger.warn("删除序列文件失败");
        }
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                for (Iterator<MessageBean> iterator = queue.iterator(); iterator.hasNext();) {
                    MessageBean mb = iterator.next();
                    if (!doSend(mb, false))
                        break;
                    iterator.remove();
                }
            }
        }, 1, 1, TimeUnit.SECONDS);
    }

    public void shutdown() {
        if (!queue.isEmpty()) {
            logger.debug("队列中存在未发送邮件,序列化到本地:" + sdfile.getAbsolutePath());

            try {
                SerializationUtils.serialize(queue, new FileOutputStream(sdfile));
            } catch (FileNotFoundException e) {
                logger.error("序列化失败:" + e.getMessage(), e);
                return;
            }
        }
    }

    public static final class MessageBean implements Serializable {
        /**
         * 
         */
        private static final long serialVersionUID = 1L;

        private String subject;
        private boolean html = true;
        private String text;
        private String to;

        public MessageBean(String subject, boolean html, String text, String to) {
            super();
            this.subject = subject;
            this.html = html;
            this.text = text;
            this.to = to;
        }

    }
}


public class Limit {

    private int limit;
    private long time;
    private TimeUnit unit;

   //getter...setter...

    public Limit() {

    }

    public Limit(int limit, long time, TimeUnit unit) {
        this.limit = limit;
        this.time = time;
        this.unit = unit;
    }

    public long toMill() {
        return unit.toMillis(time);
    }
}
通过改变limit(这里是写死了,100秒内最多发送一封邮件),可以粗粒度的控制发送频率。
配置:
<bean class="me.qyh.blog.service.impl.MailSender" destroy-method="shutdown"></bean>
ps:这里只是向管理员发送邮件,而非向其他用户发送邮件,面对对象不同,实现也不一样。
最后再利用这个实现一个基于邮件的日志服务(基于logback,基本就是ch.qos.logback.classic.net.SMTPAppender):
public class MailAppendar extends AppenderBase<ILoggingEvent> {

    // ~ 14 days
    static final int MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;

    long lastTrackerStatusPrint = 0;
    int delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;

    private MailSender mailSender;

    private Layout<ILoggingEvent> subjectLayout;
    private Layout<ILoggingEvent> layout;
    private EventEvaluator<ILoggingEvent> eventEvaluator;

    private Discriminator<ILoggingEvent> discriminator = new DefaultDiscriminator<ILoggingEvent>();
    private CyclicBufferTracker<ILoggingEvent> cbTracker;
    private int errorCount;

    private boolean includeCallerData = false;
    private String subjectStr;

    // value "%logger{20} - %m" is referenced in the docs!
    private static final String DEFAULT_SUBJECT_PATTERN = "%logger{20} - %m";

    @Override
    protected void append(ILoggingEvent eventObject) {
        if (mailSender == null) {
            ApplicationContext ctx = ApplicationContextProvider.getApplicationContext();
            if (ctx != null)
                try {
                    mailSender = ctx.getBean(MailSender.class);
                } catch (BeansException e) {
                    // ignore;
                }
        }
        if (!checkEntryConditions()) {
            return;
        }

        String key = discriminator.getDiscriminatingValue(eventObject);
        long now = System.currentTimeMillis();
        final CyclicBuffer<ILoggingEvent> cb = cbTracker.getOrCreate(key, now);

        if (includeCallerData) {
            eventObject.getCallerData();
        }
        eventObject.prepareForDeferredProcessing();
        cb.add(eventObject);

        try {
            if (eventEvaluator.evaluate(eventObject)) {
                // clone the CyclicBuffer before sending out asynchronously
                CyclicBuffer<ILoggingEvent> cbClone = new CyclicBuffer<ILoggingEvent>(cb);
                // see http://jira.qos.ch/browse/LBCLASSIC-221
                cb.clear();

                // build MessageBean

                sendMail(cbClone, eventObject);

            }
        } catch (EvaluationException ex) {
            errorCount++;
            if (errorCount < CoreConstants.MAX_ERROR_COUNT) {
                addError("SMTPAppender's EventEvaluator threw an Exception-", ex);
            }
        }

        // immediately remove the buffer if asked by the user
        if (eventMarksEndOfLife(eventObject)) {
            cbTracker.endOfLife(key);
        }

        cbTracker.removeStaleComponents(now);

        if (lastTrackerStatusPrint + delayBetweenStatusMessages < now) {
            addInfo("MailAppender [" + name + "] is tracking [" + cbTracker.getComponentCount() + "] buffers");
            lastTrackerStatusPrint = now;
            // quadruple 'delay' assuming less than max delay
            if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) {
                delayBetweenStatusMessages *= 4;
            }
        }
    }

    private void sendMail(CyclicBuffer<ILoggingEvent> cb, ILoggingEvent lastEventObject) {
        try {
            StringBuffer sbuf = new StringBuffer();
            String header = layout.getFileHeader();
            if (header != null) {
                sbuf.append(header);
            }
            String presentationHeader = layout.getPresentationHeader();
            if (presentationHeader != null) {
                sbuf.append(presentationHeader);
            }

            int len = cb.length();
            for (int i = 0; i < len; i++) {
                ILoggingEvent event = cb.get();
                sbuf.append(layout.doLayout(event));
            }

            String presentationFooter = layout.getPresentationFooter();
            if (presentationFooter != null) {
                sbuf.append(presentationFooter);
            }
            String footer = layout.getFileFooter();
            if (footer != null) {
                sbuf.append(footer);
            }

            String subjectStr = "Undefined subject";
            if (subjectLayout != null) {
                subjectStr = subjectLayout.doLayout(lastEventObject);

                // The subject must not contain new-line characters, which cause
                // an SMTP error (LOGBACK-865). Truncate the string at the first
                // new-line character.
                int newLinePos = (subjectStr != null) ? subjectStr.indexOf('\n') : -1;
                if (newLinePos > -1) {
                    subjectStr = subjectStr.substring(0, newLinePos);
                }
            }
            String contentType = layout.getContentType();
            MessageBean mb = new MessageBean(subjectStr, !ContentTypeUtil.isTextual(contentType), sbuf.toString());
            mailSender.send(mb);
        } catch (Exception e) {
            addError("Error occurred while sending e-mail notification.", e);
        }
    }

    @Override
    public void start() {
        if (cbTracker == null) {
            cbTracker = new CyclicBufferTracker<ILoggingEvent>();
        }

        if (this.eventEvaluator == null) {
            OnErrorEvaluator onError = new OnErrorEvaluator();
            onError.setContext(getContext());
            onError.setName("onError");
            onError.start();
            this.eventEvaluator = onError;
        }

        if (subjectStr == null) {
            subjectStr = DEFAULT_SUBJECT_PATTERN;
        }
        PatternLayout pl = new PatternLayout();
        pl.setContext(getContext());
        pl.setPattern(subjectStr);
        // we don't want a ThrowableInformationConverter appended
        // to the end of the converter chain
        // This fixes issue LBCLASSIC-67
        pl.setPostCompileProcessor(null);
        pl.start();
        subjectLayout = pl;

        super.start();
    }

    synchronized public void stop() {
        this.started = false;
    }

    private boolean eventMarksEndOfLife(ILoggingEvent eventObject) {
        Marker marker = eventObject.getMarker();
        if (marker == null)
            return false;

        return marker.contains(ClassicConstants.FINALIZE_SESSION_MARKER);
    }

    public boolean checkEntryConditions() {
        if (!this.started) {
            addError("Attempting to append to a non-started appender: " + this.getName());
            return false;
        }
        if (this.eventEvaluator == null) {
            addError("No EventEvaluator is set for appender [" + name + "].");
            return false;
        }
        if (this.layout == null) {
            addError("No layout set for appender named [" + name
                    + "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout");
            return false;
        }
        if (mailSender == null) {
            addError("mail Sender is null");
            return false;
        }
        return true;
    }

    public void setLayout(Layout<ILoggingEvent> layout) {
        this.layout = layout;
    }

    public void setEvaluator(EventEvaluator<ILoggingEvent> eventEvaluator) {
        this.eventEvaluator = eventEvaluator;
    }

    public void setSubject(String subject) {
        this.subjectStr = subject;
    }

    public void setIncludeCallerData(boolean includeCallerData) {
        this.includeCallerData = includeCallerData;
    }
}
因为MailAppender不受spring容器的管理,所以这里获取MailSender对象只能通过ApplicationContextProvider来获取:
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext ac) throws BeansException {
        context = ac;
    }
}
<bean class="me.qyh.blog.service.impl.ApplicationContextProvider" />

logback.xml中的配置:

    <appender name="mailAppender" class="me.qyh.blog.mail.MailAppendar">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>.%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %n
            </pattern>
        </layout>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
为了防止邮件发送过程中发生异常,陷入日志记录死循环,所以MailSender的日志应该单独记录,比如:
    <logger name="me.qyh.blog.service.impl.MailSender" level="ERROR"
        additivity="false">
        <appender-ref ref="errorDailyRollingFileAppender" />
    </logger>