前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(2)

SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(2)

作者头像
干货满满张哈希
发布2021-12-30 15:57:15
5680
发布2021-12-30 15:57:15
举报
文章被收录于专栏:干货满满张哈希

本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务映射,通过健康检查接口判断实例健康状态,然后直接使用 OpenFeign 生成对应域名的 Feign Client。Spring Cloud 生态中,对 OpenFeign 进行了封装,其中的 Feign Client 的各个组件,也是做了一定的定制化,可以实现在 OpenFeign Client 中集成服务发现与负载均衡。在此基础上,我们还结合了 Resilience4J 组件,实现了微服务实例级别的线程隔离,微服务方法级别的断路器以及重试。

我们先来分析下 Spring Cloud OpenFeign

Spring Cloud OpenFeign 解析

HTTP 编码解码器,与 spring-boot 中的编码解码器相结合

Spring Cloud 中的任何组件,都是基于 Spring Boot 而实现的。由于 Spring Boot 中已经有了 HTTP 编码解码器,就可以不用单独给 OpenFeign 单独再实现 HTTP 编码解码器了,而是考虑将 OpenFeign 的编码解码器接口用 Spring Boot 的 HTTP 编码解码器实现。

在 FeignClientsConfiguration 中,提供了默认的实现:

代码语言:javascript
复制
//由于初始化顺序以及 NamedContextFactory 的 Configuration 初始化的原因,这里需要注入 ObjectFactory 而不是直接注入 HttpMessageConverters 防止找不到 Bean
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() { 
	return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}

@Bean
@ConditionalOnMissingBean
//我们这里忽略 Pageable 类存在的情况
//针对 Spring Data 的分页包装 Pageable 的兼容实现也比较简单,这里忽略
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
	return springEncoder(formWriterProvider, encoderProperties);
}

private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
	FeignEncoderProperties encoderProperties) {
    AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
    
    if (formWriter != null) {
    	return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters, encoderProperties);
    }
    else {
    	return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
    }
}
基于 SpringDecoder 的解码器

通过源码可以看出,默认的 Decoder 是经过几层包装的 Decoder,分别包括:

  • OptionalDecoder:用于处理 Java JDK 中的 Optional 封装类的解码器
  • ResponseEntityDecoder:用于处理 spring-web 中对于请求响应封装类 HttpEntity 的解码器
  • SpringDecoder:使用 Spring 的解码器实现的 Feign 的 Decoder

传入 SpringDecoder 的 HttpMessageConverters 对象,是 spring-web 的所有 HttpMessageConverter 集合。HttpMessageConverter 是 spring-web 中对于 HTTP 请求和响应的 body 进行编码解码的工具。其接口结构是:

代码语言:javascript
复制
public interface HttpMessageConverter<T> {
    //判断 clazz 类型是否可以被当前 HttpMessageConverter 所读取
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    //判断 clazz 类型是否可以被当前 HttpMessageConverter 写
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    //获取所有支持的 MediaType
	List<MediaType> getSupportedMediaTypes();
    //通过 clazz 类型获取该 HttpMessageConverter 支持的 MediaType
    //默认实现是,如果该类型可以被当前 HttpMessageConverter 读或者写,那么返回 getSupportedMediaTypes 所有支持的 MediaType
	default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
		return (canRead(clazz, null) || canWrite(clazz, null) ?
				getSupportedMediaTypes() : Collections.emptyList());
	}

    //从 inputMessage 中读取并解析出 clazz 类型的对象,当请求的 Content-Type 为支持的 MediaType 时
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

    //将对象 t 序列化写入 HttpOutputMessage,当请求的 accept 为支持的 MediaType 时
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

spring boot 内置了很多 HttpMessageConverter,我们也可以实现自己的 HttpMessageConverter,去实现我们自定义 MediaType,例如我们这里定义一个 :

代码语言:javascript
复制
public class CustomizedHttpMessageConverter implements HttpMessageConverter<Student> {
	@Override
	public boolean canRead(Class<?> clazz, MediaType mediaType) {
		return clazz.equals(Student.class);
	}

	@Override
	public boolean canWrite(Class<?> clazz, MediaType mediaType) {
		return clazz.equals(Student.class);
	}
	
	public static class StudentMediaType extends MediaType {
		public StudentMediaType() {
			super("application", "student", StandardCharsets.UTF_8);
		}
	}

	@Override
	public List<MediaType> getSupportedMediaTypes() {
		return List.of(new StudentMediaType());
	}

	@Override
	public Student read(Class<? extends Student> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
		String temp = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
		String[] split = temp.split(",");
		return new Student(
				Long.parseLong(split[0]),
				split[1]);
	}

	@Override
	public void write(Student student, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
		outputMessage.getBody().write((student.getId() + "," + student.getName()).getBytes(StandardCharsets.UTF_8));
	}
}

之后,与前面类似,将其配置到 spring boot 兼容 MVC 配置中:

代码语言:javascript
复制
@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
	@Override
	public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
		converters.add(new CustomizedHttpMessageConverter());
	}
}

编写 Controller,测试:

代码语言:javascript
复制
@RestController
@RequestMapping("/test")
public class TestController {
	@PostMapping("/post-to-student")
	public Student postToStudent(@RequestBody Student student) {
		return student;
	}
}

使用 postman 类似的工具,指定 HTTP 请求头:

代码语言:javascript
复制
Content-Type:application/student
Accept:application/student

Body 是:

代码语言:javascript
复制
1,zhx

请求后,就会走到 CustomizedHttpMessageConverter 的 read 解析成 Student 对象,之后响应的 student 也会被 CustomizedHttpMessageConverter 的 write 写入响应 Body

由此可见,由于 SpringEncoder 的存在,我们可以复用 Spring 内置的 HttpMessageConverter,同时也能扩展自定义我们自己的 HttpMessageConverter,非常方便

ResponseEntityDecoder 的代码比较简单,实现的效果就是解码的时候,忽略 HttpEntity 这个 spring-web 对于 HTTP 响应的包装类:

代码语言:javascript
复制
@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
    //是否是带有形参的 HttpEntity<?> 
	if (isParameterizeHttpEntity(type)) {
	    //将形参类型取出
		type = ((ParameterizedType) type).getActualTypeArguments()[0];
		//使用形参类型,解析 response
		Object decodedObject = this.decoder.decode(response, type);
        //填充 HttpEntity 其中的返回对象,状态码等信息
		return createResponse(decodedObject, response);
	}
	else if (isHttpEntity(type)) {
	    //空形参,代表没有 body 或者忽略 body,仅填充 HttpEntity 状态码等信息
		return createResponse(null, response);
	}
	else {
	    //如果不是 HttpEntity,直接解码
		return this.decoder.decode(response, type);
	}
}

这个其实为了和 RestTemplate 的响应兼容,RestTemplate 可以返回 HttpEntity,但是底层 HTTP 请求返回的 body 其实并没有包装这个类型。

同理,JDK 中的 Optional 包装类,也需要做同样的事情,这个就是通过 OptionalDecoder 实现的。

基于 SpringEncoder 的编码器

SpringEncoder 编码器也非常简单,也是基于 spring 中的 HttpMessageConverter。这里我们就不再赘述。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/11/08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Spring Cloud OpenFeign 解析
    • HTTP 编码解码器,与 spring-boot 中的编码解码器相结合
      • 基于 SpringDecoder 的解码器
      • 基于 SpringEncoder 的编码器
相关产品与服务
负载均衡
负载均衡(Cloud Load Balancer,CLB)提供安全快捷的四七层流量分发服务,访问流量经由 CLB 可以自动分配到多台后端服务器上,扩展系统的服务能力并消除单点故障。轻松应对大流量访问场景。 网关负载均衡(Gateway Load Balancer,GWLB)是运行在网络层的负载均衡。通过 GWLB 可以帮助客户部署、扩展和管理第三方虚拟设备,操作简单,安全性强。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档