Spring Boot扩展REST内容协商
题记
在java
开发中,Spring Boot
的应用范围越来越广,对于Spring Boot
的深入研究也显得尤为重要。那么为什么会使用Spring Boot
做我们基础框架的支撑呢,下面是Spring Boot
的介绍:
Create stand-alone Spring applications
-- 创建一个独立的Spring应用
Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
-- 嵌入式的web服务器Tomcat、Jetty、Undertow(无需部署 war 文件)
Provide opinionated 'starter' dependencies to simplify your build configuration
-- 提供建议的 "启动" 依赖关系, 以简化生成配置
Automatically configure Spring and 3rd party libraries whenever possible
-- 尽可能自动配置 spring 和第三方库
Provide production-ready features such as metrics, health checks and externalized configuration
-- 具备为生产准备的特性,如指标、运行状况检查和外部化配置
Absolutely no code generation and no requirement for XML configuration
-- 尽可能的无需代码生成并且无XML配置
话不多说,开始进入本次分享的主题,在Spring Boot
中@EnableAutoConfiguration
的使用显得十分普遍,使用越多遇到的问题可能也就越多。为什么笔者会这样说呢,因为在程序开发的过程中,引入新技术一般会有两个原因:
第一:更便捷,能明显提高开发效率,否则一般的团队很少会主动尝试新事物。毕竟新的就意味着有风险
第二:对程序有显著地提升(此处是指可以是性能,也可以是健壮性,可用性,并发处理能力等等)
为什么使用
在上面的陈述中,我们得出结论Spring Boot
给我们带来最大的好处是便捷,在日常的开发过程中Spring Boot
自动装配的特性给我们开发减少了很多重复性的工作,如何实现自动装配可以参照另外的文章Spring Boot自动装配
和一次Http请求到达 SpringMvc做了什么。
知道了Spring Boot
诸多便利,我们在一次REST
请求发起之后,我们都做收到了什么,返回的内容又是根据什么格式去实现的呢?
下图描述的是我们访问http://spring.io/
从请求头和响应头中我们知道如下参数:
名称 | 作用 |
---|---|
Content-Encoding | 客户端发送内容格式 |
Content-Type | 发送的实体数据的数据类型 |
Accept | 客户端希望返回的数据类型 |
Accept-Encoding | 客户端希望返回的内容格式 |
参考默认实现
REST内容协商交易分发
// Spring Web Mvc交易分发器
public class DispatcherServlet extends FrameworkServlet {
//执行分发动作
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 实际处理的handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
实际执行handler
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 处理请求信息,包括请求参数媒体类型选择及处理,和实际执行InvokcationHandler的调用
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
//执行handler调用完成,数据写回媒体类型选择及处理
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
}
throw ex;
}
}
处理请求参数,读取可适配的媒体类型
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType; //application/json;charset=UTF-8
boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
}
处理返回数据,查询可适配的媒体类型列表
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
...
List<MediaType> mediaTypesToUse;
MediaType contentType = outputMessage.getHeaders().getContentType();
if (contentType != null && contentType.isConcrete()) {
mediaTypesToUse = Collections.singletonList(contentType);
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);//application/json;charset=UTF-8
结果展示
当选择Content-Type
为application/json
媒体类型时,MappingJackson2HttpMessageConverter
作为被选中的媒体类型,进行处理
开始实现
经由上面文章了解源码之后,我们开始对REST
内容协商做更加深入的探讨。在此,我们实现的终极目标就是自定义实现text/user
媒体类型的格式
首先找到MappingJackson2HttpMessageConverter
是在什么时候被加载的
//在接口WebMvcConfigurer中,很容易找到extendMessageConverters这个扩展点
public interface WebMvcConfigurer {
...
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
在此我们添加自己的MessageConverter
@Configuration
public class RestWebMvcConfigurer implements WebMvcConfigurer {
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 不建议添加到 converters 的末尾,由于本次返回类型使用的是application/json,所以不影响结果,但是在自定义返回类型的时候需要保证UserHttpMessageConverter的优先级最高,如何保证,可参考List的特性,使用set(index,value)实现
converters.add(new UserHttpMessageConverter());
}
}
实现自定义UserHttpMessageConverter
public class UserHttpMessageConverter extends AbstractGenericHttpMessageConverter<User> {
public UserHttpMessageConverter() {
// 设置支持的 MediaType
super(new MediaType("text", "user"));
}
@Override
protected void writeInternal(User user, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//输出user信息
OutputStream outputStream = outputMessage.getBody();
byte[] bytes = JSON.toJSONString(user).getBytes();
outputStream.write(bytes);
}
@Override
protected User readInternal(Class<? extends User> 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);
User user = new User();
// 加载字符流成为 Properties 对象
Properties properties = new Properties();
properties.load(reader);
user.setId(Long.valueOf(properties.getProperty("id")));
user.setName(String.valueOf(properties.getProperty("name")));
return user;
}
@Override
public User read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return readInternal(null, inputMessage);
}
}
添加测试方法
@RestController
public class UserRestController {
/**
* 自定义媒体类型text/user
* @param user
* @return
*/
@PostMapping(value = "/echo/users",
consumes = "text/user;charset=UTF-8"
)
public User users(@RequestBody User user) {
return user;
}
}
开始测试
测试Content-Type调整
输入、输出结果展示
在此我们自定义的媒体类型生效,本次REST
内容协商基本功能实现,可扩展的地方还有(根据不同的媒体类型响应不同的头信息,作为客户端可根据此进行公共处理)。
往期文章
博客地址:https://blog.csdn.net/shang_xs
微信公众号地址:http://mp.weixin.qq.com/mp/homepage?__biz=MzUxMzk4MDc1OQ==&hid=2&sn=c6c58c06f6f8403af27b6743648b3055&scene=18#wechat_redirect