feign-form
feign-form copied to clipboard
an encoder support muliple pojos and multipleFile Array
when I refactor a project running on prouduct environment, I have to reserve some structual like it used to be. When I use feign, I found it not supported multiple Pojos likes below
// a consumer Feign Client example
@FeignClient(name= "spring-cloud-producer")
public interface HelloRemote3 {
@RequestLine(value = "POST /hello3")
public String hello3(
@Param(value = "name") String name,
@Param(value = "date") Date date,
@Param(value = "pojoA") PojoA pojoA,
@Param(value = "pojoBs") List<PojoB> pojoBs,
@Param(value = "pojoCMap") Map<String, PojoC> pojoCMap,
@Param(value = "file") MultipartFile file,
@Param(value = "files") MultipartFile[] files
);
}
but thanks Mr.pcan, I finally found A way to support this feature by rewrite Encoder with feignContact;
@see https://github.com/pcan/feign-client-test/blob/master/src/main/java/it/pcan/test/feign/client/FeignSpringFormEncoder.java
then , you can accept request in Producer SpringMvc Controller with @RequestPart ,like below
// producer Controller
@RestController
public class HelloController {
@RequestMapping(value = "/hello3")
public String index3(
@RequestPart(value = "name", required = false) String name,
@RequestPart(value = "date", required = false) Date date,
@RequestPart(value = "pojoA", required = false) PojoA pojoA,
@RequestPart(value = "pojoBs", required = false) List<pojoB> pojoBs,
@RequestPart(value = "pojoCMap", required = false) Map<String, pojoC> pojoCMap,
@RequestPart(value = "file", required = false) MultipartFile file,
@RequestPart(value = "files", required = false) MultipartFile[] files
) {
String result = "hello3 producer enter success \n";
result += " name: " + name;
result += " \n ------------ " + date;
result += " \n ------------" + JSONObject.toJSONString(pojoA);
result += " \n ------------ " + pojoBs;
result += " \n ------------ " + pojoCMap;
return result;
}
}
now, it works fine in my project. a complete demo @see https://github.com/jianguyuxing/feign-multiple-pojos could you support this feature in next version ?
hm, I didn't get what do you mean exactly.
To enable support for many body objects in a single method call - you need to rewrite feign.Contract, not the feign.Encoder, otherwise you will get "IllegalStateException: Method has too many Body parameters". feign.Contract is responsible for parsing your client's interface.
If you would like to have a support of MultipartFile
, MultipartFile[]
and even List<MultipartFile>
- you need to take a look at this, I already have it (btw, thanks for the reminder, I just fixed a little bug there)
@jianguyuxing, maybe you another Mr.pcan's example for feign.Contract?
hm, I dont think we need to rewrite feign.Contract. I use it in my project likes below,
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new Contract.Default();
}
@Bean
public Encoder feignSpringFormEncoder() {
return new FeignSpringFormEncoder();
}
}
It works fine now.
this is a simple and complete demo written by Pierantonio Cangianiello, @see https://github.com/pcan/feign-client-test
In fact , I rewrite nothing when Mr.Pcan's encoder is complete now likes below
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
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;
import java.util.Map.Entry;
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;
/**
* A custom {@link feign.codec.Encoder} that supports Multipart requests. It uses
* {@link HttpMessageConverter}s like {@link RestTemplate} does.
*
* @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() {
}
/**
* {@inheritDoc }
*/
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (isFormRequest(bodyType)) {
final HttpHeaders multipartHeaders = new HttpHeaders();
multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
encodeMultipartFormRequest((Map<String, ?>) object, multipartHeaders, template);
} else {
final HttpHeaders jsonHeaders = new HttpHeaders();
jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
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<String, ?> formMap, HttpHeaders multipartHeaders, RequestTemplate template) throws EncodeException {
if (formMap == null) {
throw new EncodeException("Cannot encode request with null form.");
}
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
for (Entry<String, ?> entry : formMap.entrySet()) {
Object value = entry.getValue();
if (isMultipartFile(value)) {
map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value));
} else if (isMultipartFileArray(value)) {
encodeMultipartFiles(map, 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<String, 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 org.springframework.web.client.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 (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 org.springframework.http.HttpOutputMessage}. It's needed to
* provide the request body output stream to
* {@link org.springframework.http.converter.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;
}
}
}
@jianguyuxing I have same question about that , but I haven't found a good way, it looks like we can only rewrite the contract , do you have any better solution?
https://github.com/OpenFeign/feign-form/issues/68#issuecomment-479816726
@jianguyuxing I have same question about that , but I haven't found a good way, it looks like we can only rewrite the contract , do you have any better solution?
@JokerSun no,you needn't rewrite contract but encoder. And you should use feignContract instead of springMvcContract. (feign use SpringMvcContract by default when we do not specify it)
it's a complete demo @see https://github.com/jianguyuxing/feign-multiple-pojos