DEV Community

Cover image for Dependency Injection in Java is easy - Part 3 - Spring Context
Tomer Figenblat
Tomer Figenblat

Posted on • Edited on

Dependency Injection in Java is easy - Part 3 - Spring Context

Part 3 - Spring Context

This post is part of a multiple-part tutorial. As the heading suggests, this part will focus on Dependency Injection using Spring Context.

Note, Spring is so much more than a DI Framework, but this tutorial revolves around Spring Framework IoC Container, also known as Spring Context.
Spring Boot or any other Spring component is outside the scope of this tutorial.

You can check out the code for this tutorial part in Github.

Background

It's advised to start with Part 1 - A Design Pattern, which this part requires. You can skip Part 2 - Google Guice.

If you have read Part 2, you can skip to the incorporating spring section.

Dependency Injection Frameworks

DI Frameworks concept is pretty straightforward; the framework creates and manages our dependencies for us.
On one end, we provide the framework instructions for creating our dependencies; on the other, we ask the framework for instances of the dependencies.

Comparing multiple frameworks, we'll notice different features, component names, default behaviors, and probably different implementations under the hood. But the gist will be the same: a container that holds our dependencies. Let's explore some similarities between various frameworks:

Not a believer yet ❔ Alright, let's throw C# in the mix ❕

We can go on. But the point is made.
😎

Under the hood, DI Frameworks builds factories providing dependencies based on multiple criteria, such as type and name.

At the base level, there are three typical scopes for dependencies living in a DI Framework container:

  • Eager Singleton: one instance of the dependency will be created immediately upon the framework's instantiation; the same instance will be provided for every request.

  • Lazy Singleton: one instance of the dependency will be created only when requested. Upon its instantiation, the same instance will be provided for every request.

  • Non-Singleton: A new instance of the dependency will be provided for every request.

There are more scopes, but these are the three most commonly used. Some frameworks offer different scopes than others. Such as per session, per request, per dependency, etc.

Example App

Heads up: The example app next is based on Part 1 - A Design Pattern; you can skip to the incorporating spring section.

Mail Collector App

Let's build an app pulling emails from both Gmail and Microsoft leveraging the Dependency Injection pattern with Spring Context.

Contracts

An Enum called MailSource for categorizing the email source:

public enum MailSource {
  GMAIL,
  MICROSOFT;
}
Enter fullscreen mode Exit fullscreen mode

An abstract class Mail for contracting mail objects.

public abstract class Mail {
  public abstract String from();

  public abstract String subject();

  public abstract MailSource source();

  @Override
  public String toString() {
    return String.format("Got mail by %s, from %s, with the subject %s", source(), from(), subject());
  }
}
Enter fullscreen mode Exit fullscreen mode

An interface for contracting services responsible for pulling Mail from suppliers, the MailService.

public interface MailService {
  List<Mail> getMail();
}
Enter fullscreen mode Exit fullscreen mode

And last, an interface for contracting an engine responsible for collecting Mail from multiple services, the MailEngine.

public interface MailEngine {
  List<Mail> getAllMail();
}
Enter fullscreen mode Exit fullscreen mode

Implementations

Mail

The concrete Mail implementations were designed with a builder pattern for convenience and immutability.
The Gmail Mail implementation, GmailImpl:

public final class GmailImpl extends Mail {
  private final String setFrom;
  private final String setSubject;

  private GmailImpl(final String from, final String subject) {
    setFrom = from;
    setSubject = subject;
  }

  @Override
  public String from() {
    return setFrom;
  }

  @Override
  public String subject() {
    return setSubject;
  }

  @Override
  public MailSource source() {
    return MailSource.GMAIL;
  }

  public static GmailImpl.Builder builder() {
    return new GmailImpl.Builder();
  }

  public static final class Builder {
    private String prepFrom;
    private String prepSubject;

    public Builder from(final String setFrom) {
      prepFrom = setFrom;
      return this;
    }

    public Builder subject(final String setSubject) {
      prepSubject = setSubject;
      return this;
    }

    public GmailImpl build() {
      requireNonNull(emptyToNull(prepFrom), "from cannot be empty or null");
      requireNonNull(emptyToNull(prepSubject), "subject cannot be empty or null");

      return new GmailImpl(prepFrom, prepSubject);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Micsorosft Mail implementation, MicrosoftImpl:

public final class MicrosoftImpl extends Mail {
  private final String setFrom;
  private final String setSubject;

  private MicrosoftImpl(final String from, final String subject) {
    setFrom = from;
    setSubject = subject;
  }

  @Override
  public String from() {
    return setFrom;
  }

  @Override
  public String subject() {
    return setSubject;
  }

  @Override
  public MailSource source() {
    return MailSource.MICROSOFT;
  }

  public static MicrosoftImpl.Builder builder() {
    return new MicrosoftImpl.Builder();
  }

  public static final class Builder {
    private String prepFrom;
    private String prepSubject;

    public Builder from(final String setFrom) {
      prepFrom = setFrom;
      return this;
    }

    public Builder subject(final String setSubject) {
      prepSubject = setSubject;
      return this;
    }

    public MicrosoftImpl build() {
      requireNonNull(emptyToNull(prepFrom), "from cannot be empty or null");
      requireNonNull(emptyToNull(prepSubject), "subject cannot be empty or null");

      return new MicrosoftImpl(prepFrom, prepSubject);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
Mail Services

The Gmail MailService implementation:

public final class GmailService implements MailService {
  @Override
  public List<Mail> getMail() {
    //This is where the actual Gmail API access goes.
    //We'll fake a couple of emails instead.
    var firstFakeMail =
        GmailImpl.builder()
            .from("a.cool.friend@gmail.com")
            .subject("wanna get together and write some code?")
            .build();

    var secondFakeMail =
        GmailImpl.builder()
            .from("an.annoying.salesman@some.company.com")
            .subject("wanna buy some stuff?")
            .build();

    return List.of(firstFakeMail, secondFakeMail);
  }
}
Enter fullscreen mode Exit fullscreen mode

The Microsoft MailService implementation:

public final class MicrosoftService implements MailService {
  @Override
  public List<Mail> getMail() {
    //This is where the actual Microsoft API access goes.
    //We'll fake a couple of emails instead.
    var firstFakeMail =
        MicrosoftImpl.builder()
            .from("my.boss@work.info")
            .subject("stop writing tutorials and get back to work!")
            .build();

    var secondFakeMail =
        MicrosoftImpl.builder()
            .from("next.door.neighbor@kibutz.org")
            .subject("do you have philips screwdriver?")
            .build();

    return List.of(firstFakeMail, secondFakeMail);
  }
}
Enter fullscreen mode Exit fullscreen mode
Mail Engine
public final class RobustMailEngine implements MailEngine {
  private final Set<MailService> mailServices;

  public RobustMailEngine(final Set<MailService> setMailSerices) {
    mailServices = setMailSerices;
  }

  @Override
  public List<Mail> getAllMail() {
    return mailServices.stream().map(MailService::getMail).flatMap(List::stream).collect(toList());
  }
}
Enter fullscreen mode Exit fullscreen mode

The Main App

This is the app itself, the MailCollectorApp:

public final class MailCollectorApp {
  private MailEngine engine;

  public MailCollectorApp(final MailEngine setEngine) {
    engine = setEngine;
  }

  public String getMail() {
    var ret = "No mail found.";
    if (!engine.getAllMail().isEmpty()) {
      ret = Joiner.on(System.lineSeparator()).join(engine.getAllMail());
    }
    return ret;
  }

  public static void main(final String... args) {
    var gmailService = new GmailService();
    var microsoftService = new MicrosoftService();

    var engine = new RobustMailEngine(Set.of(gmailService, microsoftService));

    var app = new MailCollectorApp(engine);

    System.out.println(app.getMail());
  }
}
Enter fullscreen mode Exit fullscreen mode

Executing the main method will print:

Got Mail by GMAIL, from a.cool.friend@gmail.com, with the subject wanna get together and write some code?
Got Mail by GMAIL, from an.annoying.salesman@some.company.com, with the subject wanna buy some stuff?
Got Mail by MICROSOFT, from my.boss@work.info, with the subject stop writing tutorials and get back to work!
Got Mail by MICROSOFT, from next.door.neighbor@kibutz.org, with the subject do you have a star screwdriver?
Enter fullscreen mode Exit fullscreen mode

This application uses the dependency injection design pattern. The dependencies are currently controlled by the main method, so it should be easy to incorporate Spring Context.

Incorporating Spring Context

Include maven dependency

First, let's add this to our pom.xml in the dependencies section:
Note that this version was the latest when this tutorial was written.

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.2.8.RELEASE</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Mark Autowired

We need to tell Spring about the dependencies we want to be injected. But it's only mandatory for property and method based injections; in our example, we're using constructor based injection, so there's no need to annotate with @Autowierd.

But for this tutorial's sake, we'll use the @Autowierd to demonstrate what dependencies we want to be injected. Let's do this for the concrete engine class:

public final class RobustMailEngine implements MailEngine {
  private final Set<MailService> mailServices;

  @Autowired
  public RobustMailEngine(final Set<MailService> setMailSerices) {
    mailServices = setMailSerices;
  }

  @Override
  public List<Mail> getAllMail() {
    return mailServices.stream().map(MailService::getMail).flatMap(List::stream).collect(toList());
  }
}
Enter fullscreen mode Exit fullscreen mode

And for the app class:

public final class MailCollectorApp {
  private MailEngine engine;

  @Autowired
  public MailCollectorApp(final MailEngine setEngine) {
    engine = setEngine;
  }

  public String getMail() {
    var ret = "No mail found.";
    if (!engine.getAllMail().isEmpty()) {
      ret = Joiner.on(System.lineSeparator()).join(engine.getAllMail());
    }
    return ret;
  }

  //...
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to instruct Spring on instantiating those dependencies, called beans in spring's world.

Create Beans

Let's create a Spring Configuration Class for creating beans; multiple approaches exist to achieve that. For instance, using Spring's Annotation Based Configuration:

@Configuration
public class DIConfiguration {
  @Bean
  @Scope(BeanDefinition.SCOPE_PROTOTYPE)
  public Set<MailService> getServices() {
    return Set.of(new GmailService(), new MicrosoftService());
  }

  @Lazy
  @Bean
  public MailEngine getEngine(final Set<MailService> services) {
    return new RobustMailEngine(services);
  }
}
Enter fullscreen mode Exit fullscreen mode

Another approach to creating beans in Spring is marking classes as @Component, which is how to tell Spring we want this class as a bean. For instance, let's mark our app class as a Component so we can later ask Spring to instantiate it:

@Lazy
@Component
public final class MailCollectorApp {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

This will, of course, be a Lazy Singleton.

Another approach to configuring Spring is using Spring's XML Based Configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="gmailService" class="info.tomfi.tutorials.mailapp.core.service.GmailService" scope="prototype"/>

    <bean id="microsoftService" class="info.tomfi.tutorials.mailapp.core.service.MicrosoftService" scope="prototype"/>

    <bean id="getEngine" class="info.tomfi.tutorials.mailapp.engine.RobustMailEngine" lazy-init="true">
      <constructor-arg>
        <set>
          <ref bean="gmailService"/>
          <ref bean="microsoftService"/>
        </set>
      </constructor-arg>
    </bean>

    <bean id="getMailApp" class="info.tomfi.tutorials.mailapp.MailCollectorApp" lazy-init="true">
      <constructor-arg>
        <ref bean="getEngine"/>
      </constructor-arg>
    </bean>

</beans>
Enter fullscreen mode Exit fullscreen mode

Both options will produce the same dependencies.

Spring's default scope is Singleton. This means that the RobustMailEngine, combined with the Lazy annotation, will be a Lazy Singleton, meaning our app will have only one instance of RobustMailEngine.

On the other end, the Set of GmailService and MicrosoftService will be created as new instances for every object that needs them, as we explicitly configured them as Prototype, which is Non-Singleton.

Note, RobustMailEngine is a dependency; it will be injected to whoever requests it, but as we configured earlier, it also needs dependencies for itself (the Set of MailService).

Spring will catch that and inject the set of mail services while instantiating the engine.

Update the app to use Spring

Getting back to our app. Let's update it to work with Spring:

@Lazy
@Component
public final class MailCollectorApp {
  private MailEngine engine;

  public MailCollectorApp(final MailEngine setEngine) {
    engine = setEngine;
  }

  public String getMail() {
    var ret = "No mail found.";
    if (!engine.getAllMail().isEmpty()) {
      ret = Joiner.on(System.lineSeparator()).join(engine.getAllMail());
    }
    return ret;
  }

  public static void main(final String... args) {
    // try (var container = new ClassPathXmlApplicationContext("spring-beans.xml")) {
    try (var container = new AnnotationConfigApplicationContext(MailCollectorApp.class, DIConfiguration.class)) {
      var app = container.getBean(MailCollectorApp.class);

      System.out.println(app.getMail());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's analyze what's going on here...

Running the main method will create a Spring container, the context for all dependencies. As we create it with an instance of both MailCollectorApp and DIConfiguration classes, the container will have the following dependencies, beans, configured in it:

  • A Set of two MailService objects (GmailService and Microsoft Service).
  • A Singleton instance of MailEngine (RobustMailEngine).
  • A Singleton instance of MailCollectorApp.

As configured, both MailCollectorApp and MainEngine are Lazy Singletons, plus, we configured the Set of MailService as Prototype. That means that there's nothing instantiated in Spring's context at this point.

The next step, asking the container for an instance of MailCollectorApp, will accomplish the following:

  • Spring will pick up the constructor in MailCollectorApp, as it's the only constructor.

  • Spring will look in its context for a type MailEngine dependency.

  • It will find the RobustMailEngine configured, which is a Lazy Singleton.

  • While trying to instantiate it, it will pick up its constructor and look for a suitable dependency with the type Set of MailService.

  • It will find the Set of GoogleService and MicrosoftService, which are Prototypes.

After preparing the groundwork, Spring will:

  • Create the set after instantiating both GmailService and MicrosoftService.

  • Instantiate the RobustMailEngine injecting the Set.

  • Instantiate the MailCollectorApp injecting the RobustMailEngine.

We then get our instance MailCollectorApp with everything we need, from which we invoke getMail to get all our Mail.

That's it, Spring Context in a nutshell.
😆

Now, Let's test our code.

Unit Tests

I will start by saying that when it comes to unit tests, if possible, I always prefer not to use DI Frameworks.

Unit tests are about testing small parts, units, of our application. We can avoid the overhead of creating the DI Context. We'll be better off simply instantiating the subject under test manually.

On the other end, if we're writing integration, acceptance tests, or any different situation when we might need to test our application end-to-end, well, in that case, a suitable DI Framework could be our best friend.

Let's move on to unit tests with a DI Framework for demonstration purposes only.

Please note that Spring creates its instances and doesn't allow outside interference. Nonetheless, we needed to inject mocks instead of the real mail services, maintaining the ability to access them and assert their behavior. We create a separate configuration class for testing Spring Context, having it instantiate the mocks for us:

@Configuration
public class DITestConfiguration {
  private MailService gmailServiceMock;
  private MailService microsoftServiceMock;
  private MailService thirdServiceMock;

  public DITestConfiguration() {
    gmailServiceMock = mock(MailService.class);
    microsoftServiceMock = mock(MailService.class);
    thirdServiceMock = mock(MailService.class);
  }

  @Lazy
  @Bean
  public MailEngine getEngine(final Set<MailService> services) {
    return new RobustMailEngine(Set.of(gmailServiceMock, microsoftServiceMock, thirdServiceMock));
  }

  public MailService getGmailServiceMock() {
    return gmailServiceMock;
  }

  public MailService getMicrosoftServiceMock() {
    return microsoftServiceMock;
  }

  public MailService getThirdServiceMock() {
    return thirdServiceMock;
  }
}
Enter fullscreen mode Exit fullscreen mode

We then use this specific testing configuration for creating our context:

public final class MailCollectorAppTest {
  private MailService gmailServiceMock;
  private MailService microsoftServiceMock;
  private MailService thirdServiceMock;

  private MailCollectorApp sut;

  private ConfigurableApplicationContext context;
  private Faker faker;

  @BeforeEach
  public void initialize() {
    faker = new Faker();

    context =
        new AnnotationConfigApplicationContext(MailCollectorApp.class, DITestConfiguration.class);

    var confWorkAround = context.getBean(DITestConfiguration.class);

    gmailServiceMock = confWorkAround.getGmailServiceMock();
    microsoftServiceMock = confWorkAround.getMicrosoftServiceMock();
    thirdServiceMock = confWorkAround.getThirdServiceMock();

    sut = context.getBean(MailCollectorApp.class);
  }

  @AfterEach
  public void cleanup() {
    context.close();
  }

  @Test
  @DisplayName(
      "make the services mocks return no mail and validate the return string as 'No mail found'")
  public void getMail_noMailExists_returnsNoMailFound() {
    willReturn(emptyList()).given(gmailServiceMock).getMail();
    willReturn(emptyList()).given(microsoftServiceMock).getMail();
    willReturn(emptyList()).given(thirdServiceMock).getMail();

    then(sut.getMail()).isEqualTo("No mail found.");
  }

  @Test
  @DisplayName(
      "make the services return legitimate mail and validate the return string as expected")
  public void getMail_foundMail_returnsExpectedString() {
    var mail1 =
        GmailImpl.builder()
            .from(faker.internet().emailAddress())
            .subject(faker.lorem().sentence())
            .build();
    var mail2 =
        MicrosoftImpl.builder()
            .from(faker.internet().emailAddress())
            .subject(faker.lorem().sentence())
            .build();
    var mail3 =
        MicrosoftImpl.builder()
            .from(faker.internet().emailAddress())
            .subject(faker.lorem().sentence())
            .build();

    willReturn(List.of(mail1)).given(gmailServiceMock).getMail();
    willReturn(List.of(mail2, mail3)).given(microsoftServiceMock).getMail();
    willReturn(emptyList()).given(thirdServiceMock).getMail();

    then(sut.getMail().split(System.lineSeparator()))
        .containsOnly(mail1.toString(), mail2.toString(), mail3.toString());
  }
}
Enter fullscreen mode Exit fullscreen mode

It's important to emphasize this is poor practice; We were better off testing without Spring, but this tutorial aims to show how to do so for rare cases where there's no better way.

In the code above, we created the container with a DITestConfiguration instance instead of a DIConfiguration instance, exposing three getters to help work around the mock injection issue. We also added a third mail service, demonstrating how easy it is. 😁

The RobustMailEngine was not mocked because there was no real reason to do so, but it could have been easily replaced with a mock or a spy.

The test class behaved exactly like the main app, except for the services being mocks instead of real objects.

Top comments (0)