Inversion of Control – Part III – Dependency Injection in Java Spring II (Example)


I will continue with using the AlertService from part 2 of the series, to show how to utilise Dependency Injection in Spring with a more common situation. Let us start with the MessageServices and bring them under control of the BeanFactory in Spring by annotating them as a Component.

@Component
public class EmailMessageService implements MessageService {
    ...
}
@Component
public class SlackMessageService implements MessageService {
    ...
}

One of these Components is the DiscFullAlertService bean. We will inject the dependency to a MessageService Bean via constructor injection.

package com.vividbreeze.designpattern.spring.di;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class DiscFullAlertService extends AlertService {

    public static final String ALERT_TOPIC = "Alert on disk /gdalogs1 almost full.";
    public static final String ALERT_MESSAGE = "disk /gdalogs1 has reached 90% of its capacity.";

    @Autowired
    public DiscFullAlertService(MessageService messageService) {
        super (messageService);
    }

    @Override
    public void processAlert(String receiver) {
        messageService.sendMessage(ALERT_TOPIC, ALERT_MESSAGE, receiver);
    }
}

Let us now run the application

@ComponentScan
public class FireAlert {

    public static void main (String args[]) {

        String receiver = "ops@vividbreeze.com";

        ApplicationContext context =
                new AnnotationConfigApplicationContext(FireAlert.class);

        AlertService alertService = context.getBean(DiscFullAlertService.class);
        alertService.processAlert(receiver);
    }
}

It looks like we might run into problems here, as we want to inject a MessageService (the abstract super-class), but there are two beans that implement the MessageService: EmailMessageService and SlackMessageService. You might find the following message in the console:

... nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.vividbreeze.designpattern.spring.di.MessageService' available: expected single matching bean but found 2: emailMessageService,slackMessageService

 

Configurations

So the BeanFactory is confused which implementation to inject into the DiscFullAlertService. One solution would be to configure the BeanFactory so that it knows which implementation to choose. In this configuration, you are able to instantiate and register a Bean with the BeanFactory, so it later can be injected.

package com.vividbreeze.designpattern.spring.di;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AlertConfiguration {

    @Bean
    public MessageService messageService() {
        return new EmailMessageService();
    }
}

The class is annotated as a class that contains the configuration (@Configuration). The configuration is implemented in form of beans (or optionally an XML-file). The bean-name is the name of the method that is annotated with @Bean (messageService, not getMessageService). This method returns the instance of the bean that is then registered with the BeanFactory. After the initialisation, a ComponentScan is performed. So when it reaches

@Autowired
public DiscFullAlertService(MessageService messageService) {
    super (messageService);
}

it the BeanFactory already has a registered Bean “messageService” and injects it in the messageService attribute of DiscFullAlertService. Notice that in our main-method, we pass the Configuration class when we initialise the ApplicationContext and we don’t perform a ComponentScan here.

public class FireAlert {

    public static void main (String args[]) {

        String receiver = "ops@vividbreeze.com";

        ApplicationContext context =
                new AnnotationConfigApplicationContext(AlertConfiguration.class);

        AlertService alertService = context.getBean(DiscFullAlertService.class);
        alertService.processAlert(receiver);
    }
}

The reason why I included the ComponentScan in the Configuration class will become more obvious when we work with a different Configuration for unit-tests. As mentioned in the previous blog-post where we might not be able to send e-mails or Slack-messages from our local computers. Thus we can use a MockMessageService that just logs the message to the console.

@Component
public class MockMessageService implements MessageService {

    private static final Logger LOG = LoggerFactory.getLogger(MockMessageService.class);

    @Override
    public void sendMessage(String subject, String message, String receiver) {
        LOG.debug("MockMessageService - subject: " + subject + ", receiver: " + receiver + ", message: " + message);
    }
}

The instance of this class will also be annotated as a component so we can access it later via context.getBean(). The configuration will then look like this

@Configuration
@ComponentScan
public class AlertTestConfiguration {

    //@Bean
    public MessageService messageService() {
        return new MockMessageService();
    }
}

The unit test can now look like this

public class FireAlertTest {

    private static final Logger LOG = LoggerFactory.getLogger(FireAlertTest.class);

    ApplicationContext context = new AnnotationConfigApplicationContext(AlertTestConfiguration.class);

    @Test
    public void sendAlertMessage() {

        AlertService alertService = context.getBean(DiscFullAlertService.class);
        alertService.processAlert("vividbreeze");
    }
}

Debugging

The dependencies are resolved during run-time. If you have many beans with lots of dependencies your application might become more prone to errors. You can see which beans are initialised under which name during start-up (when the ApplicationContext is instantiated and initialised). This log message might be a bit confusing. You can also list all beans that are registered with the BeanFactory using the following snippet

ApplicationContext context =
        new AnnotationConfigApplicationContext(FireAlert.class);

String[] beanNames = context.getBeanDefinitionNames();
for (String beanname: beanNames) {
    LOG.debug(">>> " + beanname + " - " + context.getBean(beanname).toString());
}

Remarks

This example should give an overview of how to use the Spring Framework in a simple real-life situation. I highly recommend to consult other tutorials and to experiment with small sample applications to gain a better understanding of Dependency Injection in Spring.

 

 

Chris