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 @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.
↩