为发送电子邮件创建后台线程

5
我需要在注册过程中发送电子邮件,因此我使用Java Mail API进行操作。虽然操作正常,但我发现该过程需要将近6秒的时间(这太长了),使得用户在等待Ajax调用的响应时需要等待很长时间。
因此,我决定使用后台线程发送电子邮件,以便用户无需等待Ajax调用的响应(Jersey REST Web Service调用)。
我的问题是,在Web应用程序中为每个请求创建线程是否是一个好的实践?
@Path("/insertOrUpdateUser")
public class InsertOrUpdateUser {
        final static Logger logger = Logger.getLogger(InsertOrUpdateUser.class);
        @GET
        @Consumes("application/text")
        @Produces("application/json")
        public String getSalesUserData(@QueryParam(value = "empId") String empId
                        )
                        throws JSONException, SQLException {
                JSONObject final_jsonobject = new JSONObject();
            ExecutorService executorService = Executors.newFixedThreadPool(10);
                                executorService.execute(new Runnable() {
                                 public void run() {
                                         try {
                                                        SendEmailUtility.sendmail(emaildummy);
                                                } catch (IOException e) {
                                                        logger.error("failed",e);

                                                }
                                 }
                              });

                        }


                } catch (SQLException e) {

                } catch (Exception e) {


                }


                   finally {

                        }


                return response;
        }




}

这是用于发送电子邮件的实用工具类:

这是我发送电子邮件的实用程序类

public class SendEmailUtility
{
    public static String sendmail(String sendto)
        throws IOException
    {
        String result = "fail";
        Properties props_load = getProperties();
        final String username = props_load.getProperty("username");
        final String password = props_load.getProperty("password");
        Properties props_send = new Properties();
        props_send.put("mail.smtp.auth", "true");
        props_send.put("mail.smtp.starttls.enable", "true");
        props_send.put("mail.smtp.host", props_load.getProperty("mail.smtp.host"));
        props_send.put("mail.smtp.port", props_load.getProperty("mail.smtp.port"));

        Session session = Session.getInstance(props_send,
            new javax.mail.Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication()
                {
                    return new PasswordAuthentication(username, password);
                }
            });

        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress(props_load.getProperty("setFrom")));
            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(sendto));
            message.setText("Some Text to be send in mail");
            Transport.send(message);
            result = "success";
        } catch (MessagingException e) {
            result = "fail";
            logger.error("Exception Occured - sendto: " + sendto, e);
        }
        return result;
    }
}

您好,请问这是否是在Web应用程序中最佳实践?


邮件是在服务器端发送的,不是吗?因此,无论在哪个线程中运行它,它始终需要相同的时间。也许您想向用户显示一个循环的中间进度条,指示他的请求可能需要一些时间才能完成。 - xdevs23
你有考虑过你的网络速度吗? - Enzokie
抱歉造成困惑,通过后台线程的帮助,Ajax调用不会等待电子邮件处理,因此就Ajax响应而言是可以的,但我的担忧与正在创建的线程数量有关。 - Pawan
为每个请求创建线程池是愚蠢的。当你有像这样的片段时,很难知道意图。此外,@Sean 指出排队这些电子邮件请求是一个好主意。 - Nathan Hughes
@Path("/insertOrUpdateUser") - 我不想成为那个人...但这并不是很符合RESTful的规范。 - Sean Bright
显示剩余3条评论
4个回答

9

有许多方法可以处理它,因此这取决于您的应用程序服务器是否具有足够的资源(内存、线程等)来处理您的实现,因此您是最好的人来决定采用哪种方法。

因此,如果设计合理,为执行某些操作生成并行线程并不是一个坏习惯,但通常应使用受控线程。

请注意,无论您使用 newSingleThreadExecutor() 还是 newFixedThreadPool(nThreads),在幕后都会创建一个 ThreadPoolExecutor 对象。

我的建议是在下面的列表中使用第二个选项,即“控制线程数”,并在其中指定最大线程计数。

每个请求的一个线程

在此方法中,针对GUI中的每个传入请求将创建一个线程,因此,如果您收到10个用于插入/更新用户的请求,则将生成10个线程,这些线程将发送电子邮件。

此方法的缺点是没有对线程数进行控制,因此可能会出现 StackOverflowException 或内存问题。

请务必关闭您的执行器服务,否则将浪费 JVM 资源。

// inside your getSalesUserData() method
ExecutorService emailExecutor = Executors.newSingleThreadExecutor();
        emailExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    SendEmailUtility.sendmail(emaildummy);
                } catch (IOException e) {
                    logger.error("failed", e);
                }
            }
        });
        emailExecutor.shutdown(); // it is very important to shutdown your non-singleton ExecutorService.

可控制的线程数量

在这种方法中,预定义数量的线程将存在并处理您的电子邮件发送需求。在下面的示例中,我启动了一个最大为10个线程的线程池,然后我使用LinkedBlockingQueue实现,因此这将确保如果有超过10个请求且当前所有10个线程都忙,则多余的请求将被排队而不会丢失,这是您使用QueueLinkedBlockingQueue实现所获得的优势。

您可以在应用服务器启动时初始化单例ThreadPoolExecutor,如果没有请求,则不会存在线程,因此这样做是安全的。事实上,我在我的生产应用程序中使用类似的配置。

我将生存时间秒数设置为1秒,因此如果JVM中的线程空闲超过1秒,则它将死亡。

请注意,由于相同的线程池用于处理所有请求,因此它应该是单例的,并且不要关闭此线程池,否则您的任务将永远不会执行。

// creating a thread pool with 10 threads, max alive time is 1 seconds, and linked blocking queue for unlimited queuing of requests.
        // if you want to process with 100 threads then replace both instances of 10 with 100, rest can remain same...
        // this should be a singleton
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

        // inside your getSalesUserData() method
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    SendEmailUtility.sendmail(emaildummy);
                } catch (IOException e) {
                    logger.error("failed", e);
                }
            }
        });

Java的默认线程池

这种方法与上面的方法非常相似,只是Java会为您初始化ThreadPoolExecutor,并将其设置为ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

在此方法中,线程的最大数量将为Integer.MAX_VALUE,因此线程将根据需要创建,并且存活时间为60秒。

如果您想使用这种方式,请按照以下方法进行操作。

// this should be a singleton
        ExecutorService emailExecutor = Executors.newCachedThreadPool();

        // from you getSalesUserData() method
        emailExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    SendEmailUtility.sendmail(emaildummy);
                } catch (IOException e) {
                    logger.error("failed", e);
                }
            }
        });

在最后一个选项中,ExecutorService是否也应该使用Singleton概念创建,还是为每个传入请求创建emailExecutor可以? - zee
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - hagrawal7777
你如何使用Java的默认缓存线程池(例如单元测试)测试代码? - Reneta
我让线程休眠100毫秒:Thread.sleep(100);测试通过了,因为它有足够的时间并且邮件已发送,我想这不是一个坏的测试方式。 - Reneta

2
在Java Web服务器上手动创建ExecutorService是不好的想法。在您的实现中,每个请求都会创建10个线程。
更好的解决方案是使用ManagedExecutorService示例),如果您使用JEE7或ThreadPoolTaskExecutor,如果您使用Spring(文档)。
如果您使用Tomcat,则应阅读此线程

我正在使用Tomcat,非常感谢。我可以以此为起点来完成这个任务。 - Pawan

1
最佳实践是使用单个ExecutorService为所有请求提供线程池。您可能希望使用非零但有限的线程配置ExecutorService。
这里的想法是,您将拥有一些在应用程序生命周期内重复使用的线程。如果发送电子邮件出现临时减速(或停止),则不会产生越来越多的线程。相反,您将得到越来越多的工作(要执行的电子邮件),这比额外的线程消耗更少的资源。

这似乎与我的问题相关,是否可以为我的情况配置线程池? - Pawan
1
是的,只需将您的ExecutorService在类级别上设置为静态(就像您的日志记录器一样),它将在加载类时创建一次并保持活动状态直到应用程序停止。您可以实现一个ServletContextListener来显式管理生命周期并确保正确的关闭和清理(例如,使您的应用程序在关闭之前等待所有消息发送完成)。 - Rob

0

我正在使用Java的EmailSender类。 我只是启动了一个新线程来发送邮件,因为它会阻塞主线程并导致超时异常。

    String link = "http://localhost:PORT/api/v1/registration/confirm?token=" +token;
    //Sending mail in thread beacause it block main thread
    new Thread(
    () -> emailSender.sendMail(request.getEmail(),buildEmail(request.getFirstName(),
    link))).start();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接