Feign
返回值解析
在我们项目中,微服务的所有返回参数都规定了格式,webResponse.java:
{
"success": true,
"message": "成功",
"code": 20131003,
"data": {} }
所以,对于从微服务而来的返回值,可以统一去做处理。在项目中规定所有从微服务而来的返回都带返回头
“X-Service-Response”,从而可以对带有这个响应头的返回进行特殊处理。
public class ServiceResponseDecoder implements Decoder {
private Decoder decoder;
public ServiceResponseDecoder(Decoder decoder) {
this.decoder = decoder;
}
static final List<String> JSON_TYPE_HEADERS = Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8_VALUE
, MediaType.APPLICATION_JSON_VALUE, APPLICATION_JSON_UTF8_VALUE);
@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
if (!isJsonResponse(response) || !isServiceResponse(response)) {
return decoder.decode(response, type);
}
WebReponse serviceResponse = decoder.decode(response, WebReponse.class);
if (!serviceResponse.isSuccess()) {
throw new BaseException(serviceResponse.getCode(),serviceResponse.getMessage());
}
return JsonUtil.toBean(serviceResponse.getData(), ((Class) type));
}
private boolean isServiceResponse(Response response) {
return !ObjectUtils.isEmpty(response.headers().get("X-Service-Response"));
}
private boolean isJsonResponse(Response response) {
return JSON_TYPE_HEADERS.stream()
.anyMatch(
jsonType -> jsonType.equals(
Optional.of(response.headers().get(HttpHeaders.CONTENT_TYPE))
.orElseGet(() -> Lists.newArrayList())
.stream()
.findFirst()
.get()
)
);
}
}
错误日志
feign的ErrorDecoder接口中,有个Default类,是feign默认的errorDecode方式。处理的范围是响应码不在2xx范围内的。在这里,虽然error被解析了,但是被拼成了一个字符串传给了应用层。对于我们的项目开发来说,调用接口出错时,必然需要打印日志,所以可以实现ErrorDecoder接口,完成异常日志打印的功能。
请求拦截
由于项目需求,我们有个微服务,起了多个实例,每个实例以服务名+UUID的方式注册在Eureka中,而且可能会变更。但FeignClient的加载是在程序启动时加载的,如果每个实例都配置FeignClient,既达不到动态的要求,也使得代码非常冗余。而使用Feign.builder()去创建个客户端又和整体代码风格完全迥异。阅读FeignClient的文档后,发现提供请求参数拦截的功能,于是可以使用此功能完成要求。
继承LoadBalancerFeignClient,实现其方法,代码如下所示。可以看到,在url的请求中,将FeignClient生成的http://clientName/path
转为了http://mc-client-name-uuid/path
的方式。由于拦截替换是在ribbon处理之前,所以,ribbon在截取clientName时,会使用mc-client-name-uuid来替换之前额度clientName。当然,除此之外,请求参数、请求头等均可在此处理。
@Override
public Response execute(Request request, Request.Options options) throws IOException {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
StringBuffer newURL = new StringBuffer(10).append(asUri.getScheme())
.append("://")
.append(MC_CLIENT_NAME)
.append("-")
.append(request.headers().get(MC_ID).stream().findFirst().get())
.append("/")
.append(asUri.getPath());
if (StringUtils.hasLength(asUri.getQuery())){
newURL.append("?").append(asUri.getQuery());
}
return super.execute(Request.create(request.method(), newURL.toString(), request.headers(), request.body(), request.charset()), options);
}
文件上传
原生的FeignClient不支持文件上传,但FeignClient是基于OpenFeign做的集成,而OpenFeign是支持文件上传的,那么就可以扩展FeignClient的Configuration,使其可以支持文件上传。
引入OpenFeign的两个依赖
compile "io.github.openfeign.form:feign-form:3.0.0"
compile "io.github.openfeign:feign-jackson:9.5.1"
继承FeignClient的配置文件,替换默认的对象配置。
class FormDataFeignConfiguration extends FeignClientsConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
@Primary
Encoder feignFormEncoder() {
return new FormEncoder(new SpringEncoder(this.messageConverters));
}
@Override
public Encoder feignEncoder() {
return new FormEncoder(new JacksonEncoder());
}
@Override
public Decoder feignDecoder() {
//解析文件服务器返回的参数
return new FileResponseDecoder(super.feignDecoder());
}
@Primary
@Bean
public Contract useFeignAnnotations() {
return new Contract.Default();
}
}
上传文件FeignClient接口为:
@RequestLine("POST /v1/file/upload")
@Headers("Content-Type: multipart/form-data")
JsonNode upload(@Param("file") File file, @Param("name") String fileName, @Param("type") String fileType);