本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent
在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务映射,通过健康检查接口判断实例健康状态,然后直接使用 OpenFeign 生成对应域名的 Feign Client。Spring Cloud 生态中,对 OpenFeign 进行了封装,其中的 Feign Client 的各个组件,也是做了一定的定制化,可以实现在 OpenFeign Client 中集成服务发现与负载均衡。在此基础上,我们还结合了 Resilience4J 组件,实现了微服务实例级别的线程隔离,微服务方法级别的断路器以及重试。
我们先来分析下 Spring Cloud OpenFeign
Spring Cloud 中的任何组件,都是基于 Spring Boot 而实现的。由于 Spring Boot 中已经有了 HTTP 编码解码器,就可以不用单独给 OpenFeign 单独再实现 HTTP 编码解码器了,而是考虑将 OpenFeign 的编码解码器接口用 Spring Boot 的 HTTP 编码解码器实现。
在 FeignClientsConfiguration 中,提供了默认的实现:
//由于初始化顺序以及 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);
}
}
通过源码可以看出,默认的 Decoder 是经过几层包装的 Decoder,分别包括:
传入 SpringDecoder 的 HttpMessageConverters 对象,是 spring-web 的所有 HttpMessageConverter 集合。HttpMessageConverter 是 spring-web 中对于 HTTP 请求和响应的 body 进行编码解码的工具。其接口结构是:
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,例如我们这里定义一个 :
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 配置中:
@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CustomizedHttpMessageConverter());
}
}
编写 Controller,测试:
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/post-to-student")
public Student postToStudent(@RequestBody Student student) {
return student;
}
}
使用 postman 类似的工具,指定 HTTP 请求头:
Content-Type:application/student
Accept:application/student
Body 是:
1,zhx
请求后,就会走到 CustomizedHttpMessageConverter 的 read 解析成 Student 对象,之后响应的 student 也会被 CustomizedHttpMessageConverter 的 write 写入响应 Body
由此可见,由于 SpringEncoder 的存在,我们可以复用 Spring 内置的 HttpMessageConverter,同时也能扩展自定义我们自己的 HttpMessageConverter,非常方便。
ResponseEntityDecoder 的代码比较简单,实现的效果就是解码的时候,忽略 HttpEntity 这个 spring-web 对于 HTTP 响应的包装类:
@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 编码器也非常简单,也是基于 spring 中的 HttpMessageConverter。这里我们就不再赘述。