Don't make these 10 Mistakes of Spring!

Posted by smarty_pockets on Thu, 10 Mar 2022 02:43:05 +0100

1. Error 1: paying too much attention to the bottom

We are addressing this common mistake because the "not created by me" syndrome is common in the field of software development. Symptoms include frequent rewriting of common code, which many developers have.

Although it is good and necessary to understand the internal structure and implementation of a specific library to a great extent (it can also be a good learning process), as a software engineer, constantly dealing with the same underlying implementation details is harmful to one's development career.

Abstract frameworks like Spring exist for a reason. They liberate you from repetitive manual work and allow you to focus on higher-level details - domain objects and business logic.

Therefore, accept abstraction. The next time you face a specific problem, first conduct a quick search to determine whether the library to solve the problem has been integrated into Spring; Now, you may find a suitable off the shelf solution.

For example, a very useful library. In other parts of this article, I will use the Project Lombok annotation in the example. Lombok is used as a template code generator, hoping that lazy developers won't have problems getting familiar with the library. Take Lombok's "standard Java Bean" as an example:

As you would expect, the above code is compiled as:

However, please note that if you plan to use Lombok in the IDE, you may need to install a plug-in. You can find the plug-in of Intellij IDEA version here.

2. Error 2: internal structure "leaked"

Exposing your internal structure is never a good idea because it creates inflexibility in service design and promotes bad coding practices. The internal mechanism of "disclosure" is to make the database structure accessible from some API endpoints. For example, the following POJO ("Plain Old Java Object") class represents a table in the 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. It may be tempting to return a TopTalentEntity instance, but a more flexible solution is to create a new class to represent the TopTalentEntity data on the API endpoint.

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

In this way, changes to the database backend will not require any additional changes at the service layer. Consider adding a "password" field in "TopTalentEntity" to store the Hash value of user password in the database - if there is no connector such as "TopTalentData", forgetting to change the service front end will accidentally expose some unnecessary secret information.

3. Error 3: lack of separation of concerns

With the increase of program size, code organization has gradually become a more and more important problem. Ironically, most good software engineering principles begin to collapse on Scale -- especially without much consideration for program architecture design. One of the most common mistakes developers make is to confuse code concerns, which is easy to do!

Often, what breaks the separation of concerns is to simply "pour" new functionality into existing classes. Of course, this is a good short-term solution (it requires less input for beginners), but it will inevitably become a problem in the future, whether during testing, maintenance or in between. Consider the following controller, which will return 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());
    }

}

 


At first, the code didn't seem to have any special problems; It provides a List of TopTalentData retrieved from the TopTalentEntity instance.

However, if we look closely, we can see that TopTalentController actually does something here; That is, it maps the request to a specific endpoint, retrieves data from the database, and converts the entities received from the TopTalentRepository to another format. A "cleaner" solution is to separate these concerns into their own classes. It may 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());
    }
}

 

Another advantage of this hierarchy is that it allows us to determine where the functionality resides by checking the class name. In addition, during testing, we can easily replace any class with a mock implementation if necessary.

4. 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 dealing with Spring projects. While coding styles can be controversial (usually agreed within the team or the entire company), having a common standard can ultimately greatly improve productivity. This is especially true for multi person teams; Consistency allows communication to occur without spending a lot of resources on hand-in-hand handover and providing lengthy explanations for different types of responsibilities.

Consider a Spring project that contains various configuration files, services, and controllers. Maintain semantic consistency in naming, create an easy to search structure, and any new developer can manage the code in his 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 - parsing exceptions correctly can be painful, and determining why they first occurred is even more painful.

As an API developer, ideally you want to cover all user oriented endpoints and convert them to common error formats. This means that "internal" and "error" usually return an error message. b) Directly return the stack information of the exception to the user. (in fact, these should be avoided at all costs, because in addition to the difficulty of the client, it also exposes your internal information).

For example, the common error response format may be as long as:

@Value
public class ErrorResponse {

    private Integer errorCode;
    private String errorMessage;

}
Similar things are popular in most countries API It is also often encountered in. Because it can be recorded easily and systematically, the effect is often very good. Converting exceptions to this format can be done by providing @ExceptionHandler Note to complete (note cases can be seen in Chapter 6).

5. Error 5: improper multithreading

Whether it's a desktop application or a Web application, whether it's Spring or No Spring, multithreading is hard to crack. Problems caused by parallel execution programs are 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 abandon the debugger and "manually" check the code until you find the root cause of the error.

Unfortunately, there is no one size fits all solution to such problems; Evaluate and solve the problem according to the specific situation.

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

5.1. Avoid global state

First, keep in mind the "global state" problem. If you are creating a multithreaded application, you should pay close attention to any possible global modifications and delete them if possible. If there is a reason why a global variable must remain modifiable, please use synchronization carefully and track the program performance to ensure that the system performance is not reduced due to the newly introduced waiting time.

5.2. Avoid variability

This comes directly from functional programming and applies to OOP. Declarations should avoid changes in classes and states. 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. In this way, you can make sure that there will be no contention and that the access object properties will always provide the correct values.

5.3. Record key data

Assess where your program may be abnormal and record all key data in advance. If an error occurs, you will be happy to receive information about which requests have been received and a better understanding of why your application has an error. It should be noted again that logging introduces additional file I/O, which may seriously affect the performance of applications. Therefore, please do not abuse logging.

5.4. Reuse existing implementations

Whenever you need to create your own thread (for example, make asynchronous requests to different services), reuse the existing security implementation instead of creating your own solution. This largely means using ExecutorServices and Java 8's concise functional completable futures to create threads. Spring also allows asynchronous request processing through the DeferredResult class.

6. Error 6: do not use annotation based validation

Suppose our previous TopTalent service needs an endpoint to add a new TopTalent. In addition, 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);
}

 


However, the above method (except for poor construction) is not a really "clean" solution. We are checking the validity of more than one type (i.e. TopTalentData cannot be empty, TopTalentData.name cannot be empty, and TopTalentData.name is 10 characters long) and throwing an exception when the data is invalid.

By integrating Hibernate validator in Spring, data verification can be carried out 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
}
// In addition, we must also point out that we want to TopTalentData What properties are verified in the class:
public class TopTalentData {
    @Length(min = 10, max = 10)
    @NotNull
    private String name;
}

Now, Spring will intercept the method's request and validate the parameters before calling it -- no additional manual testing is required.

Another way to do the same is to create our own annotations. Although you usually use custom annotations only when you need to go beyond Hibernate's built-in constraint set, in this example, we assume that @ Length does not exist. You can create two additional classes to verify the string Length, one for verification and one for annotation of 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;
    }
}

Note that in these cases, the best practice of separation of concerns requires that the property be marked as valid when it is null (s == null in the isValid method), and if this is an additional requirement for the property, use the @ NotNull annotation.

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

7. Error 7: (still) use xml based configuration

Although previous versions of Spring required XML, most of today's configurations can be completed through Java code or annotations; The XML configuration is only used as additional unnecessary boilerplate code.

This article (and its attached GitHub repository) uses annotations to configure spring. Spring knows which beans should be connected, because the top-level package directory to be scanned has been declared in the @ SpringBootApplication @ composite annotation, as shown below:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
Composite annotations (more information can be found in the Spring documentation) simply prompt Spring which packages should be scanned to retrieve beans. In our case, this means that this top-level package (co.kurin) 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. Error 8: ignore profile

In the development of server, one of the problems often encountered is to distinguish different configuration types, usually production configuration and development configuration. Do not manually replace various configuration items every time you switch from testing to deploying an application. A more effective method is to use profile. Recommended reading: Spring Boot Profile different environment configurations . Focus on the Java technology stack WeChat official account, reply the key word in the background: boot, get a Spring Spring latest dry cargo with long stack.

Consider a situation where you are using an in memory database for local development and a MySQL database in a production environment. Essentially, this means that you need to use different URL s and (hopefully) different credentials to access both. Let's see how we can do these two different profiles:

8.1. APPLICATION.YAML file

# 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:
8.2. APPLICATION-DEV.YAML file
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2

 

Assuming that you don't want to accidentally do anything to the production database when modifying the code, it makes sense to set the default configuration file to dev.

Then, on the server, you can provide - dspring profiles. The active = prod # parameter is given to the JVM to manually overwrite the configuration file. In addition, you can set the environment variable of the operating system to the required default profile.

9. Error 9: unable to accept dependency injection

Using Spring's dependency injection correctly means that it is allowed to connect all objects together by scanning all necessary configuration classes; This is very useful for decoupling relationships and makes testing easier, rather than through tight coupling between classes:

public class TopTalentController {

    private final TopTalentService topTalentService;

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

Let Spring connect us:

public class TopTalentController {

    private final TopTalentService topTalentService;

    public TopTalentController(TopTalentService topTalentService) {
        this.topTalentService = topTalentService;
    }
}
Misko Hevery's Google talk explains in depth the "why" of dependency injection, so let's see how it is used in practice. In the separation of concerns (common errors #3) section, we created a service and controller class.

Suppose we want to test the controller on the premise that # TopTalentService # behaves correctly. We can insert a simulated object by providing a separate configuration class to replace the actual service implementation:

@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 inject mock objects by telling Spring to use sampleinittestconfig as its configuration class:

@ContextConfiguration(classes = { SampleUnitTestConfig.class })

 

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

 

10. Error 10: lack of test or improper test

Although the concept of unit testing has existed for a long time, many developers seem to either "forget" to do it (especially if it is not "necessary") or just add it afterwards. This is obviously undesirable, because testing should not only verify the correctness of the code, but also serve as a document of how the program should behave in different scenarios.

When testing Web services, it is rare to only conduct "pure" unit tests, because communicating through HTTP usually requires calling Spring's "dispatcher servlet" and seeing what happens when an actual "HttpServletRequest" is received (making it an "integration" test, processing validation, serialization, etc.).

REST Assured, a Java DSL used to simplify the testing of REST services, has been proved to provide a very elegant solution on top of MockMVC. Consider the following code snippets 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"));
    }

}
SampleUnitTestConfig Class will TopTalentService The simulation implementation of is connected to TopTalentController All other classes are standard configurations inferred by scanning the subordinate package directory of the package where the application class is located.RestAssuredMockMvc Just used to set up a lightweight environment and /toptal/get The endpoint sends a GET Request.

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

Author: Toni Kukurin, translator: Wan Xiang

Www.cnblogs.com com/liululee/p/11235999. html  

 

Topics: Spring Boot