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