Spring Boot Application Properties

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, I’m going to show you the best way to configure the Spring Boot Application Properties file.

This is not just theoretical advice. I applied all these tips when developing RevoGain, a web application that allows you to calculate the gains you realized while trading stocks, commodities, or crypto using Revolut.

Spring Boot Application Properties file

When Spring Boot emerged, it came up with a very clever idea of aggregating all configurations in a single application.properties file.

You can either use a Java Properties file or a YAML one, but I always choose the Properties file format because I don’t have a scale ruler to fix YAML indentation issues:

Spring Boot Application Properties – Web Configuration

Here’s the list of settings I use to configure the Web layer:

## Web error page
server.error.whitelabel.enabled=false

## Web HTTPS settings
server.tomcat.remoteip.remote-ip-header=x-forwarded-for
server.tomcat.remoteip.protocol-header=x-forwarded-proto

### Web Gzip
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css

## Web static resources versioning
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/js/**,/css/**

### Web caching
spring.web.resources.cache.cachecontrol.max-age=30d

The server.error.whitelabel.enabled property disables the default Whitelabel Error Page so that we can customize an application-specific error page.

The next step is to have an error handler that allows you to customize the error page for authenticated and non-authenticated users:

@Controller
public class ApplicationErrorController
        extends BaseController implements ErrorController {

    @RequestMapping("/error")
    public String handleError(
            Model model, 
            HttpServletRequest request) {
        Object statusCodeValue = request.getAttribute(
            RequestDispatcher.ERROR_STATUS_CODE
        );

        String errorMessage = null;
        String requestPath = (String) request.getAttribute
            RequestDispatcher.ERROR_REQUEST_URI
        );

        if (statusCodeValue != null) {
            int statusCode = Integer.parseInt(statusCodeValue.toString());
            
            if (statusCode == HttpStatus.NOT_FOUND.value()) {
                errorMessage = String.format(
                    "The [%s] request could not be found.", 
                    requestPath
                );
            } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
                Object exception = request.getAttribute(
                    RequestDispatcher.ERROR_EXCEPTION
                );
                
                if(exception instanceof Throwable) {
                    String errorCause = ExceptionUtils
                        .getRootCause(
                            (Throwable) exception
                        )
                        .getMessage();
                        
                    errorMessage = String.format(
                        "The [%s] request could not be processed - %s",
                        requestPath,
                        errorCause
                    );
                } else {
                    errorMessage = String.format(
                        "The [%s] request could not be processed.", 
                        requestPath
                    );
                }
            } else {
                HttpStatus status = HttpStatus.valueOf(statusCode);
            
                errorMessage = String.format(
                    "The [%s] request failed with this status code: %s", 
                    requestPath, 
                    status
                );
            }
        }

        if(errorMessage != null) {
            model.addAttribute("error", errorMessage);
        }

        LOGGER.error(errorMessage);

        return UserContext.getCurrentUser() != null ?
            "error-logged-in" :
            "error";
    }
}

The error-logged-in.html is a Thymeleaf page that’s displayed for authenticated users, and the error.html is the Thymeleaf page for non-authenticated users. The reason I use two error pages is that the layout and especially the menu differs between the initial landing pages and the actual application layout.

The server.tomcat.remoteip settings are used to enable HTTPS when running your Spring Boot application behind a proxy server, as it’s the case on AWS:

server.tomcat.remoteip.remote-ip-header=x-forwarded-for
server.tomcat.remoteip.protocol-header=x-forwarded-proto

You should only use HTTPS for all your websites and web application available over the Internet. Not only is the communication secured, but Google is going to increase the ranking of your web pages as well.

The web compression settings are used to enable HTTP compression using GZIP for the underlying Tomcat server used by Spring Boot:

server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css

The resource versioning settings allow you to avoid caching issues when modifying JS and CSS resources and redeploying your application:

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/js/**,/css/**

So, with web resource versioning, a Thymeleaf link like this one:

<link rel="stylesheet" th:href=@{/css/main.css}/>

is going to be rendered like this:

<link rel="stylesheet" href=/css/main-894af16207c18178542fdc5f96f46a2b.css/>

The 894af16207c18178542fdc5f96f46a2b suffix is a hash value generated based on the main.css content. If you change the file content, the hash value will change as well, meaning that you are going to invalidate the old cached entries since this file is going to be fetched from the server the first time a user accesses this page.

The cache.cachecontrol.max-age setting is used for caching web resources for 30 days:

spring.web.resources.cache.cachecontrol.max-age=30d

Since we are using resource versioning, we can set a longer cache period for our web resources and make the page rendering faster since less content will be needed to be fetched from the webserver.

Spring Boot Application Properties – Database Configuration

The data access layer has the most significant impact on application performance. Hence, it’s very important to pay attention to how we configure it.

This is a list of settings that you can use if you are using MySQL and HikariCP in your Spring Boot application:

## DataSource properties
spring.datasource.url=jdbc:mysql://localhost:3306/revogain
spring.datasource.username=${REVOGAIN_DB_USER}
spring.datasource.password=${REVOGAIN_DB_PASSWORD}

## HikariCP configuration
spring.datasource.hikari.minimumIdle=0
spring.datasource.hikari.maximum-pool-size=40
spring.datasource.hikari.maxLifetime=900000
spring.datasource.hikari.transaction-isolation=TRANSACTION_READ_COMMITTED
spring.datasource.hikari.auto-commit=false
spring.datasource.hikari.data-source-properties.useServerPrepStmts=false
spring.datasource.hikari.data-source-properties.cachePrepStmts=true
spring.datasource.hikari.data-source-properties.prepStmtCacheSize=500
spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=1024

The minimum connection pool size is 0, and it can grow to at most 40 connections:

spring.datasource.hikari.minimumIdle=0
spring.datasource.hikari.maximum-pool-size=40
spring.datasource.hikari.maxLifetime=600000

Since the application uses Aurora MySQL, connection management is a little bit different than when using a standard MySQL instance. As explained in the Aurora connection management handbook, Aurora MySQL uses a pool of worker threads that can switch from one user session to another dynamically. Therefore, the connection pool size is set to a value that’s slightly lower than the connection limit imposed by the Aurora instance type (e.g., 45 for db.t2.small and db.t3.small nodes).

The spring.datasource.hikari.maxLifetime setting instructs Hikari to retire pooled connections after 10 minutes. It’s not a very good idea to keep connections open for a very long time since a connection could be closed unexpectedly by a networking failure without the pool knowing of it.

The default isolation level is set to READ_COMMITTED to optimize the number of gap locks held by MySQL when traversing the clustered index for bulk updates or deletes.

The auto-commit mode is disabled, and we are going to let Hibernate know about this via the hibernate.connection.provider_disables_autocommit setting. This way, Hibernate can acquire the database connection lazily right before executing a query or prior to flushing the Persistence Context, as opposed to the default behavior, which makes Hibernate acquire the connection right when entering a @Transactional method.

The enable statement caching, we set the following properties:

spring.datasource.hikari.data-source-properties.useServerPrepStmts=false
spring.datasource.hikari.data-source-properties.cachePrepStmts=true
spring.datasource.hikari.data-source-properties.prepStmtCacheSize=500
spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=1024

Check out this article for a detailed explanation of each of these settings.

Spring Boot Application Properties – Hibernate Configuration

This is a list of settings that you can use if you are using Spring Data JPA, which uses Hibernate behind the scenes:

## Hibernate properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
spring.jpa.properties.hibernate.jdbc.batch_size=15
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true
spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true
spring.jpa.properties.hibernate.query.plan_cache_max_size=4096

logging.level.net.ttddyy.dsproxy.listener=debug

The spring.jpa.hibernate.ddl-auto setting is set to none to disable the hbm2ddl schema generation tool since we are using Flyway to manage the database schema automatically.

The spring.jpa.show-sql is set to false to avoid Hibernate printing the SQL statements to the console. As I explained in this article, it’s better to use datasource-proxy for this task. And that’s why we set the logging.level.net.ttddyy.dsproxy.listener property to debug in development mode. Of course, in the production profile, this property is set to info.

The spring.jpa.open-in-view property is set because we want to disable the dreadful Open-Session in View (OSIV) that’s enabled by default in Spring Boot. The OSIV anti-pattern can cause serious performance and scaling issues, so it’s better to disable it right from the very beginning of your project development.

The spring.jpa.properties.hibernate.dialect property uses the org.hibernate.dialect.MySQL57Dialect value because this application is using the Aurora MySQL 5.7 version.

The spring.jpa.properties.hibernate.jdbc.time_zone property sets the default timezone to UTC to make it easier to handle timestamps across multiple timezones. For more details about handling timezones with Spring Boot, check out this article.

To enable automatic JDBC batching, we are setting the following three properties:

spring.jpa.properties.hibernate.jdbc.batch_size=15
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

The first property sets the default batch size to 15 so that up to 15 sets of bind parameter values could be grouped and sent in a single database roundtrip. The next two settings are meant to increase the likelihood of batching when using cascading. Check out this article for more details about this topic.

The spring.jpa.properties.hibernate.connection.provider_disables_autocommit property is the one that instructs Hibernate that the connection pool disables the auto-commit flag when opening database connections. Check out this article for more details about this performance tuning setting.

The spring.jpa.properties.hibernate.query.in_clause_parameter_padding setting increases the likelihood of statement caching for IN queries as it reduces the number of possible SQL statements that could get generated while varying the IN clause parameter list. Check out this article for more details about this optimization.

The spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch property is set because we want Hibernate to throw an exception in case a pagination query uses a JOIN FETCH directive. Check out this article for more details about this safety option.

The spring.jpa.properties.hibernate.query.plan_cache_max_size property is set to increase the size of the Hibernate query plan cache. By using a larger cache size, we can reduce the number of JPQL and Criteria API query compilations, therefore increasing application performance. Check out this article for more details about this performance tuning option.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

Configuring the Spring Boot application properties file is a very important task because many performance tuning settings are not enabled by default.

If you’re developing a web application with Spring Boot, Hibernate, and MySQL, then the settings in this article are going to help you as much as they helped me while developing RevoGain.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.