Knowledge Review Systematic Debugging Spring Programming Error Roots

Knowledge Review Systematic Debugging Spring Programming Error Roots #

Hello, I’m Fu Jian.

Earlier, we introduced 50 different problems. Before officially concluding the course, I think it’s necessary to review or analyze the reasons behind these problems. There may be countless manifestations of errors, but if we trace them back to their roots, we will find that there aren’t too many fundamental causes.

Of course, some students may simply attribute all the problems to “lack of skill.” However, apart from this obvious reason, I believe you should still deeply reflect on the matter. At the very least, assuming that Spring itself is prone to certain common mistakes, you should be conscious of it. So, let’s now analyze some common root causes of errors in Spring usage.

The Existence of Implicit Rules #

To make good use of Spring, you must understand some of its implicit rules, such as the default scan scope for beans and the automatic wiring of constructors, etc. Even though it may still work in most cases without understanding these rules, a slight change could result in complete failure. For example, in Lesson 1, we used Spring Boot to quickly build a simple web version of HelloWorld:

In this example, the Application class responsible for starting the program is defined as follows:

package com.spring.puzzle.class1.example1.application
// omit imports
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The HelloWorldController that provides the interface is defined as follows:

package com.spring.puzzle.class1.example1.application
// omit imports
@RestController
public class HelloWorldController {
    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld";
    };
}

However, let’s say that one day, we need to add multiple similar controllers and want to organize them in a clearer package hierarchy outside the application package. We might create a separate Controller package and adjust the class position. The modified structure is shown below:

In this case, it won’t work anymore. If we trace back to the root cause, it is likely that you overlooked the fact that @SpringBootApplication in Spring Boot has a default scan scope. This is an implicit rule. If you were not aware of it originally, the probability of making mistakes would be quite high. Similar cases are not discussed further here.

Unreasonable Default Configuration #

In addition to the reasons mentioned above, another important factor is that the default configuration of Spring may not be reasonable.

Let’s consider this question: If we were to write a framework, our biggest goal would certainly be to make it easy for users to get started, so that we can promote it better. Therefore, we wouldn’t want to write a bunch of configurations, but instead use default values. But are the default values you mentioned necessarily what the users need? Not necessarily. In this case, you may compromise and satisfy 80% of the user scenarios. So when using it, you need to consider if you belong to that remaining 20%.

Let’s review such a case together. In Case 2 of Lesson 18, when we don’t configure anything and directly use Spring Data Cassandra for operations, we actually rely on the configuration file within the Cassandra driver, located at the following directory:

.m2\repository\com\datastax\oss\java-driver-core\4.6.1\java-driver-core-4.6.1.jar!\reference.conf

We can see that there are many default configurations inside it, among which one important configuration is Consistency, which is set to LOCAL_ONE by default in the driver, as shown below:

basic.request {
    
    # The consistency level.
    #
    # Required: yes
    # Modifiable at runtime: yes, the new value will be used for requests issued after the change.
    # Overridable in a profile: yes
    consistency = LOCAL_ONE
    
    // omitted other non-critical configurations
}

When you first learn and use Cassandra, you will probably start by playing with just one machine. In this case, setting it to LOCAL_ONE is actually the most suitable option, because there is only one machine and your reads and writes can only hit one machine. So there is no problem with reads and writes.

But most Cassandra deployments in production involve multiple data centers and nodes, with a replication factor greater than 1. Therefore, using LOCAL_ONE for both reads and writes will cause problems. So, do you now understand what I mean to express? Spring adopts a bunch of default configurations for certain reasons, but they may not be suitable for your situation.

Pursuing Fancy Techniques #

Spring provides us with many easy-to-use possibilities. Sometimes, when you use Spring, you may realize that it seems to be able to work no matter how you use it. Especially when you see some more concise and efficient ways of coding online, you will be pleasantly surprised and discover that it is possible to do it this way. But is Spring really so versatile and easy to use?

Let’s quickly review Case 2 in Lesson 9. We often use @RequestParam and @PathVariable to obtain request parameters and parts of the path. However, when using these parameters frequently, you might find that their usage is not very friendly. For example, if we want to retrieve a request parameter called “name”, we would define it as follows:

@RequestParam(“name”) String name

In this case, we can see that the variable name is most likely defined as the same value as the RequestParam. So, can we define it in the following way:

@RequestParam String name

Indeed, this way is possible and can also pass local testing. Here is the complete code for you to experience the difference between the two:

@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestParam("name") String name){
    return name;
};

@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam String name){
    return name;
};

It is obvious that for students who like to pursue extreme simplicity, this cool feature is a blessing. However, when we switch to another project, it may stop working after going live and give a 500 error indicating that it cannot find a match.

I won’t go over the reason for this case again. I just want to say that through this example, you should understand that although Spring is powerful and seems to be able to handle everything, it may not always be the case in reality.

Using it for granted #

When using the Spring framework, sometimes we tend to draw conclusions without thinking. For example, when we need to handle multiple HTTP headers, our first instinct is to use a HashMap to receive them. But does this approach cover all situations? Let’s quickly review Case 1 from Lesson 10.

When parsing headers in Spring, we usually parse them on-demand. For example, if we want to use a header called “myHeaderName”, we would write the following code:

@RequestMapping(path = "/hi", method = RequestMethod.GET)
public String hi(@RequestHeader("myHeaderName") String name){
   // omitted body handling
};

Simply define a parameter, annotate it with @RequestHeader, and specify the header name to be parsed. However, if we need to parse multiple headers, the above approach will obviously result in an increasing number of parameters. In such cases, we usually use a Map to receive all the headers and process the Map directly. So we might end up with code like this:

@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestHeader() Map map){
    return map.toString();
};

After some rough testing, everything seems to work fine. Moreover, the above code conforms to the paradigm of programming against an interface, as it uses the Map interface type. However, when encountering the following request, the above interface definition will not meet expectations:

GET http://localhost:8080/hi1- myheader: h1- myheader: h2

Here, there is a header called “myHeader” with two values. If we send this request, we will find that the returned result does not include both values. The result will look like this:

{myheader=h1, host=localhost:8080, connection=Keep-Alive, user-agent=Apache-HttpClient/4.5.12 (Java/11.0.6), accept-encoding=gzip,deflate}

In fact, to receive all the headers completely, we should not directly use a Map but instead a MultiValueMap.

Taking this case as an example, let’s think about why the error occurred. You certainly knew that you needed to use a Map to receive the headers, and you probably believed it would work, but you may have overlooked the fact that the Map you used is the one provided by Spring. So sometimes, some “taken-for-granted” conclusions can actually be wrong. You must dare to assume and carefully verify in order to avoid many problems.

Irrelevant Dependency Changes #

Spring relies on a large number of other components to collaborate and complete its functionality. However, there may be multiple tools available for achieving the same functionality. For example, Spring can use either Gson or Jackson for JSON operations. What’s even more troublesome is that Spring often has dynamic dependencies, meaning it first checks if the preferred tool is available and uses it if it exists. Only when it doesn’t exist does it consider other dependent tools. This logic can result in different dependencies and, consequently, subtle behavioral “changes” when the project’s dependencies vary.

Let’s quickly review the case provided in lesson 11. First, take a look at the following code:

@RestController
public class HelloController {

    @PostMapping("/hi2")
    public Student hi2(@RequestBody Student student) {
        return student;
    }

}

This code accepts a Student object and then returns it as it is. We can test it using the following request:

POST http://localhost:8080/springmvc3_war/app/hi2
Content-Type: application/json

{
  "name": "xiaoming"
}

After testing, we’ll get the following result:

{
  "name": "xiaoming"
}

However, as the project progresses and the code remains unchanged, we may start getting the following result:

{
  "name": "xiaoming",
  "age": null
}

In this case, the age field was initially not serialized as part of the response body because no value was available. But later, it was serialized as null and returned as part of the body.

If we encounter the issue mentioned above, it is highly likely caused by the aforementioned changes in dependencies. Specifically, in the subsequent code development, we directly or indirectly depend on a new JSON parser, such as Jackson, through a dependency declaration similar to the one below:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.6</version>
</dependency>

Issues like this generally aren’t severe, but it’s crucial to be aware that when your code remains the same, changes in your dependencies can lead to “unexpected” behavior.

Common Mistakes #

In fact, besides the reasons mentioned above, there are many mistakes that all frameworks similar to Spring have to deal with. For example, when processing an HTTP request, there may be issues when the Path Variable contains special characters like “/”, and most of the time, extra handling is required. Let’s review the example in Lesson 9.

When parsing a URL, we often use the @PathVariable annotation. For example, we frequently see code like this:

@RestController
@Slf4j
public class HelloWorldController {
    @RequestMapping(path = "/hi1/{name}", method = RequestMethod.GET)
    public String hello1(@PathVariable("name") String name){
        return name;
    };  
}

When we access this service with http://localhost:8080/hi1/xiaoming, it will return “xiaoming”, meaning that Spring sets name as the corresponding value in the URL.

It seems smooth, but what if this name contains a special character “/” (for example, http://localhost:8080/hi1/xiao/ming)? What will happen then? If we don’t think carefully, we might say “xiao/ming”. However, more astute programmers will realize that this access will result in an error.

Actually, aside from Spring, you may also need to handle this issue when using other HTTP service frameworks. This kind of problem is a common one and not specific to Spring.

By examining the root cause of the above error, you should realize that, in addition to lack of expertise, part of the reason lies in our “arbitrary” reliance on Spring’s convenience. Precisely because it’s convenient, we rarely think about its internal working mechanisms. When we extensively use Spring, we may inadvertently step into pitfalls. So, when using Spring, it is worth boldly making assumptions, cautiously verifying them, and learning from others’ mistakes and best practices. Only by doing so can we master Spring forever and use it with more proficiency and confidence!