前言
在上篇文章深入探究ASP.NET Core读取Request.Body的正确方式中我们探讨了很多人在日常开发中经常遇到的也是最基础的问题,那就是关于Request.Body的读取方式问题,看是简单实则很容易用不好。笔者也是非常荣幸的得到了许多同学的点赞支持,心理也是非常的兴奋。在此期间在技术交流群中,有一位同学看到了我的文章之后提出了一个疑问,说关于ASP.NET Core文件上传IFormFile和Request.Body之间存在什么样的关系。由于笔者没对这方面有过相关的探究,也没敢做过多回答,怕误导了那位同学,因此私下自己研究了一番,故作此文,希望能帮助更多的同学解除心中的疑惑。
IFormFile的使用方式
考虑到可能有的同学对ASP.NET Core文件上传操作可能不是特别的理解,接下来咱们通过几个简单的操作,让大家简单的熟悉一下。
简单使用演示
首先是最简单的单个文件上传的方式
[HttpPost]public string UploadFile (IFormFile formFile){return $"{formFile.FileName}--{formFile.Length}--{formFile.ContentDisposition}--{formFile.ContentType}";}
非常简单的操作,通过IFormFile实例直接获取文件信息,这里需要注意模型绑定的名称一定要和提交的表单值的name保持一致,这样才能正确的完成模型绑定。还有的时候我们是要通过一个接口完成一批文件上传,这个时候我们可以使用下面的方式
[HttpPost]public IEnumerable<string> UploadFiles(List<IFormFile> formFiles){return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}");}
直接将模型绑定的参数声明为集合类型即可,同时也需要注意模型绑定的名称和上传文件form的name要保持一致。不过有的时候你可能连List这种集合类型也不想写,想通过一个类就能得到上传的文件集合,好在微软够贴心,给我们提供了另一个类,操作如下
[HttpPost]public IEnumerable<string> UploadFiles3(IFormFileCollection formFiles){return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}");}
对微软的代码风格有了解的同学看到名字就知道,IFormFileCollection其实也是对IFormFile集合的封装。有时候你可能都不想使用IFormFile的相关模型绑定,可能是你怕记不住这个名字,那还有别的方式能操作上传文件吗?当然有,可以直接在Request表单中获取上传文件信息
[HttpPost]public IEnumerable<string> UploadFiles2(){IFormFileCollection formFiles = Request.Form.Files;return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}");}
其实它的本质也是获取到IFormFileCollection,不过这种方式更加的灵活。首先是不需要模型绑定名称不一致的问题,其次是只要有Request的地方就可以获取到上传的文件信息。
操作上传内容
如果你想保存上传的文件,或者是直接读取上传的文件信息,IFormFile为我们提供两种可以操作上传文件内容信息的方式
- 一种是将上传文件的Stream信息Copy到一个新的Stream中
- 另一种是直接通过OpenReadStream的方式直接获取上传文件的Stream信息
两种操作方式大致如下
[HttpPost]public async Task<string> UploadFile (IFormFile formFile){if (formFile.Length > 0){//1.使用CopyToAsync的方式using var stream = System.IO.File.Create("test.txt");await formFile.CopyToAsync(stream);//2.使用OpenReadStream的方式直接得到上传文件的StreamStreamReader streamReader = new StreamReader(formFile.OpenReadStream());string content = streamReader.ReadToEnd();}return $"{formFile.FileName}--{formFile.Length}--{formFile.ContentDisposition}--{formFile.ContentType}";}
更改内容大小限制
ASP.NET Core会对上传文件的大小做出一定的限制,默认限制大小约是2MB(以字节为单位)左右,如果超出这个限制,会直接抛出异常。如何加下来我们看一下如何修改上传文件的大小限制通过ConfigureServices的方式直接配置FormOptions的MultipartBodyLengthLimit
public void ConfigureServices(IServiceCollection services){services.Configure<FormOptions>(options =>{// 设置上传大小限制256MBoptions.MultipartBodyLengthLimit = 268435456;});}
这里只是修改了对上传文件主题大小的限制,熟悉ASP.NET Core的同学可能知道,默认情况下Kestrel对Request的Body大小也有限制,这时候我们还需要对Kestrel的RequestBody大小进行修改,操作如下所示
public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{webBuilder.ConfigureKestrel((context, options) =>{//设置Body大小限制256MBoptions.Limits.MaxRequestBodySize = 268435456;});webBuilder.UseStartup<Startup>();});
很多时候这两处设置都需要配合着一起使用,才能达到效果,用的时候需要特别的留意一下。
源码探究
上面我们大致演示了
IFormFile
的基础操作,我们上面的演示大致划分为两类,一种是通过模型绑定的方式而这种方式包含了
IFormFile
、
List<IFormFile>
、
IFormFileCollection
三种方式 ,另一种是通过
Request.Form.Files
的方式,为了搞懂他们的关系,就必须从模型绑定下手。
始于模型绑定
首先我们找到关于操作FormFile相关操作模型绑定的地方在FormFileModelBinder类的BindModelAsync方法[点击查看源码?]我们看到了如下代码,展示的代码删除了部分逻辑,提取的是涉及到我们要关注的流程性的操作
public async Task BindModelAsync(ModelBindingContext bindingContext){//获取要绑定的参数类型var createFileCollection = bindingContext.ModelType == typeof(IFormFileCollection);//判断模型绑定参数类型是IFormFileCollection类型或可兼容IFormFileCollection类型//其中ModelBindingHelper.CanGetCompatibleCollection是用来判断模型绑定参数是否可以兼容IFormFileCollectionif (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext)){return;}//判断模型绑定参数是否是集合类型ICollection<IFormFile> postedFiles;if (createFileCollection){postedFiles = new List<IFormFile>();}else{//不是集合类型的的话,包装成为集合类型//其中ModelBindingHelper.GetCompatibleCollection是将模型绑定参数绑包装成集合类型postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext);}//获取要模型绑定的参数名称var modelName = bindingContext.IsTopLevelObject? bindingContext.BinderModelName ?? bindingContext.FieldName: bindingContext.ModelName;//给postedFiles添加值,postedFiles将承载上传的所有文件await GetFormFilesAsync(modelName, bindingContext, postedFiles);if (postedFiles.Count == 0 &&bindingContext.OriginalModelName != null &&!string.Equals(modelName, bindingContext.OriginalModelName, StringComparison.Ordinal) &&!modelName.StartsWith(bindingContext.OriginalModelName + "[", StringComparison.Ordinal) &&!modelName.StartsWith(bindingContext.OriginalModelName + ".", StringComparison.Ordinal)){modelName = ModelNames.CreatePropertyModelName(bindingContext.OriginalModelName, modelName);await GetFormFilesAsync(modelName, bindingContext, postedFiles);}object value;//如果模型参数为IFormFileif (bindingContext.ModelType == typeof(IFormFile)){//并未获取上传文件相关直接返回if (postedFiles.Count == 0){return;}//集合存在则获取第一个value = postedFiles.First();}else{//如果模型参数不为IFormFileif (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject){return;}var modelType = bindingContext.ModelType;//如果模型参数为IFormFile[]则直接将postedFiles转换为IFormFile[]if (modelType == typeof(IFormFile[])){Debug.Assert(postedFiles is List<IFormFile>);value = ((List<IFormFile>)postedFiles).ToArray();}//如果模型参数为IFormFileCollection则直接使用postedFiles初始化FileCollectionelse if (modelType == typeof(IFormFileCollection)){Debug.Assert(postedFiles is List<IFormFile>);value = new FileCollection((List<IFormFile>)postedFiles);}//其他类型则直接赋值else{value = postedFiles;}}bindingContext.Result = ModelBindingResult.Success(value);}
上面的源码中涉及到了ModelBindingHelper模型绑定帮助类[点击查看源码?]相关的方法,主要是封装模型绑定公共的帮助类。涉及到的我们需要的方法逻辑,上面备注已经说明了,这里就不展示源码了,因为它对于我们的流程来说并不核心。
上面我们看到了用于初始化绑定集合的核心操作是
GetFormFilesAsync
方法[点击查看源码?]话不多说我们来直接看下它的实现逻辑
private async Task GetFormFilesAsync(string modelName,ModelBindingContext bindingContext,ICollection<IFormFile> postedFiles){//获取Request实例var request = bindingContext.HttpContext.Request;if (request.HasFormContentType){//获取Request.Formvar form = await request.ReadFormAsync();//遍历Request.Form.Filesforeach (var file in form.Files){//FileName如果未空的话不进行模型绑定if (file.Length == 0 && string.IsNullOrEmpty(file.FileName)){continue;}//FileName等于模型绑定名称的话则添加postedFilesif (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase)){postedFiles.Add(file);}}}else{_logger.CannotBindToFilesCollectionDueToUnsupportedContentType(bindingContext);}}
看到这里得到的思路就比较清晰了,由于源码需要顺着逻辑走,我们大致总结一下关于FormFile模型绑定相关
- 为了统一处理方便,不管是上传的是单个文件还是多个文件,都会被包装成
ICollection<IFormFile>
集合类型
-
ICollection<IFormFile>
集合里的值就是来自于
Request.Form.Files
- 可绑定的类型
IFormFile
、
List<IFormFile>
、
IFormFileCollection
等都是由
ICollection<IFormFile>
里的数据初始化而来
- 如果模型参数类型是
IFormFile
实例非集合类型,那么会从
ICollection<IFormFile>
集合中获取第一个
- 模型绑定的参数名称要和上传的
FileName
保持一致,否则无法进行模型绑定
RequestForm的Files来自何处
通过上面的模型绑定我们了解到了
ICollection<IFormFile>
的值来自
Request.Form.Files
而得到RequestForm的值是来自
ReadFormAsync
方法,那么我们就从这个方法入手看看RequestForm是如何被初始化的,这是一个扩展方法来自于RequestFormReaderExtensions扩展类[点击查看源码?]大致代码如下
public static Task<IFormCollection> ReadFormAsync(this HttpRequest request, FormOptions options,CancellationToken cancellationToken = new CancellationToken()){// 一堆判断逻辑由此省略var features = request.HttpContext.Features;var formFeature = features.Get<IFormFeature>();//首次请求初始化没有Form的时候初始化一个FormFeatureif (formFeature == null || formFeature.Form == null){features.Set<IFormFeature>(new FormFeature(request, options));}//调用了HttpRequest的ReadFormAsync方法return request.ReadFormAsync(cancellationToken);}
没啥可说的直接找到HttpRequest的ReadFormAsync方法,我们在上篇文章了解过HttpRequest抽象类默认的实现类是DefaultHttpRequest,所以我们找到DefaultHttpRequest的ReadFormAsync方法[点击查看源码?]看一下它的实现
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken){return FormFeature.ReadFormAsync(cancellationToken);}
从代码中可以看到ReadFormAsync方法的返回值值来自FormFeature的ReadFormAsync方法,找到FormFeature的定义
private IFormFeature FormFeature => _features.Fetch(ref _features.Cache.Form, this, _newFormFeature)!;//其中_newFormFeature的定义来自其中委托的r值就是DefaultHttpRequest实例private readonly static Func<DefaultHttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default);
通过上面这段两段代码我们可以看到,无论怎么兜兜转转,最后都来到了FormFeature这个类,而且实例化这个类的时候接受的值都是来自于DefaultHttpRequest实例,其中还包含FormOptions,看着有点眼熟,不错上面我们设置的上传大小限制值的属性
MultipartBodyLengthLimit
正是来自这里。所有最终的单子都落到了
FormFeature
类的
ReadFormAsync
方法[点击查看源码?]找到源码大致如下所示
public Task<IFormCollection> ReadFormAsync() => ReadFormAsync(CancellationToken.None);public Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken){if (_parsedFormTask == null){if (Form != null){_parsedFormTask = Task.FromResult(Form);}else{_parsedFormTask = InnerReadFormAsync(cancellationToken);}}return _parsedFormTask;}
最终指向了
InnerReadFormAsync
这个方法,而这个方法正是初始化Form的所在,也就是说涉及到Form的初始化相关操作就是在这里进行的,因为这个方法的逻辑比较多所以我们只关注ContentType是
multipart/form-data
的逻辑,这里我们也就只保留这类的相关逻辑省去了其他的逻辑,有需要了解的同学可以自行查看源码[点击查看源码?]
private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancellationToken){FormFileCollection? files = null;using (cancellationToken.Register((state) => ((HttpContext)state!).Abort(), _request.HttpContext)){var contentType = ContentType;// 判断ContentType为multipart/form-data的时候if (HasMultipartFormContentType(contentType)){var formAccumulator = new KeyValueAccumulator();//得到boundary数据//Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit);// 把针对文件上传的部分封装到MultipartReadervar multipartReader = new MultipartReader(boundary, _request.Body){//Header个数限制HeadersCountLimit = _options.MultipartHeadersCountLimit,//Header长度限制HeadersLengthLimit = _options.MultipartHeadersLengthLimit,//Body长度限制BodyLengthLimit = _options.MultipartBodyLengthLimit,};//获取下一个可解析的节点,可以理解为每一个要解析的上传文件信息var section = await multipartReader.ReadNextSectionAsync(cancellationToken);//不为null说明已从Body解析出的上传文件信息while (section != null){// 在这里解析内容配置并进一步传递它以避免重新分析if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition)){throw new InvalidDataException("");}if (contentDisposition.IsFileDisposition()){var fileSection = new FileMultipartSection(section, contentDisposition);// 如果尚未对整个正文执行缓冲,则为文件启用缓冲section.EnableRewind(_request.HttpContext.Response.RegisterForDispose,_options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);// 找到结尾await section.Body.DrainAsync(cancellationToken);var name = fileSection.Name;var fileName = fileSection.FileName;FormFile file;//判断Body默认的流是否被修改过,比如开启缓冲就会修改//如果Body不是默认流则直接服务Bodyif (section.BaseStreamOffset.HasValue){file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName);}else{// 如果没有被修改过则获取MultipartReaderStream的实例file = new FormFile(section.Body, 0, section.Body.Length, name, fileName);}file.Headers = new HeaderDictionary(section.Headers);//如果解析出来了文件信息则初始化FormFileCollectionif (files == null){files = new FormFileCollection();}if (files.Count >= _options.ValueCountLimit){throw new InvalidDataException("");}files.Add(file);}else if (contentDisposition.IsFormDisposition()){var formDataSection = new FormMultipartSection(section, contentDisposition);var key = formDataSection.Name;var value = await formDataSection.GetValueAsync();formAccumulator.Append(key, value);if (formAccumulator.ValueCount > _options.ValueCountLimit){throw new InvalidDataException("");}}else{//没解析出来类型}section = await multipartReader.ReadNextSectionAsync(cancellationToken);}if (formAccumulator.HasValues){formFields = new FormCollection(formAccumulator.GetResults(), files);}}}// 如果可重置,则恢复读取位置为0(因为Body被读取到了尾部)if (_request.Body.CanSeek){_request.Body.Seek(0, SeekOrigin.Begin);}//通过files得到FormCollectionif (files != null){Form = new FormCollection(null, files);}return Form;}
这部分源码比较多,而且这还是精简过只剩下
ContentType
为
multipart/form-data
的内容,不过从这里我们就可以看出来FormFile的实例确实是依靠Request的Body里。其核心就在
MultipartReader
类的
ReadNextSectionAsync
方法返回的Section数据[点击查看源码?]通过上面的循环可以看到它是循环读取的,它通过解析Request信息持续的迭代MultipartSection信息,这种操作方式正是处理一次上传存在多个文件的情况,具体操作如下所示
private readonly BufferedReadStream _stream;private readonly MultipartBoundary _boundary;private MultipartReaderStream _currentStream;public MultipartReader(string boundary, Stream stream, int bufferSize){//stream即是传递下来的RequestBody_stream = new BufferedReadStream(stream, bufferSize);_boundary = new MultipartBoundary(boundary, false);//创建MultipartReaderStream实例_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit };}public async Task<MultipartSection?> ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()){//清空上一个节点的信息await _currentStream.DrainAsync(cancellationToken);// 如果返回了空值表示为最后一个节点if (_currentStream.FinalBoundaryFound){// 清空最后一个节点的挂载数据await _stream.DrainAsync(HeadersLengthLimit, cancellationToken);return null;}//读取header信息var headers = await ReadHeadersAsync(cancellationToken);_boundary.ExpectLeadingCrlf = true;//组装MultipartReaderStream实例_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit };//判断流是否是原始的HttpRequestStreamlong? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null;//通过上面信息构造MultipartSection实例return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset };}
这里可以看出传递下来的RequestBody被构建出了
MultipartReaderStream
实例,即MultipartReaderStream包装了RequestBody中的信息[点击查看源码?]看名字也知道它也是实现了Stream抽象类
internal sealed class MultipartReaderStream : Stream{}
而且我们看到
BodyLengthLimit
正是传递给了它的LengthLimit属性,而BodyLengthLimit正是设置限制上传文件的大小的属性,我们找到使用LengthLimit属性的地方,代码如下所示[点击查看源码?]
private int UpdatePosition(int read){//更新Stream的Position的值,即更新读取位置_position += read;//继续读取if (_observedLength < _position){//保存已经读取了的位置_observedLength = _position;//如果读取了位置大于LengthLimit则抛出异常if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()){throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");}}return read;}
从这段代码我们可以看出,正是此方法限制了读取的Body大小,通过我们对Stream的了解,这个UpdatePosition方法也必然会在Stream的Read方法也即是此处的MultipartReaderStream的Read方法中调用[点击查看源码?]这样才能起到限制的作用,大致看一下Read方法的实现代码
public override int Read(byte[] buffer, int offset, int count){//如果已经读到了结尾则直接返回0if (_finished){return 0;}PositionInnerStream();var bufferedData = _innerStream.BufferedData;// 匹配boundary的读取边界int read;if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)){// 匹配到了可读取的边界读取并返回if (matchOffset > bufferedData.Offset){read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset));//返回读取的长度正是调用的UpdatePositionreturn UpdatePosition(read);}var length = _boundary.BoundaryBytes.Length;Debug.Assert(matchCount == length);var boundary = _bytePool.Rent(length);read = _innerStream.Read(boundary, 0, length);_bytePool.Return(boundary);Debug.Assert(read == length);//读取RequestBody信息var remainder = _innerStream.ReadLine(lengthLimit: 100);remainder = remainder.Trim();//说明读取到了boundary的结尾if (string.Equals("--", remainder, StringComparison.Ordinal)){FinalBoundaryFound = true;}Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder);_finished = true;//返回读取的长度0说明读到了结尾return 0;}read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count));//这里同样是UpdatePositionreturn UpdatePosition(read);}
通过这里就可清楚的看到MultipartReaderStream的Read方法就是在解析读取的RequestBody的FormData类型的信息,解析成我们可以直接读取或者直接保存成文件的的原始的文件信息,它还有一个异步读取的ReadAsync方法其实现原理类似,在这里咱们就不在展示源码了。最后我们再来看一下MultipartSection类的实现[点击查看源码?]我们上面知道了MultipartReaderStream才是在RequestBody中解析到文件上传信息的关键所在,因此MultipartSection也就是包装了读取好的文件信息,我们来看一下它的代码实现
public class MultipartSection{/// <summary>/// 从header中得到的ContentType类型/// </summary>public string? ContentType{get{if (Headers != null && Headers.TryGetValue(HeaderNames.ContentType, out var values)){return values;}return null;}}/// <summary>/// 从header中得到的ContentDisposition信息/// </summary>public string? ContentDisposition{get{if (Headers != null && Headers.TryGetValue(HeaderNames.ContentDisposition, out var values)){return values;}return null;}}/// <summary>/// 读取到的Header信息/// </summary>public Dictionary<string, StringValues>? Headers { get; set; }/// <summary>/// 从RequestBody中解析到的Stream信息,即MultipartReaderStream或其他RequestBody实例/// </summary>public Stream Body { get; set; } = default!;/// <summary>/// 已经被读取过的Stream位置/// </summary>public long? BaseStreamOffset { get; set; }}
不出所料,这个类正是包装了上面一堆针对HTTP请求信息中读取到的关于上传的文件信息,由于上面设计到了几个类,而且设计到了一个大致的读取流程,为了防止同学们看起来容易蒙圈,这里咱们大致总结一下这里的读取流程。通过上面的代码我们了解到了涉及到的几个重要的类
MultipartReader
、
MultipartReaderStream
、
MultipartSection
知道这几个类在做什么就能明白到底是怎么通过RequestBody解析到文件信息的。大致解释一下这几个类在做些什么
- 通过
MultipartReader
类的ReadNextSectionAsync方法可以得到MultipartSection的实例
-
MultipartSection
类包含的就是解析出RequestBody里的文件相关的信息包装起来,
MultipartSection
的Body属性的值正是
MultipartReaderStream
的实例。
-
MultipartReaderStream
类正是通过读取RequestBody里的各种boundary信息转换为原始的文件内容的Stream信息
-
FormFile
的
CopyToAsync
和
OpenReadStream
方法都是Stream操作,而操作的Stream是来自
MultipartReaderStream
实例
总结
这次的分析差不多就到这里了, 本篇文章主要讨论了ASP.NET Core文件上传操作类IFormFile与RequestBody的关系,即如果通过RequestBody得到IFormFile实例相关,毕竟是源码设计到的东西比较多也比较散乱,我们再来大致的总结一下
- 无论在Action上对
IFormFile
、
List<IFormFile>
、
IFormFileCollection
等进行模型绑定,其实都是来自模型绑定处理类FormFileModelBinder,而这个类正是根据Request.Form.File的处理来判断如何进行模型绑定的。
- 而Request.Form.File本身其实就是IFormFileCollection类型的,它的值也正是来自对RequestBody的解析,也正是我们今天的结论File的值来自RequestBody。
- 从RequestBody解析到IFormFileCollection是一个过程,而IFormFileCollection实际上是IFormFile的集合类型,从RequestBody解析出来的也是单个IFormFile类型,通过不断的迭代添加得到的IFormFileCollection集合。
- 而从RequestBody中解析出来上传的文件到IFormFile涉及到了几个核心类,即
MultipartReader
、
MultipartReaderStream
和
MultipartSection
。其中MultipartSection是通过MultipartReader的ReadNextSectionAsync方法得到的,里面包含了解析好的上传文件相关信息。而MultipartSection正是包装了MultipartReaderStream,而这个类才是真正读取RequestBody得到可读取的文件原始Stream的关键所在。
到了这里本文的全部内容就差不多结束了,希望本文能给大家带来收获。我觉得有时候看源码能解决许多问题和心中的疑惑,因为我们作为程序员每天写的也就是代码,所以没有比程序员直接读取代码能更好的了解想了解的信息了。但是读源码也有一定的困难,毕竟是别人的代码,思维存在一定的偏差,更何况是一些优秀的框架,作者们的思维很可能比我们要高出很多,所以很多时候读起来会非常的吃力,即便如此笔者也觉得读源码是了解框架得到框架信息的一种比较行之有效的方式。
?欢迎扫码关注我的公众号?