Merikanto

一簫一劍平生意,負盡狂名十五年

Spring Boot - 05 Configurations

This is the fifth post in a series of posts to cover different aspects of Spring Boot. Please note that the entire post isn’t necessarily only written in English. In this post, I am going to cover Spring Boot’s configurations.

Configuration properties are nothing more than properties on beans in the Spring application context, that can be set from one of several property sources, including JVM system properties, command-line arguments, and environment variables.



Summary

  • Spring beans can be annotated with @ConfigurationProperties to enable injection of values from one of several property sources.

  • Configuration properties can be set in command-line arguments, environment variables, JVM system properties, properties files, or yml files.

  • Configuration properties can be used to override autoconfig settings, including the ability to specify a data-source URL and logging levels.

  • Spring profiles can be used with property sources to conditionally set configuration properties based on the active profiles.



The Context

There are two different kinds of configurations in Spring, namely:

  • Bean wiring: Mandate application components to be created as beans in the Spring application context, and how they should be injected into each other.
  • Property injection: sets values on beans in the Spring application context.

In Spring’s XML and Java-based configuration, these two types of configurations are often declared explicitly in the same place. In Java configuration, an @Bean-annotated method is likely to both instantiate a bean and then set values to its properties.


For example, the following @Bean method declares a DataSource for an embedded H2 database:

1
2
3
4
5
6
7
8
9
10
@Bean
public DataSource dataSource() {
return new EmbeddedDataSourceBuilder()
.setType(H2)

// set String properties with the name of SQL scripts
.addScript("taco_schema.sql")
.addScripts("user_data.sql", "ingredient_data.sql")
.build();
}

The config above only applies in older version. This is completely unnecessary in Spring Boot.

If the H2 dependency is available in the runtime classpath, then Spring Boot automatically creates an appropriate DataSource bean in the Spring application context.


The Spring environment abstraction abstracts the origins of properties, so that beans can consume properties from Spring itself. Spring pulls from several property sources, then aggregates those properties into a single source from which Spring beans can be injected.

  • JVM system properties
  • OS environment variables
  • Command-line arguments
  • Application property configuration files ( application.yml )

Example: 4 ways of specifying a server port:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. application.properties
server.port=9090

# 2. application.yml
server:
port: 9090

# 3. Externally (command-line argument)
java -jar cloud-0.0.1-SNAPSHOT.jar --server.port=9090

# 4. Set OS Environment variable
# (Spring will interpret SERVER_PORT as server.port with no problem)
export SERVER_PORT=9090


Fine-tuning Autoconfig

Datasource

Configure an external database:

JDBC driver class is not needed. Spring can figure out from the database URL.

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://localhost/cloud
username: root
password:

Spring Boot uses this connection data when autoconfiguring the Datasource bean. This bean will be pooled using Tomcat’s JDBC connection pool (CP) if it’s available on the classpath. If it’s not availbale, Spring will choose from other connection pool implementation in the classpath:

  • HikariCP
  • Commons DBCP 2

The way to specify database initialization scripts:

1
2
3
4
5
6
7
8
spring:
datasource:
schema: # main/resources
- schema-1.sql
- schema-2.sql
data: # main/resources
- data-1.sql
- data-2.sql

Configure datasource in JNDI, and have Spring look it up from there.

1
2
3
4
# JNDI will override other datasource properties
spring:
datasource:
jndi-name: java:/comp/env/jdbc/cloudDS

Embedded Server

If we set port to 0, then Spring will start on a randomly chosen available port.

1
server.port: 0

This is useful when running automated integration tests, to ensure that any concurrently running tests don’t clash on a hard-coded port number.


Set up the container servlet to handle HTTPS requests.

First step, create a keystore using JDK’s keytool:

1
$ keytool -keystore mykeys.jks -genkey -alias tomcat -keyalg RSA

Set properties to enable HTTPS:

1
2
3
4
5
6
7
server:
port: 8443
ssl:
# file://URL
key-store: file:///home/merikanto/mykeys.jks
key-store-password: passwd
key-password: passwd

Logging

Spring Boot configures logging via Logback, and the logging level is INFO by default.

For full control, we can create logback.xml under the root of classpath (src/main/resources).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>

<logger name="root" level="INFO"/>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>

</configuration>

Partial config in application.yml:

Specify my own path & file name, customize logging levels.

1
2
3
4
5
6
logging:
path: /home/merikanto/logs/
file: Cloud.log
level:
root: WARN
org.springframework.security: DEBUG

Use special property values, ${ } as the placeholder marker.

1
2
greeting:
welcome: You are using ${spring.application.name}.


Create Custom Config

Configuration properties are properties of beans, that have been designated to accept configurations from Spring’s environment abstraction.

Spring Boot use @ConfigurationProperties to support property injection of configuration properties. When placed on any Spring bean, it specifies that the properties of that bean can be injected from Spring environment properties. We will use an example to explain the process: Customize pagination properties.


Customize Pagination

Sample Repository & Controller:

1
2
3
4
5
6
7
8
9
10
// Respository
List<Order> findByUserOrderByPlacedAtDesc(User user);

// Controller
@GetMapping
public String ordersForUser(@AuthenticationPrincipal User user, Model model) {

model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user));
return "orderList";
}

Add hard-coded Pagination:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Respository
// Changed the signature of this method by adding @param Pageable
List<Order> findByUserOrderByPlacedAtDesc(User user, Pageable pageable);

// Controller
@GetMapping
public String ordersForUser(@AuthenticationPrincipal User user, Model model) {

// Pagination
Pageable pageable = PageRequest.of(0, 20);
model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
return "orderList";
}

Add pageSize, so pagination is not hard-coded

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
// Repository is the same as above; Controller is below

@Controller
@RequestMapping("/orders")
@SessionAttributes("order")

// This annotation! Custom Configuration!
@ConfigurationProperties(prefix="taco.orders")
public class OrderController {

// pageSize, the added property
private int pageSize = 20;

public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}

@GetMapping
public String ordersForUser(@AuthenticationPrincipal User user, Model model) {

// PageRequest.of
Pageable pageable = PageRequest.of(0, pageSize);

model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
return "orderList";
}
}

@ConfigurationProperties are in fact often placed on beans, whose sole purpose in the application is to be holders of configuration data. This keeps configuration-specific details out of the controllers and other application classes.


Add to application.yml:

1
taco.orders.pageSize: 10

Or export as environmental variable:

1
$ export TACO_ORDERS_PAGESIZE=10

Note that in the Controller, we set pageSize = 20. But that doesn’t matter, because properties in application.yml will override the code in controller.


Another way: Extract the pagination config to a separate Entity class

1
2
3
4
5
6
7
8
9
10
// Entity class, using Lombok
@Data

// Enable component scanning
// Spring will auto discover and create it as a bean in the application context
@Component
@ConfigurationProperties(prefix="taco.orders")
public class OrderProps {
private int pageSize = 20;
}

Use it in Controller

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
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {

private OrderRepository orderRepo;

private OrderProps props;

// Constructor with the two parameters above
public OrderController(OrderRepository orderRepo, OrderProps props) {
this.orderRepo = orderRepo;
this.props = props;
}


@GetMapping
public String ordersForUser(@AuthenticationPrincipal User user, Model model) {

// 注意右边,改变的是 props.getPageSize()
Pageable pageable = PageRequest.of(0, props.getPageSize());

model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
return "orderList";
}
}

👉 Now Order Controller is no longer responsible for handling its own configuration properties. This keeps the code in Controller neater, and allows you to reuse the properties in OrderProps in any other beans.


New Rules: Apply validation to page size. Only need to modify OrderProps Entity.

1
2
3
4
5
6
7
8
9
10
11
@Data
@Component
@ConfigurationProperties(prefix="taco.orders")
@Validated // 这个 Annotation 加上
public class OrderProps {

// 加上这两行
@Min(value=5, message="must be between 5 and 25")
@Max(value=25, message="must be between 5 and 25")
private int pageSize = 20;
}

Declare Property Metadata

In application.yml:

1
2
# pageSize is equivalent to page-size
taco.order.page-size: 10

Warning: Spring Boot configuration annotation processor not configured.

Because there’s missing metadata about the configuration property. Therefore we add metadata in /resources/META-INF:

1
2
3
4
5
6
7
8
{
"properties": [
{
"name": "taco.orders.page-size",
"type": "java.lang.String",
"description": "Sets maximum number of orders to display in a list."
}
] }


Config with Profiles

When applications are deployed to different run-time environments, usually some configuration details differ. Therefore, we can use Spring profiles . Profiles are a type of conditional configuration where different beans, configuration classes, and configuration properties are applied or ignored based on what profiles are active at runtime.


Define Profile-Specific Properties

Old way of configuring different profiles:

Use application-{profile name}.yml for different profiles.


An easier way to specify profile-specific properties, in just one file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
logging.level.tacos: DEBUG

# Profile 之间用横线隔开
---
spring:
# Production profile, separate from the profile above
profiles: prod

datasource:
url: jdbc:mysql://localhost/cloud
username: root
password:

logging.level.tacos: WARN

Activate Profiles

If we simply set it this way:

1
2
3
spring.profiles.active: 
- prod
- audit

Then prod became the default profile, and we didn’t achieve the benefit of separating production-specific profiles from dev ones. Therefore, it’s better to set the active profiles with environment variables:

1
$ export SPRING_PROFILES_ACTIVE=prod,audit

With JAR:

1
$ java -jar cloud.jar --spring.profiles.active=prod,audit

If we deploy to Cloud Foundry, a profile named cloud is automatically activated. If Cloud Foundry is the production environment, make sure to specify production-specific properties under the cloud profile.


Conditionally Create Beans with Profiles

Normally, any bean declared in a Java configuration class is created regardless of which profile is active. But we can conditionally create beans with different profiles via annotations as such:

1
2
3
4
5
@Bean
@Profile({"dev", "qa"}) // Add this line

// The data will only be loaded if the dev or qa profile is active
public CommandLineRunner dataLoader(...){}

To exclude a profile:

1
2
// Always load the data, when prod is inactive
@Profile("!prod")

Add Profile to an entire configuration class:

1
2
3
4
5
6
7
@Profile({"!prod", "!qa"})
@Configuration
public class DevelopmentConfig {

@Bean
public CommandLineRunner dataLoader(...) {}
}