繁体   English   中英

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

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

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

这是一个简单的示例,但通常电子邮件消息可以附加在任何服务方法中,包括不直接由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 使用单例来保存状态(即电子邮件文本)并将其注入每个服务类是否是不好的做法?

一些注意事项是:

  • 这不是一个网络应用程序。 这是一个每天在 cron 作业上运行的应用程序。
  • 附加在消息中的文本的顺序并不重要

这真的取决于你的目标。

您编写的代码似乎有效,因此您的问题实际上是关于尝试实现设计完美。 当然还有其他模式可以应用——例如某种调度或通知程序——但为什么呢? 如果它有效,并且速度足够快,则继续处理其他问题。 它很慢,或者无法满足您的需求,那是另一回事。 但在这种情况下,您应该收集性能指标并有条不紊地进行攻击。

我不确定你的单身想法。 本质上,emailMessage 代表了经典模型-视图-控制器范式中的“模型”。 当然,你可以用它做一个单例,但真正做的只是将模型类从在堆栈上传递到永久驻留在(一次)内存中。 如果您的记忆因创建十亿封电子邮件而不断更新/删除,那可能是有道理的,但我怀疑您是否有这个问题。 Singleton 可能不会受到伤害,但它提供了什么帮助?

不要让完美成为善的敌人

创建一个 MailBuilder 类作为您的域模型并传递它。 根据哪个服务执行的操作,您可以为每个特定的服务调用提供一个方法。 例如 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());
        }
    }

坚持您的业务逻辑并考虑您共享的内容,我认为考虑到StringBuilder 不是线程安全的,对同一个对象使用多个副作用调用可能不是最好的方法。

我宁愿从您想要跟踪结果的每个调用中返回一条消息,并将它们存储在一个数据结构中,就像一个堆栈一样,只是为了做一个例子,并考虑收集到的数据来构建邮件消息。

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

是的,您可以使用 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;
}

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

@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()只使用一个参数是电子邮件正文,并且您不需要实例化发送SimpleMailMessage类来调用emailSender.send()方法。 在另一种语言中,您在 bean 中发送消息的常量部分并将可变部分作为参数发送到发送方法。

Java 发送电子邮件指南

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

// 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());
}

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM