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:
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:
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:
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:
pom.xml
, navigate to the org.springframework.boot:spring-boot-starter-parent
dependency
(with Ctrl-Click, if supported)org.springframework.boot:spring-boot-dependencies
parent.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.
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>
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:
@Transactional
by a TransactionTemplate
.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
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
}
}
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é