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

Terms and theory

Inversion of Control

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

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.

tree graph of various IoC implementations: service locator, events, delegates, DI

Spring Framework

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

Bean

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

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:

  1. add @Transactional to the saveUsers() method, if saving all users in one transaction is a viable option,
  2. 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,
  3. programmatically retrieve the proxy for this and use it to call the transactional method.

Practice

1. Add Spring Boot

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);
	}
}

// ...

2. Implement HelloWorld Spring @Controller

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 to false

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.

3. Make the controller return random numbers

Extend the controller to return random numbers when accessing GET /number endpoint.

3.1 Implement NumberService

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();
	}
}

3.2 Define NumberService bean

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);
}

3.3 Inject the bean into the controller

Inject the defined randomNumberService bean into the controller. There are three autowiring strategies:

  1. constructor-based injection (preferred)
  2. setter-based injection
  3. 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).

3.4 Add GET /number endpoint returning random numbers

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 🎉

3.5 Implement NumberService once again

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:

  1. use autowiring by name - rename the constructor parameter / setter parameter / field to match the bean name
  2. make one bean primary - use the @Primary annotation to make Spring use that one, unless it's told otherwise
  3. use qualifier (preferred) - use the @Qualifier annotation with value set to bean name

3.6 Use @RequestScope to re-create bean for each request

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());
}

4. Try to deploy the app to standalone Tomcat (optional)

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.

Sources

⚠️ **GitHub.com Fallback** ⚠️