“ 横眉冷对千夫指,俯首甘为孺子牛。——鲁迅”

1. 前言
经过前面几章关于文件上传功能的设计及开发,相信大家对上传功能都比较清楚了,涉及的业务相对比较复杂,同时涉及的技术点也比较多(切块上传、分布式锁、分布式存储等等),但是这些都是比较主流并且做项目的时候经常碰到、用到的技术,大家要掌握好。除此之外,我们更要学习好如何去分析和设计一个复杂的功能点,为什么这么说呢?我带过项目比较清楚,很多时候安排一个需求下去,需求说的很清楚了,当时也都表示理解了,但是到真实的代码阶段,结果非常的糟糕。或许你说这是新手吧,其实很多工作几年的,仍然是这样一个状态,其实真正的原因是什么?真正的原因是自己并没有完全理解需求,以为理解了,那只是理解需求是干什么而已,把需求转换代码的这一个过程,自己一直迷迷糊糊,完全跟着感觉做,写到哪算哪,最后检查代码,发现写的乱七八糟的。
大致归纳一下开发中常见的情况:
没有思路,不懂如何下手,主要是没有理解需求或者是把需求转换代码这一个过程束手无策。遇到这种情况的,一般是刚入行或者基础不是很好或者是没有啥实际项目经验的同学,大部分都是没有形成这方面的思维,很正常。可以把一个完整功能,拆分多个小点去分析,自己动手画图、伪代码、写注释去分析,最后才是动手写代码。
我记得我当初带一个同事去做切块上传的时候,怎么说都无法想象切块上传是怎么做的,但是最后我把代码写完出来之后,他就表示很简单,能看的懂了。为什么?其实就是自己没有认真去思考,或者干脆就是有依赖性的被动思考。
还有的情况是,工作好几年了,写的代码仍然乱七八糟的,思路新奇的同学,相信你身边也会有这样的同事。我想说的是,如果你一直做的是传统项目,那么即使工作再多年,你的项目经验仍然是不足以支撑你去做解决方案的选择和技术选型,需要不断的提高自己,扩展自己的眼界。
不爱写注释、不爱梳理需求,喜欢一上来直接开始写代码,最后的结果往往是事倍功半
个人建议
画流程图,根据需求通过 visio 工具或者 processOn 把流程画出来,流程图画完之后,你的思路就会变的很清晰,并且很容易发现问题的所在。
分析,包括这个功能涉及哪些难点、解决方案是什么?每种方案的优缺点是什么?这些问题梳理清楚。
写注释,先在方法内部把逻辑先用中文写好注释,然后再根据注释去实现逻辑。
以上的建议,你或许觉得浪费时间,其实是养成良好习惯的开始,当你时间久了之后,发现你一拿到需求心中立马知道如何去做了。
以上的归纳,其实就是告诉大家要重视需求分析、设计、捋清思路、最后才是真正的代码阶段,否则将会是事倍功半。
通过前面几章的讲解,上传功能已经算是完成了,但是这里还是得带大家去对整个上传功能做全面的总结,把整个上传功能模块给完善好。
思考1:通常情况下,对上传文件大小有限制的因素有哪些呢?
思考2:我们如何灵活的控制文件大小呢?
2. 文件大小限制
2.1 SpringBoot限制
如果用户选择上传一个70M的文件上传,报错如下:
Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (75252412) exceeds the configured maximum (10485760)
源码查看:
① 源码入口类
public class MultipartAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MultipartConfigElement multipartConfigElement() {
//点击这里进入下一个类
return this.multipartProperties.createMultipartConfig();
}
}
② 进入 createMultipartConfig() 方法内部
@ConfigurationProperties(prefix = "spring.http.multipart", ignoreUnknownFields = false)
public class MultipartProperties {
private String maxFileSize = "1MB";
private String maxRequestSize = "10MB";
public MultipartConfigElement createMultipartConfig() {
MultipartConfigFactory factory = new MultipartConfigFactory();
if (StringUtils.hasText(this.fileSizeThreshold)) {
factory.setFileSizeThreshold(this.fileSizeThreshold);
}
if (StringUtils.hasText(this.location)) {
factory.setLocation(this.location);
}
if (StringUtils.hasText(this.maxRequestSize)) {
factory.setMaxRequestSize(this.maxRequestSize);//大小限制的地方
}
if (StringUtils.hasText(this.maxFileSize)) {
factory.setMaxFileSize(this.maxFileSize);
}
return factory.createMultipartConfig();
}
}
通过以上源码,我们发现 SpringBoot 对文件大小进行了限制
解决方法1:application.properties 配置
原因:MultipartProperties和application.properties做了关联了,
@ConfigurationProperties
注解表示关联
spring.http.multipart.maxRequestSize=xxx
解决方法2:自定义MultipartConfigElement
原因:请看上面的
源码入口
,它使用@ConditionalOnMissingBean
修饰该方法,表示如果不存在自定义该类的话,它默认的才生效
@Configuration
public class FileSizeBean {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//文件最大
factory.setMaxFileSize("10240MB"); //KB,MB
// 设置总上传数据总大小
factory.setMaxRequestSize("102400MB");
return factory.createMultipartConfig();
}
}
2.2 Dubbo大小限制
Dubbo
默认请求和响应的数据包大小是 8M,需要手工修改大小
#disk-service-web/resource/application.properties
spring.dubbo.protocol.payload=2147483647
超时限制,文件太大处理比较耗时,经常出现处理超时,所以配置超时时间
@Service(interfaceClass=IFileService.class,timeout=60000)//设置timeout
@Component(value="fileServiceImplTest")
@Transactional
public class FileServiceImpl implements IFileService{
}
2.3 Nginx大小限制
如果部署是通过 Nginx 转发前端请求,此时用户选择上传一个 70M 的文件上传,那么报错如下:
<html>
<head>
<title>413 Request Entity Too Large</title>
</head>
<body bgcolor="white">
<center>
<h1>413 Request Entity Too Large</h1>
</center>
<hr>
<center>nginx/1.13.0</center>
</body>
</html>
解决如下,修改nginx/conf/nginx.conf
location /disk-web {
proxy_pass http://192.168.1.2:8012;
client_max_body_size 128m; //配置大小
}
3. 手工控制文件大小
如果这样一个需求:系统限制单个文件不能超过 10G,系统跑一段时间发现压力太大,然后领导说调整为 1G。
方案一:在相应的业务代码里面写死,如果需求变更则重新修改然后打包重新部署,相信大家都清楚这是一种方法的弊端;
方案二:通过 AOP(获取拦截器也是可以的)去拦截目标方法,并且把限制大小的值存放配置中心,需求变更只需要到配置中心修改即可,根本不需要重启代码,并且不污染业务代码。
第一步:在对应的工程新建相应的类文件
disk-web-perpc
|-- com.micro.aop
| |-- FileCheckAop.java (切块上传Aop)
| |-- FileMergeAop.java (切块合并Aop)
第二步:代码实现
@Aspect
@Component
public class FileMergeAop {
@NacosValue("${maxsize}")
private long maxsize; //把它配置在Nacos配置中心
//拦截目标方法
@Pointcut("execution(* com.micro.controller.FileCommonUploadController.checkFile(..))")
private void pointcut(){}
@Before("pointcut()")
public void beforeDownload(JoinPoint jp){
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response=attr.getResponse();
//获取前端传递过来的文件大小
String fileSize=request.getParameter("filesize");
if(!StringUtils.isEmpty(fileSize)){
long filesize=Long.parseLong(fileSize);
if(filesize>maxsize){
throw new RuntimeException("单文件上传,最大不能超过"+maxsize);
}
}
}
}
4. 其他细节总结
4.1 获取用户信息
思考:大家回过头看一下
切块上传接口
,为什么要在方法内部通过token去获取用户信息呢?而不是在拦截器去做呢?
//其他方法基本上都在拦截器获取token并且处理;这里需要单独获取token
public Result uploadChunk(MultipartFile file,ChunkPojo chunkPojo,String token) {
}
原因:WebUploader.js插件传递的参数,后端不能使用request.getParameter
的方式获取参数,必须在方法内定义具体参数,SpringBoot
会给方法参数自动注入值。具体原因是WebUploader
提交的请求没有 request data
而是request payload
。
4.2 MultipartFile临时目录问题
问题:发现上传文件有时会出现以下这个错误,重启后会正常。
Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [C:\Users\HuWeiJian\AppData\Local\Temp\tomcat.4476717864971067098.8101\work\Tomcat\localhost\ROOT] is not valid
原因分析:浏览器上传的文件,SpringBoot会保存到哪里?内存还是本地磁盘?为了提高性能应该不会是内存,否则多个人同时上传文件,则内存早就爆了,应该是存储临时目录。为了验证我们的想法,通过查看源码:
第一步: 我们通常是通过byte[] bytes=file.getBytes()
把它作为主入口,查看它到底是从哪里获取字节流。

发现它有两个实现类,到底是使用哪个实现类呢?
第二步: 从MultipartAutoConfiguration
入手去找原因,熟悉 SpringBoot 的同学应该都知道 XxxAutoConfiguration 是自动配置类,它一般是看源码的入口。

其实到这一步我们大概猜的出来了因为是StandardMultipartFile
,因为StandardServletMultipartResolver
名字有点类似,但是还是往下看。

继续进入

到这里,我们终于确定 SpringBoot 内部使用的是StandardMultipartFile
第三步: 进入StandardMultipartFile
的getBytes()
方法

发现是通过part
来获取输入流,那么我们继续跟进

发现是通过fileItem
来获取输入流,那么我们继续跟进

其实,到这里我们发现,isInMemory()
判断文件是否在内存里面,如果不在则去通过FileInputStream
去本地磁盘找。
第四步: 那么大家会不会有疑问,它什么时候保存内存,什么时候保存磁盘呢?
好了,源码看到这里,大家可以私下再去看吧,因为源码实在太多,不可能讲的完,还得自己去摸索一遍,大家可以根据我的思路去重新看一遍。其实它的底层原理是 10k 则保存在内存,超过 10k 则保存本地磁盘。
那么,怎么解决上面的那个错误呢?
//方式1:在application.properties
spring.http.multipart.location=自定义路径
//方式2:自定义类
@Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setLocation("/app/pttms/tmp");
return factory.createMultipartConfig();
}
//为什么以上配置就能生效呢?大家可以去MultipartAutoConfiguration这个类找原因即可。
4.3、Dubbo无法传递MultipartFile原因
思考:为什么Dubbo接口的参数不能使用MultipartFile而是使用byte[],这个问题身边的同事好几个都问过我同样的问题。
public class FileService{
//错误
public void uploadChunk(MultipartFile file);
//正确
public void uploadChunk(byte[] bytes);
}
分析1:Dubbo接口调用,底层走的是网络通信,网络通信数据传输是byte[],因此所有的数据都得转换成byte[]来传输,如果某个对象想要转换byte[]则必须要经过序列化,因此需要实现序列化接口。
分析2:它的实现类已经StandardMultipartFile
已经实现序列化接口了,为什么传递不了呢?
private static class StandardMultipartFile implements MultipartFile, Serializable {
}
分析3:大家回顾一下上面的源码。MultipartFile 的文件是存储在临时目录的,你把 MultipartFile 传递过去,它在别的服务根本无法获取到其本地的文件,大家应该明白为什么不能传递了吧?如果不是微服务模式(远程调用),Service 接口传递 MultipartFile 是完全没有问题的,因为它们是同一个 Tomcat。
5、小结
这一章节涉及的内容还是比较多,希望大家都能去消化掉,主要讲解三个限制文件大小的细节和其解决方案;如何通过Aop+配置中心灵活控制文件大小;通过跟进源码来分析几个常见的问题;希望本章的内容能对你起到帮助的作用。
纸上得来终觉浅,绝知此事要coding...




