IoC, DI, Spring - fidransky/kiv-pia-labs GitHub Wiki
TOTD:
- learn about IoC, DI and its practical application
- turn our plain Java app into a Spring Boot app
In traditional environments, the programmer has a complete control over the flow of the program from start to end.
In IoC, there is always a framework or encapsulating container responsible for the flow.
IoC is popularly called the Hollywood principle - “Don’t call us, we’ll call you”.
Dependency injection is one of the IoC implementations. Traditionally, the programmer is responsible for resource allocation, object creation and so on. In DI, you just ask the framework or container to provide a dependency.
Spring is a huge framework covering almost any area of programming but at its core, there is an IoC and DI implementation^1.
Spring Framework provides a unified API for many tasks (messaging, persistence, caching) and its ubiqitous use of DI allows you to switch between underlying technologies easily - often just by configuration, with no changes to the source code.
Spring makes the process of integrating with a library seamless and takes away the pain of dependency management, configuration and lets you focus on building out the features of your application.
Spring vs. Spring Boot
Spring Boot, built on top of Spring Framework, is one of the modern Java web app frameworks focused on developer experience and development speed.
Set of features quoted from the official web:
- create stand-alone Spring applications
- embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- provide opinionated 'starter' dependencies to simplify your build configuration
- automatically configure Spring and 3rd party libraries whenever possible
- provide production-ready features such as metrics, health checks, and externalized configuration
- absolutely no code generation and no requirement for XML configuration
Usually a singleton object managed by Spring and injected as a dependency. Beans can be defined using @Component
-like class annotations or created in a @Configuration
class.
By default, beans are created only once during application context creation and then used for the whole application run (i.e. context scoped beans). However, beans may also be re-created for each request, session and more^2.
Define bean using @Component
-like class annotations:
Defining beans using class annotations is the most straightforward, yet not always viable option. You just annotate your classes with one of following annotations:
-
@Component
- generic type of bean, use meta-annotations instead (@Service
,@Repository
etc.) -
@Service
- bean containing some logic -
@Repository
- bean providing data access -
@Controller
- bean providing web layer I/O -
@Configuration
- class configuring beans in case they cannot be created automatically
@Service
public class RandomNumberService implements NumberService {
private final Random random;
public RandomNumberService(Random random) {
this.random = random;
}
@Override
public Number getNumber() {
// ...
}
}
Create bean in a @Configuration
class:
External libraries often don't use class annotations on their services. In order to integrate such libraries with Spring, one must create a @Configuration
bean and create library beans manually.
@Configuration
public class NumberConfiguration {
@Bean
public NumberService randomNumberService(RandomGenerator randomGenerator) {
return new RandomNumberService(randomGenerator);
}
}
Proxy objects are an integral part of Spring and it's necessary to understand some of their limitations.
When you ask Spring to inject a dependency to a bean, you don't get the instance of the class you expect but a proxy object. This object wraps the methods of the original object so any additional processing (based on annotations or XML configuration) is possible. This means, however, that calling methods within the same class lack this possibility, as the calls are passed directly without the proxy object.
The most frequent case when it causes problems is database transaction handling:
@Transactional
public void saveUser(User user) {
// ...
}
public void saveUsers(List<User> users) {
users.stream.forEach(this::saveUser);
}
In this case, when calling saveUser()
from some other bean, the transaction is created as expected. However, when calling saveUsers()
(which in turn calls saveUser()
), no transaction is created.
There are three ways to handle this problem:
- add
@Transactional
to thesaveUsers()
method, if saving all users in one transaction is a viable option, - add another layer - a separate service containing only the
saveUser()
method and another one using the service as a dependency and providing single and multiple save possibilities, - programmatically retrieve the proxy for
this
and use it to call the transactional method.
Use preconfigured Spring Boot Initializr to explore Spring Boot application base.
Merge generated pom.xml
with the existing pom.xml
. Copy everything else (classes, properties) to corresponding paths in your project.
Optionally, make Application
log all beans defined in the current Spring application context:
private static final Logger LOG = getLogger(Application.class);
// ...
// prints all registered beans whenever application context is refreshed - most commonly on application startup
@EventListener
public void onContextRefreshed(ContextRefreshedEvent event) {
LOG.info("*************** APPLICATION CONTEXT REFRESHED ***************");
for (String beanName : event.getApplicationContext().getBeanDefinitionNames()) {
LOG.info(beanName);
}
}
// ...
Create a new HelloWorldController
class, annotate it with Spring @Controller
and make it say hello.
Reveal solution
package cz.zcu.kiv.pia.labs.controller;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@ResponseBody
public class HelloWorldController {
// create endpoint at http://localhost:8080/hello
@GetMapping(path = "/hello", produces = MediaType.TEXT_HTML_VALUE + "; charset=utf-8")
public String sayHello(@RequestParam(value = "from", required = false) String from) {
var builder = new StringBuilder("Hello World");
if (from != null) {
builder.append(" from ").append(from);
}
builder.append("!");
return "<h1>" + builder + "</h1>";
}
}
Access the newly created endpoint at http://localhost:8080/hello.
Note the key parts:
- HTTP method set using
@GetMapping
meta-annotation - path set via
path
attribute - content type and encoding set via
produces
attribute - query parameter bound using
@RequestParam
annotation, the parameter name equals the argument name by default - query parameters are required by default - to make them optional, set
required
attribute tofalse
Also, note that the whole controller class is annotated with @ResponseBody
annotation. Without it, controllers take method return value and pass it to a template resolver to try and find a matching template to be rendered.
Extend the controller to return random numbers when accessing GET /number
endpoint.
Define NumberService
interface so that the controller doesn't depend on one specific implementation - i.e. use the so-called interface injection.
Reveal solution
package cz.zcu.kiv.pia.labs.number;
public interface NumberService {
/**
* @return a single number, either random or constant
*/
Number getNumber();
}
Next, implement the interface using java.util.random.RandomGenerator
to generate random numbers.
Reveal solution
package cz.zcu.kiv.pia.labs.number;
import java.util.random.RandomGenerator;
/**
* Implementation of {@link NumberService} returning random numbers.
*/
public class RandomNumberService implements NumberService {
private final RandomGenerator randomGenerator;
public RandomNumberService(RandomGenerator randomGenerator) {
this.randomGenerator = randomGenerator;
}
@Override
public Number getNumber() {
return randomGenerator.nextLong();
}
}
Now, defined a bean called randomNumberService
. First, using @Service
class-level meta-annotation:
@Service("randomNumberService")
Alternatively, using @Configuration
class:
@Bean
public NumberService randomNumberService(RandomGenerator randomGenerator) {
return new RandomNumberService(randomGenerator);
}
Inject the defined randomNumberService
bean into the controller. There are three autowiring strategies:
- constructor-based injection (preferred)
- setter-based injection
- field-based injection
constructor-based injection:
private final NumberService numberService;
public HelloWorldController(NumberService numberService) {
this.numberService = numberService;
}
setter-based injection:
private NumberService numberService;
@Autowired
public void setNumberService(NumberService numberService) {
this.numberService = numberService;
}
field-based injection:
@Autowired
private NumberService numberService;
By default, Spring falls back to using autowiring by type. As a result, autowiring works even though our NumberService
bean is named randomNumberService
(rather than numberService
).
Finally, add a getRandomNumber
controller method and use the injected randomNumberService
to return random numbers.
@GetMapping("/number")
public String getRandomNumber() {
return numberService.getNumber().toString();
}
Now, accessing GET /pia-labs/spring/number
endpoint returns random numbers 🎉
Let's create one more NumberService
implementation: ConstantNumberService
which returns constant number set by constructor. Then, register it as a bean called constantNumberService
.
Reveal solution
package cz.zcu.kiv.pia.labs.number;
import org.springframework.stereotype.Service;
/**
* Implementation of {@link NumberService} returning a constant number set by constructor.
*/
@Service("constantNumberService")
public class ConstantNumberService implements NumberService {
private final Number number;
public ConstantNumberService(Number number) {
this.number = number;
}
@Override
public Number getNumber() {
return number;
}
}
Now, we have two beans implementing NumberService
in the Spring application context. You might be asking: how does Spring know which one to inject?
It doesn't. We have to tell Spring which one we want to use. There are multiple ways to do so:
- use autowiring by name - rename the constructor parameter / setter parameter / field to match the bean name
- make one bean primary - use the
@Primary
annotation to make Spring use that one, unless it's told otherwise - use qualifier (preferred) - use the
@Qualifier
annotation with value set to bean name
With the ConstantNumberService
implemented, use it to play with scoped beans. We can tell Spring to recreate given bean before each request using the @RequestScope
annotation. This way, we can force the ConstantNumberService
implementation to actually return random numbers, just by defining the bean differently:
@Bean
@RequestScope
public NumberService constantNumberService(RandomGenerator randomGenerator) {
return new ConstantNumberService(randomGenerator.nextLong());
}
Change packaging
of the Maven module to war
.
Make Application
extend SpringBootServletInitializer
and override its configure
method:
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
Run mvn package
and deploy resulting pia-labs.war
file from target/
folder to standalone Tomcat.
- https://www.codeproject.com/Articles/592372/Dependency-Injection-DI-vs-Inversion-of-Control-IO
- https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring
- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-container-config
- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.traditional-deployment.war