我应该使用 Spring 单例 bean 来构建电子邮件消息吗?

Should I use a Spring singleton bean to build up an email message?

@Service class,我在其他 @Service class 上调用了几个方法。服务方法做一些工作并向电子邮件添加一些文本。该电子邮件将发送给管理员,让他们知道已完成的工作。

这是一个简单的示例,但通常可以在任何服务方法中附加电子邮件消息,包括 MainService 未直接调用的服务方法。与日志记录类似,消息总是附加到现有消息。它没有以任何其他方式修改。

@Service
public class MainService {    

   private final Service1 service1;
   private final Service2 service2;
   private final Mail mail;

   public void doWork() {

       StringBuilder emailMessage = new StringBuilder();

       List<Item> items = service1.method1(emailMessage);
       Employee employee = service1.method2(emailMessage);
       service1.method3(emailMessage);

       service2.method1(emailMessage);
       service2.method2(emailMessage);

       //send one email to the admins 
       mail.sendEmailToAdmins("Batch process completed", emailMessage.toString());
    }
}

@Service
public class Service1 {

    public List<Item> method1(StringBuilder emailMessage) {

         //some work to remove items
        
        String message = String.format("Following items were removed", items);
        log.info(message);
        emailMessage.append(message);
        return items;
    }
}

有没有办法避免绕过emailMessage?使用单例来保存状态(即电子邮件消息文本)并将其注入每个服务是否是一种不好的做法class?

一些注意事项是:

这真的取决于你的目标。

您编写的代码似乎有效,所以您的问题实际上是关于如何实现 design-perfection。您当然可以应用其他模式 - 例如某种调度或通知程序 - 但为什么呢?如果它有效,并且是 fast-enough,那么继续解决其他问题。它很慢,或者不能满足您的需求,那就是另一回事了。但在那种情况下,您应该收集性能指标并有条不紊地对其进行攻击。

我不确定你的 singlton 想法。本质上,emailMessage 代表 classic model-view-controller 范例中的 'model'。当然,您可以从中创建一个单例,但真正要做的是将模型 class 从 passed-on-the-stack 永久驻留在内存中(一次)。如果您的记忆因创建十亿封电子邮件而不断 new/deletes 抖动,那可能是有道理的,但我怀疑您有这个问题。 Singleton 可能不会受到伤害,但它能提供什么帮助?

不要让完美成为优秀的敌人

创建一个 MailBuilder class 作为您的域模型并传递它。根据哪个服务的作用,您可以为每个特定的服务调用提供一个方法。例如 MailBuilder.appendForServiceA(args)、MailBuilder.appendForServiceB(args)。这样您就可以真正地自行测试构建器。

你的思路是正确的,如果你看到不同服务与某个对象的相似交互,你可以将整个过程以 Beans 的形式提取出来。

比如我们可以这样描述这个过程:

  • 一个 EmailBuilder 对象作为输入(正如其他人已经提到的)
  • 以及对该对象的一系列转换
  • EmailBuilder 现在可以构建包含所有必要转换的电子邮件对象

下面是我将如何使用 Spring Beans 实现它,这里的 @Order 注释是设置的基础。

    @lombok.Data
    @lombok.Builder
    static class Email {
        private String to;
        private String from;
        @Singular
        private List<String> sections;
    }

    @Bean
    @Order(1)
    Consumer<Email.EmailBuilder> addFromFieldToEmail() {
        return emailBuilder -> emailBuilder.from("abcd@efghijk.xyz");
    }

    @Bean
    @Order(2)
    Consumer<Email.EmailBuilder> addToFieldToEmail() {
        return emailBuilder -> {
            // Get user email from context for example
            // SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            emailBuilder.to("user@domain.com");
        };
    }

    @Bean
    @Order(3) // the bean order is taken in consideration when we autowire
              // a list of similar beans. This ensures the processing order
    Consumer<Email.EmailBuilder> addGreetingSectionToEmailBody() {
        return emailBuilder -> emailBuilder.section(
                String.format("Dear %s,", emailBuilder.to)
                + "\nwelcome to our website!"
                + "\n"
        );
    }

    @Bean
    @Order(4)
    Consumer<Email.EmailBuilder> addMarketingSectionToEmailBody() {
        return emailBuilder -> emailBuilder.section(
                "Do you know that if you invite your friends you get 200 Schmeckles in credit?"
                + "[Invitation Link](https://efghijk.xyz/invite?user=123&token=abcdef123)"
        );
    }

    @Bean // creating a Callable bean, and not an EmailBuilder ensures
          // the creation of a new builder each time.
          // An alternative would be to create an EmailBuilder bean of scope prototype
          // see https://www.baeldung.com/spring-bean-scopes#prototype
    Callable<Email.EmailBuilder> emailBuilderProvider(final List<Consumer<Email.EmailBuilder>> customizers) {
        return () -> {
            final Email.EmailBuilder builder = Email.builder();
            customizers.forEach(customizer -> customizer.accept(builder));
            return builder;
        };
    }

    @Bean
    MailService mailService(final Callable<Email.EmailBuilder> emailBuilderProvider) {
        return new MailService(emailBuilderProvider);
    }

    @RequiredArgsConstructor
    class MailService {

        private final Callable<Email.EmailBuilder> emailBuilderProvider;

        void sendMail() throws Exception {
            Email email = emailBuilderProvider.call().build();
//            mail.sendEmailToAdmins("Batch process completed", email.toString());
        }
    }

坚持您的业务逻辑并考虑您分享的内容我认为对同一对象使用多个 side-effects 调用,考虑到 StringBuilder is not thread-safe,可能不是最好的方法。

我宁愿 return 来自每个调用的消息,您希望从中跟踪结果并将它们存储在数据结构中,例如堆栈,只是为了举例,并考虑构建邮件消息收集的数据。

如果需要,可以轻松调整此方法以获得线程安全实现。

是的,您可以使用 Bean 发送电子邮件:

@Bean
public JavaMailSender getJavaMailSender() {
    JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
    mailSender.setHost("host");
    mailSender.setPort(<port number>);

    mailSender.setUsername("user_name");
    mailSender.setPassword("password");

    ///////////////////////////////
    //Set subject, to
    ///////////////////////////////

    Properties props = mailSender.getJavaMailProperties();
    props.put("mail.transport.protocol", "smtp");
    props.put("mail.smtp.auth", "true");
    props.put("mail.smtp.starttls.enable", "true");
    props.put("mail.debug", "true");

    return mailSender;
}

并将其自动连接到您的服务 class,如下所示:

@Autowired
public JavaMailSender emailSender;

public void sendSimpleMessage(
  String to, String subject, String text) {
    ...
    SimpleMailMessage message = new SimpleMailMessage();
    message.setTo(to);
    message.setSubject(subject);
    message.setText(text);
    emailSender.send(message);
    ...
}

在您的情况下,因为您的电子邮件收件人和主题是不变的,您可以在 JavaMailSender Bean 中设置这些参数并调用 emailSender.send() 只有一个参数,即电子邮件正文,而您不需要需要实例化 send SimpleMailMessage class 来调用 emailSender.send() 方法。在另一种语言中,您在 bean 中消息的常量部分并将变化的部分作为参数发送到发送方法。

Guide to Send Emails in Java

我觉得更好的方法是 return 来自您的子服务的包装器 class。包装器 class 可以有两个属性,一个通用类型 payload 包含服务实际返回的任何数据,另一个字段 summary 类型 String 包含消息您可以在电子邮件内容中使用。

// our wrapper class
class ServiceReply<T> {
  private String summary;
  private T payload;

  // constructors/getters/setters... prefer a builder though
}

您的服务如下所示

@Service
public class Service1 {
    public ServiceReply<List<Item>> method1() {
         //some work to remove items
        String message = String.format("Following items were removed", items);
        log.info(message);
        return new ServiceReply<>(message, items);
    }
}

并且您在 MainService.java 中的 doWork 方法简化为如下内容

public void doWork() {

  StringBuilder emailMessage = new StringBuilder();

  final ServiceReply<List<Item>> replyService1Method1 = service1.method1();
  emailMessage.append(replyService1Method1.getSummary());
  List<Item> items = replyService1Method1.getPayload();

  // other method calls omitted for brevity 

  //send one email to the admins 
  mail.sendEmailToAdmins("Batch process completed", emailMessage.toString());
}

这样,虽然您最终会编写更多的代码(在上面的示例中每个方法调用多写一行),但您不必费心跨方法传递消息对象。