Blog-Archiv

Mittwoch, 13. November 2019

Spring Boots for Java

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:

  1. How Spring Boot boots
  2. How to inject dependencies with Spring Boot
  3. How to configure with Spring Boot annotations
  4. 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 the matchIfMissing = 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.

Conclusion

These boots are made for walking, and that's just what they'll do, one of these days these boots are gonna walk all over you ...




Keine Kommentare: