从 Spring 的 rest 控制器同时支持 application/json 和 application/x-www-form-urlencoded

Posted

技术标签:

【中文标题】从 Spring 的 rest 控制器同时支持 application/json 和 application/x-www-form-urlencoded【英文标题】:Supporting application/json and application/x-www-form-urlencoded simultaneously from Spring's rest controller 【发布时间】:2018-06-04 02:26:09 【问题描述】:

我正在编写一个 REST 端点,它需要同时支持 application/x-www-form-urlencoded 和 application/json 作为请求正文。我做了以下配置,

@RequestMapping(method = RequestMethod.POST, produces =  MediaType.APPLICATION_JSON_VALUE , consumes =           
        MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE , path = Constants.ACCESS_TOKEN_V1_ENDPOINT)
public OAuth2Authorization createAccessTokenPost(
        @RequestBody(required = false) MultiValueMap<String, String> paramMap)  ..

虽然它单独支持 application/x-www-form-urlencoded 或 application/json(当我从 consumes = 中注释掉一种内容类型时),但它不能同时支持两者。有什么想法吗?

【问题讨论】:

您在日志中看到了什么异常? 您好,感谢您的回复。 org.springframework.web.HttpMediaTypeNotSupportedException:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:237) 不支持内容类型'application/json;charset=UTF-8' .springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:150) at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor。 Accept和Content-Type请求头分别携带什么值? 感谢您的回复。接受:application/x-www-form-urlencoded;内容类型:application/x-www-form-urlencoded 请查看以下链接:***.com/questions/42462450/… 【参考方案1】:

所以默认情况下 RestControllers 可以相当容易地处理 application/json 并且可以从 @RequestBody 注释参数创建请求 pojo,而 application/x-www-form-urlencoded 需要更多的工作。一个解决方案可能是创建一个额外的 RestController 方法,该方法具有相同的映射端点来处理传入的不同类型的请求(application/json, application/x-www-form-urlencoded 等)。这是因为application/x-www-form-urlencoded 端点需要使用@RequestParam 而不是@RequestBody 注释(用于application/json)。

例如,如果我想为 /emp 托管一个将 application/jsonapplication/x-www-form-urlencoded 作为 Content-Types 的 POST 端点并使用服务来做某事,我可以创建这样的重载方法

@Autowired
private EmpService empService;

    @PostMapping(path = "/emp", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity createEmp(final @RequestHeader(value = "Authorization", required = false) String authorizationHeader,
                                final @RequestParam Map<String, String> map) 
        //After receiving a FORM URLENCODED request, change it to your desired request pojo with ObjectMapper
        final ObjectMapper mapper = new ObjectMapper();
        final TokenRequest tokenRequest = mapper.convertValue(map, CreateEmpRequest.class);
        return empService.create(authorizationHeader, createEmpRequest);
    

    @PostMapping(path = "/emp", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity createEmp(final @RequestHeader(value = "Authorization", required = false) String authorizationHeader,
                                final @RequestBody CreateEmpRequest createEmpRequest) 
        //Receieved a JSON request, the @RequestBody Annotation can handle turning the body of the request into a request pojo without extra lines of code
        return empService.create(authorizationHeader, createEmpRequest);
    

【讨论】:

【参考方案2】:

根据我的发现,spring 不支持同时支持内容类型“application/x-www-form-urlencoded”、“application/json”和“application/xml”。

我想到的原因:Spring 通过解析 JSON 和 XML 类型并将其注入标有 @RequestBody spring 注释的 java pojo 来处理它们。但是,x-www-form-urlencoded 必须注入到标有@RequestBodyMultiValueMap&lt;&gt; 对象中。不会同时支持标有@RequestBody 的两种不同的java 类型,因为spring 可能不知道在哪里注入payload。

可行的解决方案

application/x-www-form-urlencoded”可以在 API 中得到支持。也就是可以使用@RequestBody注解注入到spring的MultiValueMap&lt;&gt;中。

为了在同一方法上支持 JSON 和 XML,我们可以利用 servlet 规范和基于它们构建的 spring 类将有效负载提取为流。

示例代码

import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.MultiValueMap;

// usual REST service class

@Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;

@Autowired
private Jaxb2RootElementHttpMessageConverter jaxb2RootElementHttpMessageConverter;

public ResponseEntity<Object> authorizationRequestPost(HttpServletResponse response, HttpServletRequest request,@RequestBody(required = false) MultiValueMap<String, String> parameters) 

    // this MultiValueMap<String,String> will contain key value pairs of "application/x-www-form-urlencoded" parameters.

    // payload object to be populated
    Authorization authorization = null;

    HttpInputMessage inputMessage = new ServletServerHttpRequest(request) 
                    @Override
                    public InputStream getBody() throws IOException 
                        return request.getInputStream();
                    
                ;

    if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) 
        authorization = (Authorization) mappingJackson2HttpMessageConverter.read(Authorization.class, inputMessage);
     
    else if (request.getContentType().equals(MediaType.APPLICATION_XML_VALUE)) 
        authorization = (Authorization)jaxb2RootElementHttpMessageConverter.read(Authorization.class, inputMessage);
    
    else
        // extract values from MultiValueMap<String,String> and populate Authorization
    

// remaining method instructions

请注意,使用这种方法可以支持任何自定义数据类型/标记/格式。 Spring的org.springframework.http.converter.HttpMessageConverter&lt;&gt;可以扩展写解析逻辑。

另一种可能的方法可能是执行相同逻辑的 AOP 样式解决方案:通过从 HttpServlet 输入流中提取有效负载并注入到有效负载对象中来解析有效负载。

第三种方法是编写一个过滤器来执行逻辑。

【讨论】:

【参考方案3】:

不可能使用单个 Spring 控制器方法同时处理 application/jsonapplication/x-www-form-urlencoded 请求。

Spring通过ServletRequest.getParameter(java.lang.String)获取application/x-www-form-urlencoded数据,文档说:

对于 HTTP servlet,参数包含在查询字符串或发布的表单数据中。

如果参数数据是在请求正文中发送的,例如发生在 HTTP POST 请求中,那么直接通过 getInputStream() 或 getReader() 读取正文可能会干扰该方法的执行。

所以,如果你的方法参数被注解@RequestBody,Spring会读取请求体并解析到方法参数对象。但是application/x-www-form-urlencoded 会导致Spring 通过调用ServletRequest.getParameter(java.lang.String) 来填充参数对象。

【讨论】:

【参考方案4】:

只是为了做到这一点,上面的answer 不起作用,因为即使您不使用@RequestBody 注释MultiValueMap,它也会始终检查contentType==MediaType.APPLICATION_FORM_URLENCODED_VALUE,在其余情况下再次解析为@987654325 @。

【讨论】:

以上是关于从 Spring 的 rest 控制器同时支持 application/json 和 application/x-www-form-urlencoded的主要内容,如果未能解决你的问题,请参考以下文章

SpringMVC restful风格

仅使用 Spring Security 自定义令牌无状态保护 REST 控制器,同时保持常规状态完整 Web 登录正常工作

使用 OData 接口开发 Spring REST 服务

Spring的Restful

Spring Webflux注解的rest控制器不支持ServerHttpRequest作为方法参数:java.lang.NoSuchMethodException

如何在 Spring Boot 中同时公开 SOAP Web 服务和 RESTful API?