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:
- How Spring Boot boots
- How to inject dependencies with Spring Boot
- How to configure with Spring Boot annotations
- How to configure using
application.properties
files
For 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.
Maven Dependencies
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
Java Sources
Following screenshot shows the outline of the example project. There are no tests yet.
Main Class
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:
- Scan the CLASSPATH for all kinds of bean annotations like
@Component
,@Service
,@Controller
,@Repository
,@ScheduledJob
, in all classes beside and below the main class; these all are beans - When finding a
@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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package fri.springboot; import java.util.*; import javax.inject.Inject; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import fri.springboot.service.HelloWorldService; import fri.springboot.service.OutputService; @SpringBootApplication public class MySpringBootApplication { public static void main(String[] args) { SpringApplication.run(MySpringBootApplication.class, args); } @Inject private OutputService outputService; public MySpringBootApplication(HelloWorldService helloWorldService) { helloWorldService.sayHello(); //outputService.println("Hello OutputService!"); // can't yet use injected fields here! } @Bean // found because @SpringBootApplication is also @Configuration public CommandLineRunner commandLine(ApplicationContext applicationContext) { return (args) -> { outputService.println("ApplicationName: "+applicationContext.getApplicationName()); outputService.println("DisplayName: "+applicationContext.getDisplayName()); outputService.println("StartupDate: "+new Date(applicationContext.getStartupDate())); outputService.println("Given arguments: "+args.length); for (String arg : args) { outputService.println("\t"+arg); } outputService.println("BeanDefinitionCount: "+applicationContext.getBeanDefinitionCount()); String[] beanNames = applicationContext.getBeanDefinitionNames(); Arrays.sort(beanNames); int i = 0; for (String beanName : beanNames) { i++; outputService.println(i+": "+beanName); } }; } } |
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
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.
Configurations
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).
- When the property "output" is not present,
@ConditionalOnProperty(name = "output", havingValue = "stdout", matchIfMissing = true)
will win due to thematchIfMissing = true
attribute. - When the property "output" is present and has value "stdout",
also
@ConditionalOnProperty(name = "output", havingValue = "stdout", matchIfMissing = true)
will win. - When the property "output" is present and has value "stderr",
@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.
Application Properties Switching
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.