(This post has been updated at blog.hcf.dev with a later version of Spring Boot.)
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.
@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 @Component
s, but not vice-versa.
For each class annotated with @Component
, Spring:
Instantiates a single instance,
For each instance field annotated with
@Value
, evaluate the SpEL expression1 and initialize the field with the result,For each method annotated with
@Bean
within a@Configuration
class, invoke the method exactly once to obtain the bean value, and,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 ) |
GET 2
|
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 "@Value
s" may be calculated and injected and "@Bean
s" 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. ↩
Top comments (0)