question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

an encoder support muliple pojos and multipleFile Array

See original GitHub issue

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 ?

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:1
  • Comments:6

github_iconTop GitHub Comments

1reaction
jianguyuxingcommented, Apr 1, 2019

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;
        }

    }

}
1reaction
xxlabazacommented, Mar 29, 2019

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)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Upload Multiple Files in Spring Boot using JPA, Thymeleaf ...
Open Mysql command line. Run create database filedb; and hit enter. FileModal.java. Let's write a simple POJO class that will serve as input...
Read more >
Struts Multiple Files Upload Example - CodeJava.net
How to implement functionality to upload multiple files with ... In this POJO action class, we use three arrays to store uploaded files: ......
Read more >
Binding external configurations to POJO classes - CalliCoder
External configurations allow you to work with the same code in different environments. In this article, we'll learn how to define and use ......
Read more >
Howto upload multiple files with Spring Boot and test it with ...
I solved it using an Array instead of a Set with nested files. Java: @RequestMapping(value = "/upload", method = RequestMethod.
Read more >
POJO Customization — Java Sync - MongoDB
In our guide on POJOs, we show how to specify a PojoCodecProvider which contains ... to serialize multiple POJO classes to documents in...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found