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 的过程继续。

两个版本的区别归结为this and this

现在更有趣的部分来了,这当然是修复。 我有两个解决方案,但我只展示第一个,只提第二个。

这是第一个基于我对问题的解释的解决方案。 我承认这个解决方案确实看起来有点侵入性,但它就像一个魅力。

我们需要做的是改变 auto-configured ContentNegotiationManager 以便用我们自己定制的 PathExtensionContentNegotiationStrategy 替换提供的 PathExtensionContentNegotiationStrategyBeanPostProcessor.

可以很容易地执行这样的操作
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} 路径变量中包含文本。 您不需要向您的应用程序添加任何类型的额外配置。