'Should I use a Spring singleton bean to build up an email message?
From a @Service class, I'm calling several methods on other @Service classes. The service methods do some work and add some text to an email message. That email message is sent to admins to let them know of the work that was done.
Here's a simple example but in general the email message may be appended within any service method including ones that are not directly called by the MainService. Similar to logging, The message is always appended to an existing message. It is not modified in any other way.
@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;
}
}
Is there a way to avoid passing around emailMessage? Would it be bad practice to use a singleton to hold state (i.e., email message text) and inject that into each service class?
A few notes are:
- This is not a web application. It's an application that runs on a cron job daily.
- Order of the text appended in the message is not important
Solution 1:[1]
Create a MailBuilder class as your domain model and pass it around. Depending which service does what you can have a method for each specific service call. For example MailBuilder.appendForServiceA(args), MailBuilder.appendForServiceB(args). This way you can actually test the builder on its own.
Solution 2:[2]
You are thinking in the right direction, if you see similar interactions from different services with a certain object, you can extract the whole process in the form of Beans.
For example, we can describe the process as follows:
- An EmailBuilder object as input (as the others already mentioned)
- And a series of transformations on that object
- The EmailBuilder now can build an Email Object that holds all necessary transformations
Here is how I would implement this with Spring Beans, the @Order annotation here is fundamental to the setup.
@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("[email protected]");
}
@Bean
@Order(2)
Consumer<Email.EmailBuilder> addToFieldToEmail() {
return emailBuilder -> {
// Get user email from context for example
// SecurityContextHolder.getContext().getAuthentication().getPrincipal();
emailBuilder.to("[email protected]");
};
}
@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());
}
}
Solution 3:[3]
Sticking to your business logic and considering what you've shared I think that using multiple side-effects call on same object, considering that StringBuilder is not thread-safe, is probably not the best approach.
I would rather return a message from each call from which you want to track the outcome and store them in a data structure, like a stack just to make an example, and building the mail messages considering the data collected.
This approach could be easily tweaked to get a thread safe implementation if needed.
Solution 4:[4]
I feel a better way would be to return a wrapper class from your child services. The wrapper class can have two attributes, a generic type payload to contain any data that's actually retuned by the service, and another field summary of type String containing the message that you can use in your email content.
// our wrapper class
class ServiceReply<T> {
private String summary;
private T payload;
// constructors/getters/setters... prefer a builder though
}
The your service looks somethings like below
@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);
}
}
and your doWork method in MainService.java reduces to something as below
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());
}
This way, though you end up writing a little more code (1 extra line per method call in the above sample), you don't have to bother about passing the message object across methods.
Solution 5:[5]
Yes you can use Bean for sending emails:
@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;
}
And autowire it to your service class as following:
@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);
...
}
In your case, because your email to and subject are constant, you can set these parameters at JavaMailSender Bean and call emailSender.send() wit only one argument that is email body and you don't need instantiate send SimpleMailMessage class for calling emailSender.send() method. In another language, you message's constant part in your bean and send varying part to send method as arguments.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | filip |
| Solution 2 | Ahmed Sayed |
| Solution 3 | |
| Solution 4 | Debargha Roy |
| Solution 5 |
