这一章节我们会写一个PropertiesHttpMessageConverter,这个Converter主要是可以接受Properties形式的请求参数,并以Properties的形式在返回给客户端。什么意思呢?一般我们请求体是这样写的:
{
name:1,
id:1
}
现在我们写的Converter要求按照如下形式传递也可以被解析:
name:1
id:1
1.我们先写一个SpringBoot的引导类
@SpringBootApplication(scanBasePackages = {
"com.imooc.web.controller",
"com.imooc.web.config"
})
public class SpringBootRestBootstrap {
public static void main(String[] args) {
SpringApplication.run(SpringBootRestBootstrap.class, args);
}
}
这个引导类没什么特殊的,不赘述
2.Controller
@RestController
public class PropertiesRestController {
@PostMapping(value = "/add/props",
consumes = "text/properties;charset=UTF-8" // Content-Type 过滤媒体类型
)
public Properties addProperties(@RequestBody Properties properties) {
return properties;
}
}
这个controller只能够处理contentType是text/properties的请求,Controller接受的是一个Properties类型的参数。
3.关键!PropertiesHttpMessageConverter
public class PropertiesHttpMessageConverter extends AbstractGenericHttpMessageConverter<Properties> {
public PropertiesHttpMessageConverter() {
// 设置支持的 MediaType
super(new MediaType("text", "properties"));
}
@Override
protected void writeInternal(Properties properties, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// Properties -> String
// OutputStream -> Writer
HttpHeaders httpHeaders = outputMessage.getHeaders();
MediaType mediaType = httpHeaders.getContentType();
// 获取字符编码
Charset charset = mediaType.getCharset();
// 当 charset 不存在时,使用 UTF-8
charset = charset == null ? Charset.forName("UTF-8") : charset;
// 字节输出流
OutputStream outputStream = outputMessage.getBody();
// 字符输出流
Writer writer = new OutputStreamWriter(outputStream, charset);
// Properties 写入到字符输出流
properties.store(writer,"From PropertiesHttpMessageConverter");
}
@Override
protected Properties readInternal(Class<? extends Properties> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
// 字符流 -> 字符编码
// 从 请求头 Content-Type 解析编码
HttpHeaders httpHeaders = inputMessage.getHeaders();
MediaType mediaType = httpHeaders.getContentType();
// 获取字符编码
Charset charset = mediaType.getCharset();
// 当 charset 不存在时,使用 UTF-8
charset = charset == null ? Charset.forName("UTF-8") : charset;
// 字节流
InputStream inputStream = inputMessage.getBody();
InputStreamReader reader = new InputStreamReader(inputStream, charset);
Properties properties = new Properties();
// 加载字符流成为 Properties 对象
properties.load(reader);
return properties;
}
@Override
public Properties read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return readInternal(null, inputMessage);
}
}
主要是把Http请求读取进来并转换成Properties对象。
4.生效
@Configuration
public class RestWebMvcConfigurer implements WebMvcConfigurer {
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 不建议添加到 converters 的末尾
converters.add(new PropertiesHttpMessageConverter());
converters.set(0, new PropertiesHttpMessageConverter()); // 添加到集合首位
}
}
把这个converter添加进去,然后还要把它设置到首位,否则的话Properties会被Mapping2Jackson这个converter转换,因为Properties默认可以被当做json格式返回的。这就是我们为什么要把PropertiesHttpMessageConverter给放到第一位的原因!
虽然这样做我们已经添加了我们想要的Converter,但是呢,这样做会有这么几个问题。首先我们的请求参数必须要有@RequestBody才可以生效,有没有别的办法呢?有的,自定义 HandlerMethodArgumentResolver,我们不依赖 @RequestBody , 实现 Properties 格式请求内容,解析为 Properties 对象的方法参数,实现步骤如下:
1.实现HandlerMethodArgumentResolver - PropertiesHandlerMethodArgumentResolver
public class PropertiesHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Properties.class.equals(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 复用 PropertiesHttpMessageConverter
PropertiesHttpMessageConverter converter = new PropertiesHttpMessageConverter();
ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;
// Servlet Request API
HttpServletRequest request = servletWebRequest.getRequest();
HttpInputMessage httpInputMessage = new ServletServerHttpRequest(request);
return converter.read(null, null, httpInputMessage);
}
}
其实resolveAugument的代码看似很多,但是和我们使用了之前的PropertiesHttpMessageConverter来完成操作,所以还好,重点看一下supportsParameter,虽然一行代码搞定了,但是它确实这里实现的关键点,表示了我们的请求参数必须是Properties类型,这个是我们可以让Controller拿掉@RequestBody注解的关键所在!
2.RequestMappingHandlerAdapter#setArgumentResolvers
@Configuration
public class RestWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
@PostConstruct
public void init() {
// 获取当前 RequestMappingHandlerAdapter 所有的 Resolver 对象
List<HandlerMethodArgumentResolver> resolvers = requestMappingHandlerAdapter.getArgumentResolvers();
List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>(resolvers.size() + 1);
// 添加 PropertiesHandlerMethodArgumentResolver 到集合首位
newResolvers.add(new PropertiesHandlerMethodArgumentResolver());
// 添加 已注册的 Resolver 对象集合
newResolvers.addAll(resolvers);
// 重新设置 Resolver 对象集合
requestMappingHandlerAdapter.setArgumentResolvers(newResolvers);
}
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 添加 PropertiesHandlerMethodArgumentResolver 到集合首位
// 添加自定义 HandlerMethodArgumentResolver,优先级低于内建 HandlerMethodArgumentResolver
// if (resolvers.isEmpty()) {
// resolvers.add(new PropertiesHandlerMethodArgumentResolver());
// } else {
// resolvers.set(0, new PropertiesHandlerMethodArgumentResolver());
// }
}
}
大家注意看一下addArgumentResolvers,和上面的messageConverter的添加方式很像,但是很可惜,即使我们这样添加了,而且使用了resolvers.set(0, new PropertiesHandlerMethodArgumentResolver());也不能让我们的PropertiesHandlerMethodArgumentResolver优先级很高,后台会采用MapMethodProcessor作为内置的处理器来处理我们的输入参数,然后报错,setCustomAugumentResolver即使设置到第一位,也不行,原因如下,看看RequestMappingHandlerAdapter的代码:
/**
* Provide resolvers for custom argument types. Custom resolvers are ordered
* after built-in ones. To override the built-in support for argument
* resolution use {@link #setArgumentResolvers} instead.
*/
public void setCustomArgumentResolvers(@Nullable List<HandlerMethodArgumentResolver> argumentResolvers) {
this.customArgumentResolvers = argumentResolvers;
}
英文说的很明显了,内置的优先,有办法解决这个问题吗?有,看下面
/**
* Configure the complete list of supported argument types thus overriding
* the resolvers that would otherwise be configured by default.
*/
public void setArgumentResolvers(@Nullable List<HandlerMethodArgumentResolver> argumentResolvers) {
if (argumentResolvers == null) {
this.argumentResolvers = null;
}
else {
this.argumentResolvers = new HandlerMethodArgumentResolverComposite();
this.argumentResolvers.addResolvers(argumentResolvers);
}
}
RequestMappingHandlerAdapter会由SpringBoot自动装配进来,我们可以把这个Adpater引入进来,直接进行操作,所以看到了在上面的init方法里面,我们把自定义的resolver放到了第一个里面~
这样我们的@RequestBody可以和我们讲再见了,那么有没有办法不依赖 @ResponseBody ,实现 Properties 类型方法返回值,转化为 Properties 格式内容响应内容,有的,其实很类似,不过我们还是来实现一下:
1.实现HandlerMethodReturnValueHandler - PropertiesHandlerMethodReturnValueHandler
public class PropertiesHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 判断方法的返回类型,是否与 Properties 类型匹配
return Properties.class.equals(returnType.getMethod().getReturnType());
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 强制装换
Properties properties = (Properties) returnValue;
// 复用 PropertiesHttpMessageConverter
PropertiesHttpMessageConverter converter = new PropertiesHttpMessageConverter();
ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;
// Servlet Request API
HttpServletRequest request = servletWebRequest.getRequest();
String contentType = request.getHeader("Content-Type");
// 获取请求头 Content-Type 中的媒体类型
MediaType mediaType = MediaType.parseMediaType(contentType);
// 获取 Servlet Response 对象
HttpServletResponse response = servletWebRequest.getResponse();
HttpOutputMessage message = new ServletServerHttpResponse(response);
// 通过 PropertiesHttpMessageConverter 输出
converter.write(properties, mediaType, message);
// 告知 Spring Web MVC 当前请求已经处理完毕
mavContainer.setRequestHandled(true);
}
}
2.RequestMappingHandlerAdapter#setReturnValueHandlers
@Configuration
public class RestWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
@PostConstruct
public void init() {
// 获取当前 RequestMappingHandlerAdapter 所有的 Resolver 对象
List<HandlerMethodArgumentResolver> resolvers = requestMappingHandlerAdapter.getArgumentResolvers();
List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>(resolvers.size() + 1);
// 添加 PropertiesHandlerMethodArgumentResolver 到集合首位
newResolvers.add(new PropertiesHandlerMethodArgumentResolver());
// 添加 已注册的 Resolver 对象集合
newResolvers.addAll(resolvers);
// 重新设置 Resolver 对象集合
requestMappingHandlerAdapter.setArgumentResolvers(newResolvers);
// 获取当前 HandlerMethodReturnValueHandler 所有的 Handler 对象
List<HandlerMethodReturnValueHandler> handlers = requestMappingHandlerAdapter.getReturnValueHandlers();
List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>(handlers.size() + 1);
// 添加 PropertiesHandlerMethodReturnValueHandler 到集合首位
newHandlers.add(new PropertiesHandlerMethodReturnValueHandler());
// 添加 已注册的 Handler 对象集合
newHandlers.addAll(handlers);
// 重新设置 Handler 对象集合
requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);
}
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 添加 PropertiesHandlerMethodArgumentResolver 到集合首位
// 添加自定义 HandlerMethodArgumentResolver,优先级低于内建 HandlerMethodArgumentResolver
// if (resolvers.isEmpty()) {
// resolvers.add(new PropertiesHandlerMethodArgumentResolver());
// } else {
// resolvers.set(0, new PropertiesHandlerMethodArgumentResolver());
// }
}
}