Day609.SpringWebURL解析常见错误 -Spring编程常见错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day609.SpringWebURL解析常见错误 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。
SpringWebURL解析常见错误
虽然Spring很强大,他有很多很多的功能。
但是他最主要大部分的使用领域还是在Web开发
领域。
针对Web开发,那必然会涉及到Http请求
,那请求的URI
就十分的重要。
Spring是如何对Http请求中URI进行解析的呢???而期间Spring会出现很多哪些常见的问题呢?
一、当 @PathVariable 遇到 /
在解析一个 URL 时,我们可能会使用到 @PathVariable
这个注解。例如我们会经常见到如下风格的代码:
@RestController
@Slf4j
public class HelloWorldController
@RequestMapping(path = "/hi1/name", method = RequestMethod.GET)
public String hello1(@PathVariable("name") String name)
return name;
;
但但 name 中含有特殊字符 / 时(例如http://localhost:8080/hi1/xiao/ming ),会如何?如果我们不假思索,或许答案是"xiao/ming"?然而稍微敏锐点的程序员都会判定这个访问是会报错的,具体错误参考:
如图所示,当 name 中含有 /,这个接口不会为 name 获取任何值,而是直接报 Not Found 错误。
当然这里的“找不到”并不是指 name 找不到,而是指服务于这个特殊请求的接口。
实际上,这里还存在另外一种错误,即当 name 的字符串以 / 结尾时,/ 会被自动去掉。
例如我们访问 http://localhost:8080/hi1/xiaoming/,Spring 并不会报错,而是返回 xiaoming。
针对这两种类型的错误,应该如何理解并修正呢?
Spring URL 匹配执行方法AbstractHandlerMethodMapping#lookupHandlerMethod:
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception
List<Match> matches = new ArrayList<>();
//尝试按照 URL 进行精准匹配
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null)
//精确匹配上,存储匹配结果
addMatchingMappings(directPathMatches, matches, request);
if (matches.isEmpty())
//没有精确匹配上,尝试根据请求来匹配
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)
//处理多个匹配的情况
//省略其他非关键代码
return bestMatch.handlerMethod;
else
//匹配不上,直接报错
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
大致为如下步骤:
- 根据 Path 进行 精确匹配
这个步骤执行的代码语句是"this.mappingRegistry.getMappingsByUrl(lookupPath)
",实际上,它是查询 MappingRegistry#urlLookup,它的值可以用调试视图查看,如下图所示:
查询 urlLookup 是一个精确匹配
Path 的过程。
很明显,http://localhost:8080/hi1/xiao/ming 的 lookupPath 是"/hi1/xiao/ming",并不能得到任何精确匹配。
这里需要补充的是,"/hi1/name"这种定义本身也没有出现在 urlLookup 中。
- 假设 Path 没有精确匹配上,则执行模糊匹配
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request)执行模糊匹配
显然,"/hi1/name"这个匹配方法已经出现在待匹配候选中了。
具体匹配过程可以参考方法 RequestMappingInfo#getMatchingCondition:
public RequestMappingInfo getMatchingCondition(HttpServletRequest request)
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());
当匹配会查询所有的信息,例如 Header、Body 类型以及 URL 等。如果有一项不符合条件,则不匹配。
在我们的案例中,当使用 http://localhost:8080/hi1/xiaoming 访问时,其中 patternsCondition 是可以匹配上的,也就是在模糊匹配中。
实际的匹配方法执行是通过 AntPathMatcher#match 来执行,判断的相关参数可参考以下调试视图:
当我们使用 http://localhost:8080/hi1/xiao/ming 来访问时,AntPathMatcher 执行的结果是"/hi1/xiao/ming"匹配不上"/hi1/name"。
- 根据匹配情况返回结果
如果找到匹配的方法,则返回方法;
如果没有,则返回 null。
在本案例中,http://localhost:8080/hi1/xiao/ming 因为找不到匹配方法最终报 404 错误。
追根溯源就是 AntPathMatcher 匹配不了"/hi1/xiao/ming"和"/hi1/name"。
另外,我们再回头思考 http://localhost:8080/hi1/xiaoming/ 为什么没有报错而是直接去掉了 /。
这里我直接贴出了负责执行 AntPathMatcher 匹配的 PatternsRequestCondition#getMatchingPattern 方法的部分关键代码:
private String getMatchingPattern(String pattern, String lookupPath)
//省略其他非关键代码
if (this.pathMatcher.match(pattern, lookupPath))
return pattern;
//尝试加一个/来匹配
if (this.useTrailingSlashMatch)
if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath))
return pattern + "/";
return null;
在这段代码中,AntPathMatcher 匹配不了"/hi1/xiaoming/“和”/hi1/name",所以不会直接返回。
进而,在 useTrailingSlashMatch 这个参数启用时(默认启用),会把 Pattern 结尾加上 / 再尝试匹配一次。
如果能匹配上,在最终返回 Pattern 时就隐式自动加 /。
很明显,我们的案例符合这种情况,等于说我们最终是用了"/hi1/name/“这个 Pattern,而不再是”/hi1/name"。所以自然 URL 解析 name 结果是去掉 / 的。
二、错误使用 @RequestParam、@PathVarible 等注解
当如下定义声明@RequestParam
@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;
;
很明显,对于喜欢追究极致简洁的同学来说,这个酷炫的功能是一个福音。
但当我们换一个项目时,有可能上线后就失效了,然后报错 500,提示匹配不上。
要理解这个问题出现的原因,首先我们需要把这个问题复现出来。例如我们可以修改下 pom.xml 来关掉两个选项:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<debug>false</debug>
<parameters>false</parameters>
</configuration>
</plugin>
上述配置显示关闭了 parameters 和 debug,这 2 个参数的作用你可以参考下面的表格:
通过上述描述,我们可以看出这 2 个参数控制了一些 debug 信息是否加进 class 文件中。
我们可以开启这两个参数来编译,然后使用下面的命令来查看信息:
javap -verbose HelloWorldController.class
执行完命令后,我们会看到以下 class 信息:
debug 参数开启的部分信息就是 LocalVaribleTable,而 paramters 参数开启的信息就是 MethodParameters。
观察它们的信息,你会发现它们都含有参数名 name。
如果你关闭这两个参数,则 name 这个名称自然就没有了。
而这个方法本身在 @RequestParam 中又没有指定名称,那么 Spring 此时还能找到解析的方法么?
答案是否定的,这里我们可以顺带说下 Spring 解析请求参数名称的过程,参考代码 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);
其中 NamedValueInfo 的 name 为 @RequestParam 指定的值。
很明显,在本案例中,为 null。所以这里我们就会尝试调用 parameter.getParameterName() 来获取参数名作为解析请求参数的名称。
但是,很明显,关掉上面两个开关后,就不可能在 class 文件中找到参数名了,这点可以从下面的调试试图中得到验证:
当参数名不存在,@RequestParam 也没有指明,自然就无法决定到底要用什么名称去获取请求参数,所以就会报本案例的错误。
三、未考虑参数是否可选
当如下定义Controller,当我们只传了name,但没传address的情况下,会出现http的400错误
,即(http://localhost:8080/hi4?name=xiaoming)
@RequestMapping(path = "/hi4", method = RequestMethod.GET)
public String hi4(@RequestParam("name") String name, @RequestParam("address") String address)
return name + ":" + address;
;
RequestParamMethodArgumentResolver 对参数解析的一些关键操作,参考其父类方法 AbstractNamedValueMethodArgumentResolver#resolveArgument:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
//省略其他非关键代码
//获取请求参数
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());
//省略后续代码:类型转化等工作
return arg;
- 查看 namedValueInfo 的默认值,如果存在则使用它
这个变量实际是通过下面的方法来获取的,参考 RequestParamMethodArgumentResolver#createNamedValueInfo:
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter)
RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
- 在 @RequestParam 没有指明默认值时,会查看这个参数是否必须,如果必须,则按错误处理
- 如果不是必须,则按 null 去做具体处理
说了以上,那提供几种解决方案:
- 设置 @RequestParam 的
默认值
@RequestParam(value = "address", defaultValue = "no address") String address
- 设置 @RequestParam 的
required 值
@RequestParam(value = "address", required = false) String address)
- 标记任何名为
Nullable 且 RetentionPolicy 为 RUNTIME 的注解
//org.springframework.lang.Nullable 可以
//edu.umd.cs.findbugs.annotations.Nullable 可以
@RequestParam(value = "address") @Nullable String address
- 修改参数类型为
Optional
@RequestParam(value = "address") Optional address
在 Spring Web 中,默认情况下,请求参数是必选项。
四、Date格式参数格式错误
Spring 支持日期类型的转化,于是我们可能会写出类似下面这样的代码:
@RequestMapping(path = "/hi6", method = RequestMethod.GET)
public String hi6(@RequestParam("Date") Date date)
return "date is " + date ;
;
然后,我们使用一些看似明显符合日期格式的 URL 来访问,例如 http://localhost:8080/hi6?date=2021-5-1 20:26:53,我们会发现 Spring 并不能完成转化,而是报错如下:
此时,返回错误码 400,错误信息为"Failed to convert value of type ‘java.lang.String’ to required type 'java.util.Date"。
针对Spring,他在处理参数转换的时候,需要知道source和target,并通过去寻找对应的参数转换器
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
if (source == null)
return null;
Class<?> sourceClass = sourceType.getType();
Class<?> targetClass = targetType.getType();
//根据源类型去获取构建出目标类型的方法:可以是工厂方法(例如 valueOf、from 方法)也可以是构造器
Member member = getValidatedMember(targetClass, sourceClass);
try
if (member instanceof Method)
//如果是工厂方法,通过反射创建目标实例
else if (member instanceof Constructor)
//如果是构造器,通过反射创建实例
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);
当使用 ObjectToObjectConverter 进行转化时,是根据反射机制带着源目标类型来查找可能的构造目标实例方法,例如构造器或者工厂方法,然后再次通过反射机制来创建一个目标对象。
所以对于 Date 而言,最终调用的是下面的 Date 构造器:
public Date(String s)
this(parse(s));
然而,我们传入的 2021-5-1 20:26:53 虽然确实是一种日期格式,但用来作为 Date 构造器参数是不支持的,最终报错,并被上层捕获,转化为 ConversionFailedException 异常。
同样,提供对应的解决方案:
- 使用 Date 默认
支持的格式
http://localhost:8080/hi6?date=Sat, 12 Aug 1995 13:30:00 GMT
- 指定告诉Spring对应的转换格式
@DateTimeFormat
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date
五、总结
-
当我们使用 @PathVariable 时,一定要注意传递的值是不是含有 / ;
-
当我们使用 @RequestParam、@PathVarible 等注解时,一定要意识到一个问题,虽然下面这两种方式(以 @RequestParam 使用示例)都可以,但是后者在一些项目中并不能正常工作,因为很多产线的编译配置会去掉不是必须的调试信息。
@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestParam("name") String name)
return name;
;
//方式2:没有显式指定RequestParam的“name”,这种方式有时候会不行
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam String name)
return name;
;
- 任何一个参数,我们都需要考虑它是可选的还是必须的。同时,你一定要想到参数类型的定义到底能不能从请求中自动转化而来。Spring 本身给我们内置了很多转化器,但是我们要以合适的方式使用上它。另外,Spring 对很多类型的转化设计都很贴心,例如使用下面的注解就能解决自定义日期格式参数转化问题。
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date
在最后,当我们定义如下代码
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam("name") String name)
return name;
;
http://localhost:8080/hi2?name=xiaoming&name=hanmeimei
,那Spring会如何处理这个name参数,并解析呢????
Spring会吧同名的参数转换为String[],再String[] —> String,并通过
,
分隔开。
所以结果是xiaoming,hanmeimei
以上是关于Day609.SpringWebURL解析常见错误 -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章
Day611.SpringWebBody转化常见错误 -Spring编程常见错误
Day640.Java 8的日期时间类问题 -Java业务开发常见错误
Day617.SpringData常见错误 -Spring编程常见错误
Day615.SpringSecurity常见错误 -Spring编程常见错误