Spring Boot Part 3: Dependency Injection and @RestController

This series of articles will examine Spring Boot features. This third article builds on the series by demonstrating the basics of Spring Dependency Injection. To create demonstrable code the example also creates a @RestController implementation, a simple shared property server where clients may put and get property values.

Complete source code for the series and for this part are available on Github.

Note that this post’s details and the example source code has been updated for Spring Boot version 2.5.3 so some output may show older Spring Boot versions.

@ComponentScan and Dependency Injection

This is not an exhaustive description and only describes the simplest (but arguably most common) features of Spring’s dependency injection.

The Spring Boot process starts in the Launcher::main method (unchanged from the implementation described in parts 1 and 2 of this series) with the construction of a SpringApplication and invocation of its run method. Annotating the class with @SpringBootApplication is the equivalent of annotating with @Configuration, @EnableAutoConfiguration, and @ComponentScan.

@SpringBootApplication
@NoArgsConstructor @ToString @Log4j2
public class Launcher {
    public static void main(String[] argv) throws Exception {
        SpringApplication application = new SpringApplication(Launcher.class);

        application.run(argv);
    }
}

SpringApplication starts its analysis with the application.Launcher class. The @EnableAutoConfiguration indicates Spring Boot should attempt to “guess” as necessary. The @ComponentScan annotation indicates that Spring Boot should start scanning classes in the application package (containing package for Launcher) for classes annotated with @Component. Note: Annotations-types annotated with @Component are also components. For example, @Configuration, @Controller, and @RestController are all @Components, but not vice-versa.

For each class annotated with @Component, Spring:

  1. Instantiates a single instance,

  2. For each instance field annotated with @Value, evaluate the SpEL expression1 and initialize the field with the result,

  3. For each method annotated with @Bean within a @Configuration class, invoke the method exactly once to obtain the bean value, and,

  4. For each field annotated with @Autowired, assign the corresponding value obtained by evaluating a @Bean method.

Again, the above is a gross oversimplification, not exhaustive, and relies on handwaving but should be enough to get started.

The sample code for this article does not require @Value injection but a previous article provides examples in its MysqldConfiguration implementation:

    @Value("${mysqld.home}")
    private File home;

    @Value("${mysqld.defaults.file:${mysqld.home}/my.cnf}")
    private File defaults;

    @Value("${mysqld.datadir:${mysqld.home}/data}")
    private File datadir;

    @Value("${mysqld.port}")
    private Integer port;

    @Value("${mysqld.socket:${mysqld.home}/socket}")
    private File socket;

    @Value("${logging.path}/mysqld.log")
    private File console;

The above code takes advantage of specifying default values in SpEL expressions and automated type conversion.

The simple property server implemented here-in creates a “dictionary” bean within the DictionaryConfiguration:

@Configuration
@NoArgsConstructor @ToString @Log4j2
public class DictionaryConfiguration {
    @Bean
    public Map<String,String> dictionary() {
        return new ConcurrentSkipListMap<>();
    }
}

And that bean is wired into the DictionaryRestController as follows:

@RestController
...
@NoArgsConstructor @ToString @Log4j2
public class DictionaryRestController {
    @Autowired private Map<String,String> dictionary = null;
    ...
}

The next section describes the implementation of the @RestController.

@RestController Implementation

The @RestController implemented here-in provides the following web API:

Method URI Query Parameters Returns
GET http://localhost:8080/dictionary/get key The value associated with key (may be null)
GET2 http://localhost:8080/dictionary/put key=value The previous value associated with key (may be null)
GET http://localhost:8080/dictionary/remove key The value previously associated with key (may be null)
GET http://localhost:8080/dictionary/size NONE int
GET http://localhost:8080/dictionary/entrySet NONE Array of key-value pairs
GET http://localhost:8080/dictionary/keySet NONE Array of key values

DictionaryRestController is annotated with @RestController and @RequestMapping with value = { "/dictionary/" } indicating request paths will be prefixed with /dictionary/ and produces = "application/json" indicating that HTTP responses should be encoded in JSON.

@RestController
@RequestMapping(value = { "/dictionary/" }, produces = MediaType.APPLICATION_JSON_VALUE)
@NoArgsConstructor @ToString @Log4j2
public class DictionaryRestController {
    @Autowired private Map<String,String> dictionary = null;
    ...
}

The dictionary map is @Autowired as described in the previous section.

The implementation of the /dictionary/put method is:

    @RequestMapping(method = { RequestMethod.GET }, value = { "put" })
    public Optional<String> put(@RequestParam Map<String,String> parameters) {
        if (parameters.size() != 1) {
            throw new IllegalArgumentException();
        }

        Map.Entry<String,String> entry = parameters.entrySet().iterator().next();
        String result = dictionary.put(entry.getKey(), entry.getValue());

        return Optional.ofNullable(result);
    }

Spring will inject the request’s query parameters in the method call as parameters. The method verifies that exactly one query parameter is specified, puts that key-value into the dictionary, and returns the result (the previous value for that key in the map). Spring interprets a String as literal JSON so the method wraps the result in an Optional to force Spring to encode to JSON.

The implementation of the /dictionary/get method is:

    @RequestMapping(method = { RequestMethod.GET }, value = { "get" })
    public Optional<String> get(@RequestParam Map<String,String> parameters) {
        if (parameters.size() != 1) {
            throw new IllegalArgumentException();
        }

        Map.Entry<String,String> entry = parameters.entrySet().iterator().next();
        String result = dictionary.get(entry.getKey());

        return Optional.ofNullable(result);
    }

Again, there must be exactly one query parameter and the result is wrapped in an Optional. The implementation of the /dictionary/remove request is nearly identical.

The implementation of the /dictionary/size method is:

    @RequestMapping(method = { RequestMethod.GET }, value = { "size" })
    public int size(@RequestParam Map<String,String> parameters) {
        if (! parameters.isEmpty()) {
            throw new IllegalArgumentException();
        }

        return dictionary.size();
    }

No query parameters should be specified. The implementation of the /dictionary/entrySet is nearly identical with a method return type of Set<Map.Entry<String,String>>:

    @RequestMapping(method = { RequestMethod.GET }, value = { "entrySet" })
    public Set<Map.Entry<String,String>> entrySet(@RequestParam Map<String,String> parameters) {
        if (! parameters.isEmpty()) {
            throw new IllegalArgumentException();
        }

        return dictionary.entrySet();
    }

And the implementation of /dictionary/keySet follows the same pattern.

The Maven project POM provides a spring-boot:run profile described in the first article of this series and the server may be started with mvn -B -Pspring-boot:run. When started with this profile, the Spring Boot Actuator is available. The @RestController handler mappings may be verified with the following query:

$ curl -X GET http://localhost:8081/actuator/mappings \
> | jq '.contexts.application.mappings.dispatcherServlets[][]
        | {handler: .handler, predicate: .predicate}'
{
  "handler": "application.DictionaryRestController#remove(Map)",
  "predicate": "{GET /dictionary/remove, produces [application/json]}"
}
{
  "handler": "application.DictionaryRestController#get(Map)",
  "predicate": "{GET /dictionary/get, produces [application/json]}"
}
{
  "handler": "application.DictionaryRestController#put(Map)",
  "predicate": "{GET /dictionary/put, produces [application/json]}"
}
{
  "handler": "application.DictionaryRestController#size(Map)",
  "predicate": "{GET /dictionary/size, produces [application/json]}"
}
{
  "handler": "application.DictionaryRestController#entrySet(Map)",
  "predicate": "{GET /dictionary/entrySet, produces [application/json]}"
}
{
  "handler": "application.DictionaryRestController#keySet(Map)",
  "predicate": "{GET /dictionary/keySet, produces [application/json]}"
}
{
  "handler": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)",
  "predicate": "{ /error, produces [text/html]}"
}
{
  "handler": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)",
  "predicate": "{ /error}"
}
{
  "handler": "ResourceHttpRequestHandler [\"classpath:/META-INF/resources/webjars/\"]",
  "predicate": "/webjars/**"
}
{
  "handler": "ResourceHttpRequestHandler [\"classpath:/META-INF/resources/\", \"classpath:/resources/\", \"classpath:/static/\", \"classpath:/public/\", \"/\"]",
  "predicate": "/**"
}

Using curl to verify the put operation (note the difference in return values from the first and second invocations):

$ curl -X GET -i http://localhost:8080/dictionary/put?foo=bar
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 11 Dec 2019 19:56:43 GMT

null

$ curl -X GET -i http://localhost:8080/dictionary/put?foo=bar
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 11 Dec 2019 19:56:44 GMT

"bar"

And then verify the previous put with the get operation:

$ curl -X GET -i http://localhost:8080/dictionary/get?foo
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 11 Dec 2019 19:59:22 GMT

"bar"

Retrieving the dictionary entry set demonstrates complex JSON encoding:

$ curl -X GET -i http://localhost:8080/dictionary/entrySet
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 11 Dec 2019 20:00:24 GMT

[ {
  "foo" : "bar"
} ]

And supplying a query parameter to size demonstrates error handling:

$ curl -X GET -i http://localhost:8080/dictionary/size?foo
HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 11 Dec 2019 20:03:42 GMT
Connection: close

{
  "timestamp" : "2019-12-11T20:03:42.110+0000",
  "status" : 500,
  "error" : "Internal Server Error",
  "message" : "No message available",
  "trace" : "java.lang.IllegalArgumentException\n\tat application.DictionaryRestController.size(DictionaryRestController.java:65)\n...",
  "path" : "/dictionary/size"
}

Summary

This article demonstrates basic Spring dependency injection through showing how “@Values” may be calculated and injected and “@Beans” may be created and “@Autowired” in a @RestController implementation.

Part 4 of this series discusses Spring MVC and implements a simple internationalized clock application as an example.

[1] SpEL also provides access to the properties defined in the application.properties resources.

[2] It is unfortunate that the GET HTTP method combined with the Map put method may cause confusion and a more sophisticated API definition might reasonably use POST or PUT methods for their semantic value.