Spring Boot扩展REST内容协商

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-Typeapplication/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

猜你喜欢

转载自blog.csdn.net/shang_xs/article/details/86682299