Spring is arguably one of the most popular Java frameworks and a powerful beast to tame. While the basic concepts are fairly easy to grasp, it still takes a lot of time and effort to become a strong Spring developer.

In this article, we’ll cover some common errors in Spring, especially for Web applications and Spring Boot. As stated on the Spring Boot website, Spring Boot has a fairly rigid view of how production-ready applications should be built. This article will attempt to emulate this view and provide an overview of some of the techniques. These techniques will fit well into standard Spring Boot Web application development.

If you’re not familiar with Spring Boot but still want to try out some of the following, I’ve also created a GitHub repository for this article. If you get confused reading this, I recommend cloning the code and using it on your local computer.

1. Common Mistake # 1: Focusing too much on the bottom line

We are addressing this common error because the “not invented by me” syndrome is common in software development. Symptoms include frequent rewriting of common code, which is common to many developers.

While it is largely good and necessary (and can be a good learning process) to understand the internals of a particular library and its implementation, dealing with the same underlying implementation details over and over again can be detrimental to one’s development career as a software engineer. Abstract frameworks like Spring exist for a reason, freeing you from repetitive manual labor and allowing you to focus on higher-level details — domain objects and business logic.

Therefore, embrace abstraction. The next time you’re faced with a particular problem, start by doing a quick search to determine whether the library that addresses the problem has been integrated into Spring; Now, you may find a suitable off-the-shelf solution. For example, a useful library, I’ll use Project Lombok annotations in my examples throughout the rest of this article. Lombok is used as a boilerplate code generator in the hope that lazy developers won’t have problems familiarizing themselves with the library. As an example, see what Lombok’s “standard Java Beans” look like:

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

As you might expect, the above code compiles to:

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() {
    }
}Copy the code

Note, however, that if you plan to use Lombok in your IDE, you will most likely need to install a plug-in, which can be found here for the Intellij IDEA version.

2. Common Mistake 2: Internal structure “leak”

Exposing your internal structure is never a good idea because it creates inflexibility in service design, which promotes bad coding practices. The internal mechanism of “leakage” manifests itself in making the database structure accessible from some API endpoint. For example, the following POJO (” Plain Old Java Object “) class represents a table in a database:

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

Suppose you have an endpoint that needs to access TopTalentEntity data. It may be tempting to return a TopTalentEntity instance, but a more flexible solution is to create a new class to represent TopTalentEntity data on an API endpoint.

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

In this way, changes to the database back end will not require any additional changes in the service layer. Consider adding a “password” field to TopTalentEntity to store the Hash value of a user’s password in the database — without a connector like TopTalentData, forgetting to change the service front end could accidentally expose some unwanted secret information.

3. Common mistake # 3: Lack of separation of concerns

As programs grow in size, code organization becomes an increasingly important issue. Ironically, most good software engineering principles begin to break down in scale — especially without much consideration for program architecture design. One of the most common mistakes developers make is confusing code concerns, and it’s easy to do!

Often, what breaks the separation of concerns is simply “pouring” new functionality into existing classes. Of course, this is a great short-term solution (it requires less input for beginners), but it will inevitably become a problem in the future, whether during testing, maintenance, or something in between. Consider the following controller, which returns TopTalentData from the database.

@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()); }}Copy the code

At first, there seems to be nothing particularly wrong with this code; It provides a List of TopTalentData retrieved from the TopTalentEntity instance. However, on closer inspection, we can see that the TopTalentController actually does something here; That is, it maps requests to specific endpoints, retrieves data from the database, and converts entities received from TopTalentRepository into another format. A “cleaner” solution is to separate these concerns 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()); }}Copy the code

Another advantage of this hierarchy is that it allows us to determine where functionality resides by examining the class name. In addition, we could easily replace any classes with mock implementations if needed during testing.

4. Common error 4: Lack of exception handling or improper handling

The theme of consistency is not unique to Spring (or Java), but it is still an important aspect to consider when working on Spring projects. While coding styles can be controversial (often agreed upon internally by teams or throughout the company), having a common standard can ultimately greatly increase productivity. This is especially true for multi-person teams; Consistency allows communication to take place without having to spend a lot of resources on hand-holding or providing lengthy explanations of different types of responsibilities.

Consider a Spring project with various configuration files, services, and controllers. Semantically consistent naming creates an easily searchable structure that allows any new developer to manage the code in their own way; For example, add the Config suffix to the configuration class, the Service layer ends with Service, and the Controller ends with Controller.

Closely related to the topic of consistency, server-side error handling deserves special emphasis. If you’ve ever had to deal with an exception response from a poorly written API, you probably know why — correctly parsing exceptions can be a pain, and determining why they happened in the first place is even more painful.

As an API developer, ideally you want to override all user-facing endpoints and convert them to common error formats. This usually means having a common Error code and description, rather than avoiding solving the problem: a) returning a “500 Internal Server Error” message. B) Return the exception stack information directly to the user. (In fact, this should be avoided at all costs, because in addition to being difficult for the client to handle, it also exposes your internal information).

For example, a common error response might look like this:

@Value
public class ErrorResponse {

    private Integer errorCode;
    private String errorMessage;

}Copy the code

Similar things happen in most popular apis, and because they can be easily and systematically documented, they tend to work well. Converting an exception to this format can be done by providing the @ExceptionHandler annotation to the method (see Chapter 6 for an example of the annotation).

5. Common mistake # 5: Improper multithreading

Whether it’s a desktop application or a Web application, whether it’s Spring or No Spring, multi-threading is hard to crack. Caused by parallel execution problem is creepy and elusive, and often difficult to debug – in fact, due to the nature of the problem, once you realize that you are dealing with a parallel execution problem, you may have to completely give up the debugger, and “manual” check code, until you find the root reason for the error. Unfortunately, there is no one-size-fits-all solution to such problems; Evaluate the situation according to the specific scenario, and then approach the problem from what you think is the best Angle.

Of course, ideally, you also want to avoid multithreading errors altogether. Again, there is no such thing as a one-size-fits-all approach, but there are some practical considerations for debugging and preventing multithreading errors:

5.1. Avoid global states

First, keep in mind the “global state” problem. If you’re building a multithreaded application, keep an eye out for any global changes and, if possible, remove them altogether. If there is a reason why a global variable must remain modifiable, carefully use synchronization and track program performance to determine that there is no system performance degradation due to the newly introduced wait time.

5.2. Avoid variability

This comes directly from functional programming and applies to OOP, where declarations should avoid class and state changes. In short, this means abandoning setter methods and having private final fields on all model classes. The only time their values change is during construction. This way, you can be sure that there will be no contention problems and that the access object properties will always provide the correct values.

5.3. Record key data

Assess where exceptions might occur in your program and pre-record all key data. If an error occurs, you will be happy to have information about what requests were received and to better understand why your application is experiencing an error. Again, logging introduces additional file I/O that can seriously affect application performance, so don’t abuse logging.

5.4. Reuse existing implementations

Whenever you need to create your own threads (for example, making asynchronous requests to different services), reuse existing security implementations instead of creating your own solutions. A lot of this means creating threads using ExecutorServices and Java 8’s neat functional CompletableFutures. Spring also allows asynchronous request processing through the DeferredResult class.

Common mistake # 6: Not using annotation-based validation

Suppose our previous TopTalent service required an endpoint to add a new TopTalent. Also, suppose that for some reason each new noun needs to be 10 characters long. One way to do this might 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);
}Copy the code

However, the above approach (aside from being poorly constructed) is not really a “clean” solution. We are checking the validity of more than one type (i.e., TopTalentData must not be empty, toptalentData.name must not be empty, and toptalentData.name is 10 characters long) and throwing exceptions if data is invalid.

By integrating Hibernate Validator with Spring, data validation can be done more cleanly. Let’s first refactor 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
}Copy the code

In addition, we must also indicate what attributes we want to validate in the TopTalentData class:

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

Spring now intercepts a method’s request and validates its parameters before invoking it — no additional manual testing required.

Another way to achieve the same functionality is to create our own annotations. While you usually only use custom annotations when you need to go beyond Hibernate’s built-in constraint set, in this case, we’ll assume @Length doesn’t exist. You can create two additional classes to validate string lengths, one for validation and one for annotating attributes:

@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;
    }
}Copy the code

Note that best practices for separation of concerns in these cases require that the attribute be marked as valid when it is NULL (s == NULL in the isValid method), and use the @notnull annotation if this is an additional requirement of the attribute.

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

7. Common mistake # 7: using XML-based configuration

Although previous versions of Spring required XML, much of the configuration is now done through Java code or annotations; The XML configuration is just additional unnecessary boilerplate code.

This article (and the accompanying GitHub repository) uses annotations to configure Spring, which knows which beans to connect to because the top-level package directory to be scanned is declared in the @SpringBootApplication composite annotation, as follows:

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

Composite annotations (for more information, see the Spring documentation) simply indicate to Spring which packages should be scanned to retrieve beans. In our case, this means that the top-level package (Co. Kukurin) will be used to retrieve:

  • @Component (TopTalentConverter.MyAnnotationValidator)
  • @RestController (TopTalentController)
  • @Repository (TopTalentRepository)
  • @Service (TopTalentService) class

If we have any additional @Configuration annotation classes, they will also check the Java-based Configuration.

8. Common mistake 8: Ignoring profiles

A common problem encountered in server-side development is distinguishing between different configuration types, usually production and development. Instead of manually replacing various configuration items each time you switch from test to deploy the application, it is more efficient to use profiles.

Consider the case where you are using an in-memory database for local development and a MySQL database in production. Essentially, this means you need to use different urls and (hopefully) different credentials to access both. Let’s see how these two different profiles can be done:

8.1. APPLICATION. YAML files

# 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:Copy the code

8.2. APPLICATION – DEV. YAML files

spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2Copy the code

Assuming you don’t want to accidentally do anything to the production database while changing your code, it makes sense to set the default configuration file to dev. Then, on the server, you can manually overwrite the configuration file by providing the -dspring.profiles. active=prod parameter to the JVM. Alternatively, you can set the operating system’s environment variables to the desired default profile.

Common mistake # 9: Unable to accept dependency injection

Proper use of Spring’s dependency injection means allowing it to wire all objects together by scanning all necessary configuration classes; This is very useful for decoupling relationships and makes it easier to test instead of doing this through tight coupling between classes:

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

We let Spring do the wiring for us:

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

Misko Hevery’s Google Talk explains the “why” of dependency injection in depth, so let’s look at how it’s used in practice. In the separation of concerns (Common mistake #3) section, we created a service and controller class. Suppose we want to test the controller on the premise that TopTalentService behaves correctly. Instead of the actual service implementation, we can insert a mock object by 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; }}Copy the code

We can then inject mock objects by telling Spring to use SampleUnitTestConfig as its configuration class:

@ContextConfiguration(classes = { SampleUnitTestConfig.class })Copy the code

After that, we can inject the Bean into the unit test using the context configuration.

10. Common mistake # 10: Lack of testing, or improper testing

Although the concept of unit testing has been around for a long time, many developers seem to either “forget” to do it (especially if it’s not “required”) or simply add it as an afterthought. This is obviously not desirable, because tests should not only verify that the code is correct, but also serve as documentation of how the program should behave in different scenarios.

“Pure” unit testing is rarely done when testing Web services, because communicating over HTTP usually requires invoking Spring’s DispatcherServlet, And see what happens when you receive an actual HttpServletRequest (making it an “integration” test, handling validation, serialization, and so on). REST Assured, a Java DSL for simplifying testing REST services, on top of MockMVC, has proven to provide a very elegant solution. Consider the following code snippet 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")); }}Copy the code

The SampleUnitTestConfig class connects the mock implementation of TopTalentService to TopTalentController, while all the other classes are standard configurations inferred by scanning the subpackage directory of the package in which the application class resides. RestAssuredMockMvc is simply used to set up a lightweight environment and send a GET request to the/Toptal/GET endpoint.

11. Become a Spring master

Spring is a powerful framework that is easy to get started with, but requires some commitment and time to fully master. In the long run, taking the time to familiarize yourself with frameworks will definitely increase your productivity and ultimately help you write cleaner code and become a better developer.

For more resources, Spring In Action is an excellent hands-on book that covers many of Spring’s core topics.

Original: https://www.toptal.com/spring/top-10-most-common-spring-framework-mistakes

By Toni Kukurin

Translator: Wan Wan