Spring Boot 2.0.0.M4和@PathVariable的电子邮件地址给出了HTTP 500错误

时间:2017-10-06 12:39:50

标签: spring-mvc tomcat spring-boot spring-restcontroller

我正在尝试从Spring Boot .innerHTML迁移到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状态代码和非JSON内容类型,如下所示:

enter image description here

当用户名PathVariable最后包含500时,我可以重现此问题。

我使用嵌入式Tomcat作为应用程序服务器。 Woth Spring Boot 1.5.7相同的功能工作正常。如何使用Spring Boot 2.0.0.M4?

P.S。

我知道将电子邮件地址作为网址参数发送是一种不好的做法。我只是对这个特殊情况感兴趣。

4 个答案:

答案 0 :(得分:1)

试试这个好友,{username:.+}而不是{username}

编辑:我发现@是URL的保留字符,不应该在URL中使用。您需要对您的网址进行编码。

类似的东西:alex@test.com - &gt;亚历克斯%40test.com

来源:Another stackoverflow question

答案 1 :(得分:1)

您正在观察的问题深入到Spring WebMvc内部。

根本原因是Spring正在推测接受的响应类型。 详细地说,在alex@test.com的情况下实际为接受的响应类型提供答案的策略类是ServletPathExtensionContentNegotiationStrategy,它根据路径中的内容进行猜测。

由于com是有效的文件扩展名类型(请参阅this),Spring Boot 2.0.0.M4会尝试使用该mime类型转换您{{1}的响应}类到那个mime类型(当然也失败了),因此回到它的默认错误响应。

首先解决此问题的方法是使用值指定HTTP标头ControllerAdvice Accept

不幸的是,Spring 2.0.0.M4仍然不会使用这个mime类型,因为ServletPathExtensionContentNegotiationStrategy策略优先于HeaderContentNegotiationStrategy

此外使用application/json或(就像alex这样的事情),Spring没有猜到mime类型,因此允许常规流程继续进行。

这是有效的原因是Spring Boot 1.5.7.RELEASE是Spring没有尝试将com映射到mime类型,因此使用了一个默认的响应类型,它允许将响应对象转换为JSON的过程继续。

两个版本之间的差异归结为thisthis

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

这是第一个以我对问题的解释为基础的解决方案。 我承认这个解决方案确实有点干扰,但它的作用就像一个魅力。

我们需要做的是更改自动配置的alex@test.gr,以便用我们自己的自定义ContentNegotiationManager替换提供的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;
        }
    }
}

可以实现的第二个解决方案是利用Spring默认使用的OptionalPathExtensionContentNegotiationStrategy类的功能。

基本上,您需要做的是确保对validateUsername端点的每个HTTP请求都包含名为org.springframework.web.accept.PathExtensionContentNegotiationStrategy.SKIP的属性,其值为true

答案 2 :(得分:1)

This article显示了如何阻止Spring使用路径扩展进行内容协商(至少对我来说是Spring Boot 1.5.x有用):

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override 
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 
        configurer.favorPathExtension(false); 
    }
}

答案 3 :(得分:1)

最短的解决方案是按如下所示修改您的网址

@RequestMapping(value = "/validate-username/{username}/"

注意:我在URL末尾使用斜杠'/'。因此,该URL可以完美地适用于.com , .us路径变量中包含文本的任何类型的电子邮件地址或{username}。 您无需在应用程序中添加任何种类的额外配置。