Blog post

Spring framework pitfalls

Jonathan Vila

Jonathan Vila

Developer Advocate - Java

5 min read

  • Clean Code
  • Java
  • Spring

Spring is a famous framework, used in more than 60% of applications nowadays, which makes it easy to create stand-alone, production-grade applications.

It introduces tons of new classes, interfaces, and APIs in order to help in the development, that a developer needs to learn and use. In the process of coding with Spring Boot, new bugs, misconfigurations, and security issues can be introduced that will impact the code quality of our applications.

With several rules covering Spring, Sonar can help detect those issues giving consistency across the changes introduced as part of the lifecycle of our applications. Let’s see some of the most important Spring Boot issues detected by Sonar analyzers.

Spring framework pitfalls

In this article, we are going to focus on 3 important points when we code using Spring framework: transactional operations, persistent entities, and bean definitions.

Transactional Operations

All the database operations need to be committed, to become available to other connections. So, for every operation done to the database, the process is to open a transaction, change the data, commit the transaction, or if anything fails rollback the transaction.

Spring helps by allowing us to annotate methods with @Transactional which creates proxies behind the scenes to generate code running as part of our code, in order to handle those transactions for us.

But you can have chains of method calls, where an operation consists of several changes to the database, and those changes are split into several methods for clarity. There’s when Transaction propagation takes place.

Usually, we will have the entry point method with the @Transactional annotation, starting the transaction, and the rest of the methods in the call chain will not specify the annotation, allowing the first method to do the whole commit. That’s the REQUIRED default propagation method. If there’s no transaction running it will create one.

But, you can say, sometimes reality is more complex and I have methods that are part of different operations, and sometimes my method can be the only operation to be done in a transaction. 

In these chains of calls, it’s a requirement to keep compatible transaction propagation

However, it is important to keep in mind that Spring will not consider the transaction specifications on self-invocation. That means when you call a method from another method in the same class, Spring will use the “this” approach to refer to the receiver method, so the code Spring generates as a proxy to handle transactions will not be executed.

public class ABHandler  {
  public void saveAB(A a, B b) {
      saveA(a);
      saveB(b);
  }

  @Transactional
  public void saveA(A a) {
       dao.saveA(a);
  }

  @Transactional
  public void saveB(B b) {
       dao.saveB(b);
  }
}

public class GlobalHandler {
  public void save(A a, B b) {
    ABHandler abHandler = new ABHandler();
    abHandler.saveAB(a, b); // Non compliant
    abHandler.saveA(a);
}

In this code, on the call to saveAB from the object GlobalHandler is reaching a method without transactionality enabled, and then it self-invokes method saveA with a @Transactional specified. As we learned before, this call from saveAB to saveA is not going to use the proxies Spring has generated, therefore no transaction will be created. 

In order to avoid this, we should specify the transactionality in the method saveAB to ensure that the transaction is created and managed.

public class ABHandler  {
  @Transactional
  public void saveAB(A a, B b) {
      saveA(a);
      saveB(b);
  }

  @Transactional
  public void saveA(A a) {
       dao.saveA(a);
  }

  @Transactional
  public void saveB(B b) {
       dao.saveB(b);
  }
}

public class GlobalHandler {
  public void save(A a, B b) {
    ABHandler abHandler = new ABHandler();
    abHandler.saveAB(a, b); 
    abHandler.saveA(a);
}

Sonar has a rule that detects these issues and can save you from incompatible transaction propagation.

Persistent entities

One of the benefits of using frameworks like Spring Boot is the ease of interacting with the persistence layer. 

In order to use typed objects and properties, Java provides the @Entity annotation to represent a relational table and Spring provides the @Document annotation to represent MongoDB and ElasticSearch documents. In all these cases Spring will use the information in the element and create a bridge between the object domain and the database one.

@Entity
public class Wish {
  Long productId;
  Long quantity;
  User user;
  Client client;
}

It’s important to understand that these objects represent data objects with a direct conversion to the stored element in the database. Therefore all the fields carried by the object will be saved in the database.

Spring also provides methods to generate REST API services that will be executed when the user makes an HTTP request to that server. These methods also allow the use of entities/documents as arguments that Spring will map from the request payload.

@Controller
public class PurchaseOrderController {
  @RequestMapping(path = "/saveForLater", method = RequestMethod.POST)

  public String saveForLater(Wish wish) { // Noncompliant
    session.save(wish);
  }
}

In this case, we have a situation where an attacker can send requests with information in unexpected fields that, in case of using directly those entities, will reach our database. In our example, an attacker could send information about an impersonated User, for example.

This is why it is always encouraged to use DTO objects that will be used to translate the information coming from the user into the database Entity/Document considering only the required information and even doing a sanitizing process on the translation. 

public class WishDTO {
  Long productId;
  Long quantity;
  Long clientId;
}

@Controller
public class PurchaseOrderController {
  @RequestMapping(path = "/saveForLater", method = RequestMethod.POST)

  public String saveForLater(WishDTO wish) {
    Wish persistentWish = new Wish();
    persistentWish.productId = wish.productId;
    persistentWish.quantity = wish.quantity;
    persistentWish.client = getClientById(wish.clientId);
    persistentWish.user = getUserFromSession();
    session.save(persistentWish);
  }
}

Sonar's rule prevents you from using a persistent entity as an argument of @RequestMapping methods.

Bean definitions

We can agree that one of the main powers of using Spring is the Dependency Injection allowing the user to define beans that will be injected into other objects and their lifespan. With this feature classes only need to know what their dependencies are but not about how and when they have to be instantiated and deleted.

Spring comes also with a great bean discovery mechanism, that will scan our source code packages searching for bean definitions, and the Spring context will instantiate them according to the configuration (lazy, eager).

But, as you can imagine, with great power comes great responsibility. The scanning mechanism can impact the performance of our application and even produce runtime errors that are hard to spot during the coding phase.

If we define the start scan point in the default package, that is without specifying the package in the class that is used as @SpringBootApplication or @ComponentScan or setting explicitly the default package to the ComponentScan annotation, Spring will scan the entire classpath leading to a long start-up time and very likely runtime errors as Spring classes will be scanned too.

import org.springframework.boot.SpringApplication;

@SpringBootApplication // Noncompliant default package

public class RootBootApp {
}

@ComponentScan("")
public class Application {

}

You should always have a package in your application as the starting point of the bean scan for Spring.

package com.mycompany.myproject;

import org.springframework.boot.SpringApplication;

@SpringBootApplication
public class RootBootApp {

}

@ComponentScan("com.mycompay.myproject")
public class Application {

}

The Sonar rule ensures that you don't use @SpringBootApplication and @ComponentScan on the default project.


On the consumer side of those beans, Spring offers, with its Dependency Injection framework, a powerful injection mechanism that makes very easy-to-use instances of beans, with specific life scopes, without having to worry about when and where those beans have been created or deleted.

These beans can be easily injected into your classes using the @Autowired annotation. But in the case where you have dependency chains between beans, the injection could be done way before it is needed impacting the performance of your application.

@Configuration
public class ​FooConfiguration {

  @Autowired
  private ​DataSource dataSource​;  // Noncompliant, early injection

  @Bean
  public ​MyService myService() {
    return new ​MyService(this​.dataSource​);
  }

}

In order to avoid this the injection should be requested as late as possible, only when it’s needed. In order to accomplish this, parameter injection should be used instead of @Autowired, telling Spring that the bean needs to be created just before the creation of the dependent bean. This way you won’t have beans running before they are needed.

@Configuration
public class ​FooConfiguration {

 @Bean
  public ​MyService myService(DataSource dataSource) {
    return new ​MyService(dataSource);
  }

}

This Sonar rule ensures that beans are only instantiated when they are needed by using parameter injection instead of @Autowired for dependent beans.

Conclusions

Spring offers several features in order to help development, but all this power comes with complex configurations in order to cover all the different usages. 

It’s important to understand the Spring limitations and the pitfalls in order to get the best value out of it, but it’s not always easy to spot the code that can cause a huge impact on performance and stability.

Sonar tools offer several rules that will cover and spot these issues, warning you while coding using your preferred IDE, or checking the code base during the CI/CD pipeline and making the Quality Gate fail, preventing that code from being merged in your repository. 


Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles.