Chapter 4. annotations

4.1. annotations: Configuration with Annotations

4.2. Introduction

In this lab you will gain experience using the annotation support from Spring to configure the rewards application. You will use an existing setup and transform that to use annotations such as @Autowired, @Repository and @Service to configure the components of the application. You will then run a top-down system test that uses JUnit 5.

What you will learn:

  1. How to use some of Spring’s dependency injection annotations such as @Autowired
  2. The advantages and drawbacks of those annotations
  3. How to implement your own bean lifecycle behaviors

Specific subjects you will gain experience with:

  1. Annotation-based dependency injection
  2. How to use Spring component scanning

Estimated time to complete: 45 minutes

4.3. Quick Instructions

Quick instructions for this exercise have been embedded within the lab materials in the form of TODO comments.

To get the most from this lab, please follow the TODO steps in the order they appear. Please do not jump ahead to later TODOs in the same file or some features of the lab won’t work as described.

[Tip] Tip

To display the TODO steps, open the Tasks view (Window >> Show View >> Tasks (not Task List)). Use the view’s small down arrow (top-right of Tasks window) to select a Configure Contents …​ or Filter …​ menu. You’ll find the instructions are easy to follow if you configure TODOs to display On any element in the same project.

Occasionally, TODO’S defined within XML files disappear from the Tasks view (i.e. gaps in the number sequence). To correct this, go to Preferences >> General >> Editors >> Structured Text Editor >> Task Tags pane. Check Enable searching for Task Tags and click Clean and Redetect Tasks. On the Filters tab, ensure XML content type is checked.

The following sequence diagram will help you to perform the TODOs for implementing the bean life cycle behaviors.

Lifecycle of JdbcRestaurantRepository

Figure 4.1. Lifecycle of the JdbcRestaurantRepository



4.4. Detailed Instructions

4.4.1. Reviewing the application

In this lab, we are using a version of the rewards application that is already fully functional. Essentially, it is the completed version of the last exercise. It has repository implementations that are backed by JDBC and which connect to an in-memory embedded HSQLDB database. We will then rewrite some of the application code to make use of annotations.

4.4.1.1. First verify that everything works

(TODO-01) The project features an integration test that verifies the system’s behavior. It’s called RewardNetworkTests and lives in the rewards package. Run this test by right-clicking on it and selecting 'Run As…​' followed by 'JUnit Test'. The test should run successfully.

Now open the application configuration called RewardsConfig.java (use CTRL+SHIFT+T [Windows/Linux] or COMMAND+SHIFT+T [Mac] to popup the "Open Type" search dialog and quickly navigate to it). Review the @Beans that wires up all the dependencies. As you can see, we’re using constructor arguments.

Remember that the infrastructure components (the DataSource for example) are located in a separate application configuration class. If you navigate back to the test you will see that the setUp() method specifies the TestInfrastructureConfig.java infrastructure configuration file.

4.4.2. Dependency injection using Spring’s @Autowired annotation

(TODO 02) So you’ve fully reviewed the entire application and you’ve seen nothing out of the ordinary. We’re now going to refactor the application to use annotation based configuration. In RewardsConfig.java, remove the @Bean methods for all beans. Also remove the @Autowired DataSource. In other, the class should contain no methods and no variables.

Try re-running the test. It should fail now. Spring has no idea how to inject the dependencies anymore, since you have removed the configuration directive. Next, we’ll start adding configuration metadata using stereotype annotations and the @Autowired annotation.

(TODO 03) Open the RewardNetworkImpl class and annotate it with one of the available stereotypes. It is definitely not a repository or controller, so we should use @Component or @Service (@Service is probably more descriptive here). Also annotate the constructor with @Autowired OR you can annotate the individual private fields with @Autowired (annotating the constructor is less typing).

(TODO 04) Now open JdbcRewardRepository and annotate it with a stereotype annotation. Since it is a repository class, the @Repository annotation is the obvious choice here. Mark the setDataSource() method with that same @Autowired annotation. This will tell Spring to inject the setter with a instance of a bean matching the DataSource type. You could use field-level injection instead if you prefer, the application will work the same either way.

(TODO 05) Open the JdbcAccountRepository class, annotate it as a @Repository, and annotate the setDataSource() method with @Autowired.

(TODO 06) Annotate the JdbcRestaurantRepository class with @Repository. But this time we will use the @Autowired annotation on the constructor instead of a setter. If you take a look at the constructor you will see why, it calls a populateRestaurantCache method, and this method requires a reference to the DataSource in order to access the DB.

(TODO 07) Although our classes are now properly annotated, we still have to tell Spring to search through our Java classes to find the annotated classes and carry out the configuration. To do this, open RewardsConfig.java and add the @ComponentScan("rewards") annotation. This annotation turns on a feature called component scanning which looks for all classes annotated with annotations such as @Component, @Repository or @Service and creates Spring beans from those classes. It also enables detection of the dependency injection annotations. The "rewards" argument is the base package that we want Spring to look from, this will keep Spring from unnecessarily scanning all org.* and com.* packages on the classpath.

Once you’ve added this, save all your changes and re-run the test and see that it passes.

4.4.3. Working with Init and Destroy callbacks

If you recall the design described in Reward Dining: The Course Reference Domain, restaurant data is read often but rarely changes. You can browse JdbcRestaurantRepository and see that it has been implemented using a simple cache. Restaurant objects are cached to improve performance (see methods populateRestaurantCache and clearRestaurantCache for more details).

The cache works as follows:

  1. When JdbcRestaurantRepository is initialized it eagerly populates its cache by loading all restaurants from its DataSource.
  2. Each time a finder method is called, it simply queries Restaurant objects from its cache.
  3. When the repository is destroyed, the cache should be cleared to release memory.

For convenience, the full sequence is shown again below.

JDBC Restaurant Repository Lifecycle

Figure 4.2. The JdbcRestaurantRepository life-cycle



4.4.3.1. Initialization

Open JdbcRestaurantRepository in the rewards.internal.restaurant package. Notice that we are using the constructor to inject the dependency. You can run the test RewardNetworkTests and see that it works well as it is now.

However, what if you had decided to use setter injection instead of constructor injection? It is interesting to understand what happens then.

(TODO 08) Change the dependency injection style from constructor injection to setter injection: Move the @Autowired from the constructor to the setDataSource method. Now, execute RewardNetworkTests to verify. It should fail and you should see a NullPointerException. Why did the test fail? Investigate the stack-trace to see if you can determine the root cause.

Inside JdbcRestaurantRepository, the default constructor is now used by Spring instead of the alternate constructor. This means the populateRestaurantCache() is never called. Moving this method to the default constructor will not address the issue as it requires the datasource to be set first. Instead, we need to cause populateRestaurantCache() to be executed after all initialization is complete.

(TODO 09) Scroll to the populateRestaurantCache method and add a @PostConstruct annotation to cause Spring to call this method during the initialization phase of the lifecyle. You can also remove the populateRestaurantCache() call from the constructor if you like. Re-run the test now and it should pass.

A valid argument can be made that populateRestaurantCache should never have been in the constructor, and using @PostConstruct is a better approach.

4.4.3.2. Destroy

Your test seems to run fine, let us now have a closer look.

(TODO-10) Open JdbcRestaurantRepository and add a simple print statement in clearRestaurantCache so we can see when it is being run:

    System.out.println("clearRestaurantCache invoked");

(TODO-11) Re-run RewardNetworkTests - check the console output. Note that your clearRestaurantCache invoked message was not generated so clearRestaurantCache is not called, which means that your cache is never cleared.

Add an annotation to mark this method to be called on shutdown.

Save your work and run RewardNetworkTests one more time. You should now see clearRestaurantCache invoked output to the console.

[Tip] Tip

Later in this course, you will learn that there is a more elegant way to work with JUnit. By using Spring’s Testing support an ApplicationContext can actually be created automatically so you do not have to do it by hand.

When this is done, you’ve completed this section! Your repository is being successfully integrated into your application, and Spring is correctly issuing the lifecycle callbacks to populate and clear your cache. Good job!