以下内容是在实际开发中遇到的问题整理,结合多方资料整理的,如侵权,请联系本人整改。
在实际开发中,使用feign时遇到以下几个问题,将解决办法附在后面,代码可用。
1.feign不支持文件上传;
2.feign服务间调用数据超过10M会报错,报错如下;
Could not read document: UT000020 : Connection terminated as request was larger than 10485769;
nested exception is java.io.Exception:UT000020 : Connection terminated as request was larger than 10485769;
3.feign调用时返回字段为null问题:
list为null时显示[],字符串为null显示""等,解决方案是使用fastjson替换Jacksom(springboot2.1.1默认采用的json converter是MappingJackson,需要移除),要达到的效果:
4.feign不支持多pojo实体问题,查了很多资料,都是将多实体封装为map,本博客代码完全解决这个问题,可以直接传多个实体,看配置代码,可以直接使用;
以上所有问题全部解决方案参考如下代码,这是一个老外写的解决方案,不仅支持传递文件、文件数组,而且支持传递多个实体。
1.定义feign配置类
package com.config.feign;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import feign.Contract;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@configuration
public class FeignConfig {
// 启用Fegin自定义注解 如@RequestLine @Param
@Bean
public Contract feignContract(){
return new Contract.Default();
}
//feign 实现多pojo传输与MultipartFile上传 编码器,需配合开启feign自带注解使用
//此处实现是靠第二步自定义的表单编码器
@Bean
public Encoder feignEncoder() {
return new FeignSpringFormEncoder();
}
//消息转换器
@Bean
public Decoder feignDecoder(){
return new SpringDecoder(feignHttpMessageConverter());
}
private ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
final HttpMessageConverters httpMessageConverters =
new HttpMessageConverters(getFastJsonConverter());
return () -> httpMessageConverters;
}
private FastJsonHttpMessageConverter getFastJsonConverter() {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
List<MediaType> supportedMediaTypes = new ArrayList<>();
MediaType mediaTypeJson =
MediaType.valueOf(MediaType.APPLICATION_JSON_UTF8_VALUE);
supportedMediaTypes.add(mediaTypeJson);
converter.setSupportedMediaTypes(supportedMediaTypes);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat,SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.WriteMapNullValue,
SerializerFeature.DisableCircularReferenceDetect);
// 设置时间格式
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
converter.setFastJsonConfig(fastJsonConfig);
return converter;
}
}
2.自定义表单编码器
package com.xhwl.order.config.feign;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* A custom {@link feign.codec.Encoder} that supports Multipart requests. It uses
* {@link HttpMessageConverter}s like {@link RestTemplate} does.
* feign 实现多pojo传输与MultipartFile上传 编码器,需配合开启feign自带注解使用
* @author Pierantonio Cangianiello
*/
public class FeignSpringFormEncoder implements Encoder {
private final List<HttpMessageConverter<?>> converters = new RestTemplate().getMessageConverters();
public static final Charset UTF_8 = Charset.forName("UTF-8");
public FeignSpringFormEncoder() {
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
final HttpHeaders multipartHeaders = new HttpHeaders();
final HttpHeaders jsonHeaders = new HttpHeaders();
multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
if (isFormRequest(bodyType)) {
encodeMultipartFormRequest((Map<Object, ?>) object, multipartHeaders, template);
} else {
encodeRequest(object, jsonHeaders, template);
}
}
/**
* Encodes the request as a multipart form. It can detect a single {@link MultipartFile}, an
* array of {@link MultipartFile}s, or POJOs (that are converted to JSON).
*
* @param formMap
* @param template
* @throws EncodeException
*/
private void encodeMultipartFormRequest(Map<Object, ?> formMap, HttpHeaders multipartHeaders, RequestTemplate template) throws EncodeException {
if (formMap == null) {
throw new EncodeException("Cannot encode request with null form.");
}
LinkedMultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();
for (Map.Entry<Object, ?> entry : formMap.entrySet()) {
Object value = entry.getValue();
if (isMultipartFile(value)) {
map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value));
} else if (isMultipartFileArray(value)) {
encodeMultipartFiles(map, (String) entry.getKey(), Arrays.asList((MultipartFile[]) value));
} else {
map.add(entry.getKey(), encodeJsonObject(value));
}
}
encodeRequest(map, multipartHeaders, template);
}
private boolean isMultipartFile(Object object) {
return object instanceof MultipartFile;
}
private boolean isMultipartFileArray(Object o) {
return o != null && o.getClass().isArray() && MultipartFile.class.isAssignableFrom(o.getClass().getComponentType());
}
/**
* Wraps a single {@link MultipartFile} into a {@link HttpEntity} and sets the
* {@code Content-type} header to {@code application/octet-stream}
*
* @param file
* @return
*/
private HttpEntity<?> encodeMultipartFile(MultipartFile file) {
HttpHeaders filePartHeaders = new HttpHeaders();
filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(), file.getInputStream());
return new HttpEntity<>(multipartFileResource, filePartHeaders);
} catch (IOException ex) {
throw new EncodeException("Cannot encode request.", ex);
}
}
/**
* Fills the request map with {@link HttpEntity}s containing the given {@link MultipartFile}s.
* Sets the {@code Content-type} header to {@code application/octet-stream} for each file.
*
* @param the current request map.
* @param name the name of the array field in the multipart form.
* @param files
*/
private void encodeMultipartFiles(LinkedMultiValueMap<Object, Object> map, String name, List<? extends MultipartFile> files) {
HttpHeaders filePartHeaders = new HttpHeaders();
filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
for (MultipartFile file : files) {
Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(), file.getInputStream());
map.add(name, new HttpEntity<>(multipartFileResource, filePartHeaders));
}
} catch (IOException ex) {
throw new EncodeException("Cannot encode request.", ex);
}
}
/**
* Wraps an object into a {@link HttpEntity} and sets the {@code Content-type} header to
* {@code application/json}
*
* @param o
* @return
*/
private HttpEntity<?> encodeJsonObject(Object o) {
HttpHeaders jsonPartHeaders = new HttpHeaders();
jsonPartHeaders.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(o, jsonPartHeaders);
}
/**
* Calls the conversion chain actually used by
* {@link RestTemplate}, filling the body of the request
* template.
*
* @param value
* @param requestHeaders
* @param template
* @throws EncodeException
*/
private void encodeRequest(Object value, HttpHeaders requestHeaders, RequestTemplate template) throws EncodeException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders);
try {
Class<?> requestType = value.getClass();
MediaType requestContentType = requestHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : converters) {
if (messageConverter.canWrite(requestType, requestContentType)) {
((HttpMessageConverter<Object>) messageConverter).write(
value, requestContentType, dummyRequest);
break;
}
}
} catch (IOException ex) {
throw new EncodeException("Cannot encode request.", ex);
}
HttpHeaders headers = dummyRequest.getHeaders();
if (headers != null) {
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
template.header(entry.getKey(), entry.getValue());
}
}
/*
we should use a template output stream... this will cause issues if files are too big,
since the whole request will be in memory.
*/
template.body(outputStream.toByteArray(), UTF_8);
}
/**
* Minimal implementation of {@link HttpOutputMessage}. It's needed to
* provide the request body output stream to
* {@link HttpMessageConverter}s
*/
private class HttpOutputMessageImpl implements HttpOutputMessage {
private final OutputStream body;
private final HttpHeaders headers;
public HttpOutputMessageImpl(OutputStream body, HttpHeaders headers) {
this.body = body;
this.headers = headers;
}
@Override
public OutputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
/**
* Heuristic check for multipart requests.
*
* @param type
* @return
* @see feign.Types#MAP_STRING_WILDCARD
*/
static boolean isFormRequest(Type type) {
return MAP_STRING_WILDCARD.equals(type);
}
/**
* Dummy resource class. Wraps file content and its original name.
*/
static class MultipartFileResource extends InputStreamResource {
private final String filename;
private final long size;
public MultipartFileResource(String filename, long size, InputStream inputStream) {
super(inputStream);
this.size = size;
this.filename = filename;
}
@Override
public String getFilename() {
return this.filename;
}
@Override
public InputStream getInputStream() throws IOException, IllegalStateException {
return super.getInputStream(); //To change body of generated methods, choose Tools | Templates.
}
@Override
public long contentLength() throws IOException {
return size;
}
}
}
然后在feignClient接口中使用@RequestLine标记请求路径,使用@Param注解标记每一个请求参数 ,RequestLine注解的格式是@RequestLine(value = “POST 请求路径”)
请求方式和路径之间须有一个空格。 表单提交的话请求方式只能是post
调用实例:
@FeignClient(name= "spring-cloud-producer")
public interface HelloRemote2 {
@RequestLine(value = "POST /hello3")
public String hello3(
@Param(value = "name") String name,
@Param(value = "number2") Integer number,
@Param(value = "date") Date date,
@Param(value = "advertiser") Advertiser advertiser,
@Param(value = "material") Material material
@Param(value = "materials") List<Material> materials,
@Param(value = "advertiserMap") Map<String, Advertiser> advertiserMap,
@Param(value = "file1") MultipartFile file1,
@Param(value = "files") MultipartFile[] files
);
}
然后在生产者服务的控制器端,采用@RequestPart注解接收每一个参数
@RequestMapping(value = "/hello3")
public String index3(
@RequestPart(value = "name", required = false) String name,
@RequestPart(value = "number", required = false) Integer number,
@RequestPart(value = "date", required = false) Date date,
@RequestPart(value = "advertiser", required = false) Advertiser advertiser,
@RequestPart(value = "material", required = false) Material material,
@RequestPart(value = "materials", required = false) List<Material> materials,
@RequestPart(value = "advertiserMap", required = false) Map<String, Advertiser> advertiserMap,
@RequestPart(value = "file1", required = false) MultipartFile file1,
@RequestPart(value = "files", required = false) MultipartFile[] files
) {
String result = "hello3成功进入生产者 \n";
return result;
}
3.spring boot集成fastJson
package com.xhwl.order.config.feign;
/**
* @Author liganggang
* @Date 2020/12/4 13:49
* @Version 1.0
*/
/**
* WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现 WebMvcConfigurer 这个接口
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 使用 fastjson 代替 jackson
*
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
/*
先把JackSon的消息转换器删除.
备注:(1)源码分析可知,返回json的过程为:
Controller调用结束后返回一个数据对象,for循环遍历conventers,找到支持application/json的HttpMessageConverter,然后将返回的数据序列化成json。
具体参考org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法
(2)由于是list结构,我们添加的fastjson在最后。因此必须要将jackson的转换器删除,不然会先匹配上jackson,导致没使用 fastjson
*/
for (int i = converters.size() - 1; i >= 0; i--) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
converters.remove(i);
}
}
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
//自定义fastjson配置
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
SerializerFeature.WriteMapNullValue, // 是否输出值为null的字段,默认为false,我们将它打开
SerializerFeature.WriteNullListAsEmpty, // 将Collection类型字段的字段空值输出为[]
SerializerFeature.WriteNullStringAsEmpty, // 将字符串类型字段的空值输出为空字符串
SerializerFeature.WriteNullNumberAsZero, // 将数值类型字段的空值输出为0
SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.DisableCircularReferenceDetect // 禁用循环引用
);
fastJsonHttpMessageConverter.setFastJsonConfig(config);
// 添加支持的MediaTypes;不添加时默认为*/*,也就是默认支持全部
// 但是MappingJackson2HttpMessageConverter里面支持的MediaTypes为application/json
// 参考它的做法, fastjson也只添加application/json的MediaType
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
converters.add(fastJsonHttpMessageConverter);
}
}