我应该使用 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?
一些注意事项是:
- 这不是网络应用程序。这是一个每天在 cron 作业上运行的应用程序。
- 邮件中附加文本的顺序并不重要
这真的取决于你的目标。
您编写的代码似乎有效,所以您的问题实际上是关于如何实现 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 中消息的常量部分并将变化的部分作为参数发送到发送方法。
我觉得更好的方法是 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());
}
这样,虽然您最终会编写更多的代码(在上面的示例中每个方法调用多写一行),但您不必费心跨方法传递消息对象。
从 @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?
一些注意事项是:
- 这不是网络应用程序。这是一个每天在 cron 作业上运行的应用程序。
- 邮件中附加文本的顺序并不重要
这真的取决于你的目标。
您编写的代码似乎有效,所以您的问题实际上是关于如何实现 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 中消息的常量部分并将变化的部分作为参数发送到发送方法。
我觉得更好的方法是 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());
}
这样,虽然您最终会编写更多的代码(在上面的示例中每个方法调用多写一行),但您不必费心跨方法传递消息对象。