Spring 启动 2.0.0.M4 和电子邮件地址作为 @PathVariable 给出 HTTP 500 错误
Spring Boot 2.0.0.M4 and email address as @PathVariable gives HTTP 500 error
我正在尝试从 Spring 引导 1.5.7
迁移到 2.0.0.M4
这是我的休息控制器:
@RestController
@RequestMapping("/v1.0/users")
public class UsersController {
@Autowired
private UserService userService;
@RequestMapping(value = "/validate-username/{username}", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.OK)
public void validateUsername(@PathVariable String username) {
throw new EntityAlreadyExistsException();
}
...
}
这是异常处理程序:
@ControllerAdvice
public class GlobalControllerExceptionHandler {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.CONFLICT)
public Map<String, ResponseError> handleEntityAlreadyExistsException(EntityAlreadyExistsException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.debug("API error", e);
return createResponseError(HttpStatus.CONFLICT.value(), e.getMessage());
}
}
对于以下用户名,例如:alex
一切正常,我收到 409
状态代码,内容类型为 application/json; charset=UTF-8
但如果以下用户名,例如,alex@test.com
我的端点 returns 500
状态代码和非 JSON 内容类型,如下所示:
当用户名 PathVariable 末尾包含 .com
时,我可以重现此问题。
我使用嵌入式 Tomcat 作为应用程序服务器。值得 Spring Boot 1.5.7 相同的功能运行良好。如何让它与 Spring Boot 2.0.0.M4 一起工作?
P.S.
我知道将电子邮件地址作为 URL 参数发送是一种不好的做法。我只是对这个特殊情况感兴趣。
试试这个朋友,{username:.+}
而不是你的 {username}
编辑:我发现 @ 是 URL 的保留字符,不应在 URL 中使用。您需要对 URL.
进行编码
类似于:alex@test.com -> alex%40test.com
来源:Another Whosebug question
您观察到的问题深入 Spring WebMvc 内部。
根本原因是 Spring 正在推测接受的响应类型。
详细地说,在 alex@test.com
的情况下,实际上为接受的响应类型提供答案的策略 class 是 ServletPathExtensionContentNegotiationStrategy,它根据在路径中找到的内容进行猜测。
由于 com
是有效的文件扩展名类型(参见 this),Spring Boot 2.0.0.M4 尝试使用该 mime 类型来转换您的您的 ControllerAdvice
class 对该 mime 类型的响应(当然会失败),因此返回到默认的错误响应。
解决此问题的第一个方法是指定具有值的 HTTP header Accept
application/json
个。
不幸的是Spring 2.0.0.M4 仍然不会使用这种 mime 类型,因为 ServletPathExtensionContentNegotiationStrategy 策略优先于 HeaderContentNegotiationStrategy。
此外,使用了 alex
或(甚至像 alex@test.gr
这样的东西),Spring 没有猜到任何 MIME 类型,因此允许正常流程继续进行。
这个工作的原因是 Spring Boot 1.5。7.RELEASE 是 Spring 没有尝试将 com 映射到 mime 类型,因此使用默认响应类型这允许将响应 object 转换为 JSON 的过程继续。
现在更有趣的部分来了,这当然是修复。
我有两个解决方案,但我只展示第一个,只提第二个。
这是第一个基于我对问题的解释的解决方案。
我承认这个解决方案确实看起来有点侵入性,但它就像一个魅力。
我们需要做的是改变 auto-configured ContentNegotiationManager
以便用我们自己定制的 PathExtensionContentNegotiationStrategy
替换提供的 PathExtensionContentNegotiationStrategy
。 BeanPostProcessor
.
可以很容易地执行这样的操作
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.ListIterator;
@Configuration
public class ContentNegotiationManagerConfiguration {
@Bean
public ContentNegotiationManagerBeanPostProcessor contentNegotiationManagerBeanPostProcessor() {
return new ContentNegotiationManagerBeanPostProcessor();
}
private static class ContentNegotiationManagerBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; //no op
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof ContentNegotiationManager)) {
return bean;
}
final ContentNegotiationManager contentNegotiationManager = (ContentNegotiationManager) bean;
ListIterator<ContentNegotiationStrategy> iterator =
contentNegotiationManager.getStrategies().listIterator();
while (iterator.hasNext()) {
ContentNegotiationStrategy strategy = iterator.next();
if (strategy.getClass().getName().contains("OptionalPathExtensionContentNegotiationStrategy")) {
iterator.set(new RemoveHandleNoMatchContentNegotiationStrategy());
}
}
return bean;
}
}
private static class RemoveHandleNoMatchContentNegotiationStrategy
extends PathExtensionContentNegotiationStrategy {
/**
* Don't lookup file extensions to match mime-type
* Effectively reverts to Spring Boot 1.5.7 behavior
*/
@Override
protected MediaType handleNoMatch(NativeWebRequest request, String key) {
return null;
}
}
}
第二种可以实施的解决方案是利用 OptionalPathExtensionContentNegotiationStrategy class 的功能,默认情况下由 Spring 使用。
基本上您需要做的是确保对您的 validateUsername
端点的每个 HTTP 请求都包含一个名为 org.springframework.web.accept.PathExtensionContentNegotiationStrategy.SKIP
的属性,其值为 true
This article 展示了如何阻止 Spring 使用路径扩展进行内容协商(至少为我工作 Spring Boot 1.5.x):
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
}
}
最短的可能解决方案是修改您的 URL,如下所示
@RequestMapping(value = "/validate-username/{username}/"
注意:我在 URL 的末尾使用了斜杠 '/'
。因此,URL 将完美适用于任何类型的电子邮件地址或 .com , .us
在您的 {username}
路径变量中包含文本。
您不需要向您的应用程序添加任何类型的额外配置。
我正在尝试从 Spring 引导 1.5.7
迁移到 2.0.0.M4
这是我的休息控制器:
@RestController
@RequestMapping("/v1.0/users")
public class UsersController {
@Autowired
private UserService userService;
@RequestMapping(value = "/validate-username/{username}", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.OK)
public void validateUsername(@PathVariable String username) {
throw new EntityAlreadyExistsException();
}
...
}
这是异常处理程序:
@ControllerAdvice
public class GlobalControllerExceptionHandler {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.CONFLICT)
public Map<String, ResponseError> handleEntityAlreadyExistsException(EntityAlreadyExistsException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.debug("API error", e);
return createResponseError(HttpStatus.CONFLICT.value(), e.getMessage());
}
}
对于以下用户名,例如:alex
一切正常,我收到 409
状态代码,内容类型为 application/json; charset=UTF-8
但如果以下用户名,例如,alex@test.com
我的端点 returns 500
状态代码和非 JSON 内容类型,如下所示:
当用户名 PathVariable 末尾包含 .com
时,我可以重现此问题。
我使用嵌入式 Tomcat 作为应用程序服务器。值得 Spring Boot 1.5.7 相同的功能运行良好。如何让它与 Spring Boot 2.0.0.M4 一起工作?
P.S.
我知道将电子邮件地址作为 URL 参数发送是一种不好的做法。我只是对这个特殊情况感兴趣。
试试这个朋友,{username:.+}
而不是你的 {username}
编辑:我发现 @ 是 URL 的保留字符,不应在 URL 中使用。您需要对 URL.
进行编码类似于:alex@test.com -> alex%40test.com
来源:Another Whosebug question
您观察到的问题深入 Spring WebMvc 内部。
根本原因是 Spring 正在推测接受的响应类型。
详细地说,在 alex@test.com
的情况下,实际上为接受的响应类型提供答案的策略 class 是 ServletPathExtensionContentNegotiationStrategy,它根据在路径中找到的内容进行猜测。
由于 com
是有效的文件扩展名类型(参见 this),Spring Boot 2.0.0.M4 尝试使用该 mime 类型来转换您的您的 ControllerAdvice
class 对该 mime 类型的响应(当然会失败),因此返回到默认的错误响应。
解决此问题的第一个方法是指定具有值的 HTTP header Accept
application/json
个。
不幸的是Spring 2.0.0.M4 仍然不会使用这种 mime 类型,因为 ServletPathExtensionContentNegotiationStrategy 策略优先于 HeaderContentNegotiationStrategy。
此外,使用了 alex
或(甚至像 alex@test.gr
这样的东西),Spring 没有猜到任何 MIME 类型,因此允许正常流程继续进行。
这个工作的原因是 Spring Boot 1.5。7.RELEASE 是 Spring 没有尝试将 com 映射到 mime 类型,因此使用默认响应类型这允许将响应 object 转换为 JSON 的过程继续。
现在更有趣的部分来了,这当然是修复。 我有两个解决方案,但我只展示第一个,只提第二个。
这是第一个基于我对问题的解释的解决方案。 我承认这个解决方案确实看起来有点侵入性,但它就像一个魅力。
我们需要做的是改变 auto-configured ContentNegotiationManager
以便用我们自己定制的 PathExtensionContentNegotiationStrategy
替换提供的 PathExtensionContentNegotiationStrategy
。 BeanPostProcessor
.
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.ListIterator;
@Configuration
public class ContentNegotiationManagerConfiguration {
@Bean
public ContentNegotiationManagerBeanPostProcessor contentNegotiationManagerBeanPostProcessor() {
return new ContentNegotiationManagerBeanPostProcessor();
}
private static class ContentNegotiationManagerBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; //no op
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof ContentNegotiationManager)) {
return bean;
}
final ContentNegotiationManager contentNegotiationManager = (ContentNegotiationManager) bean;
ListIterator<ContentNegotiationStrategy> iterator =
contentNegotiationManager.getStrategies().listIterator();
while (iterator.hasNext()) {
ContentNegotiationStrategy strategy = iterator.next();
if (strategy.getClass().getName().contains("OptionalPathExtensionContentNegotiationStrategy")) {
iterator.set(new RemoveHandleNoMatchContentNegotiationStrategy());
}
}
return bean;
}
}
private static class RemoveHandleNoMatchContentNegotiationStrategy
extends PathExtensionContentNegotiationStrategy {
/**
* Don't lookup file extensions to match mime-type
* Effectively reverts to Spring Boot 1.5.7 behavior
*/
@Override
protected MediaType handleNoMatch(NativeWebRequest request, String key) {
return null;
}
}
}
第二种可以实施的解决方案是利用 OptionalPathExtensionContentNegotiationStrategy class 的功能,默认情况下由 Spring 使用。
基本上您需要做的是确保对您的 validateUsername
端点的每个 HTTP 请求都包含一个名为 org.springframework.web.accept.PathExtensionContentNegotiationStrategy.SKIP
的属性,其值为 true
This article 展示了如何阻止 Spring 使用路径扩展进行内容协商(至少为我工作 Spring Boot 1.5.x):
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
}
}
最短的可能解决方案是修改您的 URL,如下所示
@RequestMapping(value = "/validate-username/{username}/"
注意:我在 URL 的末尾使用了斜杠 '/'
。因此,URL 将完美适用于任何类型的电子邮件地址或 .com , .us
在您的 {username}
路径变量中包含文本。
您不需要向您的应用程序添加任何类型的额外配置。