Spring is a mix of dependency injection container and general-purpose library for everything around web applications. I call it the Java for superheroes, because it gives you "magical powers". Magic in Java mostly refers to reflection techniques. For source code maintenance this means you can not simply remove unused public methods or classes, or rename them, because they could have been used in XML application contexts. This was true especially in times when Spring was driven through XML.
Nowadays Spring is driven through annotations. The difference is that you can not use Spring as configuration utility any more, because annotations are compiled into .class
files, and you can not adapt such application-contexts customer-specifically without recompilation. With XML you could do that. To fill the gap, Spring Boot introduced a new way to integrate property files.
Spring Boot is more than yet another Spring library. It is a new concept to implement applications, targeting the cloud and microservice world. In this article I would like to show some basic Spring Boot techniques:
application.properties
filesFor this I will introduce an example application with three interconnected service classes and two configuration classes that are related to application*.properties
files. The idea is to output a "Hello World" message in configurable ways.
Following is the project-object-model pom.xml
file that has to be placed in the Maven project's root directory. You can also install the Eclipse Spring Tools Suite 4 plugin (marketplace), and then create a "Spring Boot Project". Alternatively you can download one of the starter sets on the Spring Initializr page and then rewrite it.
<?xml version="1.0"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>fri.springboot-test</groupId>
<artifactId>my-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The spring-boot-starter-parent
parent POM provides a stable mix of library versions that may contain most of what you will need for your application, so you don't have to take care of versions any more. The parent of spring-boot-starter-parent
is spring-boot-dependencies
.
The properties
element overrides the Java version from the Spring parent POM to be 1.8.
The dependencies
element contains just spring-boot-starter
. This is not enough for a web application, but for now I just want to test the Spring Boot base functionality (i.e. how it boots).
Additionally I want to use the standard @Inject
annotation instead of Spring's @Autowired
, thus I include the javax.inject
library for field-injection.
Here is the list of libraries resulting from this Maven pom.xml
:
mvn dependency:tree
+- org.springframework.boot:spring-boot-starter:jar:2.2.1.RELEASE:compile
| +- org.springframework.boot:spring-boot:jar:2.2.1.RELEASE:compile
| | \- org.springframework:spring-context:jar:5.2.1.RELEASE:compile
| | +- org.springframework:spring-aop:jar:5.2.1.RELEASE:compile
| | +- org.springframework:spring-beans:jar:5.2.1.RELEASE:compile
| | \- org.springframework:spring-expression:jar:5.2.1.RELEASE:compile
| +- org.springframework.boot:spring-boot-autoconfigure:jar:2.2.1.RELEASE:compile
| +- org.springframework.boot:spring-boot-starter-logging:jar:2.2.1.RELEASE:compile
| | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
| | | +- ch.qos.logback:logback-core:jar:1.2.3:compile
| | | \- org.slf4j:slf4j-api:jar:1.7.29:compile
| | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.12.1:compile
| | | \- org.apache.logging.log4j:log4j-api:jar:2.12.1:compile
| | \- org.slf4j:jul-to-slf4j:jar:1.7.29:compile
| +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
| +- org.springframework:spring-core:jar:5.2.1.RELEASE:compile
| | \- org.springframework:spring-jcl:jar:5.2.1.RELEASE:compile
| \- org.yaml:snakeyaml:jar:1.25:runtime
\- javax.inject:javax.inject:jar:1:compile
Following screenshot shows the outline of the example project. There are no tests yet.
A Spring Boot application is represented by a main class (→MySpringBootApplication.java), containing a main()
method, the class being annotated by either the @SpringBootApplication
annotation or a subset of it, like @ComponentScan
and @EnableAutoConfiguration
. These two represent the most important boot-functionalities:
@Component
, @Service
, @Controller
, @Repository
, @ScheduledJob
, in all classes beside and below the main class; these all are beans@Configuration
class, execute all its @Bean
methods to generate beans; such a class is a replacement for the XML application-context.Here is my Spring Boot main class:
1 | package fri.springboot; |
The static main()
method on line 16 calls Spring and passes its owner class to it. Spring then will call the constructor. Then the resulting MySpringBootApplication
instance will be available as bean, by default as singleton.
The MySpringBootApplication
class contains a field-injection on line 20. Field injection is a little risky, because you can not yet use the injected field in constructor, a field can be injected only after the constructor has finished. Originally Spring promoted just constructor- and setter-injection, but field-injection has turned out to be the most popular way to define dependencies.
The constructor on line 23 receives a HelloWorldService
bean as parameter. You don't need to annotate this parameter, Spring will automatically pass the correct bean, derived from its type. Then sayHello()
gets called on that service bean. Expected is a line of console output, either to stdout or to stderr, this should be configurable. Also the text of the Hello-message should be configurable (default "Hello World for Everyone!").
The remaining part of the class on line 29 outputs application context information. It uses the output-service that was injected on line 20. The commandLine()
method is annotated with @Bean
, and a @SpringBootApplication
class automatically also is @Configuration
, thus the method will be called by Spring to generate a bean. In this case the bean is a lambda implementing the functional interface CommandLineRunner
. Spring Boot will call the run()
method of any bean that implements CommandLineRunner
or ApplicationRunner
when booting.
That all is the magic that will generate following output on stdout (Spring banner left out):
Hello World for Everyone!
ApplicationName:
DisplayName: org.springframework.context.annotation.AnnotationConfigApplicationContext@769f71a9
StartupDate: Wed Nov 13 20:24:35 CET 2019
Given arguments: 0
BeanDefinitionCount: 41
1: applicationTaskExecutor
2: commandLine
3: helloWorldServiceImpl
4: mbeanExporter
5: mbeanServer
6: mySpringBootApplication
7: objectNamingStrategy
8: org.springframework.aop.config.internalAutoProxyCreator
9: org.springframework.boot.autoconfigure.AutoConfigurationPackages
10: org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
11: org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
12: org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$ClassProxyingConfiguration
13: org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
14: org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
15: org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
16: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
17: org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
18: org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
19: org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
20: org.springframework.boot.context.internalConfigurationPropertiesBinder
21: org.springframework.boot.context.internalConfigurationPropertiesBinderFactory
22: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata
23: org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator
24: org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
25: org.springframework.context.annotation.internalAutowiredAnnotationProcessor
26: org.springframework.context.annotation.internalCommonAnnotationProcessor
27: org.springframework.context.annotation.internalConfigurationAnnotationProcessor
28: org.springframework.context.event.internalEventListenerFactory
29: org.springframework.context.event.internalEventListenerProcessor
30: outputServiceImpl
31: outputStderrConfiguration
32: outputStdoutConfiguration
33: outputStream
34: propertySourcesPlaceholderConfigurer
35: spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties
36: spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties
37: spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
38: springApplicationAdminRegistrar
39: taskExecutorBuilder
40: taskSchedulerBuilder
41: textServiceImpl
Services are stateless functionalities encapsulating business logic. Mostly they also provide transaction management and caching. They connect presentation logic (user interface) with persistently stored data (database, file and document stores). You always use services through interfaces.
Following three services do not provide any of these things, but at least they all encapsulate one aspect of text output. Here is the service that will print some "Hello" retrieved from a text-service through an output-service:
package fri.springboot.service;
public interface HelloWorldService
{
void sayHello();
}
package fri.springboot.service.impl;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
import fri.springboot.service.*;
@Service
public class HelloWorldServiceImpl implements HelloWorldService
{
@Inject
private OutputService outputService;
@Inject
private TextService greetingService;
public void sayHello() {
outputService.println(greetingService.getText());
}
}
The @Service
annotation currently is the same as @Component
, a simple bean-annotation. Spring may provide some built-in functionality for @Service
in future.
We see two @Inject
services dependencies. One will provide output, one will provide text, as done in the sayHello()
method.
Here is the output-service:
package fri.springboot.service;
public interface OutputService
{
void println(String line);
}
package fri.springboot.service.impl;
import java.io.PrintStream;
import javax.inject.Inject;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import fri.springboot.service.OutputService;
@Service
public class OutputServiceImpl implements OutputService
{
@Inject
@Qualifier("outputStream") // refers to method outputStream() in @Configuration
private PrintStream stream;
@Override
public void println(String line) {
stream.println(line);
}
}
Now this is not so easy to understand any more. The injected PrintStream
is not a bean, it is a standard Java runtime library class, part of the JRE. Nevertheless this is injected here, by means of a @Bean
named outputStream
that in fact is a PrintStream
. The @Qualifier
annotation tells Spring the name of the bean, in case the type of the field does not provide enough information. By default, beans are named like the method that generates them, so we'd expect some @Bean
generated by a @Configuration
method called outputStream()
, see Output*Configuration
classes below.
Last not least here is the text-service:
package fri.springboot.service;
public interface TextService
{
String getText();
}
package fri.springboot.service.impl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import fri.springboot.service.TextService;
@Service
public class TextServiceImpl implements TextService
{
@Value("${greeting}")
private String greeting;
public String getText() {
return greeting;
}
}
The @Value
annotation refers to a Spring Boot application-property. It is written as "${name_of_the_property}". The value of that property gets injected into the field greeting
. That way we can configure the output text even after deployment of the application by providing an alternative application.properties
file.
Read Spring documentation about where this file will be searched by Spring Boot. By default it is in src/main/resources/application.properties
:
greeting = Hello World for Everyone!
#output = stderr
#output = stdout
"Hello World for Everyone!" is the text we saw in the console output above.
When Spring Boot finds a class annotated with @Configuration
, it will call all methods inside of it that are annotated with @Bean
. The resulting beans will be available in application context.
package fri.springboot.configuration;
import java.io.PrintStream;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OutputStderrConfiguration
{
@Bean
@ConditionalOnProperty(name = "output", havingValue = "stderr")
public PrintStream outputStream() {
return System.err;
}
}
package fri.springboot.configuration;
import java.io.PrintStream;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OutputStdoutConfiguration
{
@Bean
@ConditionalOnProperty(name = "output", havingValue = "stdout", matchIfMissing = true)
public PrintStream outputStream() {
return System.out;
}
}
The "outputStream" bean is what the @Qualifier
in output-service referred to. Both configuration classes provide a method outputStream()
, so both resulting beans would be named "outputStream". Will we get two beans with the same name, or will one overwrite the other?
The Spring Boot condition-annotations will take care of that. An application property is used to decide (thus this is configurable after deployment).
@ConditionalOnProperty(name = "output", havingValue = "stdout", matchIfMissing = true)
will win due to the matchIfMissing = true
attribute. @ConditionalOnProperty(name = "output", havingValue = "stdout", matchIfMissing = true)
will win. @ConditionalOnProperty(name = "output", havingValue = "stderr")
will win. So just when property "output" carries the value "stderr", the output-service will write to stderr. In all other cases it will write to stdout.
That way Spring Boot introduced a configuration language in the shape of annotations by which you can implement conditional logic. But you must know the semantic of the Spring annotations to be able to read such source code. Lots of conditional annotations are available.
On startup you can switch to a different application.properties
file, using a naming convention on the file name. When you run your Spring Boot application without command line parameters, it will search for a file named application.properties
. But when you run it with --spring.profiles.active=dev
like in
mvn package
java -jar target/my-spring-boot-starter-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
it will look for
application-dev.properties
instead of application.properties
. In this case the name of the active profile is "dev". You can extend this concept by any identifier you like.
ɔ⃝ Fritz Ritzberger, 2019-11-13