Hello. Today we are sharing the first part of the article, the translation of which was prepared especially for students of the course "Developer on the Spring Framework" . Let's get started!
Spring is perhaps one of the most popular Java development platforms. This is a powerful, but rather difficult to learn tool. Its basic concepts are fairly easy to understand and learn, but it takes time and effort to become an experienced Spring developer.
In this article, we will look at some of the most common mistakes made when working in Spring and related, in particular, to the development of web applications and the use of the Spring Boot platform. As noted on
the Spring Boot website , Spring Boot takes a
standardized approach to creating ready-to-use applications, and this article will follow this approach. It will give a number of recommendations that can be effectively used in the development of standard web applications based on Spring Boot.
In case you are not very familiar with the Spring Boot platform, but want to experiment with the examples in this article, I created a
GitHub repository with additional materials for this article . If at some point you are a little confused reading this article, I would advise you to create a clone of this repository and experiment with the code on your computer.
Common mistake number 1. Switching to too low-level programming
We easily succumb to this common mistake, since the
“syndrome of rejection of someone else’s development” is quite typical for the programming environment. One of its symptoms is the constant rewriting of pieces of commonly used code, and this is a symptom seen by many programmers.
Studying the internal structure and implementation features of a particular library is often a useful, necessary and interesting process, but if you constantly write the same type of code while doing low-level implementation of functions, this may turn out to be harmful for you as a software developer. That is why abstractions and platforms such as Spring exist and are used - they save you from repetitive manual work and allow you to concentrate on objects of your subject area and program code logic at a higher level.
Therefore, abstractions should not be avoided. The next time you are faced with the need to solve a specific problem, first do a quick search and find out if the library that solves this problem is already built into Spring — you will likely find a suitable turnkey solution. One such useful library is
Project Lombok , the annotations from which I will use in the examples in this article. Lombok is used as a template code generator, so the lazy developer who lives in each of us will be very happy to get acquainted with this library. See, for example, how a
standard JavaBean component looks in Lombok:
@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }
As you may have already understood, the above code is converted to the following form:
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() { } }
Note that you will most likely have to install the appropriate plug-in if you want to use Lombok in your integrated development environment. The IntelliJ IDEA version of this plug-in can be found
here .
Common mistake number 2. "Leak" of internal structures
Giving access to internal structures has never been a good idea, since it impairs the flexibility of the service model and, as a result, contributes to the formation of a bad programming style. The “leak” of internal structures is manifested in the fact that the database structure becomes accessible from certain API endpoints. For example, suppose the following “good old Java object” (POJO) 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; } }
Assume that there is an endpoint that needs to access the data of a
TopTalentEntity
object. To return instances of
TopTalentEntity
seems like a tempting idea, but a more flexible solution would be to create a new class that represents TopTalentEntity data for the API endpoint:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
Thus, making changes to the internal components of the database will not require additional changes at the service level. Let's see what happens if the password field is added to the
TopTalentEntity
class to store user password hashes in the database: if there is no connector, such as
TopTalentData
, and the developer forgets to change the interface part of the service, this can lead to very unwanted disclosure of secret information!
Common mistake number 3. Combining functions that would be better to distribute in the code
Organizing application code as it grows becomes an increasingly important task. Oddly enough, most of the principles of effective programming cease to work when a certain scale of development is reached, especially if the application architecture has not been well thought out. And one of the most frequently made mistakes is the combination of functions in the code that are more reasonable to implement separately.
The reason for the violation of the principle of
separation of responsibilities is usually the addition of new functionality to existing classes. Perhaps this is a good momentary solution (in particular, it requires writing a smaller amount of code), but in the future it will inevitably become a problem, including at the stages of testing and maintaining the code and between them. Consider the following controller that returns
TopTalentData
from the 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 glance, it seems that there are no obvious errors in this code fragment. It provides a list of
TopTalentData
objects, which is taken from instances of the
TopTalentEntity
class. But if you look at the code more closely, we will see that in reality
TopTalentController
performs several actions here, namely, it correlates requests with a specific endpoint, retrieves data from the repository, and converts objects received from
TopTalentRepository
into a different format. A cleaner solution should separate these functions into separate classes. For example, it might look 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 additional advantage of this hierarchy is that it allows us to understand where a function is located simply by looking at the class name. Subsequently, during testing, we can easily replace the code of any of these classes with surrogate code, if the need arises.
Common mistake number 4. Uniform code and poor error handling
The topic of code uniformity is not unique to Spring (or to Java in general), but, nevertheless, is an important aspect that must be considered when working with projects in Spring. Choosing a specific programming style may be a topic of discussion (and is usually consistent within the development team or throughout the entire company), but in any case, the presence of a common standard for writing code helps increase work efficiency. This is especially true if several people work on the code. Uniform code can be passed from developer to developer without spending a lot of effort on maintaining it or on lengthy explanations about why these or those classes are needed.
Imagine that there is a Spring project in which there are various configuration files, services, and controllers. Keeping semantic uniformity in their naming, we create a structure by which you can easily search and in which any developer can easily understand the code. For example, you can add the Config suffix to the names of configuration classes, the Service suffix to the service names, and the Controller suffix to the controller names.
Server-side error handling is closely related to code uniformity and deserves special attention. If you've ever handled exceptions coming from a poorly written API, you probably know how hard it is to correctly understand the meaning of these exceptions, and even harder to determine why they actually occurred.
As an API developer, ideally, you would like to cover all the endpoints the user is working with and lead them to use a single error message format. Usually this means that you need to use standard error codes and their description and abandon dubious decisions such as giving the user a “500 Internal Server Error” message or stack trace results (the latter option, by the way, should be avoided by all means, since you are revealing internal data, and besides, such results are difficult to process on the client side).
Here, for example, what the general format of the error message might look like:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
A format similar to this one is often found in the most popular APIs and, as a rule, works fine because it can be easily and systematically documented. You can convert an exception to this format by adding the
@ExceptionHandler
annotation to the method (for an example of the annotation, see the "Common Mistake # 6" section).
Common mistake number 5. Incorrect work with multithreading
Implementing multithreading can be a difficult task, regardless of whether it is used in desktop applications or in web applications, in Spring projects or projects for other platforms. Problems caused by the parallel execution of programs are difficult to track, and dealing with them with a debugger is often very difficult. If you understand that you are dealing with a parallel execution error, then most likely you will have to abandon the debugger and examine the code manually until the root cause of the error is found. Unfortunately, there is no standard way to solve such problems. In each case, it is necessary to assess the situation and “attack” the problem with the method that seems best in the given conditions.
Ideally, of course, you would like to completely avoid the errors associated with multithreading. Although there is no universal recipe here, I can still offer some practical recommendations.
Avoid using global states
First, always remember the problem of “global states”. If you are developing a multi-threaded application, you need to carefully monitor absolutely all globally modifiable variables, and if possible, get rid of them altogether. If you still have a reason why the global variable should be modifiable, properly implement
synchronization and monitor the performance of your application - you should make sure that it does not slow down due to added waiting periods.
Avoid mutable objects
This idea comes directly from the principles of
functional programming and, being adapted to the principles of OOP, states that mutable classes and mutable states should be avoided. In short, this means that you should refrain from setting methods (setters) and have private fields with the final modifier in all model classes. The only time their values change is when the object is created. In this case, you can be sure that there are no problems associated with the competition for resources, and when accessing the properties of the object, the correct values will always be obtained.
Log important data
Evaluate where problems may occur in the application and set up a logging of all important data in advance. If an error occurs, you will be happy to have information about what requests have been received, and you will be able to better understand the reasons for the malfunctioning of your application. However, keep in mind that logging involves additional file I / O and should not be abused, as this can adversely affect application performance.
Use ready-made thread implementations
When you need to spawn your threads (for example, to create asynchronous requests to various services), use ready-made safe thread implementations instead of creating your own. In most cases, you can use the
ExecutorService abstractions and the spectacular functional abstractions
CompletableFuture for Java 8 to create threads. The Spring platform also allows you to handle asynchronous requests using the
DeferredResult class.
The end of the first part.