SpringCloud+Feign环境下文件上传与form-data同时存在的解决办法(2)

时间:2020-12-05 18:30:52

书接上文。

上文中描述了如何在 SpringCloud+Feign环境下上传文件与form-data同时存在的解决办法,实践证明基本可行,但却会引入其他问题。

主要导致的后果是:

1. 无法与普通Feign方法并存

2. 几率性(不确定条件下)导致其他form-data类型参数无法识别,无法正常工作,错误信息大致如下:

org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present

分析原因发现是Feign的Encoder体系中缺乏对应的配置从而无法工作;但将这些Encoder一一补上却过于困难,因此,决定换一个思路,使用Spring技术解决该问题。

该问题的本质是controller层传参时参数的编解码问题,因此,应该属于HttpMessageConverter范畴的事情,这参考SpringEncoder的代码即可得知。

最终,解决方案如下:

1. 去掉依赖,因为不再需要

             <dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.2.2</version>
</dependency> <dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.2.2</version>
</dependency>

2. 去掉类  FeignSpringFormEncoder ,不再需要

3. 注解  RequestPartParam 、类  RequestPartParamParameterProcessor 以及 bean 的定义均与上文保持一致

4. 参考类  FormHttpMessageConverter  添加类  LinkedHashMapFormHttpMessageConverter  ,并注意使用 @Conponent注解进行Spring托管。代码如下:

 import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.multipart.MultipartFile; import javax.mail.internet.MimeUtility; import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; /**
* 参考 FormHttpMessageConverter
*/
@Component
public class LinkedHashMapFormHttpMessageConverter implements HttpMessageConverter<LinkedHashMap<String, ?>> { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>(); private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>(); private Charset charset = DEFAULT_CHARSET; private Charset multipartCharset; public LinkedHashMapFormHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA); StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316 this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter()); MultipartFileHttpMessageConverter multipartFileHttpMessageConverter = new MultipartFileHttpMessageConverter();
this.partConverters.add(multipartFileHttpMessageConverter); applyDefaultCharset();
} @Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
} public void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
Assert.notEmpty(partConverters, "'partConverters' must not be empty");
this.partConverters = partConverters;
} public void addPartConverter(HttpMessageConverter<?> partConverter) {
Assert.notNull(partConverter, "'partConverter' must not be null");
this.partConverters.add(partConverter);
} public void setCharset(Charset charset) {
if (charset != this.charset) {
this.charset = (charset != null ? charset : DEFAULT_CHARSET);
applyDefaultCharset();
}
} private void applyDefaultCharset() {
for (HttpMessageConverter<?> candidate : this.partConverters) {
if (candidate instanceof AbstractHttpMessageConverter) {
AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
// Only override default charset if the converter operates with a charset to begin with...
if (converter.getDefaultCharset() != null) {
converter.setDefaultCharset(this.charset);
}
}
}
} public void setMultipartCharset(Charset charset) {
this.multipartCharset = charset;
} @Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
} @Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!LinkedHashMap.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return false;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
} @Override
public LinkedHashMap<String, String> read(Class<? extends LinkedHashMap<String, ?>> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
throw new HttpMessageNotReadableException("Not supportted for read.");
} @Override
@SuppressWarnings("unchecked")
public void write(LinkedHashMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException { writeMultipart((LinkedHashMap<String, Object>) map, outputMessage);
} private void writeMultipart(final LinkedHashMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
final byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII")); MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
HttpHeaders headers = outputMessage.getHeaders();
headers.setContentType(contentType); if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
}
});
}
else {
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
}
} private void writeParts(OutputStream os, LinkedHashMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, Object> entry : parts.entrySet()) {
String name = entry.getKey();
Object part = entry.getValue();
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
} @SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
return;
}
}
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType.getName() + "]");
} protected byte[] generateMultipartBoundary() {
return MimeTypeUtils.generateMultipartBoundary();
} protected HttpEntity<?> getHttpEntity(Object part) {
return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
} protected String getFilename(Object part) {
if (part instanceof Resource) {
Resource resource = (Resource) part;
String filename = resource.getFilename();
if (filename != null && this.multipartCharset != null) {
filename = MimeDelegate.encode(filename, this.multipartCharset.name());
}
return filename;
} else if (part instanceof MultipartFile) {
MultipartFile multipartFile = (MultipartFile) part;
String filename = multipartFile.getName();
if (filename == null || filename.isEmpty()) {
filename = multipartFile.getOriginalFilename();
}
return filename;
}
else {
return null;
}
} private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
writeNewLine(os);
} private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
os.write('-');
os.write('-');
writeNewLine(os);
} private static void writeNewLine(OutputStream os) throws IOException {
os.write('\r');
os.write('\n');
} private static class MultipartHttpOutputMessage implements HttpOutputMessage { private final OutputStream outputStream; private final HttpHeaders headers = new HttpHeaders(); private boolean headersWritten = false; public MultipartHttpOutputMessage(OutputStream outputStream) {
this.outputStream = outputStream;
} @Override
public HttpHeaders getHeaders() {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
} @Override
public OutputStream getBody() throws IOException {
writeHeaders();
return this.outputStream;
} private void writeHeaders() throws IOException {
if (!this.headersWritten) {
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
byte[] headerName = getAsciiBytes(entry.getKey());
for (String headerValueString : entry.getValue()) {
byte[] headerValue = getAsciiBytes(headerValueString);
this.outputStream.write(headerName);
this.outputStream.write(':');
this.outputStream.write(' ');
this.outputStream.write(headerValue);
writeNewLine(this.outputStream);
}
}
writeNewLine(this.outputStream);
this.headersWritten = true;
}
} private byte[] getAsciiBytes(String name) {
try {
return name.getBytes("US-ASCII");
}
catch (UnsupportedEncodingException ex) {
// Should not happen - US-ASCII is always supported.
throw new IllegalStateException(ex);
}
}
} private static class MimeDelegate { public static String encode(String value, String charset) {
try {
return MimeUtility.encodeText(value, charset, null);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
}

5. 参考类  ResourceHttpMessageConverter  添加类  MultipartFileHttpMessageConverter ,代码如下:

 import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile; import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream; public class MultipartFileHttpMessageConverter extends AbstractHttpMessageConverter<MultipartFile> { public MultipartFileHttpMessageConverter() {
super(MediaType.APPLICATION_OCTET_STREAM);
} @Override
protected boolean supports(Class<?> clazz) {
return MultipartFile.class.isAssignableFrom(clazz);
} @Override
protected MultipartFile readInternal(Class<? extends MultipartFile> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
throw new HttpMessageNotReadableException("Not supportted for read.");
} @Override
protected MediaType getDefaultContentType(MultipartFile multipartFile) {
try {
String contentType = multipartFile.getContentType();
MediaType mediaType = MediaType.valueOf(contentType);
if (mediaType != null) {
return mediaType;
}
} catch (Exception ex) {
}
return MediaType.APPLICATION_OCTET_STREAM;
} @Override
protected Long getContentLength(MultipartFile multipartFile, MediaType contentType) throws IOException {
long contentLength = multipartFile.getSize();
return (contentLength < 0 ? null : contentLength);
} @Override
protected void writeInternal(MultipartFile multipartFile, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
writeContent(multipartFile, outputMessage);
} protected void writeContent(MultipartFile multipartFile, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
try {
InputStream in = multipartFile.getInputStream();
try {
StreamUtils.copy(in, outputMessage.getBody());
}
catch (NullPointerException ex) {
// ignore, see SPR-13620
}
finally {
try {
in.close();
}
catch (Throwable ex) {
// ignore, see SPR-12999
}
}
}
catch (FileNotFoundException ex) {
// ignore, see SPR-12999
}
} }

按照上述配置即可工作,但需要严格注意:

由于使用 LinkedHashMapFormHttpMessageConverter 的原因,会导致:

当某次http请求的参数或者返回值中包含 LinkedHashMap 且其请求的MediaType兼容 MULTIPART_FORM_DATA 时,该次参数或者返回值会错误的被 LinkedHashMapFormHttpMessageConverter 处理,但此时很显然是不正确的处理方式。

对应的办法:避开该情况,或者有更好的办法请务必分享给我,不胜感激。