2

我有一个非常简单的 Java webapp,它在开发系统上表现出一些非常奇怪的行为。问题从注册处理程序开始,它的实现如下:

//XXX:  this shouldn't really be 'synchronized', but I've declared it as such 
//      for the sake of debugging this issue
public synchronized ModelAndView submitRegister(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String email = request.getParameter("email");
    String pass = request.getParameter("pass");
    String conf = request.getParameter("conf");
    String name = request.getParameter("name");

    EntityManager em = DatabaseUtil.getEntityManager(request);

    //[make sure required fields are present and valid, etc.]

    User user = getUserForEmail(email, em);
    if (user != null) {
        //[user already exists, go to error page]
    }

    //create the new user
    em.getTransaction().begin();
    try {
        user = new User();
        //[set fields, etc.]
        em.persist(user); 

        //[generate e-mail message contents]
        boolean validEmail = EmailUtility.sendEmail(admin, recip, subject, message, null, recip);
        if (validEmail) {

            em.getTransaction().commit();
            //[go to 'registration successful' page]
        }

        em.getTransaction().rollback();
        //[go to error page]
    }
    catch (Exception e) {
        em.getTransaction().rollback();
        //[go to error page]
    }
}

问题出现在EmailUtility.sendEmail()通话中。此方法的代码非常简单:

public static boolean sendEmail(String fromAddress, String to, String subject, String message, String fromHeaderValue, String toHeaderValue) {
    try {
        Session session = getMailSession(to);
        Message mailMessage = new MimeMessage(session);
        mailMessage.setFrom(new InternetAddress(fromAddress));
        if (fromHeaderValue != null) {
            mailMessage.setHeader("From", fromHeaderValue);
        }
        if (toHeaderValue != null) {
            mailMessage.setHeader("To", toHeaderValue);
        }
        mailMessage.setHeader("Date", new Date().toString());
        mailMessage.setRecipients(RecipientType.TO, InternetAddress.parse(to, false));
        mailMessage.setSubject(subject);
        mailMessage.setContent(message, "text/html;charset=UTF-8");
        Transport.send(mailMessage);
        return true;
    } catch (Throwable e) {
        LOG.error("Failed to send e-mail!", e);
        return false;
    } 
}

发生的情况是,当代码到达调用时EmailUtility.sendEmail(),不是调用该方法,而是通过 submitRegister() 递归执行。这很容易成为我见过的最奇怪的事情之一。

有一段时间我什至不相信那是实际发生的事情。但此时我已经通过同步所涉及的方法并在两种方法的每一行添加打印语句来确认它。 submitRegister()递归,并且sendEmail()永远不会被调用。我不知道这怎么可能。

令人沮丧的是,完全相同的代码运行在生产服务器上。仅在开发系统上才会出现此问题。

欢迎任何有关可能导致此问题的原因以及我可以做些什么来解决它的建议。

4

2 回答 2

2

你是对的,这是不可能的 :) 如果你不喜欢调试,我建议你去掉所有其他代码,进行大量日志记录,看看会发生什么。从以下内容开始:

public synchronized ModelAndView submitRegister(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    LOG.debug("submitRegister: " + this.toString);
    EmailUtility.sendEmail("a@x.y", "b@x.y", "subject", "message", "from", "to");
}

public static boolean sendEmail(String fromAddress, String to, String subject, String message, String fromHeaderValue, String toHeaderValue) {
    LOG.debug("sendEmail: " + this.toString());
}

toString它将向您显示所涉及的课程。我的猜测是:

  • 您的第一次通话失败,因此sendEmail永远不会被调用
  • submitRegister由其他人多次触发,而不是由EmailUtility.sendEmail语句触发。

如果您让剥离的版本正常工作,请开始放回您的代码,一次一次和平,看看哪里出了问题:)

于 2012-09-21T05:46:49.693 回答
2

好的,我将此归结为几个不同的问题一起工作:

  1. 在开发系统上,缺少类路径javax.mail.Address。这导致EmailUtility类无法初始化,并且会NoClassDefFoundErrorsendEmail()调用中抛出一个,然后该方法的任何代码都可以执行。

  2. 中的代码submitRegister()有一个catch Exception块,但NoClassDefFoundErrorextends Error,而不是Exception. 所以它catch Exception完全绕过了这个障碍。

  3. 实际捕获的 Spring 控制器Error有一些我遇到过的最可疑的“错误处理”代码:

    try {
        Method serviceMethod = this.getControllerClass().getMethod(method, HttpServletRequest.class, HttpServletResponse.class);
        if (this.doesMethodHaveAnnotation(serviceMethod, SynchronizedPerAccount.class)) {
            synchronized(this.getAccountLock(request)) {
                super.doService(request, response);
            }
        }
        else {
            //don't need to execute synchronously
            super.doService(request, response);
        }
    }
    catch (Throwable ignored) {
        super.doService(request, response);
    }
    

因此,它NoClassDefFoundError正在传播回 Spring 控制器,该控制器正在捕获它并尝试重新调用该doService()方法,从而导致submitRegister()再次被调用。这不是递归(尽管无法通过仅查看调试输出来判断),而是 Spring 控制器为同一个请求调用了两次。对于给定的请求,它从未被调用超过两次,因为在第二次doService()调用周围没有尝试/捕获。

长话短说,我修补了这些问题并解决了问题。

于 2012-09-21T07:07:29.187 回答