问题说明
项目中需要用到feign调用多文件的接口,出现这个问题的前提是已经参照多数博客中的方法,为feign客户端重写了编码器的配置,可以参照feign多文件上传。我不知道这些博主有没有出现我这个问题,但是我按照他们的配置后,确出现了以下问题,前台返回了这个错误
未知异常!原因是: null
后台的异常是,无关紧要的省略了
feign.codec.EncodeException
.
.
.
Caused by: java.lang.NullPointerException
at feign.form.util.PojoUtil.isUserPojo(PojoUtil.java:50)
at feign.form.multipart.PojoWriter.isApplicable(PojoWriter.java:40)
at feign.form.MultipartFormContentProcessor.findApplicableWriter(MultipartFormContentProcessor.java:159)
at feign.form.MultipartFormContentProcessor.process(MultipartFormContentProcessor.java:86)
at feign.form.FormEncoder.encode(FormEncoder.java:105)
at cn.farmall.file.config.FeignSpringFormEncoder.encode(FeignSpringFormEncoder.java:42)
at feign.ReflectiveFeign$BuildEncodedTemplateFromArgs.resolve(ReflectiveFeign.java:372)
... 100 more
解决办法
在自定义的在自定义的的编码中,修改传输文件的委托编码器顺序
public FeignSpringFormEncoder(Encoder delegate) {
super(delegate);
MultipartFormContentProcessor processor = (MultipartFormContentProcessor)this.getContentProcessor(ContentType.MULTIPART);
//前移添加MultipartFiles的委托编码器(多文件)
processor.addFirstWriter(new SpringManyMultipartFilesWriter());
//前移添加MultipartFile的委托编码器(单文件)
processor.addFirstWriter(new SpringSingleMultipartFileWriter());
//别人的方法,添加MultipartFiles的委托编码器(多文件)
// processor.addWriter(new SpringManyMultipartFilesWriter());
//别人的方法,添加MultipartFile的委托编码器(单文件)
// processor.addWriter(new SpringSingleMultipartFileWriter());
}
问题分析(以下不感兴趣可跳过,只是记录自己思考的过程,内容顺序上可能有些杂乱)
开始出现这问题的时候,我一直以为,我经过一番删代码,找其他办法重写,再删代码,再找其他办法重写,三删代码,三找其他办法重写,一番体验和效果都极佳的百度处理后。我对未来充满了信心,喜悦之情油然而生。
找你妹啊,全都是按照一个套路,我不要面子的。于是乎我只能忽略我是个菜鸡,开始调试源码。(这个反反复复的弄了几次,虽然已经找到错误位置,可是开始并没有想到怎么处理,主要是刚刚开始学这个,对一些处理并不懂,哪怕是参考人家的代码,也不懂为什么要这么做,自己菜就不多说了。)
- 为什么出现空指针的异常,从异常日志可以看到是 feign.form.util.PojoUtil.isUserPojo(PojoUtil.java:50) 这个地方出现的异常,这是什么,不能着急,从根源跟踪一些它在搞什么东西,打个断点跟踪(以下代码说明断点位置而已,防止有人看,多说一点废话,万一有跟我一样的萌新呢,要一步一步来),这个方法在参考文章中的FeignSpringFormEncoder 方法
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
//单个文件
if (bodyType.equals(MultipartFile.class)) {
MultipartFile file = (MultipartFile) object;
Map<String, Object> data = Collections.singletonMap(file.getName(), object);
super.encode(data, MAP_STRING_WILDCARD, template);
return;
} else if (bodyType.equals(MultipartFile[].class)) { //多个文件
MultipartFile[] file = (MultipartFile[]) object;
if(file != null) {
Map<String, Object> data = Collections.singletonMap(file.length==0?"":file[0].getName(), object);
//断点打在下面这里
super.encode(data, MAP_STRING_WILDCARD, template);
return;
}
}
super.encode(object, bodyType, template);
}
- 自定义的编码器都调用的父类的方法,看看父类做了什么
@Override
@SuppressWarnings("unchecked")
public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
String contentTypeValue = getContentTypeValue(template.headers());
val contentType = ContentType.of(contentTypeValue);
if (!processors.containsKey(contentType)) {
delegate.encode(object, bodyType, template);
return;
}
//通过判断传输数据的类型转换数据,我们指明数据是以MAP_STRING_WILDCARD(Form)提交
Map<String, Object> data;
if (MAP_STRING_WILDCARD.equals(bodyType)) {
data = (Map<String, Object>) object;
} else if (isUserPojo(bodyType)) {
data = toMap(object);
} else {
delegate.encode(object, bodyType, template);
return;
}
val charset = getCharset(contentTypeValue);
//processors是一个Map,里面有两个processor
//MULTIPART-> MultipartFormContentProcessor
//URLENCODED-> UrlencodedFormContentProcessor
//这里是去处理数据了,跟进去看看(contentType = MULTIPART)
processors.get(contentType).process(template, charset, data);
}
- MultipartFormContentProcessor.process 方法在定义的writers中找到第一个适合处理数据的writer去处理数据
@Override
public void process (RequestTemplate template, Charset charset, Map<String, Object> data) throws EncodeException {
val boundary = Long.toHexString(System.currentTimeMillis());
val output = new Output(charset);
for (val entry : data.entrySet()) {
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
continue;
}
//这里查找适用的编码器,看看它做什么
val writer = findApplicableWriter(entry.getValue());
writer.write(output, boundary, entry.getKey(), entry.getValue());
}
output.write("--").write(boundary).write("--").write(CRLF);
val contentTypeHeaderValue = new StringBuilder()
.append(getSupportedContentType().getHeader())
.append("; charset=").append(charset.name())
.append("; boundary=").append(boundary)
.toString();
template.header(CONTENT_TYPE_HEADER, Collections.<String>emptyList()); // reset header
template.header(CONTENT_TYPE_HEADER, contentTypeHeaderValue);
// Feign's clients try to determine binary/string content by charset presence
// so, I set it to null (in spite of availability charset) for backward compatibility.
val bytes = output.toByteArray();
val body = Request.Body.encoded(bytes, null);
template.body(body);
try {
output.close();
} catch (IOException ex) {
throw new EncodeException("Output closing error", ex);
}
}
MultipartFormContentProcessor.findApplicableWriter 方法调用各个子类的isApplicable方法判断writer适不适合
private Writer findApplicableWriter (Object value) {
//循环writer集,看看哪个可以处理这个数据直接返回,找不到就用默认的
for (val writer : writers) {
if (writer.isApplicable(value)) {
return writer;
}
}
return defaultPerocessor;
}
- 那writers 有什么writer,通过端口可以发现有以下的编码器
它们都是哪来的,在MultipartFormContentProcessor 构造函数里面默认添加了集中,而后两种是在自定义的 FeignSpringFormEncoder 中为MultipartFormContentProcessor添加的
public MultipartFormContentProcessor (Encoder delegate) {
writers = new LinkedList<Writer>();
addWriter(new ByteArrayWriter());
addWriter(new FormDataWriter());
addWriter(new SingleFileWriter());
addWriter(new ManyFilesWriter());
addWriter(new SingleParameterWriter());
addWriter(new ManyParametersWriter());
addWriter(new PojoWriter(writers));
defaultPerocessor = new DelegateWriter(delegate);
}
public FeignSpringFormEncoder(Encoder delegate) {
super(delegate);
MultipartFormContentProcessor processor = (MultipartFormContentProcessor)this.getContentProcessor(ContentType.MULTIPART);
//前移添加MultipartFiles的委托编码器(多文件)
processor.addFirstWriter(new SpringManyMultipartFilesWriter());
//前移添加MultipartFile的委托编码器(单文件)
processor.addFirstWriter(new SpringSingleMultipartFileWriter());
//别人的方法,添加MultipartFiles的委托编码器(多文件)
// processor.addWriter(new SpringManyMultipartFilesWriter());
//别人的方法,添加MultipartFile的委托编码器(单文件)
// processor.addWriter(new SpringSingleMultipartFileWriter());
}
在这些writer中,猛然发现一个扎眼的词PojoWriter, 错误里面不就有Pojo PojoWriter 里面判断用的方法就是PojoUtil.isUserPojo
public static boolean isUserPojo (@NonNull Object object) {
val type = object.getClass();
//这里,我们传过来的数据是MultipartFile[],它是找不到包名的,所以这里出现了空指针
val packageName = type.getPackage().getName();
return !packageName.startsWith("java.");
}
阿弥陀佛,我找到你了,我要度化你这顽劣的代码。可是这是框架上的源码啊,就找这都搞了半天。
Artem Labazin:你敢动吗?
我:我不敢动。
鉴于对大佬的敬畏心,姑且放过这顽码,我只能另谋他路。
发动脑力技能:正常思考:按照正常来说,这里应该是返回false,但是因为找不到包名,直接空指针异常打断了查找writer的过程,而自己添加的文件writer就不能拥有姓名。其实在 MultipartFormContentProcessor.findApplicableWriter只要找到合适的writer就直接返回了,所以并不需要进到PojoWriter。那是不是可以把我们自己添加的writer 排在 PojoWriter前就可以了呢,于是我就找了一些addWriter 都有哪些方法,就发现了addFirstWriter,直接加在最前面,这里本来就是用来处理文件的嘛,加在最前面它不香吗