SpringCloud-MessageConverter上传文件出现中文乱码的问题及解决方案

  问题:最近需要完成一个文件上传的业务功能,使用的框架是SpringCloud,业务代码写好后测试,发现接收到的文件,所有中文都变成了"?",在业务代码中打好断点后发现是在请求从Feign转发向server时出现了乱码。

  定位问题:FormHttpMessageCoverter在springMVC5.0以下的版本虽然有deaault_charset,并且初始是UTF-8,但实际使用的编码是定死的US-ASCII!!!是Spring的大坑!!!

  解决办法:

    1.将springMVC的核心包升至5.x版本即可,参考https://blog.csdn.net/schzrj/article/details/81147656,这位博主说升级spring-web包就行了,在maven中心库查询后发现,spring-web是MVC的核心包,对其他包都有依赖,要保持相同等级,所以就是要将MVC全部升级,请大家谨慎选择。

    2.重写FormHttpMessageCoverter,详见下方,解决问题的详细过程。

  定位问题的详细过程:

      1.在出问题的业务代码处追踪过去,Feign转发请求时首先进入了Encoder,公司的项目在这里重写了一个Encoder,重写是为了改变converters的default_charset,在这里对之后流程产生的结果就是给MultipartFile统一的header:"multipart/form-data"。

      2.最关键的一步来了,文件需要通过流的形式传输,那么在Encoder中,就有一个通过辨识header,分发进合适的converter的过程:

private final List<HttpMessageConverter<?>> converters = new RestTemplate().getMessageConverters();
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

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 (Map.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(), DEFAULT_CHARSET);
    }

      第一点提到,现在的header中是"content-type":"multipart/form-data",这里的converter的canwrite就是通过content-type判断的。此时分发的converter是AllEncompassingFormHttpMessageConverter。里面并没有实际业务代码,只有一些属性供canwrite方法判断,继承自FormHttpMessageConverter,这是实际发挥效果的converter。

   3.一步追踪,发现在FormHttpMessageConverter,将file转成了一个HttpEntity,然后将该Entity转入ResourceHttpMessageConverter写成了一个流,并且组装进MultipartHttpOutputMessage中,这个OutputMessage由FormHttpMessageConverter持有,然后写入ResourceHttpMessageConverter中自己的流,准备转发给server,此时代码如下:

protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		try {
			InputStream in = resource.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
		}
	}

    注意上方代码,resource中的inputstream是输出目标,outputMessage是输入源,这里的流程是取出message中的流,再写入输出目标,问题就出现在这个getBody中,如下代码:

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

    终于找到了问题所在,这里被写死了,US-ASCII不能表示中文,所以写好的流中文全部变成了?。

  解决问题的详细过程:

    先捋清整个问题的流程,首先Feign转发文件至Encoder,Encoder为multipartfile赋上header,并且根据这个header筛选出合适的converter。此处合适的converter是AllEncompassingFormHttpMessageConverter,这是FormHttpMessageConverter的一个子类,但未重写任何方法,只初始化了一些值以支持converter的筛选,在FormHttpMessageConverter中将文件包成一个HTTPentity转发向ResourceHttpMessageConverter,在该converter中转成输出流,发给server层。问题就出在转成输出流的准备阶段。

  再捋捋解决问题的思路,首先起最终作用的MultipartHttpOutputMessage需要重写,这是一个FormHttpMessageConverter的私有静态类,所以无法直接继承,我们需要重写formconverter。第二点,formconverter内部属性内部方法都为私有,只有一个write方法为public,经过查找调用轨迹发现,以write方法为入口,在这个类中调用私有方法,所以无法通过继承覆盖重写,只能全部重写。第三点,converter默认有7个,而决定调用哪一个是在其子类AllEncompassingFormHttpMessageConverter中,相当于是个入口,我们无法直接修改,所以必须重写该子类。

  重写流程如下:

    1.新建一个类,命名为MyFormHttpMessageConverter,将formconverter的所有内容直接加进来(只贴入了MultipartHttpOutputMessage部分的代码),将ASC改为UTF-8。(在这里我依然直接写死,实际上可以将重写的类继承于AbstractHttpMessageConverter,然后UTF-8处改为charset变量,这样就可以在config中使用set更改charset了,更加灵活)

public class MyFormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
	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(DEFAULT_CHARSET);
			}catch (NullPointerException ex) {
				// Should not happen - US-ASCII is always supported.
				throw new IllegalStateException(ex);
			}
		}
	}
}

      2.新建一个类,命名为MyAllEncompassingFormHttpMessageConverter,除了命名以外,其余内容与AllEncompassingFormHttpMessageConverter相同,只是继承自MyFormHttpMessageConverter。

      3.这一步会根据个人代码不同有不同情况,我的encoder中的converters是New出来的,就像如下:

private final List<HttpMessageConverter<?>> converters = new RestTemplate().getMessageConverters();

那么在自己写的configration中修改是肯定没作用的。所以就在我的encoder构造方法里进行添加

    public SpringMultipartEncoder() {
        multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
        jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
        int i = 0;
		for(HttpMessageConverter<?> messageConvert : converters){
			if(messageConvert instanceof AllEncompassingFormHttpMessageConverter){
				//解決FormHttpMessageConverter 中getAsciiBytes(String name) 方法造成上传文件名乱码
				converters.set(i, new MyAllEncompassingFormHttpMessageConverter());
			}
			i++;
		}
        
    }

这里采用set是担心converters的位置会不会有影响....

而其他情况下,如下代码即可:

	//扩展MessageConvert
	//JsonMessageConverter 默认编码格式UTF-8, StringMessageConvert 默认编码格式为ISO8859-1,更改为UTF-8
	@Override
	public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
		int i = 0;
		for(HttpMessageConverter<?> messageConvert : converters){
			if(messageConvert instanceof StringHttpMessageConverter){
				((StringHttpMessageConverter)messageConvert).setDefaultCharset(Charset.forName("UTF-8"));
			}else if(messageConvert instanceof AllEncompassingFormHttpMessageConverter){
				//解決FormHttpMessageConverter 中getAsciiBytes(String name) 方法造成上传文件名乱码
				converters.set(i, new MyAllEncompassingFormHttpMessageConverter());
			}
			i++;
		}
	}

代码结束,在converters筛选处打好断点,跟着代码走,发现走进了我们自己重写的代码,并且转码正常,解决!

总结:

       1.这次定位问题给我长了个教训,断点调试也要科学的断点调试,分析bug可能发生的地方,然后逐行仔细跟踪才行,不能全靠看风水定位。

       2.spring的一些代码风格印象深刻,public类本身只留一个public方法作为入口,其余方法都为私有,子类继承自该public类,子类私有,子类不重写任何方法,形成一个作为入口使用的的类适配器,通过子类调用父类入口,再调用父类的私有方法。有利有弊,这保证了代码的安全性,但对重写极其不友善。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值