Article

Spring tips, part 2

A selection of short and practical tips for Spring Boot applications.

Intermediate
Florian Beaufumé
Florian Beaufumé LinkedIn X GitHub
Published 13 Oct 2024 - 5 min read
Spring tips, part 2

Table of contents

Introduction

Spring is a very rich set of frameworks and libraries. This means that it may be easy to miss some useful features or best practices. In this series of articles, I will share some tips that can help you to improve your Spring applications.

These tips will be short and practical. They will cover various aspects of Spring application development, such as configuration, coding, performances, tests, etc.

This articles in this series:

  1. Spring tips, part 1: banner, graceful shutdown, non-Spring bean call, DB connection pool, test rollback
  2. Spring tips, part 2 (this article): log coloring, Maven dependencies, local call trap, self-injection, self-lookup

Log coloring

When running Spring applications, the logs are usually correctly colored by the console or the IDE. But in some cases they are not, for example when running Spring tests with IntelliJ:

Without coloring

In such situations, you can try to force the logs coloring with this configuration parameter in your local development profile or test profile:

spring.output.ansi.enabled=always

This works fine with IntelliJ when running Spring tests, the output is now nicer:

With coloring

Prefer Spring Boot dependencies

Spring Boot supports a lot of third party libraries. When you need to use a library, you should first check if Spring Boot already provides a dependency for it. If it does, then you should stick with the dependency version supported by Spring Boot for two major reasons: to prevent potential conflicts and to automatically use newer library versions when you upgrade Spring Boot.

As a reminder, the easiest way to enable Spring Boot dependency management, is to use the Spring Boot parent POM:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>

Then, to bring a Spring Boot supported dependency, simply add the dependency without an explicit version attribute. A Maven example for the H2 library:

<dependencies>
...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<!-- No version attribute here, it is provided by Spring Boot dependency management -->
<scope>runtime</scope>
</dependency>
...
</dependencies>

To check if a given library is supported, you can add the dependency with no version and see if Maven or your IDE complains.

Or you can check the list of Spring Boot dependencies version properties using your IDE:

  • From your pom.xml, navigate to the org.springframework.boot:spring-boot-starter-parent dependency (with Ctrl-Click, if supported)
  • Then, on top of the newly opened file, navigate to the org.springframework.boot:spring-boot-dependencies parent.
  • In the properties section of that file, you can see the supported libraries and look for your dependency.

Here is an extract for Spring Boot 3.3.3:

<properties>
...
<gson.version>2.10.1</gson.version>
<h2.version>2.2.224</h2.version><!-- H2 is supported -->
<hamcrest.version>2.2</hamcrest.version>
...
</properties>

We can see that H2 is supported by Spring Boot 3.3.3, and that the version is 2.2.224.

Override Spring Boot dependencies when needed

This is a follow-up of the previous tip.

Sometimes you need to use a different version of a library than the one provided by Spring Boot. This may happen for security reasons, when you need to quickly update to a fixed version, or to use new features. In these cases, instead of fully declaring the updated dependency in a dependency section, you can simply override the version property for that library, using the property name found in the previous tip. An example for Maven:

<properties>
...
<h2.version>2.2.230</h2.version><!-- Override the H2 version -->
...
</properties>

<dependencies>
...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
...
</dependencies>

Do not forget to regularly check if the Spring Boot version you are using has been updated, and remove your overrides as soon as possible.

This tip also works with Spring Boot dependencies that you do not explicitly provide. For example spring-boot-starter-web brings Jackson. If you need to override the Jackson version, first look for jackson in the Spring Boot dependencies file:

<properties>
...
<influxdb-java.version>2.24</influxdb-java.version>
<jackson-bom.version>2.17.2</jackson-bom.version>
<jakarta-activation.version>2.1.3</jakarta-activation.version>
...
</properties>

We can see that the Jackson version is 2.17.2 and that the property key is jackson-bom.version. So, to force a specific version you can simply override the property in your pom.xml, no need to explicitly declare the Jackson dependency:

<properties>
...
<jackson-bom.version>2.18.0</jackson-bom.version><!-- Override the Jackson version -->
...
</properties>

<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- No need to add the Jackson dependency, Spring Boot starter web provides it -->
...
</dependencies>

Beware of the local call trap

For this tip, we will start with a question: can you find something suspicious in this method from a Spring bean?

@Transactional
private void doSomething() {
// Some code
}

The problem is that @Transactional and private do not play nice together. The method is private so it can normally only be called from the same class. But the @Transactional annotation of Spring relies on a generated proxy that is not used by calls from the same class. As a consequence the @Transactional annotation is silently ignored!

This is not limited to private calls, but to all local calls in Spring beans:

@Service
public class MyService {

public void method1() {
// Some code

// This is a local call, so the Spring proxy is not used,
// meaning that @Transactional is ignored!
method2();
}

@Transactional
public void method2() {
// Some code
}
}

This problem happens with other Spring annotations such as @PreAuthorize, @Cacheable and even your own aspects.

There are different ways to fix this issue:

  • Move the target method to another Spring service class. But this may not be a good idea in terms of application design.
  • Move the annotation(s) to another method. But once again this may not be ideal.
  • Use per-annotation alternative, for example replace @Transactional by a TransactionTemplate.
  • Use self-injection, as described in the next tip.
  • Use self-lookup, as described in a next tip.

Consider self-injection

Self-injection is a trick to solve the local call trap described in the previous section.

Instead of calling the target method directly, call it through a field that is injected with the same bean:

@Service
public class MyService {

@Autowired
private MyService self; // Self-injection

public void method1() {
self.method2(); // Do not call method2() directly or through "this", but use "self"
}

@Transactional
public void method2() {
// Some code
}
}

The self attribute is a regular Spring injection, meaning that this attribute receives a proxy of the service instance. As a consequence self.method2() call will correctly use the annotations on the target method.

Note that self-injection is a case of circular dependency, and is forbidden by default by the Spring runtime. Circular dependencies are usually considered bad application design. To enable circular dependencies in our case:

spring.main.allow-circular-references=true

Consider self-lookup

Self-lookup is another trick to solve the local call trap described earlier. It can be useful when self-injection is a no-go (for example due to the circular dependency).

As described in Call a Spring bean from a non-Spring bean we can leverage the ApplicationContextAware interface to get the Spring ApplicationContext that dynamically provides Spring bean instances:

@Service
public class MyService implements ApplicationContextAware {

private ApplicationContext context;

public void setApplicationContext(ApplicationContext applicationContext) {
context = applicationContext;
}

public void method1() {
MyService self = context.getBean(MyService.class); // Self-lookup
self.method2(); // Do not call method2() directly or through "this", but use "self"
}

@Transactional
public void method2() {
// Some code
}
}

Conclusion

There are many more Spring tips, tricks and best practices out there, but I hope you enjoyed this selection. Stay tuned for the next part of this series!

© 2007-2025 Florian Beaufumé