Mistake 1: Focusing too much on the bottom

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:

As you might expect, the above code compiles to:

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.

Mistake # 2: Internal structure “leaks”

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.

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.

@RestControllerpublic 
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) { 
       returnnew 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")
@AllArgsConstructorpublic 
class TopTalentController {
    private final TopTalentService topTalentService;
    @RequestMapping("/get")    
public List<TopTalentData> getTopTalent() {        
return topTalentService.getTopTalent();   
 }
}
@AllArgsConstructor
@Servicepublic 
class TopTalentService {
    private final TopTalentRepository topTalentRepository;   
    private final TopTalentEntityConverter topTalentEntityConverter;
    public List<TopTalentData> getTopTalent() {     
       return topTalentRepository.findAll().stream()
               .map(topTalentEntityConverter::toResponse)
               .collect(Collectors.toList());   
     }
}
@Componentpublic 
class TopTalentEntityConverter { 
   public TopTalentData toResponse(TopTalentEntity topTalentEntity) {
        returnnew 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.

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:

@Valuepublic 
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).

Mistake 5: Multithreading incorrectly

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:

Avoiding 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.

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.

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.

Reuse of 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.

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 In addition, we must specify 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(); } @Componentpublic 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) {returns == 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

Mistake 7: Using an 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:

@SpringBootApplicationpublic 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.

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:

APPLICATION. The YAML file

# set default profile to 'dev'spring.profiles.active: dev
# production database details# public id: spring.datasource. Url: 'jdbc:mysql://localhost:3306/toptal'spring.datasource.username: Rootspring. The datasource. Password: 8.2 APPLICATION - DEV. Spring YAML files. The 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.

Error 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:

@Configurationpublic 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()));
        returntopTalentService; }}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.

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(SpringJUnit4Cla***unner.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.