文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间
案例18:当 @PathVariable 遇到 /
从 URL 中获取参数,但当路径中的参数含有字符 '/' 时,会出现 Not Found 错误,而字符 '/' 在参数末尾时,又会被自动去掉
@RestController
public class HelloWorldController {
@RequestMapping(path = "/hi1/{name}", method = RequestMethod.GET)
public String hello1(@PathVariable("name") String name){
return name;
}
}
解析:根据 URL 获取匹配的 MappingHandler 执行方法过程, 1. 根据 Path 进行精确匹配;2.假设 Path 没有精确匹配上,则执行模糊匹配;3. 根据匹配情况返回结果
@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);
}
解决:可以先用"**"匹配上路径,等进入方法后再尝试去解析
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 即为"/hi1/**"
String matchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
return antPathMatcher.extractPathWithinPattern(matchPattern, path);
};
案例19:错误使用 @RequestParam、@PathVarible 等注解
第二种在项目应用中有可能会出现失败
// 1. 在 @RequestParam 中指定参数名
@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestParam("name") String name){
return name;
};
---------------------------------------------
// 2. 未指定参数名
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam String name){
return name;
};
当参数名不存在,@RequestParam 也没有指明,自然就无法决定到底要用什么名称去获取请求参数,所以就会报本案例的错误。
解决:必须显式在 @RequestParam 中指定请求参数名
使用第一种
案例20:未考虑参数是否可选
下面的例子在 请求 URL 为 xxxx/hi4?name=xiaoming&address=beijing 时并不会出问题
@RequestMapping(path = "/hi4", method = RequestMethod.GET)
public String hi4(@RequestParam("name") String name, @RequestParam("address") String address){
return name + ":" + address;
};
而在 请求 URL 为 xxxx/hi4?name=xiaoming ,未含有 address ,则会报如下错误
解析:注解名(@RequestParam)来确定解析发生的位置是在 RequestParamMethodArgumentResolver 的父类 AbstractNamedValueMethodArgumentResolver#resolveArgument 中
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 1.查看 namedValueInfo 的默认值,如果存在则使用它
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);
}
// 2. 在 @RequestParam 没有指明默认值时,会查看这个参数是否必须,如果必须,则按错误处理
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
// 3. 如果不是必须,则按 null 去做具体处理
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
//省略后续代码:类型转化等工作
return arg;
}
方法中处理请求参数有以下步骤: 1. 查看 namedValueInfo 的默认值,如果存在则使用它; 2. 在 @RequestParam 没有指明默认值时,会查看这个参数是否必须,如果必须,则按错误处理;3. 如果不是必须,则按 null 去做具体处理
解决:1.设置 @RequestParam 的默认值
@RequestParam(value = "address", defaultValue = "no address") String address
2.设置 @RequestParam 的 required 值
@RequestParam(value = "address", required = false) String address)
3. 标记任何名为 Nullable 且 RetentionPolicy 为 RUNTIME 的注解
@RequestParam(value = "address") @Nullable String address
4. 修改参数类型为 Optional
@RequestParam(value = "address") Optionaladdress
案例21:请求参数格式错误
日期格式的 URL 来访问,例如 xxx/hi6?date=2021-5-1 20:26:53,我们会发现 Spring 并不能完成转化,而是报错如下:"Failed to convert value of type 'java.lang.String' to required type 'java.util.Date"
@RequestMapping(path = "/hi6", method = RequestMethod.GET)
public String hi6(@RequestParam("Date") Date date){
return "date is " + date ;
};
解析:还是参考上面的 resolveArgument 方法
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
//省略其他非关键代码
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
//以此为界,前面代码为解析请求参数,后续代码为转化解析出的参数
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
//省略其他非关键代码
}
//省略其他非关键代码
return arg;
}
在 convertIfNecessary 中会根据 sourceClass 和 targetClass 类型寻找到对应的构造函数,
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);
}
最终会调用下面的 String 转 Date 的构造函数,而 “2021-5-1 20:26:53” 在转换为 Date 并不符合格式要求,因此会抛出错误
public Date(String s) {
this(parse(s));
}
解决: 1. 使用 Date 支持的格式
http://localhost:8080/hi6?date=Sat, 12 Aug 1995 13:30:00 GMT
2. 使用好内置格式转化器
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date