Spring Beans With Auto-Generated Implementations: How-To

Introduction

Quite a few frameworks implement the automatic creation of components from Java interfaces. The most popular one is probably Spring Data, which is a whole family of data access frameworks. Spring Data JPA, for instance, helps us run JPA queries. Here's a quick example:

Java
 
public interface ClientRepository extends JpaRepository<Client, Long> {
  List<Client> findByNameLikeAndLastActiveBefore(String name, Date date);
}


Without going into detail, Spring Data JPA will create an implementation class from that interface at runtime and register a bean of type ClientRepository in the application context. The query string will be derived from the method name. The method will run the following JPA query:

SQL
 
select x from Client x where x.name like ?1 and x.lastActive < ?2


That is a nifty declarative API in which method declarations themselves define what parameters to run the framework's underlying functionality with. And we don't have to write any implementation code.

It's a convenient way to integrate libraries and hide boilerplate that they require us to write. As a developer, I'd like to have the ability to build custom APIs that follow such patterns. This would let me build tools that are convenient in use, easily extensible, and specific to my project's business domain.

What We Will Build

In this tutorial, we will build a sample framework that implements the automatic creation of Spring beans from interfaces. The framework's domain will be e-mail and usage will be as follows:

Java
 
@EmailService
public interface RegistrationEmailService {
  void sendConfirmation(@Param("name") String name, @Param("link") String link, @To String email);
  void sendWelcome(@Param("client") @To Client client);
}


The conventions are:

NOTICE: This article as well as the source code on GitHub does not contain any examples of sending emails with Java. The purpose of this tutorial is to provide a sample framework implementation to illustrate the approach.

This tutorial is accompanied by a working code example on GitHub.

How-To

At a higher level, the framework we're about to build is to do three things:

  1. Find all interfaces marked with @EmailService.
  2. Create an implementation class for each of these interfaces.
  3. Register a Spring-managed bean for each of the interfaces.

We will use a library spring-icomponent as a base (disclosure: I am the author of this library). It covers steps 1 and 3, and partially step 2. In step 2, implementations created by the library are simply proxies that route method invocations to the user-defined handlers. All we have to do as developers is build a handler and bind it to the methods.

Let's start by adding spring-icomponent to a project. The library supports Spring 5.2.0 or newer.

Gradle:

Groovy
 
dependencies {
  implementation 'org.thepavel:spring-icomponent:1.0.8'
}


Maven:

XML
 
<dependency>
  <groupId>org.thepavel</groupId>
  <artifactId>spring-icomponent</artifactId>
  <version>1.0.8</version>
</dependency>


To set it up, add the @InterfaceComponentScan annotation to a java-config class:

Java
 
@Configuration
@ComponentScan
@InterfaceComponentScan
public class AppConfiguration {
}


This will trigger package scanning. Usage is similar to @ComponentScan. This configuration will scan from the package of AppConfiguration. You can also specify basePackages or basePackageClasses to define specific packages to scan.

By default, the scanner will search for interfaces annotated with @Component. The @EmailService annotation will be meta-annotated with @Component, so it will be picked up by the scanner with the default settings.

Building a Method Handler

A method handler is to implement the desired functionality, in our case, sending emails. It must implement the MethodHandler interface:

Java
 
@Component
public class EmailServiceMethodHandler implements MethodHandler {
  @Override
  public Object handle(Object[] arguments, MethodMetadata methodMetadata) {
    return null;
  }
}


The handle method is supplied with invocation arguments as well as method metadata so that we can derive extra execution parameters from the signature of the method being called.

Recalling the method declaration conventions we've established earlier, a method name is "send" + email type. Email type defines the email subject and body template:

Java
 
// Get the method name and cut off "send"
// Eg: "sendWelcome" -> "Welcome"
String emailType = methodMetadata.getSourceMethod().getName().substring(4);

// "Welcome" -> "welcome"
String templateName = Introspector.decapitalize(emailType);

// "welcome" -> "email.subject.welcome"
String subjectKeyInMessageBundle = "email.subject." + templateName;


In order to collect the template parameters and recipient email addresses from the invocation arguments we have to iterate through the method parameters:

Java
 
// Get metadata of the method parameters
List<ParameterMetadata> parametersMetadata = methodMetadata.getParametersMetadata();

// Collections to populate
Map<String, Object> templateParameters = new HashMap<>();
Set<String> emails = new HashSet<>();

for (int i = 0; i < parametersMetadata.size(); i++) {
  // Get annotations of the i-th method parameter
  MergedAnnotations parameterAnnotations = parametersMetadata.get(i).getAnnotations();
  Object parameterValue = arguments[i];

  // TODO: Populate templateParameters
  // TODO: Populate emails
}


MergedAnnotations is the Spring's class that represents a view of all annotations and meta-annotations declared on an element, in this case, method parameter.

If a method parameter is annotated with @Param then it's a template parameter. Here's the source code of @Param:

Java
 
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface Param {
  String value();
}


Its value attribute specifies a name of a template parameter to bind the annotated method parameter to. We should populate the templateParameters map inside the loop:

Java
 
for (int i = 0; i < parametersMetadata.size(); i++) {
  // Get annotations of the i-th method parameter
  MergedAnnotations parameterAnnotations = parametersMetadata.get(i).getAnnotations();
  Object parameterValue = arguments[i];

  if (parameterAnnotations.isPresent(Param.class)) {
    // Get the Param annotation then get the value of its `value` attribute as String
    String templateParameterName = parameterAnnotations.get(Param.class).getString("value");
    templateParameters.put(templateParameterName, parameterValue);
  }

  // TODO: Populate emails
}


If a method parameter is annotated with @To then it represents a recipient. Here's the source code of @To:

Java
 
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface To {
}


A recipient may either be a plain string or a Client object:

Java
 
if (parameterAnnotations.isPresent(To.class)) {
  if (parameterValue instanceof String) {
    emails.add((String) parameterValue);
  } else if (parameterValue instanceof Client) {
    Client client = (Client) parameterValue;
    emails.add(client.getEmail());
  }
}


Now, as we've got all the parameters necessary to send the email, we can send it:

Java
 
String subject = getMessage(subjectKeyInMessageBundle);
String body = buildFromTemplate(templateName, templateParameters);

emailSender.send(subject, body, emails);


I'm leaving the helper methods out just to keep it brief. You can find them in this tutorial's source code on GitHub. To sum it up, here are all the code snippets put together:

Java
 
@Component
public class EmailServiceMethodHandler implements MethodHandler {
  @Autowired
  EmailSender emailSender;

  @Override
  public Object handle(Object[] arguments, MethodMetadata methodMetadata) {
    // Cut off "send"
    String emailType = methodMetadata.getSourceMethod().getName().substring(4);
    String templateName = Introspector.decapitalize(emailType);
    String subjectKeyInMessageBundle = "email.subject." + templateName;

    List<ParameterMetadata> parametersMetadata = methodMetadata.getParametersMetadata();
    Map<String, Object> templateParameters = new HashMap<>();
    Set<String> emails = new HashSet<>();

    for (int i = 0; i < parametersMetadata.size(); i++) {
      MergedAnnotations parameterAnnotations = parametersMetadata.get(i).getAnnotations();
      Object parameterValue = arguments[i];

      if (parameterAnnotations.isPresent(Param.class)) {
        String templateParameterName = parameterAnnotations.get(Param.class).getString("value");
        templateParameters.put(templateParameterName, parameterValue);
      }

      if (parameterAnnotations.isPresent(To.class)) {
        if (parameterValue instanceof String) {
          emails.add((String) parameterValue);
        } else if (parameterValue instanceof Client) {
          Client client = (Client) parameterValue;
          emails.add(client.getEmail());
        }
      }
    }

    String subject = getMessage(subjectKeyInMessageBundle);
    String body = buildFromTemplate(templateName, templateParameters);

    emailSender.send(subject, body, emails);

    return null;
  }

  // helper methods omitted
}


The method handler is now complete. In the next section, we will bind this method handler to the methods in the RegistrationEmailService interface.

Building a Marker Annotation

It's as simple as this:

Java
 
@Retention(RUNTIME)
@Target(TYPE)
@Service
@Handler("emailServiceMethodHandler")
public @interface EmailService {
}


Two important things here:

By decorating an interface with @EmailService we tell the framework to route invocations of all methods in the interface to the EmailServiceMethodHandler bean:

Java
 
@EmailService
public interface RegistrationEmailService {
  void sendConfirmation(@Param("name") String name, @Param("link") String link, @To String email);
  void sendWelcome(@Param("client") @To Client client);
}


Now, RegistrationEmailService can be injected into other beans:

Java
 
@Service
public class RegistrationServiceBean implements RegistrationService {
  @Autowired
  RegistrationEmailService emailService;
  //...

  @Override
  public void register(String name, String email) {
    Client client = createClient(name, email);
    persist(client);
    emailService.sendWelcome(client);
  }

  //...
}


Adding New Methods

This tutorial's demo is a Spring Boot project with the Thymleaf dependency included. You can check out the demo project and use it as a playground to follow the steps described in this section.

Let's say the system must email clients about security policy updates and we have to build a component responsible for that. The component's API could be as follows:

Java
 
void sendSecurityPolicyUpdate(Client client, String policyLink);


The email text will be a template with two variables: client name and link. Here's a snippet of the email template built with Thymeleaf:

HTML
 
<p th:text="'Hello, ' + ${client.name} + '!'"/>
<p>
    Our security policy has been updated. Please follow the link for more info:
    <a th:href="${link}" th:text="${link}"/>
</p>


If we pick a method name "sendSecurityPolicyUpdate" then, by convention, the template name must be "securityPolicyUpdate". In a Spring Boot project with the default Thymeleaf starter configuration, the corresponding template file must be located at "resources/templates/securityPolicyUpdate.html". So we have to create the file and copy and paste the template text into it.

In order to define the email subject, we have to add a message to the message bundle with a key "email.subject.securityPolicyUpdate":

Properties files
 
email.subject.confirmation = Confirm your e-mail address
email.subject.welcome = Welcome to the app
email.subject.securityPolicyUpdate = Security policy updated


The message bundle is located at "resources/messages.properties" in Spring Boot projects by default.

The setup is complete so we can now create a new component for the policy emails:

Java
 
@EmailService
public interface PolicyEmailService {
  void sendSecurityPolicyUpdate(@Param("client") @To Client client, @Param("link") String policyLink);
}


Here's a usage example for the new component:

Java
 
String link = policyService.getPolicyLink();

clientRepository
    .getAllClientsAsStream()
    .forEach(client -> policyEmailService.sendSecurityPolicyUpdate(client, link));


Conclusion

Pretty quickly, we've built a small and simple email sending tool with business domain-specific, easily extensible API. This is just a sample intended to illustrate the approach that could help in reducing boilerplate code but, on the other hand, has some extra code overhead. So it's "this much code vs that much code" decision. When you, once again, are considering building a wrapper around some client API to simplify its usage in certain patterns, this is when this approach may potentially be beneficial.

This tutorial's source code is available on GitHub.

 

 

 

 

Top