The 10 Most Common Spring Framework Errors

Hello, Habr! I present to you the translation of the article “Top 10 Most Common Spring Framework Mistakes” by Toni Kukurin.



Spring is probably one of the most popular Java frameworks, as well as a powerful beast to tame. Although its basic concepts are fairly easy to understand, it takes time and effort to become a strong Spring developer.



In this article, we will look at some of the most common errors in Spring, especially those related to web applications and Spring Boot. As stated on the Spring Boot website , it imposes an idea of ​​how industrial applications should be built, so in this article we will try to demonstrate this idea and give an overview of some tips that will fit well into the standard Spring Boot web application development process.

If you are not very familiar with Spring Boot, but still would like to try some of the things mentioned, I created the GitHub repository that accompanies this article . If you feel that you’re lost anywhere in the article, I recommend cloning the repository to your local computer and playing with the code.



Common Mistake # 1: Get Down Too Low



We encounter this common mistake because the “not invented here” syndrome is quite common in the software development world. Symptoms include regularly rewriting fragments of frequently used code, and many developers seem to suffer from this.



Although understanding the insides of a particular library and its implementation for the most part is good and necessary (and can be an excellent learning process), constantly solving the same low-level implementation details is harmful to your development as a software engineer. There is a reason that abstractions and frameworks such as Spring exist that strictly separate you from repetitive handicrafts and allow you to focus on higher-level details - your domain objects and business logic.



Therefore, use abstractions - the next time you encounter a specific problem, first do a quick search and determine if the library that solves this problem is integrated into Spring. Currently, you are most likely to find an existing suitable solution. As an example of a useful library, in the examples of the rest of this article, I will use the annotations of the Lombok project . Lombok is used as a template code generator and the lazy developer inside you, hopefully should not have problems with the idea of ​​this library. As an example, look at what a “standard Java bean” with Lombok looks like:



@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }
      
      





As you can imagine, the above code compiles into:



 public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }
      
      





However, note that you will most likely have to install the plugin if you intend to use Lombok with your IDE. The plugin version for IntelliJ IDEA can be found here .



Common Mistake # 2: Leaking Internal Content



Revealing your internal structure is always a bad idea, because it creates inflexibility in the design of the service and, therefore, contributes to bad coding practice. The “leak” of internal content is manifested in the fact that the database structure is accessible from certain API endpoints. As an example, suppose the following POJO (”Plain Old Java Object") represents a table in your database:



 @Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }
      
      





Suppose there is an endpoint that needs to access TopTalentEntity data. No matter how tempting it is to return TopTalentEntity instances, a more flexible solution would be to create a new class to display TopTalentEntity data on the API endpoint:



 @AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
      
      





Thus, making changes to the database backend will not require any additional changes in the service layer. Think about what happens if you add a password field to TopTalentEntity to store user password hashes in the database - without a connector such as TopTalentData, if you forget to change the service, the frontend will accidentally display some very undesirable secret information!



Common Mistake # 3: Lack of Separation of Duties



As your application grows, organizing your code becomes an increasingly important issue. Ironically, most of the good principles of software development are starting to be violated everywhere - especially in cases where little attention is paid to designing the application architecture. One of the most common mistakes that developers face is mixing code responsibilities, and it's very easy to do!



What usually violates the principle of separation of duties is simply “adding” new functionality to existing classes. This, of course, is an excellent short-term solution (for starters, it requires less typing), but it will inevitably become a problem in the future, whether during testing, maintenance, or somewhere in between. Consider the following controller, which returns TopTalentData from its repository:



 @RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
      
      





At first, it is not noticeable that something is wrong with this piece of code. It provides a TopTalentData list that is retrieved from TopTalentEntity instances. However, if you look closely, we will see that in fact TopTalentController does a few things here. Namely: it maps requests for a specific endpoint, retrieves data from the repository and converts entities obtained from TopTalentRepository into another format. A “cleaner” solution would be to divide these responsibilities into their own classes. It might look something like this:



 @RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
      
      





An added benefit of this hierarchy is that it allows us to determine where the functionality is located by simply checking the class name. In addition, during testing, we can easily replace any of the classes with a mock implementation, if necessary.



Common Mistake # 4: Inconsistency and Poor Error Handling



The topic of consistency is not necessarily exclusive to Spring (or Java, for that matter), but it is still an important aspect to consider when working on Spring projects. While the style of writing code can be the subject of discussion (and usually it is a matter of agreement on the team or throughout the company), the presence of a common standard is of great help in performance. This is especially true for teams of several people. Consistency allows code to be transferred without requiring a lot of resources to maintain or provide detailed explanations regarding the responsibilities of the various classes.



Consider a Spring project with various configuration files, services, and controllers. Being semantically consistent in naming them, an easily searchable structure is created in which any new developer can control how to work with the code: for example, the suffix Config is added to configuration classes, Service suffix to services and Controller suffix to controllers.



Closely related to the topic of consistency, server-side error handling deserves special attention. If you've ever had to handle exception answers from a poorly written API, you probably know why it might be painful to parse exceptions, and it’s even more difficult to determine the reason why these exceptions originally occurred.



As an API developer, you ideally want to cover all user endpoints and translate them into a common error format. This usually means that you have a common error code and description, and not just an excuse in the form of: a) returning the message “500 Internal Server Error” or b) just resetting the stack trace to the user (which should be avoided at all costs, since it shows your insides in addition to the complexity of processing on the client side).

An example of a common error response format might be:



 @Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
      
      





Something similar is usually found in most popular APIs and usually works well, as it can be easily and systematically documented. You can translate exceptions into this format by providing the method with the @ExceptionHandler annotation (an example of the annotation is given in Common Mistake # 6).



Common Mistake # 5: Incorrect multithreading



Regardless of whether it is found in desktop or web applications, in Spring or not in Spring, multithreading can be a daunting task. Problems caused by parallel execution of programs are elusive and often extremely difficult to debug - in fact, due to the nature of the problem, once you understand that you are dealing with a parallel execution problem, you probably should completely abandon the debugger and start check your code manually until you find the cause of the error. Unfortunately, to solve such problems there is no template solution. Depending on the specific case, you will have to evaluate the situation and then attack the problem from an angle that you consider to be the best.



Ideally, of course, you would like to completely avoid multithreading bugs. Again, there is no single approach for this, but here are some practical considerations for debugging and preventing multithreading errors:



Avoid Global Status



First, always remember the “global state” problem. If you are creating a multi-threaded application, absolutely everything that can be changed globally should be carefully monitored and, if possible, completely removed. If there is a reason that the global variable should remain mutable, carefully use synchronization and monitor the performance of your application to confirm that it is not slowing down due to new waiting periods.



Avoid Mutability



This follows directly from functional programming and, in accordance with OOP, states that class variability and state change should be avoided. In short, the above means the presence of setters and private final fields in all classes of the model. Their values ​​change only during construction. Thus, you can be sure that there will be no problems in the race for resources and that accessing the properties of the object will always provide the correct values.



Log critical data



Evaluate where your application can cause problems, and pre-log all important data. If an error occurs, you will be grateful for information about what requests were received and you can better understand why your application is behaving badly. Again, it should be noted that logging increases file I / O, so you should not abuse it, as this can seriously affect the performance of your application.



Reuse existing implementations



Whenever you need to create your own threads (for example, to make asynchronous requests to various services), reuse existing secure implementations, rather than create your own solutions. For the most part, this would mean using ExecutorServices and CompletableFutures in the neat, functional style of Java 8 to create threads. Spring also allows asynchronous request processing through the DeferredResult class.



Common mistake # 6: not using annotation-based validation



Let's imagine that our TopTalent service, mentioned above, needs an endpoint to add new Super Talents. In addition, suppose that for some really good reason, each new name must be exactly 10 characters long. One way to do this could be as follows:



 @RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); }
      
      





However, the above (in addition to being poorly designed) is not really a “clean” solution. We check more than one type of validity (namely, that TopTalentData is not null, and that TopTalentData.name is not null, and that TopTalentData.name is 10 characters long) and also throws an exception if the data is invalid.



This can be done much more cleanly using the Hibernate validator with Spring. First, we rewrite the addTopTalent method to support validation:



 @RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }
      
      





In addition, we must indicate which property we want to check in the TopTalentData class:



 public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }
      
      





Spring will now intercept the request and verify it before calling the method - there is no need to use additional manual tests.



Another way we could achieve the same thing is to create our own annotations. Although custom annotations are usually used only when your needs exceed the Hibernate built-in set of constants , for this example, let's imagine that Length annotations do not exist. You must create a validator that checks the length of a string by creating two additional classes, one for checking and one for annotating properties:



 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } }
      
      





Note that in these cases, best practices for separation of duties require that you mark a property as valid if it is null (s == null in the isValid method), and then use the NotNull annotation if this is an additional requirement for the property:



 public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }
      
      





Common Mistake # 7: Using (Still) XML Configuration



Although XML was needed for previous versions of Spring, currently most of the configuration can be done exclusively with Java code / annotations. XML configurations simply represent an additional and unnecessary boilerplate.

This article (and its accompanying GitHub repository) uses annotations to configure Spring and Spring knows which beans it should connect because the root package was annotated using the @SpringBootApplication composite annotation, for example:



 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
      
      





This composite annotation (you can learn more about it in the Spring documentation) just gives Spring a hint about which packages should be scanned to extract the beans. In our particular case, this means that the following classes will be used to connect the beans, starting with the top-level package (co.kukurin):





If we had any additional classes annotated with @Configuration, they would also be checked for Java configuration.



Common mistake number 8: forget about profiles



The problem that is often encountered when designing servers is the difference between the different types of configurations, usually industrial and development configurations. Instead of manually changing the various configuration parameters each time you switch from testing to application deployment, a more efficient way would be to use profiles.



Consider the case when you use the in-memory database for local development and the MySQL database in PROM. In essence, this will mean that you will use different URLs and (hopefully) different credentials to access each of them. Let's see how this can be done with two different configuration files:



FILE APPLICATION.YAML



 # set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:
      
      





FILE APPLICATION-DEV.YAML



 spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2
      
      





Apparently, you don’t want to accidentally perform any actions on your industrial database while you mess with the code, so it makes sense to set the default profile in dev. Then on the server, you can manually override the configuration profile by specifying the -Dspring.profiles.active = prod parameter for the JVM. In addition, you can also set the OS environment variable to the desired default profile.



Common Mistake # 9: Inability to Accept Dependency Injection



Proper use of dependency injection in Spring means that it allows you to bind all your objects together by scanning all the required configuration classes; this is useful for decoupling relationships, and also makes testing much easier. Instead of hard linking classes by doing something like this:



 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }
      
      







We let Spring do the binding for us:



 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }
      
      





Google talk’s Misko Hevery explains in detail the “reasons” for dependency injection, so let's instead see how this is used in practice. In the division of responsibilities (Common Mistakes # 3), we created service and controller classes. Suppose we want to test a controller under the assumption that TopTalentService is behaving correctly. We can insert a mock object instead of the actual service implementation, providing a separate configuration class:



 @Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel") .map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } }
      
      





Then we can embed the mock object by telling Spring to use SampleUnitTestConfig as the configuration provider:



 @ContextConfiguration(classes = { SampleUnitTestConfig.class })
      
      





Then this will allow us to use the context configuration for embedding the custom bean in the unit test.



Common mistake # 10: lack of testing or incorrect testing



Despite the fact that the idea of ​​unit testing has been with us for a long time, many developers seem to “forget” to do this (especially if this is not necessary), or simply leave it for later. Obviously, this is undesirable, since tests should not only verify the correctness of your code, but also serve as documentation on how the application should behave in different situations.



When testing web services, you rarely do exceptionally “clean” unit tests, since interaction through HTTP usually requires calling DispatcherServlet Spring and looking at what happens when the actual HttpServletRequest is received (which makes it an integration test, with using validation, serialization, etc.). REST Assured - Java DSL for easily testing REST services on top of MockMVC has proven to be a very elegant solution. Consider the following code fragment with dependency injection:



 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } }
      
      





SampleUnitTestConfig enables the TopTalentService mock implementation in the TopTalentController, while all other classes are connected using the standard configuration obtained by scanning packages that have roots in the package of the Application class. RestAssuredMockMvc is simply used to create a lightweight environment and send a GET request to the / toptal / get endpoint.



Become a Spring Master



Spring is a powerful framework that is easy to get started with, but which takes some dedication and time to achieve full mastery. If you spend time getting to know the framework, it will certainly increase your productivity in the long run and ultimately help you write cleaner code and become a better developer.



If you're looking for additional resources, Spring In Action is a good practice book covering many core Spring topics.



TAGS

Java SpringFramework



Comments



Timothy Schimandle

At # 2, I think that returning a domain object is preferred in most cases. Your example custom object is one of several classes that have fields that we want to hide. But the vast majority of the objects I've worked with do not have such a restriction, and adding the dto class is just unnecessary code.

All in all a good article. Good job.



SPIRITED to Timothy Schimandle

I completely agree. It seems like an unnecessary extra layer of code has been added, I think that @JsonIgnore will help to ignore fields (albeit with flaws in the default repository detection strategies), but overall this is a great blog post. Proud to stumble ...



Arokiadoss Asirvatham

Dude, Another common beginner mistake is: 1) Cyclic Dependency, and 2) non-compliance with basic Singleton Class declaration doctrines, such as using an instance variable in beans with singleton scope.



Hlodowig

Regarding number 8, I believe that the approaches to the profiles are very unsatisfactory. Let's see:





I tried to find a way to use environment variables in my config files instead of “hard coding” the values, but so far I have not succeeded, I think I need to do more research.



Great article Tony, keep up the good work!



Translation completed: tele.gg/middle_java



All Articles