SpringCloud Feign Method has too many Body parameters 多实体与文件数组参数传递的方法 问题处理
前一段时间在使用springcloud 中的feign组件的时候报了一
Method has too many Body parameters
package com.cmbchina.ccd.itpm.consumer.service; import com.cmbchina.ccd.itpm.article.entity.Article; import com.cmbchina.ccd.itpm.common.entity.R; import com.cmbchina.ccd.itpm.common.vo.PageVo; import feign.Param; import feign.RequestLine; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Repository; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.util.List; /** * @Author zly * @Date 2019/10/24 09:35 */ @FeignClient(name = "asset-article") @Repository public interface ArticleService { @RequestLine("POST /article/add") R add(@RequestBody Article article, @RequestBody List<String> labelIds,@RequestBody MultipartFile file); }
这个FeignClient 使用了三个@RequestBody 其中两个是实体类 和一个文件对象 我想应该在大多数的情况下都会遇到会传一个实体对象和一个文件对象的情况,这个时候启动就会报出 Method has too many Body parameters 问题
好的下面就要解决这个问题
第一步
在消费者自定义个配置类
package com.cmbchina.ccd.itpm.consumer.configure; import feign.Contract; import feign.codec.Encoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Author ZLY * @Create 2019-10-29 16:45 **/ @Configuration public class FeignConfiguration { // 启用Fegin自定义注解 如@RequestLine @Param @Bean public Contract feignContract(){ return new Contract.Default(); } //feign 实现多pojo传输与MultipartFile上传 编码器,需配合开启feign自带注解使用 @Bean public Encoder feignSpringFormEncoder(){ return new FeignSpringFormEncoder(); } }
第二步 在定义个表单编码器
package com.cmbchina.ccd.itpm.consumer.configure; 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; import java.util.Map.Entry; /** * 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() { } /** * {@inheritDoc } */ @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 (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 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; } } }
然后在feignClient接口中使用@RequestLine标记请求路径,使用@Param注解标记每一个请求参数
RequestLine注解的格式是
@RequestLine(value = “POST 请求路径”)
请求方式和路径之间须有一个空格。
修改之后的feignClient接口
package com.cmbchina.ccd.itpm.consumer.service; import com.cmbchina.ccd.itpm.article.entity.Article; import com.cmbchina.ccd.itpm.common.entity.R; import com.cmbchina.ccd.itpm.common.vo.PageVo; import feign.Param; import feign.RequestLine; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Repository; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.util.List; /** * @Author zly * @Date 2019/10/24 09:35 */ @FeignClient(name = "asset-article") @Repository public interface ArticleService { @RequestLine("POST /article/add") R add(@Param("article") Article article, @Param("labelIds") List<String> labelIds,@Param("file") MultipartFile file); }
之后再生产者controller 入参方式使用 @RequestPart注解接收每一个参数。对于基础类型参数,你也可以使用RequestParam(好像没有必要,统一使用RequestPart注解不就好了)
如下:
import com.cmbchina.ccd.itpm.article.dto.ArticleDto; import com.cmbchina.ccd.itpm.article.entity.Article; import com.cmbchina.ccd.itpm.article.service.ArticleService; import com.cmbchina.ccd.itpm.common.entity.R; import com.cmbchina.ccd.itpm.common.entity.Staff; import com.cmbchina.ccd.itpm.common.utils.SessionUtil; import com.cmbchina.ccd.itpm.common.vo.PageVo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import java.util.Date; import java.util.List; /** * @Author zly * @Date 2019/10/22 15:43 */ @RestController @RequestMapping("/article") @Api("文章") public class ArticleController { @Autowired private ArticleService articleService; @ApiOperation(value = "发布文章", notes = "根据 Article对象创建文章") @PostMapping(value = "/add") public R add(@RequestPart(value = "article") Article article, @ApiParam(value = "上传文件", required = true) @RequestPart("multipartFile") MultipartFile multipartFile, @RequestPart("labelIds") List<String> labelIds, HttpServletRequest request) { return articleService.add(article, labelIds, request, multipartFile); } }
努力提高自己的技术,不忘初心