Skip to content

Configuration

BrunoRosendo edited this page Aug 3, 2023 · 6 revisions

Spring Boot simplifies the setup process for developers by enabling them to run their applications without manual configuration. Nevertheless, as projects evolve, it becomes inevitable to customize or extend the default settings provided by Spring Boot. In this section, we will explore how to configure the modules installed in the application or utilize Spring's configuration for custom classes.

Prior to proceeding, it is crucial to read Understanding Spring to gain a better grasp of Spring Beans.

Contents

Configuring Properties in Resources

The primary approach to configuring Spring Boot modules is through the properties specified in resources/application.properties. These properties encompass settings for various aspects such as database, server, or JPA configurations. Represented as simple key-value pairs, these properties follow a format similar to a common .env file.

For assistance, developers can refer to the extensive list of properties provided by Spring Boot or rely on their IDE for guidance. If unsure about the existence of a particular property, a quick Google search can often yield helpful results.

Here's an example of how to configure the development database:

spring.datasource.url=jdbc:h2:file:~/website-be-h2-db
spring.datasource.username=h2dev
spring.datasource.password=

Creating Custom Properties

In application.properties, you have the flexibility to define properties with any name you desire. Accessing these properties is straightforward – you simply declare a class (typically annotated with @Configuration) and a field annotated with @Value(<name-of-the-property>). For instance, if you create a property named cors.allow-origin, you can utilize it in a class as follows

@Value("\${cors.allow-origin}")
final lateinit var origin: String

The use of lateinit allows Spring to initialize the field without needing to use something like final val origin: String? = null. If you find any of this confusing, consider delving into further explanations about Kotlin classes.

Additionally, you can define prefixes and group related properties in a class. For example:

@ConfigurationProperties(prefix = "auth")
data class AuthConfigProperties(
    val publicKey: RSAPublicKey,
    val privateKey: RSAPrivateKey,
    val jwtAccessExpirationMinutes: Long,
    val jwtRefreshExpirationDays: Long
)

Here, the corresponding properties would be auth.public-key, auth.private-key, and so on. This way, you can organize and manage related configurations within the application.properties file more efficiently.

Creating New Property Files

You can create property files other than the default application.properties, and they function similarly. For instance, in our project, we have validation_errors.properties, which contains error messages related to validation.

There are two main approaches to employing these files. First, like using a prefix, you can create a configuration class that holds all the properties from the file by utilizing the @PropertySource annotation. Alternatively, you can use @PropertySources to specify multiple files. For example, we could create a file to handle upload configurations:

@PropertySource("classpath:upload.properties")
class UploadConfigProperties(
    val provider: String?,
    val cloudinaryUrl: String?,
    val cloudinaryBasePath: String?,
    val staticPath: String?,
    val staticServe: String?
)

The other way is how we access properties in validation_errors.properties. In this scenario, the Spring validator allows the option to use a custom message source capable of handling multiple property files, offering extensive customization capabilities. The concept should be clear once you look at the code example and read more about bean configuration.

@Configuration
class ValidationConfig {
    @Bean
    fun validatorFactory(messageSource: MessageSource): Validator {
        val validator = LocalValidatorFactoryBean()
        validator.setValidationMessageSource(messageSource)
        return validator
    }

    @Bean
    fun messageSource(): MessageSource {
        val bean = ReloadableResourceBundleMessageSource()
        bean.addBasenames(
            "classpath:validation_errors"
        )
        bean.setDefaultEncoding("UTF-8")
        return bean
    }
}

After you've done this, you can access those properties in existing or custom validators, as shown below:

@field:Size(min = Constants.Body.minSize, message = "{size.min}")
var body: String
annotation class NullOrNotBlank(
    val message: String = "{null_or_not_blank.error}",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<Payload>> = []
)

You can read more about the usage of these error messages by reading the wiki section(s) about models and validation.

Sources: TutorialsPoint and Baeldung.

Configuring with Beans

Before proceeding with this section, please ensure that you have read the Understanding Spring page and have a solid understanding of how Spring beans function.

While configuring Spring modules in a properties file can be highly useful, it does have certain limitations. For instance, what if you need to utilize some logic to configure a module? Or what if you want to configure custom classes without having to re-initialize them every time they are used? This is where configuration classes come into play!

Inside the config/ package, you will come across several classes annotated with @Configuration. These classes contain methods that define beans used throughout the project. This approach provides a straightforward way to make use of Spring's dependency injection and configure the initialization of these classes in a single place. Similar to the validation configuration example we saw earlier, let's explore another example:

@Configuration
class UploadConfig(
    private val uploadConfigProperties: UploadConfigProperties
) {
    @Bean
    fun fileUploader(): FileUploader {
        return when (uploadConfigProperties.provider) {
            "cloudinary" -> CloudinaryFileUploader(
                uploadConfigProperties.cloudinaryBasePath ?: "/",
                Cloudinary(uploadConfigProperties.cloudinaryUrl ?: throw Error("Cloudinary URL not provided"))
            )
            else -> StaticFileUploader(
                uploadConfigProperties.staticPath?.let { ResourceUtils.getFile(it).absolutePath } ?: "",
                uploadConfigProperties.staticServe ?: "localhost:8080"
            )
        }
    }
}

In this case, the UploadConfig class makes use of a class holding the properties related to uploads and determines the appropriate type of file uploader to use whenever it is required. Note that by default, a bean has a singleton scope, meaning that an instance will be initialized once during startup and used throughout the application. This can be changed by specifying bean scopes. Occasionally, it might also be useful to initialize a bean lazily.

Sources: Spring Docs (@Bean Annotation) and Spring Docs (Bean Scopes).

Logging

In this project, we utilize SLF4J as our logging framework, which offers a straightforward and consistent method to generate logs at various levels (such as info, warn, debug, error, etc.). These logs prove to be more valuable than basic console prints.

By incorporating the Logging interface, any class gains access to this logger.

interface Logging {
    val logger: Logger get() = getLogger(this::class.java)
}

Alternative approaches were considered for implementation, as explained in the Added Logging interface #118 pull request, shedding light on the rationale behind this decision. Another intriguing implementation could involve creating a Logger bean and injecting it into the classes. However, this method is more complex and has the drawback of either requiring the classes to be Spring components or needing access to the application's context.