原因
项目中,发现feign只能传输json字符串,也是就content-type为application/json,但是我想直接post参数为Map或者自定义bean,
于是我把content-type改为application/x-www-form-urlencoded,但是出现异常数据类别不支持, 于是我就挖掘一下源码:
发现
AbstractMessageConverterMethodArgumentResolver的这个类会在拦截器之后,验证参数类型
然后断点发现application/x-www-form-urlencoded是由AllEncompassingFormHttpMessageConverter处理的,
在它的父类,看到它要求参数为MulitValueMap,所以我设置为
Map, 它不支持,看了canread方法和read方法,了解了它的转换方式
查看一下,这是一个list, 实现HttpMessageConverter,可以自定义转换器,于是参考AllEncompassingFormHttpMessageConverter,自定义一个Map的
大部分我是拷贝AllEncompassingFormHttpMessageConverter,然后修改的
@Slf4j
public class CustomHttpMessageConverter implements HttpMessageConverter<Map<String, Object>> {
private List<MediaType> supportedMediaTypes = new ArrayList<>();
private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
private Charset charset = FormHttpMessageConverter.DEFAULT_CHARSET;
@Nullable
private Charset multipartCharset = FormHttpMessageConverter.DEFAULT_CHARSET;
private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE =
new MediaType(MediaType.APPLICATION_FORM_URLENCODED, FormHttpMessageConverter.DEFAULT_CHARSET);
public CustomHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.partConverters.add(new ByteArrayHttpMessageConverter());
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
if (!Map.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.getType().equalsIgnoreCase("multipart")) {
// We can't read multipart, so skip this supported media type.
continue;
}
if (supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!Map.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
}
@Override
public Map<String, Object> read(Class<? extends Map<String, Object>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = (contentType != null && contentType.getCharset() != null ?
contentType.getCharset() : this.charset);
String body = URLDecoder.decode(StreamUtils.copyToString(inputMessage.getBody(), charset));
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
Map<String, Object> result = new LinkedHashMap<>(pairs.length);
for (String pair : pairs) {
if(pair.startsWith("table")){
int idx = pair.indexOf('=');
if (idx == -1) {
result.put(URLDecoder.decode(pair, charset.name()), null);
}
else {
pair = pair.replace("table=", "");
String[] pairArr = pair.split("=");
String name = pairArr[0];
String value = "null".equals(pairArr[1]) ? null : pairArr[1];
result.put(name, value);
}
}
}
return result;
}
@Override
public void write(Map<String, Object> map, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
try {
if (isMap(map, contentType)) {
writeMappart(map, contentType, outputMessage);
}
else {
writeMapForm((Map<String, Object>) map, contentType, outputMessage);
}
} catch (IOException e) {
log.error("write error", e);
}
}
private Boolean isMap(Map<String, Object> map, @Nullable MediaType contentType){
if (contentType != null) {
return contentType.getType().equalsIgnoreCase("multipart");
}
for (Object value : map.values()) {
if (value != null && !(value instanceof String)) {
return true;
}
}
return false;
}
private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
if (contentType != null) {
return contentType.getType().equalsIgnoreCase("multipart");
}
for (List<?> values : map.values()) {
for (Object value : values) {
if (value != null && !(value instanceof String)) {
return true;
}
}
}
return false;
}
private void writeMapForm(Map<String, Object> formData, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
contentType = getFormContentType(contentType);
outputMessage.getHeaders().setContentType(contentType);
Charset charset = contentType.getCharset();
Assert.notNull(charset, "No charset"); // should never occur
byte[] bytes = serializeForm(formData, charset).getBytes(charset);
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
}
else {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
private void writeForm(Map<String, Object> formData, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
contentType = getFormContentType(contentType);
outputMessage.getHeaders().setContentType(contentType);
Charset charset = contentType.getCharset();
Assert.notNull(charset, "No charset"); // should never occur
byte[] bytes = serializeForm(formData, charset).getBytes(charset);
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
}
else {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
protected MediaType getFormContentType(@Nullable MediaType contentType) {
if (contentType == null) {
return DEFAULT_FORM_DATA_MEDIA_TYPE;
}
else if (contentType.getCharset() == null) {
return new MediaType(contentType, this.charset);
}
else {
return contentType;
}
}
protected String serializeForm(Map<String, Object> formData, Charset charset) {
StringBuilder builder = new StringBuilder();
formData.forEach((name, value) -> {
if (name == null) {
//Assert.notNull(value, "Null name in form data: " + formData);
Assert.notNull(name, "Null name in form data: " + formData);
return;
}
});
return builder.toString();
}
private boolean isFilenameCharsetSet() {
return (this.multipartCharset != null);
}
private void writeMappart(
Map<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
if (contentType == null) {
contentType = MediaType.MULTIPART_FORM_DATA;
}
byte[] boundary = generateMultipartBoundary();
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> {
writeMapParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
});
}
else {
writeMapParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
}
}
private void writeMultipart(
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
if (contentType == null) {
contentType = MediaType.MULTIPART_FORM_DATA;
}
byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = new LinkedHashMap<>(2);
if (!isFilenameCharsetSet()) {
parameters.put("charset", this.charset.name());
}
parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
contentType = new MediaType(contentType, parameters);
outputMessage.getHeaders().setContentType(contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> {
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
});
}
else {
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
}
}
private void writeMapParts(OutputStream os, Map<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, Object> entry : parts.entrySet()) {
String name = entry.getKey();
String part = String.valueOf(entry.getValue());
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
}
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
}
}
@SuppressWarnings("unchecked")
private void writeMapPart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
if (partBody == null) {
throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity);
}
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
//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() + "]");
}
@SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
if (partBody == null) {
throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity);
}
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
// 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() + "]");
}
/**
* Generate a multipart boundary.
* <p>This implementation delegates to
* {@link MimeTypeUtils#generateMultipartBoundary()}.
*/
protected byte[] generateMultipartBoundary() {
return MimeTypeUtils.generateMultipartBoundary();
}
/**
* Return an {@link HttpEntity} for the given part Object.
* @param part the part to return an {@link HttpEntity} for
* @return the part Object itself it is an {@link HttpEntity},
* or a newly built {@link HttpEntity} wrapper for that part
*/
protected HttpEntity<?> getHttpEntity(Object part) {
return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<>(part));
}
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');
}
/**
* Implementation of {@link org.springframework.http.HttpOutputMessage} used
* to write a MIME multipart.
*/
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
private final OutputStream outputStream;
private final Charset charset;
private final HttpHeaders headers = new HttpHeaders();
private boolean headersWritten = false;
public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
this.outputStream = outputStream;
this.charset = charset;
}
@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 = getBytes(entry.getKey());
for (String headerValueString : entry.getValue()) {
byte[] headerValue = getBytes(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[] getBytes(String name) {
return name.getBytes(this.charset);
}
}
最后使用配置类方式追加就行了
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//加载自定义消息参数转换器
converters.add(new CustomHttpMessageConverter());
}
}