09 Spring Web URL Resolution Common Errors

09 Spring Web URL Resolution Common Errors #

Hello, I am Fu Jian.

In the previous section, we discussed various error cases that revolve around the core functionalities of Spring, such as dependency injection, AOP, and many others. However, in reality, we mostly use Spring to build a web service. Therefore, starting from this lesson, we will focus on analyzing some common errors that are frequently encountered in Spring web development, helping you avoid these issues.

It goes without saying that here we are referring to web services that use the HTTP protocol. And when it comes to handling HTTP requests, the first thing to deal with is the URL. So today, let’s first introduce the classic cases of handling URLs in Spring. Without further ado, let’s start the demonstration directly below.

Case 1: When @PathVariable encounters / #

When parsing a URL, we often use the annotation @PathVariable. 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 using http://localhost:8080/hi1/xiaoming, it will return “xiaoming”. This means that Spring sets the variable “name” to the corresponding value in the URL.

It seems smooth, but what if the “name” variable contains a special character “/” (e.g., http://localhost:8080/hi1/xiao/ming)? If we don’t think about it, the answer might be “xiao/ming”. However, a more experienced programmer would realize that this request would result in an error. The specific error can be seen below:

Error

As shown in the image, when “name” contains “/”, Spring does not retrieve any value for the variable and instead returns a “Not Found” error. Here, “Not Found” does not refer to the “name” variable not being found, but rather to the interface responsible for serving this special request.

In fact, there is another error that can occur when the string of “name” ends with “/”. In this case, the “/” character is automatically removed. For example, if we access http://localhost:8080/hi1/xiaoming/, Spring will not report an error but instead return “xiaoming”.

How can we understand and fix these two types of errors?

Analysis #

These two errors are related to the process of matching and executing methods for a URL, so it is necessary to understand the general process of URL matching and method execution. Refer to AbstractHandlerMethodMapping#lookupHandlerMethod:

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
   List<Match> matches = new ArrayList<>();
   // Attempt to match the URL exactly
   List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
   if (directPathMatches != null) {
      // Found an exact match, store the matching result
      addMatchingMappings(directPathMatches, matches, request);
   }
   if (matches.isEmpty()) {
      // No exact match, try matching based on the request
      addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
   }

   if (!matches.isEmpty()) {
      Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
      matches.sort(comparator);
      Match bestMatch = matches.get(0);
      if (matches.size() > 1) {
         // Handle multiple matches
      }
      // Omit other non-critical code
      return bestMatch.handlerMethod;
   }
   else {
      // No match found, report an error
      return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
   }
}

There are a few basic steps involved.

1. Exact Path Matching

The code responsible for this step is “this.mappingRegistry.getMappingsByUrl(lookupPath)”. In fact, this code queries the MappingRegistry#urlLookup, which can be viewed in the debug view, as shown in the following image:

urlLookup

Querying the urlLookup is a process of exact path matching. Clearly, the lookupPath for http://localhost:8080/hi1/xiao/ming is “/hi1/xiao/ming”, and it does not result in any exact matches. It’s worth mentioning that the “/hi1/{name}” definition itself is not included in the urlLookup.

2. If there is no exact path match, perform fuzzy matching

When step 1 fails to find a match, the request is matched using fuzzy matching. The methods to be matched can be seen in the following image:

Match Candidates

Obviously, the matching method “/hi1/{name}” has already appeared among the candidate matches. The specific matching process can be seen in the method RequestMappingInfo#getMatchingCondition:

public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
    // ...
}

This process consists of several code statements.

        RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
        if (methods == null) {
            return null;
        }
        ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
        if (params == null) {
            return null;
        }
        //省略其他匹配条件
        PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
        if (patterns == null) {
            return null;
        }
        //省略其他匹配条件
        return new RequestMappingInfo(this.name, patterns,
                methods, params, headers, consumes, produces, custom.getCondition());
    }

Now we know that matching queries all the information, such as headers, body types, and URLs. If any of them do not meet the conditions, the query fails.

In our case, when accessing http://localhost:8080/hi1/xiaoming, the patternsCondition can be matched. The actual matching method execution is performed using AntPathMatcher#match, and the related parameters for judgment can be referenced in the following debug view:

However, when we access http://localhost:8080/hi1/xiao/ming, the result of AntPathMatcher’s execution is that “/hi1/xiao/ming” does not match “/hi1/{name}”.

3. Return the result based on the matching condition

If a matching method is found, return the method; otherwise, return null.

In our case, http://localhost:8080/hi1/xiao/ming returns a 404 error because no matching method is found. The root cause is that AntPathMatcher cannot match “/hi1/xiao/ming” and “/hi1/{name}”.

Furthermore, let’s reconsider why http://localhost:8080/hi1/xiaoming/ doesn’t report an error but removes the /. Here is a partial section of the key code responsible for executing the AntPathMatcher matching in the PatternsRequestCondition#getMatchingPattern method:

    private String getMatchingPattern(String pattern, String lookupPath) {
        //Omit other non-key code
        if (this.pathMatcher.match(pattern, lookupPath)) {
            return pattern;
        }
        //Try adding a / to match
        if (this.useTrailingSlashMatch) {
            if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
                return pattern + "/";
            }
        }
        return null;
    }

In this code snippet, AntPathMatcher cannot match “/hi1/xiao/ming/” and “/hi1/{name}”, so it does not return directly. Then, when the useTrailingSlashMatch parameter is enabled (which is enabled by default), the Pattern is appended with / and another match attempt is made. If it matches, the Pattern is implicitly automatically added with / when returning.

Obviously, our case fits this situation. In other words, we eventually used “/hi1/{name}/” as the Pattern, instead of “/hi1/{name}”. Therefore, the name parsed from the URL naturally removes the /.

Problem Correction #

Considering this case and the analysis of the source code, we may think that we can first use “*” to match the path, and then try to parse it after entering the method, so that it will be foolproof. The specific modified code is as follows:

    @RequestMapping(path = "/hi1/**", method = RequestMethod.GET)
    public String hi1(HttpServletRequest request){
        String requestURI = request.getRequestURI();
        return requestURI.split("/hi1/")[1];
    };

However, this modification method still has vulnerabilities. If our path name happens to contain “/hi1/”, the value returned after split is not what we want. In fact, a more suitable revised code is as follows:

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @RequestMapping(path = "/hi1/**", method = RequestMethod.GET)
    public String hi1(HttpServletRequest request){
        String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
        //matchPattern is "/hi1/**"
        String matchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); 
        return antPathMatcher.extractPathWithinPattern(matchPattern, path); 
    };

After the modification, both errors are fixed. Of course, there are other solutions, such as URL encoding the passed parameters to avoid /, or directly treating this variable as a request parameter, header, etc., instead of a part of the URL. You can choose the appropriate solution based on the specific situation.

Case 2: Incorrect Use of @RequestParam and @PathVariable Annotations #

We often use @RequestParam and @PathVariable to retrieve request parameters and parts of the path. However, when using these parameters frequently, you may find that their usage is not very user-friendly. For example, to retrieve a request parameter named “name”, we would define it like this:

@RequestParam(“name”) String name

In this case, we can see that the variable name is very likely to be defined as the value of RequestParam. So can’t we define it like this instead?

@RequestParam String name

Indeed, this is possible and can pass local testing. Here, I also provide the complete code so you can compare the differences 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;
};

Clearly, for those who prefer extreme simplicity, this cool feature is a godsend. But when we switch to another project, it may stop working after going online and throw a 500 error indicating a mismatch.

error screenshot

Case Analysis #

To understand the cause of this problem, we need to reproduce it. We can modify the pom.xml file to turn off two options, for example:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <debug>false</debug>
        <parameters>false</parameters>
    </configuration>
</plugin>

The above configuration shows that the two parameters, parameters and debug, have been turned off. You can refer to the table below for the explanation of their effects:

table

Based on the above description, we can see that these two parameters control whether some debug information is included in the class file. We can enable these two parameters for compilation and then use the following command to view the information:

javap -verbose HelloWorldController.class

After executing the command, we will see the following class information:

class info

The part of the information enabled by the debug parameter is the LocalVariableTable, and the part enabled by the parameters parameter is the MethodParameters. By observing their information, we can see that they both contain the parameter name “name”.

If you turn off these two parameters, the name will naturally be gone. In this case, if @RequestParam does not specify a name, can Spring still find the correct method to parse the parameter?

The answer is no. Let’s take a moment to explain the process of Spring resolving the names of request parameters, referring to the code AbstractNamedValueMethodArgumentResolver#updateNamedValueInfo:

private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
    String name = info.name;
    if (info.name.isEmpty()) {
        name = parameter.getParameterName();
        if (name == null) {
            throw new IllegalArgumentException(
                "Name for argument type [" + parameter.getNestedParameterType().getName() +
                "] not available, and parameter name information not found in class file either.");
        }
    }
    String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
    return new NamedValueInfo(name, info.required, defaultValue);
}

Here, NamedValueInfo’s name refers to the value specified in @RequestParam. Clearly, it is null in this case.

So we will try to call parameter.getParameterName() to get the parameter name as the name for parsing the request parameter. However, it is obvious that when the above two switches are turned off, the parameter name cannot be found in the class file, as can be verified from the debugging view below:

debugging view

When the parameter name does not exist and is not specified in @RequestParam, there is no way to determine what name should be used to retrieve the request parameter. Therefore, the error in this case will be thrown.

Solution #

When we have replicated how the problem occurs, we can naturally make it work by enabling these two parameters. However, considering the purpose of these two parameters, it is clear that they can make our program smaller, so many projects favor turning off these two parameters.

To adapt to any situation, the correct way to fix it is to explicitly specify the request parameter name in @RequestParam. The specific modification is as follows:

@RequestParam(“name”) String name

Through this case, we can see that many features may seem to work forever, but in reality, they only work under specific conditions. Additionally, let’s expand on the topic briefly: IDEs usually have debug parameters enabled, so programs run in IDEs may not be suitable for production. For example, IntelliJ IDEA enables the parameters parameter by default.

Furthermore, this case mainly focuses on @RequestParam, but @PathVariable has the same issue. Please take note of this.

Now, I will address a possible confusion: What is the difference between the parameters we discussed here and @QueryParam and @PathParam? In fact, the latter annotations are part of JAX-RS and do not require additional imports. On the other hand, @RequestParam and @PathVariable are annotations in the Spring framework and require additional dependencies to be imported. Additionally, the parameter requirements for different annotations are not completely consistent.

Case 3: Not Considering Whether Parameters are Optional #

In the previous case, we mentioned the use of @RequestParam. However, when using it, we often encounter another problem - neglecting to consider whether certain parameters are optional. For example, consider the following code:

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

When accessing http://localhost:8080/hi4?name=xiaoming&address=beijing, there is no problem. However, if a user only uses the “name” parameter in the request (i.e. [http://localhost:8080/hi4?name=xiaoming](http://localhost:8080/hi4?name=xia oming)), an error will occur directly:

Error Screenshot

At this time, an error with a status code of 400 is returned, indicating a request format error: the “address” parameter is missing.

In fact, even some beginners may be surprised when encountering this error. Since the “address” parameter does not exist, shouldn’t it be set as null instead of throwing an error? Let’s analyze this further.

Case Analysis #

To understand the root cause of this error, you need to understand where the request parameter resolution takes place.

In fact, here we can determine the position of the resolution based on the annotation name (@RequestParam) - it occurs in the RequestParamMethodArgumentResolver. Why is that?

In tracing back, in the case at hand, when the method to be executed, “hi4”, is matched based on the URL, in order to invoke it via reflection, the method arguments, name and address, must be resolved. And since they are annotated with @RequestParam, it is only natural for the resolver to use the RequestParamMethodArgumentResolver.

Next, let’s take a look at some key operations performed by the RequestParamMethodArgumentResolver for parameter resolution, referring to the AbstractNamedValueMethodArgumentResolver#resolveArgument method:

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
    MethodParameter nestedParameter = parameter.nestedIfOptional();
    // omit other non-key code
    // get the request parameter
    Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
    if (arg == null) {
        if (namedValueInfo.defaultValue != null) {
            arg = resolveStringValue(namedValueInfo.defaultValue);
        }
        else if (namedValueInfo.required && !nestedParameter.isOptional()) {
            handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
        }
        arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
    }
    // omit subsequent code: type conversion, etc.
    return arg;
}

As shown in the code, when a request parameter is missing, we usually handle it according to the following steps.

1. Check the default value of namedValueInfo, and if it exists, use it

In fact, this variable is obtained through the following method, referring to RequestParamMethodArgumentResolver#createNamedValueInfo:

@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
    RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
    return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
}

It is actually related to @RequestParam information. We can verify this conclusion by debugging, as shown in the following figure:

NamedValueInfo Default Value

2. If the default value is not specified in @RequestParam, check if the parameter is required. If it is required, throw an error

The code to determine whether or not a parameter is required is the following line:

namedValueInfo.required && !nestedParameter.isOptional()

Clearly, to determine if a parameter is required, two conditions must be met: Condition 1 is that @RequestParam specifies it as required (i.e. the property “required” is true, which is also the default value), and Condition 2 requires that the parameter marked with the @RequestParam annotation itself is not optional.

We can see the specific meaning of optional by using the MethodParameter#isOptional method:

public boolean isOptional() {
    return (getParameterType() == Optional.class || hasNullableAnnotation() ||
        (KotlinDetector.isKotlinReflectPresent() &&
        KotlinDetector.isKotlinType(getContainingClass()) &&
        KotlinDelegate.isOptional(this)));
}

In the absence of using Kotlin, “optional” means that the parameter type is Optional, or any annotation with the name Nullable and RetentionPolicy of RUNTIME.

3. If it is not required, handle it as null

If the accepted type is boolean, return false; if it is a primitive type, throw an error directly. We won’t elaborate on this here.

In combination with our case, our parameters meet the conditions determined as required in Step 2. Therefore, the method AbstractNamedValueMethodArgumentResolver#handleMissingValue will ultimately be executed:

protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
    throw new ServletRequestBindingException("Missing argument '" + name +
          "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName());
}

Problem Fix #

Through case analysis, we can easily fix this issue by giving the parameter a default value or making it non-optional. There are several methods to do this.

1. Set the default value in @RequestParam

Modify the code as follows:

@RequestParam(value = “address”, defaultValue = “no address”) String address

2. Set the “required” value in @RequestParam

Modify the code as follows:

@RequestParam(value = “address”, required = false) String address

3. Use any annotation with the name Nullable and RetentionPolicy of RUNTIME

Modify the code as follows:

//org.springframework.lang.Nullable - //edu.umd.cs.findbugs.annotations.Nullable - @RequestParam(value = “address”) @Nullable String address

4. Change the parameter type to Optional

Modify the code as follows:

@RequestParam(value = “address”) Optional address

From these fixing methods, it is easy to see that if you don’t study the source code, you may only be limited to one or two methods to solve the problem. However, by diving into the source code, there are many more ways to solve it. It should be noted here that in Spring Web, request parameters are mandatory by default.

Case 4: Incorrect Request Parameter Format #

When using Spring URL-related annotations, we can see that Spring is capable of automatic conversion. For example, in the following code, the age parameter can be defined as an int primitive type (or an Integer), instead of being limited to String type.

@RequestMapping(path = "/hi5", method = RequestMethod.GET)
public String hi5(@RequestParam("name") String name, @RequestParam("age") int age){
    return name + " is " + age + " years old";
};

Given Spring’s powerful transformation capabilities, we assume that Spring also supports the conversion of date types (and it does). So we might write code like the following:

@RequestMapping(path = "/hi6", method = RequestMethod.GET)
public String hi6(@RequestParam("Date") Date date){
    return "date is " + date ;
};

Then, if we use a URL that seems to conform to the date format, such as http://localhost:8080/hi6?date=2021-5-1 20:26:53, we will find that Spring fails to perform the conversion and throws an error as follows:

In this case, an HTTP 400 error is returned with the message “Failed to convert value of type ‘java.lang.String’ to required type ‘java.util.Date’”.

How can we understand this case? What do we need to do to achieve automatic conversion?

Case Analysis #

Whether we use @PathVariable or @RequestParam, the result we generally obtain is a String or String array. For example, when using @RequestParam, the relevant code for parsing can be found in the resolveName method of RequestParamMethodArgumentResolver:

@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
   // other non-critical code omitted
   if (arg == null) {
      String[] paramValues = request.getParameterValues(name);
      if (paramValues != null) {
         arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
      }
   }
   return arg;
}

Here, we call request.getParameterValues(name), which returns a String array, and finally returns a single String or a String array to the caller.

So it is obvious that in this test program, we return a String to the caller, and this String needs to be converted before it can be assigned to another type. For example, in the case of the int age parameter, it needs to be converted to an int primitive type. This basic process can be verified by examining the key code in AbstractNamedValueMethodArgumentResolver#resolveArgument:

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
   // other non-critical code omitted
   Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
   // the code before this line resolves the request parameters, and the code after this line converts the resolved parameters
   if (binderFactory != null) {
      WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
      try {
         arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
      }
      // other non-critical code omitted
   }
   // other non-critical code omitted
   return arg;
}

In fact, we have previously mentioned this conversion logic, so we will not go into detail here.

You just need to remember that it finds a converter based on the source type and target type in order to perform the conversion. In this case, for age, the converter found is StringToNumberConverterFactory. For the Date variable of type Date, in this case, the converter found is ObjectToObjectConverter. The conversion process for this converter is shown in the following code:

public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
   if (source == null) {
       // conversion process omitted
   }
   // other non-critical code omitted
}
return null;
}
Class<?> sourceClass = sourceType.getType();
Class<?> targetClass = targetType.getType();
// Get the method to construct the target type based on the source type: can be a factory method (e.g. valueOf, from method) or a constructor
Member member = getValidatedMember(targetClass, sourceClass);
try {
    if (member instanceof Method) {
        // If it's a factory method, create the target instance using reflection
    }
    else if (member instanceof Constructor) {
        // If it's a constructor, create the instance using reflection
        Constructor<?> ctor = (Constructor<?>) member;
        ReflectionUtils.makeAccessible(ctor);
        return ctor.newInstance(source);
    }
}
catch (InvocationTargetException ex) {
    throw new ConversionFailedException(sourceType, targetType, source, ex.getTargetException());
}
catch (Throwable ex) {
    throw new ConversionFailedException(sourceType, targetType, source, ex);
}

When using ObjectToObjectConverter for conversion, it uses reflection to search for possible methods to construct the target type based on the source type, such as constructors or factory methods. It then creates the target object using reflection. So for the Date type, it finally calls the Date constructor:

public Date(String s) {
    this(parse(s));
}

However, the input [2021-5-1 20:26:53] is not supported as a valid parameter for the Date constructor. As a result, an error occurs and is caught by the upper layer, throwing a ConversionFailedException.

Solution #

So, how can we fix this issue? Here are two methods:

  1. Use a supported format for the Date type. For example, the following test URL should work:

    http://localhost:8080/hi6?date=Sat, 12 Aug 1995 13:30:00 GMT

  2. Use a built-in format converter which supports the desired format. In Spring, to perform the conversion from String to Date, ObjectToObjectConverter is not the best converter to use. We can utilize the more powerful AnnotationParserConverter. During Spring initialization, some converters for different types, including Date, are built, such as instances of AnnotationParserConverter. But why does it not work sometimes?

    This is because AnnotationParserConverter has a requirement for the target type. We can see this from a debugging perspective by examining the construction process of an AnnotationParserConverter instance for String to Date conversion, as seen in the addFormatterForFieldAnnotation method of FormattingConversionService:

    Debugging view of FormattingConversionService#addFormatterForFieldAnnotation method

    AnnotationParserConverter requires the annototationType parameter to be DateTimeFormat. This annotationType is used to determine whether this converter can be used, as shown in the matches method of AnnotationParserConverter:

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return targetType.hasAnnotation(this.annotationType);
    }
    

    The final constructed converter information can be seen in the following image:

    Constructed converter with annotation information

    The converter constructed in the image can be used for converting String to Date, but it requires us to annotate the Date parameter with @DateTimeFormat. However, in our case, the Date parameter does not have this annotation. To use this converter and make the original URL work, we can annotate the Date parameter and provide the appropriate format. This will allow the code, which previously did not work, to work. The code modification is as follows:

    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
    Date date;
    

    This is the solution for this case. Besides this, we can also create our own converter to handle the conversion, but we won’t go into details here. Additionally, this case teaches us that although Spring provides many built-in conversion features, we need to ensure that the format meets the corresponding requirements, otherwise the code may fail.

Key Recap #

Through this lecture, we have learned about some common mistakes in parsing URLs in Spring and their underlying reasons. Let’s review the key points again:

  1. When using @PathVariable, always be careful if the passed value contains a “/”.

  2. When using annotations such as @RequestParam, @PathVariable, etc., be aware that the following two methods (shown here using @RequestParam as an example) are both valid, but the latter may not work in some projects because many production configurations remove unnecessary debugging information.

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

//Method 2: Not explicitly specifying the "name" in @RequestParam, this method may not work sometimes
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam String name){
    return name;
};
  1. For any parameter, we need to consider whether it is optional or required. At the same time, we must think about whether the parameter type can be automatically converted from the request. Spring provides many built-in converters, but we need to use them appropriately. In addition, Spring has designed thoughtful conversion mechanisms for many types. For example, the following annotation can solve the problem of converting custom date format parameters:
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date

Hope these core knowledge points can help you parse URLs efficiently.

Thought Exercise #

Regarding URL parsing, there are actually many surprising aspects, such as the following code snippet in case 2:

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

In the above application code, we can use http://localhost:8080/hi2?name=xiaoming&name=hanmeimei to test it. What do you think the result will be? Will it be xiaoming&name=hanmeimei?

Let’s discuss in the comments section!