java

SpringBoot文件上传进度监听

2020-09-14  本文已影响0人  坠尘_ae94

所有代码点击此处下载.

开始

最近在做关于文件上传的小项目(前端LayUI,后端SpringBoot),需要实现上传进度监听。首先介绍本次测试代码框架。

代码框架

作为Maven项目,首先就是引入依赖:

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.0</version>
            <scope>test</scope>
        </dependency>
<!--        Thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--        SpringBoot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

配置文件:

spring:
  servlet:
    multipart:
      max-file-size: 10000MB
      max-request-size: 100000MB

项目启动类:

@SpringBootApplication
public class UploadApplication {
    public static void main(String[] args) {
        SpringApplication.run(UploadApplication.class, args);
    }
}

控制器:

@Controller
public class IndexController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private String baseFile = "D:\\Temp\\Upload";

    @RequestMapping({"/", "/index"})
    public String index() {
        return "index";
    }

    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam("file") MultipartFile multipartFile) {
        File parentFile = new File(baseFile);
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        String originalFilename = multipartFile.getOriginalFilename();
        File destFile = new File(baseFile + File.separator + originalFilename);
        try {
            multipartFile.transferTo(destFile);
        } catch (IOException e) {
            e.printStackTrace();
            return "{\"code\":0}";
        }

        return "{\"code\":1}";
    }
}

文件上传前端:

<!DOCTYPE HTML>
<html>
<head>
    <title>File Upload</title>
    <link rel="stylesheet" href="https://www.layuicdn.com/layui/css/layui.css"/>
</head>
<body>
<div class="layui-container">
    <div class="layui-row">
        <div class="layui-col-xs4 layui-col-sm7 layui-col-md8">
            <div class="grid-demo layui-bg-green">|</div>
            <br />
            <blockquote class="layui-elem-quote">Upload</blockquote>
            <div class="layui-upload-drag" id="test10">
                <i class="layui-icon"></i>
                <p>点击上传,或将文件拖拽到此处</p>
                <div class="layui-hide" id="uploadDemoView">
                    <hr>
                    <img src="" alt="上传成功" style="max-width: 196px">
                </div>
                <br />
                <!--            进度条-->
                <div class="layui-progress layui-progress-big" lay-showpercent="true" lay-filter="demo">
                    <div id="progress" class="layui-progress-bar layui-bg-red" lay-percent="0%"></div>
                </div>
            </div>
        </div>
    </div>
    <br />
    <button type="button" class="layui-btn" id="testListAction">click to submit</button>
</div>

</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layui-v2.5.6/layui.js"></script>
<script>
    layui.use('form', function(){
        var form = layui.form;
        form.render();
    });

    layui.use(['upload','jquery','element'], function(){
        var $ = layui.jquery
            ,upload = layui.upload
            ,element = layui.element;
        element.init();
        //拖拽上传
        upload.render({
            elem: '#test10'
            ,url: "/upload"
            //限制大小 100M
            // ,size: 100000
            ,auto: false//是否选完文件后自动上传
            ,bindAction: '#testListAction'//指向一个按钮触发上传
            ,method: 'post'
            //接收任何类型的文件
            ,accept: 'file'
            //完成回调
            ,done: function(res){
                layer.msg(res.code);
                if (res.code){
                    layui.$('#uploadDemoView').removeClass('layui-hide').attr('src');
                }
                element.progress('demo','100%');
            }
        });
    });
</script>
</html>

前端主要使用了LayUI的拖拽上传组件。在想文件上传进度监听的时候,我一开始尝试使用LayUI文件上传监听,可是失败了。

后来,我在看代码时无意间发现SpringBoot的MultipartFile接口实现了InputStreamSource接口,那么就说明,它可以直接获取文件的InputStream,那么如果放弃使用transferTo方法,转而自己完成文件传输代码,同时记录已传输字节数何总字节数,再借助Redis或者Session缓存这个记录,那么便可以提供一个上传进度接口以达到上传进度监听的目的了,想到这,我快乐地开始测试。

第一次尝试——getInputStream()

思路有了代码就简单了。

v1

文件上传映射方法:

    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam("file") MultipartFile multipartFile) {
        File parentFile = new File(baseFile);
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        String originalFilename = multipartFile.getOriginalFilename();
        File destFile = new File(baseFile + File.separator + originalFilename);
        try {
//            multipartFile.transferTo(destFile);
            doUpload(multipartFile, destFile);
        } catch (IOException e) {
            e.printStackTrace();
            return "{\"code\":0}";
        }

        return "{\"code\":1}";
    }

其他东西没变,就是把transferTo方法换成了自定义的doUpload方法。

doUpload方法主要实现文件传输的具体逻辑同时缓存传输中间状态。

Progress和ProgressData都是封装的进度:

public class Progress {
    private Long bytesRead;
    private Long totalBytes;
    //Getter、Setter
}
//该类主要用于请求返回
public class ProgressData {
    private Integer data;
    private Boolean success;
    //Getter、Setter
}

这些代码毕竟都是测试用的,所以这里就不用Redis了,直接用Session得了。

    private void doUpload(MultipartFile file, File destFile) throws IOException {
        logger.info("Upload Begin {}", new Date().toString());
        Progress progress = new Progress();
        progress.setBytesRead(0l);
        progress.setTotalBytes(file.getSize());
        session.setAttribute("progress", progress);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(file.getInputStream());
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destFile));
        byte[] buffer = new byte[102400];
        int read = 0;
        while ((read = bufferedInputStream.read(buffer)) != -1) {
            bufferedOutputStream.write(buffer);
            Progress targetProgress = (Progress) session.getAttribute("progress");
//                取出数据并更新
            targetProgress.setBytesRead(targetProgress.getBytesRead() + read);
            session.setAttribute("progress", targetProgress);
        }
        logger.info("Upload end {}", new Date().toString());
        bufferedOutputStream.flush();
        session.removeAttribute("progress");
    }

剩下的就很简单了,写个上传进度监听接口:

    @ResponseBody
    @GetMapping("/progress")
    public ProgressData getProgress(HttpServletRequest request) {
        HttpSession session = request.getSession();
        Progress progress = (Progress) session.getAttribute("progress");
        ProgressData progressData = new ProgressData();
        if (progress == null) {
            progressData.setSuccess(false);
            progressData.setData(0);
            return progressData;
        }
        logger.info("read/all {}/{}", progress.getBytesRead(), progress.getTotalBytes());
        progressData.setData((int) ((double)progress.getBytesRead() * 100 / progress.getTotalBytes()));
        progressData.setSuccess(true);
        return progressData;
    }

逻辑一目了然,从Session中获取进度状态并返回。

前端进度条更新:

    layui.use('form', function(){
        var form = layui.form;
        form.render();
    });

    layui.use(['upload','jquery','element'], function(){
        var $ = layui.jquery
            ,upload = layui.upload
            ,element = layui.element;
        element.init();
        //拖拽上传
        upload.render({
            elem: '#test10'
            ,url: "/upload"
            //限制大小 100M
            // ,size: 100000
            ,auto: false//是否选完文件后自动上传
            ,bindAction: '#testListAction'//指向一个按钮触发上传
            ,method: 'post'
            //接收任何类型的文件
            ,accept: 'file'
            ,before: function (obj){
                getProgress(element);
            }
            //完成回调
            ,done: function(res){
                layer.msg(res.code);
                if (res.code){
                    layui.$('#uploadDemoView').removeClass('layui-hide').attr('src');
                }
                element.progress('demo','100%');
            }
        });
    });

    function getProgress(element) {
        $.ajax({
            url: 'http://localhost:8080/progress',
            type: 'get',
            dataType: 'json',
            success: function (data) {
                //方法中传入的参数data为后台获取的数据
                console.log(data);
                var percent = data.data;
                if (percent != 0) {
                    progress = percent;
                    element.progress('demo', progress + '%');
                }
                console.log(percent);
                $('#progress').attr("lay-percent", percent);
                if (data.data != null){
                    setTimeout(getProgress, 2000, element);
                }
            }
        })
    }

简直🐂🍺。

我美滋滋地开始运行测试,结果发现前端获取到的ProgressData全都是

{"data":0,"success":false}

我日噢,怎么回事,在我不断Debug下,终于发现了猫腻,一次Ajax请求一个Session

v2

简单,我直接用第一次请求的Session不就行了。于是我给IndexController加一个HttpSession属性:

    //    仅用于测试!!
    private HttpSession session;

在用户访问首页的时候赋值:

    @RequestMapping({"/", "/index"})
    public String index(HttpServletRequest request) {
        session = request.getSession();
        return "index";
    }

修改进度获取:

    @ResponseBody
    @GetMapping("/progress")
    public ProgressData getProgress() {
        Progress progress = (Progress) session.getAttribute("progress");
        ProgressData progressData = new ProgressData();
        if (progress == null) {
            progressData.setSuccess(false);
            progressData.setData(0);
            return progressData;
        }
        logger.info("read/all {}/{}", progress.getBytesRead(), progress.getTotalBytes());
        progressData.setData((int) ((double)progress.getBytesRead() * 100 / progress.getTotalBytes()));
        progressData.setSuccess(true);
        return progressData;
    }

测试:

upload_progress_1

这美滋滋啊,大功告成。

等我屁颠屁颠把代码传到服务器测试发现,不管怎么获取进度条数据永远都是0。

查看日志发现:

3------------------Sun Sep 13 15:23:17 CST 2020
Time Spent: 591 ms
4------------------Sun Sep 13 15:23:18 CST 2020

100M的文件0.5s通过我这破网络传完你能信?实际等待时间也远远不止0.5s,然而前端获取到的进度数据永远为0.

查询后发现,Tomcat处理文件上传不会直接上传到目的位置,而是先上传到临时目录,比如Linux下是/tmp,Windows下应该是C:\Users\ {username}\AppData\Local\Temp

logger.info(System.getProperty("java.io.tmpdir"));

但我还是没找到是啥原因造成,知道的同志麻烦教教我。

第二次尝试——自定义解析器

我又继续查资料,终于看到这么一篇文章:SpringBoot+fileUpload获取文件上传进度

觉得好像很有道理的样子,于是开始尝试。

导入文件上传依赖:

    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.3</version>
    </dependency>

自定义解析器:

public class CustomMultipartResolver extends CommonsMultipartResolver {

    @Autowired
    private FileUploadProgressListener progressListener;

    @Override
    protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
        FileUpload fileUpload = prepareFileUpload("UTF-8");
        progressListener.setSession(request.getSession());
        fileUpload.setProgressListener(progressListener);
        try {
            List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
            return parseFileItems(fileItems, "UTF-8");
        } catch (FileUploadException e) {
            e.printStackTrace();
        }
        return super.parseRequest(request);
    }
}

这里需要注意的是导入的大部分文件上传相关的类应该在包org.apache.commons.fileupload下。

进度监听器:

@Component
public class FileUploadProgressListener implements ProgressListener{

    private HttpSession session;

    public void setSession(HttpSession session) {
        this.session = session;
        Progress progress = new Progress();
        progress.setTotalBytes(0l);
        progress.setBytesRead(0l);
        session.setAttribute("progress", progress);
    }

    @Override
    public void update(long l, long l1, int i) {
        Progress progress = (Progress) session.getAttribute("progress");
        if (progress.getBytesRead() == progress.getTotalBytes()
                && progress.getBytesRead() != 0) {
            session.removeAttribute("progress");
        }
        progress.setBytesRead(l);
        progress.setTotalBytes(l1);
        session.setAttribute("progress", progress);
    }
}

配置自定义解析器替换原来的解析器:

@Configuration
public class UploadConfig {
    @Bean(name = "multipartResolver")
    public MultipartResolver multipartResolver(){
        CustomMultipartResolver customMultipartResolver = new CustomMultipartResolver();
        return customMultipartResolver;
    }
}

当然,还有,需要关闭文件上传的自动配置:

@SpringBootApplication(exclude = MultipartAutoConfiguration.class)
public class UploadApplication {
    public static void main(String[] args) {
        SpringApplication.run(UploadApplication.class, args);
    }
}

结果:

upload_progress_2

现在服务端运行正常,还剩个问题就是上传速度有些慢。

发现了个问题,MySQL字符集为UTF- 8时插入不了表情。

参考

  1. SpringBoot+fileUpload获取文件上传进度*
上一篇 下一篇

猜你喜欢

热点阅读