自定义Spring Boot 内容协商

发布时间:2026/9/16 10:49:16
自定义Spring Boot 内容协商 一、前言内容协商机制是指客户端和服务器端就响应的资源内容进行交涉然后提供给客户端最为适合的资源。内容协商会以响应资源的语言、字符集、编码方式等作为判断的基准。HTTP请求头中Content-TypeAccept等内容就是内容协商判断的标准。在Spring Boot中一个完整的内容协商过程如下图所示这个过程的核心组件组件名称说明ContentNegotiationManager内容协商管理器ContentNegotiationStrategy 控制策略MediaType媒体类型HTTP 消息媒体类型如 text/htmlRequestMapping#consumes消费媒体类型请求头 Content-Type 媒体类型映射RequestMapping#produces生产媒体类型响应头 Content-Type 媒体类型映射HttpMessageConverterHTTP消息转换器接口HTTP 消息转换器用于反序列化 HTTP 请求或序列化响应WebMvcConfigurerWeb MVC 配置器配置 REST 相关的组件HandlerMethod处理方法RequestMapping 标注的方法HandlerMethodArgumentResolver处理方法参数解析器用于 HTTP 请求中解析 HandlerMethod 参数内容HandlerMethodReturnValueHandler处理方法返回值解析器用于 HandlerMethod 返回值解析为 HTTP 响应内容HttpMessageConverter为HTTP消息转换接口Spring根据不同的媒体类型进行了相应的实现。比如上图中Accept为application/json所以在第7步中会选择使用HttpMessageConverter的实现类MappingJackson2HttpMessageConverter来处理返回值。二、自定义HttpMessageConverter2.1 介绍除了Spring给我们提供的HttpMessageConverter实现外我们也可以自定义HttpMessageConverter的实现来处理一些实际业务需求。假如现在要实现一个用于处理 Content-Type 为 text/properties 媒体类型的 HttpMessageConverter 实现类 PropertiesHttpMessageConverter当我们在请求体中传输下面内容时name:wno704 age:18能够自动转换为Properties对象。我们可以参照MappingJackson2HttpMessageConverter的实现方式来进行实现查看MappingJackson2HttpMessageConverter的原型图所以我们可以通过继承AbstractGenericHttpMessageConverter的方式来实现HttpMessageConverter接口。2.2 AbstractGenericHttpMessageConverter新建Spring Boot项目并引入spring-boot-starter-web依赖新建converter包然后创建PropertiesHttpMessageConverter继承AbstractGenericHttpMessageConverterpublicclassPropertiesHttpMessageConverterextendsAbstractGenericHttpMessageConverterProperties{OverrideprotectedvoidwriteInternal(Propertiesproperties,Typetype,HttpOutputMessageoutputMessage)throwsIOException,HttpMessageNotWritableException{}OverrideprotectedPropertiesreadInternal(Class?extendsPropertiesclazz,HttpInputMessageinputMessage)throwsIOException,HttpMessageNotReadableException{returnnull;}OverridepublicPropertiesread(Typetype,Class?contextClass,HttpInputMessageinputMessage)throwsIOException,HttpMessageNotReadableException{returnnull;}}其中readInternal为反序列化过程即将HTTP请求反序列化为参数的过程writeInternal为序列化过程将响应序列化。2.3 反序列化过程我们继续编写PropertiesHttpMessageConverterpublicclassPropertiesHttpMessageConverterextendsAbstractGenericHttpMessageConverterProperties{OverrideprotectedvoidwriteInternal(Propertiesproperties,Typetype,HttpOutputMessageoutputMessage)throwsIOException,HttpMessageNotWritableException{}OverrideprotectedPropertiesreadInternal(Class?extendsPropertiesclazz,HttpInputMessageinputMessage)throwsIOException,HttpMessageNotReadableException{PropertiespropertiesnewProperties();// 获取请求头HttpHeadersheadersinputMessage.getHeaders();// 获取 content-typeMediaTypecontentTypeheaders.getContentType();// 获取编码Charsetcharsetnull;if(contentType!null){charsetcontentType.getCharset();}charsetcharsetnull?Charset.forName(UTF-8):charset;// 获取请求体InputStreambodyinputMessage.getBody();InputStreamReaderinputStreamReadernewInputStreamReader(body,charset);properties.load(inputStreamReader);returnproperties;}OverridepublicPropertiesread(Typetype,Class?contextClass,HttpInputMessageinputMessage)throwsIOException,HttpMessageNotReadableException{returnreadInternal(null,inputMessage);}}在readInternal方法中我们获取了HTTP请求体中的输入流和编码然后调用Properties对象的load方法来将流转换为Properties对象。反序列化过程完成了我们还需将PropertiesHttpMessageConverter添加到HttpMessageConverter集合中。新建config包然后创建WebConfig配置类ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{publicvoidextendMessageConverters(ListHttpMessageConverter?converters){converters.add(newPropertiesHttpMessageConverter());}}extendMessageConverters方法为WebMvcConfigurer的默认方法这里我们重写这个方法用于将PropertiesHttpMessageConverter添加到消息转换器集合中。接着创建一个Controller来测试一波新建controller包然后创建TestControllerRestControllerpublicclassTestController{GetMapping(valuetest,consumestext/properties)publicPropertiestest(RequestBodyPropertiesproperties){returnproperties;}}我们通过GetMapping注解的consumes属性指定了方法接收的媒体类型为text/properties如果方法能够成功被调用并且可以返回Properties对象则说明我们自定义的HTTP消息转换器是可行的。启动项目使用HTTP访问2020-09-10 23:34:17.553 WARN 15492 — [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.util.Properties com.wno704.boot.controller.TestController.test(java.util.Properties)]因为我们在自定义PropertiesHttpMessageConverter的时候需要在其构造器中指定它能够处理的媒体类型我们查看MappingJackson2HttpMessageConverter的构造器看看它是怎么实现的/** * Construct a new {link MappingJackson2HttpMessageConverter} with a custom {link ObjectMapper}. * You can use {link Jackson2ObjectMapperBuilder} to build it easily. * see Jackson2ObjectMapperBuilder#json() */publicMappingJackson2HttpMessageConverter(ObjectMapperobjectMapper){super(objectMapper,MediaType.APPLICATION_JSON,newMediaType(application,*json));}所以我们在PropertiesHttpMessageConverter的构造器中添加相应的媒体类型publicPropertiesHttpMessageConverter(){super(newMediaType(text,properties));}2.4 序列化过程序列化的过程就是处理HTTP响应的过程对应PropertiesHttpMessageConverter的writeInternal方法。那为什么我们都还实现这个方法上面的Controller请求却能返回正常的JSON内容呢两个原因1.这里我们定义的是REST接口所以响应默认会被序列化为JSON格式2.由于使用converters.add(new PropertiesHttpMessageConverter());这种方式来添加自定义HTTP消息处理器默认会被添加到集合的末尾在处理JSON响应的时候被排在前面的MappingJackson2HttpMessageConverter优先处理了。所以我们要换下面这种方式来添加自定义HTTP处理器ConfigurationpublicclassWebConfig{publicvoidextendMessageConverters(ListHttpMessageConverter?converters){//converters.add(new PropertiesHttpMessageConverter());converters.add(0,newPropertiesHttpMessageConverter());}}继续实现PropertiesHttpMessageConverter的writeInternal方法OverrideprotectedvoidwriteInternal(Propertiesproperties,Typetype,HttpOutputMessageoutputMessage)throwsIOException,HttpMessageNotWritableException{// 获取请求头HttpHeadersheadersoutputMessage.getHeaders();// 获取 content-typeMediaTypecontentTypeheaders.getContentType();// 获取编码Charsetcharsetnull;if(contentType!null){charsetcontentType.getCharset();}charsetcharsetnull?Charset.forName(UTF-8):charset;// 获取请求体OutputStreambodyoutputMessage.getBody();OutputStreamWriteroutputStreamWriternewOutputStreamWriter(body,charset);properties.store(outputStreamWriter,Serialized by PropertiesHttpMessageConverter#writeInternal);}过程和反序化差不多这里是通过Properties对象的store方法来进行写操作。三、自定义HandlerMethodArgumentResolver上面这种方式必须依赖于RequestBody和ResponseBody注解除此之外我们还可以通过自定义HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler实现类的方式来处理内容协商。HandlerMethodArgumentResolver俗称方法参数解析器用于解析由RequestMapping注解或其派生的注解所标注的方法的参数。这里我们开始通过实现HandlerMethodArgumentResolver的方式来将HTTP请求体的内容自动解析为Properties对象。在com.example.demo路径下新建resolver包然后创建PropertiesHandlerMethodReturnValueHandler实现HandlerMethodArgumentResolver接口publicclassPropertiesHandlerMethodArgumentResolverimplementsHandlerMethodArgumentResolver{OverridepublicbooleansupportsParameter(MethodParameterparameter){returnProperties.class.equals(parameter.getParameterType());}OverridepublicObjectresolveArgument(MethodParameterparameter,ModelAndViewContainermavContainer,NativeWebRequestwebRequest,WebDataBinderFactorybinderFactory)throwsException{ServletWebRequestservletWebRequest(ServletWebRequest)webRequest;HttpServletRequestrequestservletWebRequest.getRequest();StringcontentTyperequest.getHeader(Content-Type);MediaTypemediaTypeMediaType.parseMediaType(contentType);// 获取编码CharsetcharsetmediaType.getCharset()null?Charset.forName(UTF-8):mediaType.getCharset();// 获取输入流InputStreaminputStreamrequest.getInputStream();InputStreamReaderinputStreamReadernewInputStreamReader(inputStream,charset);// 输入流转换为 PropertiesPropertiespropertiesnewProperties();properties.load(inputStreamReader);returnproperties;}}方法supportsParameter用于指定支持解析的参数类型这里为Properties类型。resolveArgument方法用于实现解析逻辑解析过程和上面定义的PropertiesHttpMessageConverter的readInternal方法类似。接着我们还需将PropertiesHandlerMethodArgumentResolver添加到Spring自带的HandlerMethodArgumentResolver实现类集合中。值得注意的是我们不能在配置类WebMvcConfigurer中通过重写addArgumentResolvers的方式来添加。所以我们可以通过下面这个方式来实现ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{AutowiredprivateRequestMappingHandlerAdapterrequestMappingHandlerAdapter;PostConstructpublicvoidinit(){// 获取当前 RequestMappingHandlerAdapter 所有的 ArgumentResolver对象ListHandlerMethodArgumentResolverargumentResolversrequestMappingHandlerAdapter.getArgumentResolvers();ListHandlerMethodArgumentResolvernewArgumentResolversnewArrayList(argumentResolvers.size()1);// 添加 PropertiesHandlerMethodArgumentResolver 到集合第一个位置newArgumentResolvers.add(0,newPropertiesHandlerMethodArgumentResolver());// 将原 ArgumentResolver 添加到集合中newArgumentResolvers.addAll(argumentResolvers);// 重新设置 ArgumentResolver对象集合requestMappingHandlerAdapter.setArgumentResolvers(newArgumentResolvers);}}我们在WebConfigurer配置类装配完毕的时候通过requestMappingHandlerAdapter对象的setArgumentResolvers方法来重新设置方法解析器集合将PropertiesHandlerMethodArgumentResolver添加到集合的第一个位置。之所以要将PropertiesHandlerMethodArgumentResolver添加到第一个位置是因为Properties本质也是一个Map对象而Spring内置的MapMethodProcessor就是用于处理Map参数类型的如果不将PropertiesHandlerMethodArgumentResolver优先级提高那么Properties类型参数会被MapMethodProcessor解析从而出错。配置完毕后我们改造一下TestControllerControllerpublicclassTestController2{GetMapping(valuetest2,consumestext/properties)ResponseBodypublicPropertiestest(RequestBodyPropertiesproperties){returnproperties;}GetMapping(valuetest3,consumestext/properties)ResponseBodypublicPropertiestest1(Propertiesproperties){returnproperties;}}启动项目使用http工具访问请求头中指定Content-Type为text/propertiestest2方法的参数没有被RequestBody标注访问下面这个请求可以看到方法成功执行并且返回了正确的内容说明我们自定义的方法参数解析器PropertiesHandlerMethodArgumentResolver是可行的。但是方法的返回值还是由PropertiesHttpMessageConverter的writeInternal方法解析的并且依赖于ResponseBody注解接着我们开始实现自定义方法返回值解析器并且不依赖于ResponseBody注解。四、自定义HandlerMethodReturnValueHandlerHandlerMethodArgumentResolver俗称方法返回值解析器用于解析由RequestMapping注解或其派生的注解所标注的方法的返回值。这里我们开始通过实现HandlerMethodReturnValueHandler的方式来自定义一个用于处理返回值类型为Properties类型的解析器。新建handler包然后创建PropertiesHandlerMethodReturnValueHandler实现HandlerMethodReturnValueHandlerpublicclassPropertiesHandlerMethodReturnValueHandlerimplementsHandlerMethodReturnValueHandler{OverridepublicbooleansupportsReturnType(MethodParameterreturnType){returnProperties.class.equals(returnType.getMethod().getReturnType());}OverridepublicvoidhandleReturnValue(ObjectreturnValue,MethodParameterreturnType,ModelAndViewContainermavContainer,NativeWebRequestwebRequest)throwsException{Propertiesproperties(Properties)returnValue;ServletWebRequestservletWebRequest(ServletWebRequest)webRequest;HttpServletResponseresponseservletWebRequest.getResponse();ServletServerHttpResponseservletServerHttpResponsenewServletServerHttpResponse(response);// 获取请求头HttpHeadersheadersservletServerHttpResponse.getHeaders();MediaTypecontentTypeheaders.getContentType();// 获取编码Charsetcharsetnull;if(contentType!null){charsetcontentType.getCharset();}charsetcharsetnull?Charset.forName(UTF-8):charset;// 获取请求体OutputStreambodyservletServerHttpResponse.getBody();OutputStreamWriteroutputStreamWriternewOutputStreamWriter(body,charset);properties.store(outputStreamWriter,Serialized by PropertiesHandlerMethodReturnValueHandler#handleReturnValue);}}supportsReturnType方法指定了处理返回值的类型handleReturnValue方法用于处理返回值这里的逻辑和PropertiesHttpMessageConverter的writeInternal方法基本一致不再赘述。接着将PropertiesHandlerMethodReturnValueHandler添加到到Spring自带的HandlerMethodReturnValueHandler实现类集合中添加方式和自定义HandlerMethodArgumentResolver一致ConfigurationpublicclassWebConfig{AutowiredprivateRequestMappingHandlerAdapterrequestMappingHandlerAdapter;PostConstructpublicvoidinit(){// 获取当前 RequestMappingHandlerAdapter 所有的 ArgumentResolver对象ListHandlerMethodArgumentResolverargumentResolversrequestMappingHandlerAdapter.getArgumentResolvers();ListHandlerMethodArgumentResolvernewArgumentResolversnewArrayList(argumentResolvers.size()1);// 添加 PropertiesHandlerMethodArgumentResolver 到集合第一个位置newArgumentResolvers.add(0,newPropertiesHandlerMethodArgumentResolver());// 将原 ArgumentResolver 添加到集合中newArgumentResolvers.addAll(argumentResolvers);// 重新设置 ArgumentResolver对象集合requestMappingHandlerAdapter.setArgumentResolvers(newArgumentResolvers);// 获取当前 RequestMappingHandlerAdapter 所有的 returnValueHandlers对象ListHandlerMethodReturnValueHandlerreturnValueHandlersrequestMappingHandlerAdapter.getReturnValueHandlers();ListHandlerMethodReturnValueHandlernewReturnValueHandlersnewArrayList(returnValueHandlers.size()1);// 添加 PropertiesHandlerMethodReturnValueHandler 到集合第一个位置newReturnValueHandlers.add(0,newPropertiesHandlerMethodReturnValueHandler());// 将原 returnValueHandlers 添加到集合中newReturnValueHandlers.addAll(returnValueHandlers);// 重新设置 ReturnValueHandlers对象集合requestMappingHandlerAdapter.setReturnValueHandlers(newReturnValueHandlers);}publicvoidextendMessageConverters(ListHttpMessageConverter?converters){//converters.add(new PropertiesHttpMessageConverter());converters.add(0,newPropertiesHttpMessageConverter());}}配置好后我们将TestController的test3方法的ResponseBody注解去掉重启项目再次访问:可以看到返回值成功被PropertiesHandlerMethodReturnValueHandler的handleReturnValue方法解析了。但是这里还有一个问题我们查看控制台会发现如下异常:这是因为在Spring中如果Controller中的方法没有被ResponseBody标注的话默认会把返回值当成视图的名称而这里我们并不希望解析的Properties值被当成视图名称所以我们需要在PropertiesHandlerMethodReturnValueHandler的handleReturnValue方法最后一行添加如下代码mavContainer.setRequestHandled(true);这行代码告诉Spring请求已经成功完成了无需进行后续的处理。重启项目再次访问上面的请求控制台便不再抛异常了。