暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

微服务专栏(十七):文件上传阶段性总结

修电脑的杂货店 2021-12-08
960

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


1. 前言

经过前面几章关于文件上传功能的设计及开发,相信大家对上传功能都比较清楚了,涉及的业务相对比较复杂,同时涉及的技术点也比较多(切块上传、分布式锁、分布式存储等等),但是这些都是比较主流并且做项目的时候经常碰到、用到的技术,大家要掌握好。除此之外,我们更要学习好如何去分析和设计一个复杂的功能点,为什么这么说呢?我带过项目比较清楚,很多时候安排一个需求下去,需求说的很清楚了,当时也都表示理解了,但是到真实的代码阶段,结果非常的糟糕。或许你说这是新手吧,其实很多工作几年的,仍然是这样一个状态,其实真正的原因是什么?真正的原因是自己并没有完全理解需求,以为理解了,那只是理解需求是干什么而已,把需求转换代码的这一个过程,自己一直迷迷糊糊,完全跟着感觉做,写到哪算哪,最后检查代码,发现写的乱七八糟的。

大致归纳一下开发中常见的情况:

  1. 没有思路,不懂如何下手,主要是没有理解需求或者是把需求转换代码这一个过程束手无策。遇到这种情况的,一般是刚入行或者基础不是很好或者是没有啥实际项目经验的同学,大部分都是没有形成这方面的思维,很正常。可以把一个完整功能,拆分多个小点去分析,自己动手画图、伪代码、写注释去分析,最后才是动手写代码。

  2. 我记得我当初带一个同事去做切块上传的时候,怎么说都无法想象切块上传是怎么做的,但是最后我把代码写完出来之后,他就表示很简单,能看的懂了。为什么?其实就是自己没有认真去思考,或者干脆就是有依赖性的被动思考。

  3. 还有的情况是,工作好几年了,写的代码仍然乱七八糟的,思路新奇的同学,相信你身边也会有这样的同事。我想说的是,如果你一直做的是传统项目,那么即使工作再多年,你的项目经验仍然是不足以支撑你去做解决方案的选择和技术选型,需要不断的提高自己,扩展自己的眼界。

  4. 不爱写注释、不爱梳理需求,喜欢一上来直接开始写代码,最后的结果往往是事倍功半

个人建议

  1. 画流程图,根据需求通过 visio 工具或者 processOn 把流程画出来,流程图画完之后,你的思路就会变的很清晰,并且很容易发现问题的所在。

  2. 分析,包括这个功能涉及哪些难点、解决方案是什么?每种方案的优缺点是什么?这些问题梳理清楚。

  3. 写注释,先在方法内部把逻辑先用中文写好注释,然后再根据注释去实现逻辑。

以上的建议,你或许觉得浪费时间,其实是养成良好习惯的开始,当你时间久了之后,发现你一拿到需求心中立马知道如何去做了。

以上的归纳,其实就是告诉大家要重视需求分析、设计、捋清思路、最后才是真正的代码阶段,否则将会是事倍功半。

通过前面几章的讲解,上传功能已经算是完成了,但是这里还是得带大家去对整个上传功能做全面的总结,把整个上传功能模块给完善好。

思考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...

文章转载自修电脑的杂货店,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论