精华内容
下载资源
问答
  • 通用文件上传设计

    千次阅读 2018-12-21 15:47:36
    通用上传文件设计1.实现描述2.数据库设计3.主要代码实现3.1.model3.2.dao3.3.service3.4.controller3.5.前端测试代码4.提示 1.实现描述 前端采用XMLHttpRequest进行文件上传。后端(springboot2.x)接口支持多文件...

    1.问题描述

    18年初跳槽来到新公司,在开发中惊奇的发现公司项目中的文件上传功能竟然存在好几个版本,他们为每个需要文件上传的业务实现了接口,仔细看了下这些上传文件代码,他们主要区别在于每个业务存储的文件类型、文件大小、是否要压缩需要限制或者是文件路径需要指定不同。这几个特性完全可以配置化解决问题(上家公司就是这么做的,配置化实现公用文件上传接口是一个正确的选择)。究其原因可能大家害怕更改之前的接口会照成bug,索性自己重写一个上传接口(反正是复制粘贴过来改下内容吧)。

    那么文件上传如何通过配置化实现公用呢?下面直接进入主题

    技术与接口定义

    后端服务我们直接使用springboot2.X框架,文件上传相关配置信息将入数据库,这样可支持动态修改。测试用例我提供一个html页面,或者你可以直接使用postman。更详细的内容处理请看代码(实现很简单的)

    api信息:
    接口地址:http://127.0.0.1:8080/uploadFile
    请求格式:post | multipart/form-data;

    参数名必填/类型说明
    bizType是/String不同业务对应不同的类型,根据此类型查找文件上传的配置
    bizId否/Long业务id,可以通过该id找到对于的业务信息
    isShrink否(默认false)/boolean是否需要压缩代码为了简介,暂未实现支持是否压缩
    file文件信息,支持多文件上传

    返回列表 JSON: [FileInfo]

    2.数据库设计

    文件信息配置表 file_conf , 不同业务对应不同的配置,这里主要考虑了文件类型、文件大小、文件存储路径等

    字段类型约束说明
    idint(11)必填自增主键id
    bize_typevarchar(20)必填业务类型,不同业务不同的类型
    file_type_limitvarchar(200)非必填允许上传的文件类型(mine-type标准),为空时不限制类型
    file_size_limitvarchar(20)非必填允许上传的文件大小(kb),为空时不限制大小
    pathvarchar(50)必填服务器存储文件的路径
    descriptionvarchar(100)非必填描述,如描述该业务类型对应的文件上传业务功能的业务表
    resource_realmvarchar (100)必填外部访问文件资源相对根路径
    enabledtinyint(4)必填是否可用(默认1可用,0禁用),用于禁止某个业务上传文件的功能
    creat_timedatetime必填创建时间
    last_update_timedatetime非必填最近修改时间

    文件信表 file_info ,主要作用保存文件存储的相关信息,方便某些业务需要统计上传文件或删除文件时需要

    字段类型约束说明
    idint(11)必填自增主键id
    bize_typevarchar(20)必填业务类型
    bize_idint(11)非必填业务id
    original_namevarchar (255)必填文件原名称
    new_namevarchar(50)必填(唯一)文件新名称(随机码
    file_typevarchar(20)必填文件类型
    file_sizevarchar(20)必填文件大小(kb)
    file_pathvarchar(200)必填文件服务器存储绝对路径
    relative_pathvarchar(200)必填文件相对路径,域名+此字段为该资源的请求地址
    creat_timedatetime必填创建时间
    last_update_timedatetime非必填最近修改时间
    del_flagtinyint(1)必填逻辑删除(默认0正常,1文件已被物理删除)

    3.主要代码实现

    3.1.model

    对应数据库表结构建立对应实体类

    /**
     * 文件配置信息</p>
     * table: file_conf
     * @author lilee
     * @version 1.0.0
     * @date 2018/12/20 14:55
     */
    public class FileConf {
        private Long id;  // 主键ID
        private String bizType;  // 上传服务类型
        private String fileTypeLimit; // 文件类型(mine-type标准),为空不限制上传类型
        private String fileSizeLimit; //(kb)文件限制大小,为空不限制上传大小(但要满足框架支持的上传文件大小)
        private String path; // 服务器文件夹路径
        private String description;  // 描述
        private String resourceRealm; // 访问资源路径
        private Boolean enabled; // 是否可用(默认1可用,0禁用)
        private Date createTime;  // 创建时间
        private Date lastUpdateTime;  // 最后修改时间
       
        // setter & getter
    }
    
    /**
     * 文件信息</p>
     * table: file_info
     * @author lilee
     * @version 1.0.0
     * @date 2018/12/20 14:55
     */
    public class FileInfo {
        private Long id;  // 主键ID
        private String originalName;  // 文件原名称
        private String newName; // 文件新名称
        private String fileType;  // 文件类型(image/jpg, image/png, video/mp4, xsl,doc等)
        private String fileSize; // 文件大小(kb)
        private String filePath;  // 文件服务器存储路径
        private String relativePath;  // 文件相对路径
        private Long bizId;  // 业务ID
        private String bizType;  // 上传服务类型(业务类型)
        private Date createTime;  // 创建时间
        private Date lastUpdateTime;  // 最后修改时间
        private Boolean delFlag; // 数据删除标记0=正常,1=文件已物理删除
    	
    	// setter & getter
    
    

    3.2.dao

    dao层,本实例为了简单,并未提供数据库操作相关代码,该模块需要用户根据自己项目架构自己实现
    当前文件存储到/home/data下

    @Repository
    public class FileConfDao {
    	// 根据业务类型bizType获取该业务的配置信息
        public FileConf selectByBizType(String bizType) {
            // todo 为了简单,未正真的对数据库操作 
        	// FileConf fileConf = dbdao.findByBizType(bizType);
            FileConf fileConf = new FileConf();
            fileConf.setBizType(bizType);
            fileConf.setPath("/home/data");
            fileConf.setResourceRealm("/res");
            fileConf.setEnabled(true);
            return fileConf;
        }
    	// 存储文件的信息
         public FileInfo insert(FileInfo fileInfo) {
           //  dbdao.insert(fileInfo)
            return fileInfo;
        }
    }
    

    3.3.service

    该模块实现文件上传主体,主要包括对文件参数信息验证,对文件上传是否符合业务的配置验证

    /**
     * TODO 文件上传service
     *
     * @author lilee
     * @version 1.0.0
     * @date 2018/12/20 14:55
     */
    @Service
    public class FileUploadService {
    
        @Resource
        private FileConfDao fileConfDao;
    
        // @Resource
        // private FileInfoDao fileInfoDao;
    
        protected static Logger log = LoggerFactory.getLogger(FileUploadService.class);
    
        /**
         * 文件上传
         * @param mpfList  文件信息集
         * @param bizType 业务类型(必传)
         * @param bizId   业务id
         * @param extraPath  额外的路径,首部和结尾不能带斜杠'/'
         * @return
         */
        public List<FileInfo> uploadFile(List<MultipartFile> mpfList, String bizType, Long bizId, String extraPath) {
            // 验证数据begin
            // 获取对应业务文件配置信息
            FileConf fileConf = this.fileConfDao.selectByBizType(bizType);
            if(fileConf == null){
                log.info("file conf is null");  // 打印文件配置信息
                return null;
            }
            // 验证文件信息是否符合配置信息
            if (!validateFileInfo(mpfList, fileConf)) {
                // 验证失败
                log.info("fileInfo is error");  // 打印文件配置信息
                return null;
            }
            // 信息验证end
    
            List<FileInfo> files = new ArrayList<>();
            FileInfo fileInfo = null;
            String path = fileConf.getPath();  // 文件存储的目录
            // 获取相对路径,由file_conf、额外路径
            String relativePath = fileConf.getResourceRealm() + "/"
                    + (StringUtils.isEmpty(extraPath) ? "" : extraPath + "/");
    
            // 验证服务器存储路径是否存在,若不存在,则新建文件夹
            File serFile = new File(path + relativePath);
            if (!serFile.exists()) {
                serFile.mkdirs();
            }
    
            // 循环上传文件
            for (MultipartFile mpf : mpfList) {
                String originalFileName = mpf.getOriginalFilename(); // 获取源文件名
                // 生成新文件名
                String newFileName = "F" + UUID.randomUUID().toString().replace("-", "").toUpperCase()
                        + originalFileName.substring(originalFileName.lastIndexOf("."));
                // 组装数据
                fileInfo = new FileInfo();
                fileInfo.setOriginalName(originalFileName);
                fileInfo.setFileSize(String.valueOf(mpf.getSize() / 1024)); // 单位(kb)
                fileInfo.setFileType(mpf.getContentType());     // 文件类型
                fileInfo.setNewName(newFileName);                        // 文件新名字
                fileInfo.setRelativePath(relativePath + newFileName);    // 文件相对路径
                fileInfo.setFilePath(path + relativePath + newFileName); // 文件物理路径
                fileInfo.setBizType(bizType);
                fileInfo.setBizId(bizId);
                fileInfo.setDelFlag(false);
                // 存储文件并记录到数据库
                try {
                    FileCopyUtils.copy(mpf.getBytes(), new FileOutputStream(fileInfo.getFilePath()));
                    fileConfDao.insert(fileInfo); 
                } catch (IOException e) {
                    log.error("upload file error!", e);
                    return null;
                }
                files.add(fileInfo);
            }
            return files;
        }
    
        private boolean validateFileInfo(List<MultipartFile> mpfList, FileConf fileConf) {
            if (mpfList == null || fileConf == null) { return false; }
            for (MultipartFile mpf : mpfList) {
                // 验证文件大小是否超出配置大小
                if (!StringUtils.isEmpty(fileConf.getFileSizeLimit()) && mpf.getSize() / 1024 > Integer.parseInt(fileConf.getFileSizeLimit())) {
                    return false;
                }
                // 验证文件类型是否符合文件配置的要求
                if (!StringUtils.isEmpty(fileConf.getFileTypeLimit()) && fileConf.getFileTypeLimit().indexOf(mpf.getContentType()) < 0) {
                    return false;
                }
            }
            return true;
        }
    }
    
    

    3.4.controller

    ****简单controller****

    @RestController
    public class FileUploadController {
        @Resource
        private FileUploadService fileUploadService;
    
        /**
         * 文件上传接口
         * @param request  
         * @param bizType 业务类型(必传)
         * @param bizId   业务id
         * @param extraPath  额外的路径,首部和结尾不能带斜杠'/'
         * @return
         */
        @RequestMapping(value ="/uploadFile", method = RequestMethod.POST)
        public List<FileInfo> uploadFile(MultipartHttpServletRequest request, String bizType, Long bizId, String extraPath) {
            int count = 0;
            List<FileInfo> result = this.fileUploadService.uploadFile(request.getMultiFileMap().get("fileData"), bizType, bizId, extraPath);
            return result;
        }
    }
    

    3.5.html测试代码

    使用postman更简单哦,记得考虑跨越问题
    引用博客https://www.cnblogs.com/tianyuchen/p/5594641.html

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
        <title>fileUpload Test</title>
        <script type="text/javascript">
            var xhr;
            var ot;//
            var oloaded;
            //上传文件方法
            function upladFile() {
                var url = "http://127.0.0.1:8080/uploadFile"; // 接收上传文件的后台地址 
                
                var form = new FormData(); // FormData 对象
    			var fileObj = document.getElementById("file").files; // js 获取文件对象
    			if (fileObj != null) {
    				for (var i=0; i< fileObj.length; i++) {
    					form.append('fileData', fileObj[i], fileObj[i].name);
    				}
    			}
                xhr = new XMLHttpRequest();  // XMLHttpRequest 对象
                xhr.open("post", url, true); //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
                xhr.onload = uploadComplete; //请求完成(成功)
                xhr.onerror =  uploadFailed; //请求失败
                xhr.upload.onprogress = progressFunction;		//【上传进度调用方法实现】
                xhr.upload.onloadstart = function(){			//上传开始执行方法(初始)
                    ot = new Date().getTime();   				//设置上传开始时间
                    oloaded = 0;								//设置上传开始时,以上传的文件大小为0
                };
                xhr.send(form); 
            }
            //上传进度实现方法,上传过程中会频繁调用该方法
            function progressFunction(evt) {
                 var progressBar = document.getElementById("progressBar");
                 var percentageDiv = document.getElementById("percentage");
                 // event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
                 if (evt.lengthComputable) {		
                     progressBar.max = evt.total;
                     progressBar.value = evt.loaded;
                     percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
                 }
                
                var time = document.getElementById("time");
                var nt = new Date().getTime();//获取当前时间
                var pertime = (nt-ot) / 1000; //计算出上次调用该方法时到现在的时间差,单位为s
                ot = new Date().getTime(); //重新赋值时间,用于下次计算
                
                var perload = evt.loaded - oloaded; //计算该分段上传的文件大小,单位b       
                oloaded = evt.loaded;//重新赋值已上传文件大小,用以下次计算
            
                //上传速度计算
                var speed = perload / pertime; //单位b/s
                var bspeed = speed;
                var units = 'b/s';//单位名称
                if(speed/1024 > 1){
                    speed /= 1024;
                    units = 'k/s';
                }
                if(speed/1024 > 1){
                    speed /= 1024;
                    units = 'M/s';
                }
                speed = speed.toFixed(1);
                //剩余时间
                var resttime = ((evt.total - evt.loaded) / bspeed).toFixed(1);
                time.innerHTML = ',速度:' + speed + units + ',剩余时间:' + resttime + 's';
    			if(bspeed == 0)
    			time.innerHTML = '上传已取消';
            }
            //上传成功响应
            function uploadComplete(evt) {
    			alert("上传成功!");
            }
            //上传失败
            function uploadFailed(evt) {
                alert("上传失败!");
            }
             //取消上传
            function cancleUploadFile(){
                xhr.abort();
            }
        </script>
    </head>
    <body>
        <progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
        <span id="percentage"></span><span id="time"></span>
        <br /><br />
        <input type="file" id="file" name="myfile" multiple="multiple"/>
        <input type="button" onclick="upladFile()" value="上传(多选)" />
        <input type="button" onclick="cancleUploadFile()" value="取消" />
    </body>
    </html>
    

    记得Application跨域解决:

    	@Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurerAdapter() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
                            .allowedMethods("PUT", "DELETE","GET","POST")
                            .allowedHeaders("*")
                            .exposedHeaders("access-control-allow-headers",
                                    "access-control-allow-methods",
                                    "access-control-allow-origin",
                                    "access-control-max-age",
                                    "X-Frame-Options")
                            .allowCredentials(false).maxAge(3600);
                }
            };
        }
    

    4.最后

    上传文件的接口返回的url需要另外提供接口支持,可简单nginx配置代理文件资源。



    、﹗∕
    — 〇 -
    ╱︱ ヽ
    但行好事、莫问前程! 大家好、我是lilee
    _________________ *_*______
    ____ ____ ____
    展开全文
  • 文件上传功能描述

    千次阅读 2017-06-20 19:04:34
    文件上传功能是系统操作比较常用的功能

    文件上传功能是系统操作比较常用的功能,根据客户要求不同,文件上传主要包括以下几点功能:

    1、文件类型支持:系统根据不同需求,要求支持不同的文件类型。图片一般包括:jpg、jpeg、bmp、gif等等格式,文档一般包括.doc、.docx、.xls、.xlsx、.ppt、.pptx等等。同时也为了确保系统的安全性,只允许系统允许上传的文件类型;


    2、文件大小:文件大小进行限制,上传速度决定着用户体验,同时也是系统运行的主要部分。太大的文件,上传下载存放都是文件管理必须考虑的因素。一般系统会限制文件大小,大约在50M之内;


    3、文件压缩:既然对文件大小有一定要求,自然在系统上传的时候,就需要考虑减少带宽流量,压缩可以节省很大资源。压缩技术的实现根据文件类型不同进行压缩控制,有些文件压缩率很大,有些文件压缩率小。压缩技术是上传功能是文件大小功能的延续;


    4、加密传输:文件传输主要有两种方式:http和ftp,对于普通文件,在传输的过程会忽略安全性,但对一些重要文件,在上传时需要考虑加密,加强文件管理的安全性,是系统针对不同应用需要考虑的技术点;


    5、断点续传:断点续传,顾名思义,在网络断开恢复正常的时候,要能支持继续上传的功能。这也是文件上传必须考虑的技术点。当前网络环境,不稳定因素很多,对于大的文件,很少能一次上传成功,需要多次上传,如果每次都从头开始上传,显然不符合上传要达到的目的。所以,断点续传是文件上传功能一个特点。


    综合上述几点,文件上传是系统管理常用的功能,但在设计该功能的时候,要结合客户实际需求、当前网络环境、电脑软硬件情况等进行综合考虑。

    展开全文
  • 本文详细讲解了如何实现文件上传功能。掌握全套的文件上传方法。

    重要声明:本文章仅仅代表了作者个人对此观点的理解和表述。读者请查阅时持自己的意见进行讨论。

    本文更新不及时,请到原文地址浏览:《超详细的实现上传文件功能教程,文件上传实现。》。

    一、文件上传的方式

    在程序的世界里,没有什么功能的实现方式是单一的。上传文件也不例外,我们有很多种能够实现文件上传的方法。但我们最终要采用的,必然是最熟悉、最常用的方法。文件上传通常有下面的方法进行:

    1. 将内容进行base64,将base64字符串结果通过普通请求提交给后台。
    2. 直接使用浏览器的form表单进行文件上传。
    3. 使用java模拟表单(自己封装表单格式数据)进行文件上传。

    这些文件上传方式,都是基于HTTP协议的基础,它并没有强制限定了我们要如何去编码或处理数据,相反协议的存在使世界上所有的程序差异化不是那么太大。HTTP协议规定了,提交大量数据时,可以使用POST请求来进行提交。是的,大多数后台程序都只会在POST请求下去获取大量上传的数据,而不会在GET请求下去获取。你要知道,如果你不规范提交,当然也是可以不规范的从GET下获取到提交的数据。但极其不建议这样做。

    如果你对HTTP协议还不是很了解,你可以通过这篇文章来入门对HTTP协议的了解:《【HTTP,Java】认识HTTP协议,在互联网世界里翱翔》。

    二、base64 上传文件

    1、HTML网页内使用base64

    如果你要在网页中使用base64的方式将文件提交给后台,那么你不得不面临一个很严峻的问题就是:如何在网页中奖文件内容读取并转换成base64的数据。很幸运HTML5为我们提供了解决方案。

    FileReader提供了读取文件的能力。但它可不是你想象的提供一个文件路径就可以读取了。它能读取的文件需是来自下面的途径:

    1. 通过<input type="file">节点选择的文件。
    2. 通过拖放(DataTransfer)文件事件得到的文件。
    3. Canvas通过mozGetAsFile()方法返回的文件。

    下面以input选择的文件为例进行讲解。

    a、选择文件并转换为base64

    直接在html文件中写一个input并将其type设定为file即可立即实现一个文件选择功能。现在同时给出读取文件的示列代码:

    <body>
        <!-- 文件选择和上传按钮 -->
        <input type="file" id="fileSelecter"><button onclick="uploadFile()">上传</button>
    
        <!-- 上传逻辑 -->
        <script>
            function uploadFile() {
    
                // 找到文件文件选择框
                var fileInput = document.querySelector("#fileSelecter");
    
                // 获取选择的文件
                // (因为input是支持选择多个文件的,所以获取文件通过files字段,如果单个文件也是在这个files列表里。)
                var file = fileInput.files.item(0);
    
                // 判断一下
                if (file == null) {
                    // 没有选择文件。就什么都不处理。
                    return;
                }
    
                // 使用FileReader读取文件。
                var fileReader = new FileReader();
    
                fileReader.addEventListener("error", function (ev) {
                    // 文件读取出错时,执行此方法。
                    // 通过 fileReader.error 可以获取到错误信息。
                });
    
                fileReader.addEventListener("load", function (ev) {
                    // 文件读取成功后调用此方法。
                    // 通过 fileReader.result 即可获取到文件内容。
                });
    
                fileReader.addEventListener("loadstart", function (ev) {
                    // 读取开始时此方法被调用。
                });
    
                fileReader.addEventListener("loadend", function (ev) {
                    // 文件读取结束时执行此方法。
                    // 无论读取成功,还是读取失败。
                    // 总之,在结束读文件操作时,此方法都会调用。
                });
    
                fileReader.addEventListener("abort", function (ev) {
                    // 文件读取被中断时,此方法调用。
                    // 你可以通过 fileReader.abort() 方法随时中断文件的读取。
                });
    
                fileReader.addEventListener("progress", function (ev) {
                    // 读取文件过程不是一次性读完的,会进行多次读取。
                    // 没读取一次,本方法执行一次。
                });
    
                // 将文件内容读取为 base64 内容。通过 fileReader.result 即可返回base64的数据内容。
                fileReader.readAsDataURL(file);
            }
        </script>
    </body>
    

    上述代码中详细注释了使用FileReader如何读取文件的使用方式。通过监听load事件,即可获取到文件读取的结果数据。如果你将这个结果打印出来,你将看到类似下面的数据:

    data:image/jpeg;base64,/9j/4AAQSkZJergxawffwetcwgIKFUEGNiwgefnwkef...KCAcHCg0
    

    如果你看到了类似的数据。那么你就算是成功一半了。

    注意: 其中data:image/jpeg;base64,这一段属于对base64数据体的描述。文件真实base64内容只有逗号后面部分。如果后端接口明确告知你只需要传递后面的数据,那么你还需要自行将前面的描述去除。

    b、上传结果到接口

    现在建立请求将得到的结果提交到服务器即可。在load监听事件里我们可以成功的获取到base64的结果,因此只需要将提交数据代码写在监听里即可。这里我直接使用JQuerypost方法进行数据提交。代码如下:

    fileReader.addEventListener("load", function (ev) {
        // 文件读取成功后调用此方法。
        // 通过 fileReader.result 即可获取到文件内容。
        var result = fileReader.result;
        $.post("https://www.microanswer.cn/test/uploadBase64", {
            base64Data: result
        }, function (response) {
            // 服务器响应了我们的上传请求。
        });
    });
    

    如果你对JQuery的post方法不是很熟悉,请参考:【JQuery】JQuery常用方法总结、大全

    2、Java程序内使用base64

    在使用java作为后台程序的时候,有时候也经常通过http向其他服务器上传内容,那如果是使用base64方式,如何将数据以base64提交呢?其实非常简单,和上一小节里提到的BASE64Decoder有点类似,Java里还有一个类BASE64Encoder,他可以方便的将文件转为一个base64字符串,看下面的示列代码:

    // 将文件转为base64字符串
    public String file2Base64(String filePath) {
        // 建立文件对象
        File file = new File(filePath);
        BASE64Encoder base64Encoder = new BASE64Encoder();
        ByteArrayOutputStream out = null;
        FileInputStream fileInputStream = null;
        // 标准流读取代码模板。
        try {
            out = new ByteArrayOutputStream();
            fileInputStream = new FileInputStream(file);
            byte[] data = new byte[1024];
            int datasize = 0;
            while ((datasize = fileInputStream.read(data)) != -1) {
                out.write(data, 0, datasize);
            }
            out.flush();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
            }catch (Exception ignore) {}
        }
        return base64Encoder.encode(out.toByteArray());
    }
    

    有了base64字符串,就可以方便的进行上传了。相信后台程序一般都会有一个封装好了的网络请求工具类,下面示列一个典型的上传代码:

    String base64 = file2Base64("D:/temp/test.doc");
    HashMap<String, String> param = new HashMap<>();
    param.put("base64Data", base64);
    String response = HttpUtil.postFormUrlEncode("https://www.microanswer.cn/test/uploadBase64", param);
    

    此处使用的HttpUtil工具你可以通过下面的maven依赖获取:

    <dependency>
        <groupId>cn.microanswer</groupId>
        <artifactId>HttpUtil</artifactId>
        <version>1.0.1</version>
    </dependency>
    

    3、后台接口

    上述请求中,将数据以 base64Data 字段提交给了接口。现在只需要处理服务器响应后的事情了。先看看接口是如何获取数据的把:

    @RequestMapping("/uploadBase64")
    public Object uploadBase64Data(HttpServletRequest request) throws Exception {
        String base64Data = request.getParameter("base64Data");
    
        // 保存文件后缀名。
        String fileExtName = "";
        // 保存文件真实的base64数据
        String realBase64;
    
        // 判断是否有前缀
        if (base64Data.startsWith("data:") && base64Data.contains("base64,")) {
            String[] typeAndData = base64Data.split(",");
    
            // 截取 data:image/jpeg;base64, 为=>  jpeg;base64,
            String tempType = typeAndData[0].split("/")[1];
            // 拿到 ; 号前面的内容作为文件后缀。
            fileExtName = "." + tempType.substring(0, tempType.indexOf(";"));
    
            // 拿到真实base64数据。
            realBase64 = typeAndData[1];
        } else {
            // 没有前缀,则认为所有字符串都是base64实际内容。
            realBase64 = base64Data;
        }
    
        // 对 base64 字符串进行原数据读取
        byte[] bytes = new BASE64Decoder().decodeBuffer(realBase64);
    
        // 构建文件将数据保存
        File file = new File("D:/temp/" + UUID.randomUUID().toString() + fileExtName);
        FileOutputStream fo = new FileOutputStream(file);
    
        // 输出文件内容
        fo.write(bytes);
        fo.flush();
        fo.close();
    
        // 返回成功
        JSONObject object = new JSONObject();
        object.put("code", 200);
        object.put("msg", "上传成功");
        return object;
    }
    

    三、form表单上传文件

    对于前端来说,这种方式无疑是最简单的上传方式,因为甚至可以做到不写一点JavaScript代码就实现了。为了对前端上传文件有更深入了解,使用form表单上传时也有两种上传方案,一种是直接使用form节点的,另一种则是通过FromData对象来完成。

    1、使用html的form节点

    下面直接展示示列代码:

    <form method="post"
          action="https://www.microanswer.cn/test/uploadFile"
          enctype="multipart/form-data">
        请选择文件:<input type="file" name="fileData"><br>
        请输入文件描述:<input type="text" name="fileDescription"><br>
        <input type="submit" value="提交">
    </form>
    

    这样,前端就实现了文件上传了。提交给接口uploadFile要怎么去获取里面的文件,前端不用管了。

    2、使用 FormData 对象

    当使用FormData完成文件上传时,事情就变得稍微复杂一点了。但是有它的优势。直接在页面上使用form表单会造成页面的跳转,上传成功后就不会再停留在当前页面了。而使用 FormData 进行文件上传则可以完成异步上传,达到不跳页面的效果。

    首先要清除一点,FormData对象要提交数据时,其文件数据是来自于<input type="file">选择的文件。加入现在你已经选好了文件并且拿到了file对象,那么下面示列一个如何将此数据上传到服务端的代码:

    // 文件上传方法。 callback 是上传完成回调。
    function uploadFile(file, callback) {
        var formData = new FormData();
        formData.append("fileData", file); // 添加文件到 formData。
        formData.append("fileDescription", "文件上传描述信息。"); // 添加一个描述字段。
    
        // 进行上传
        $.ajax({
            url: "https://www.microanswer.cn/test/uploadFile",
            type: 'post',
            data: formData,
            contentType: false, // 不指定contentType,这样让JQuery主动识别
            processData: false, // 不让JQuery处理上传的数据,
            dataType: 'json',   // 预期返回数据格式。
            success: function (response) {
                callback(response);
            },
            error: function (xmlHttpRequest, statusStr, exception) {
                callback(undefined, exception);
            }
        });
    }
    

    代码中可以看到,首先使用 FormData 创建了一个实例,然后将文件和描述信息放入其中,最后通过 JQuery 的 ajax 方法将数据提交给后台接口。

    四、使用Java模拟表单

    java里没有类似formdata的上传文件辅助类,我们需要根据表单在上传文件时数据具体是如何进行提交的流程进行自己使用java来实现。摆在我们面前的首要任务就是要搞明白表单提交的数据结构,只有详细了解了其结构,才能使用Java进行完美的模拟。

    1、文件上传的数据内容格式

    通过表单上传文件显然是HTTP协议内的一部分内容,因此我们不妨直接翻开HTTP协议里针对表单上传文件相关的定义文档:RFC1867。下面是一些重点内容的截取部分(博主能力有限,翻译得不好还请将就凑合):

    multipart/form-data的定义

    数据格式内容举例

    ww

    2、Java代码实现表单

    通过上述HTTP协议定义文档中的描述,相信文件表单在数据提交过程中数据传输方式和格式已经有了大致的了解。现在是时候使用Java实现这样一个功能了。

    a、单个表单文件单元

    根据定义,一个input可以支持选择多个文件的,而在传输时,每个文件的传输需要提供一些基本信息。因此,直接为单个文件单元实现为一个类:

    /**
     * InputFile.java
     * 用于放在表单 Input 里的文件单元。
     */
    public class InputFile {
    
        // 保存当前需要传递的文件引用。
        private File file;
    
        // 如果要传递的数据直接是二进制数据了,这里也提供一个变量,使支持二进制数据直接传递。
        private byte[] byteData;
    
        // 在不清楚上传的数据是什么,但确切知道上传的内容是一个inputstream里的内容。这里也提供一个变量,使支持输入流直接传递。
        private InputStream inputStream;
    
        public InputFile(File file) { this.file = file; }
    
        // 对于数组,不要直接引用,而是取一份其拷贝的数据。
        public InputFile(byte[] byteData) {
            this.byteData = new byte[byteData.length];
            System.arraycopy(byteData, 0, this.byteData, 0, byteData.length);
        }
        public InputFile(InputStream inputStream) { this.inputStream = inputStream; }
    
    
        // 当进行提交时,使用此方法,此方法将会把当前单元持有的[文件\数据\流]进行上传。
        public void submit(OutputStream outputStream) throws Exception{
    
            // 输出是,首先建立基本信息输出。
            StringBuilder baseInfo = new StringBuilder();
    
            if (file != null) {
                // 有文件,则输出文件。
                baseInfo.append(" filename=\"").append(file.getName()).append("\"").append(Constant.LINE_SEPARATOR);
                baseInfo.append("Content-Type: ").append(new MimetypesFileTypeMap().getContentType(file)).append(Constant.LINE_SEPARATOR);
                baseInfo.append(Constant.LINE_SEPARATOR);
    
                inputStream = new FileInputStream(file);
    
            } else {
                // 输出数据 或 流,但因为不知道名称,因此填写一个随机名称。
                baseInfo.append(" filename=\"")
                        .append(UUID.randomUUID().toString().replaceAll("-", ""))
                        .append("\"")
                        .append(Constant.LINE_SEPARATOR)
                        .append("Content-Type: application/octet-stream").append(Constant.LINE_SEPARATOR)
                        .append(Constant.LINE_SEPARATOR);
            }
    
            outputStream.write(baseInfo.toString().getBytes(Constant.charset));
    
            // 将具备的流信息输出。
            if (inputStream != null) {
                byte[] datas = new byte[1024];
                int datasize = 0;
    
                while ((datasize = inputStream.read(datas))!= -1) {
                    outputStream.write(datas, 0, datasize);
                }
    
                if (file != null) {
                    inputStream.close();
                }
            } else if (byteData != null && byteData.length > 0) {
                // 将数据输出。
                outputStream.write(byteData);
            }
    
            outputStream.write(Constant.LINE_SEPARATOR.getBytes(Constant.charset));
        }
    }
    
    b、Input单元

    input单元里可以放置一个键值对,用来传递数据,同时,它还可以传递键和一系列文件,上一节的文件单元就可以放在这个Input单元中。则可以设计出 Input单元类如下:

    /**
     * Input.java
     * 表单里面的一条input输入单元。
     */
    public class Input {
        // 表单支持的类型枚举
        public enum Type { text,file }
    
        // 此 input 要提交的name值。
        private String name;
    
        // 此 input 要提交的value值。当type为text时才会提交此数据。
        private String value;
    
        // 此 input 要提交的文件。当type为file时才会提交此数据。
        private ArrayList<InputFile> inputFiles;
    
        // 此表单的类型,目前只有:text 和 file
        private Type type;
    
        // 允许直接使用键值对构造一个text的input。
        public Input(String name, String value) {
            this.name = name;
            this.value = value;
            this.type = Type.text;
        }
    
        // 允许使用name和文件列表构造一个file的input。
        public Input(String name, InputFile... inputFile) {
            this.name = name;
            this.inputFiles = new ArrayList<>();
            this.inputFiles.addAll(Arrays.asList(inputFile));
            this.type = Type.file;
        }
    
        // 在提交前,允许添加文件。
        public Input addFile(InputFile inputFile) {
            this.inputFiles.add(inputFile);
            return this;
        }
    
        // 在提交前,允许修改要提交的值。
        public Input setValue(String value) {
            this.value = value;
            return this;
        }
    
    
        // 将此input内的数据进行提交。
        public void submit(OutputStream outputStream) throws Exception {
            // 先构建基本信息输出。
            StringBuilder baseInfo = new StringBuilder();
            baseInfo.append("content-disposition: form-data; name=\"").append(name).append("\";");
    
            if (type == Type.text) {
                baseInfo.append(Constant.LINE_SEPARATOR);
                baseInfo.append(Constant.LINE_SEPARATOR);
                baseInfo.append(value);
                baseInfo.append(Constant.LINE_SEPARATOR);
    
                String s = baseInfo.toString();
                outputStream.write(s.getBytes(Constant.charset));
            } else {
    
                if (inputFiles != null && inputFiles.size() > 0) {
    
                    if (inputFiles.size()  == 1) {
    
                        outputStream.write(baseInfo.toString().getBytes(Constant.charset));
                        inputFiles.get(0).submit(outputStream);
                    } else {
                        String boundary = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 6);
    
                        // 此input有多个文件。先输出内容体的基本信息。
                        baseInfo.append(Constant.LINE_SEPARATOR);
                        baseInfo.append("Content-Type: multipart/mixed, boundary=").append(boundary);
                        baseInfo.append(Constant.LINE_SEPARATOR);
                        baseInfo.append(Constant.LINE_SEPARATOR);
    
                        // 循环输出文件内容
                        for (InputFile infile: inputFiles) {
                            baseInfo.append("--").append(boundary).append(Constant.LINE_SEPARATOR);
                            baseInfo.append("content-disposition: attachment;");
                            outputStream.write(baseInfo.toString().getBytes(Constant.charset));
                            infile.submit(outputStream);
    
                            baseInfo = new StringBuilder();
                        }
    
                        // 追加上最后一个 boundary。
                        baseInfo.append("--").append(boundary).append("--").append(Constant.LINE_SEPARATOR);
                        outputStream.write(baseInfo.toString().getBytes(Constant.charset));
                    }
                }
            }
        }
    }
    
    
    c、 form 表单

    实现了 Input 之后,最后一个便是form实现了,form只需要实现其基本的数据组装即可完成:

    /**
     * Form.java
     * 模拟HTML页面表单行为。
     */
    public class Form {
    
        private ArrayList<Input> inputs;
    
        public Form() {
            this.inputs = new ArrayList<>();
        }
    
        // 往表单中添加一个input数据。
        public Form addInput(Input input) {
            this.inputs.add(input);
            return this;
        }
    
        // 提交这个表单。传入你要提交到的目标地址。
        // 此处代码比较简单,可以按需自己实现或修复其中存在的问题。
        public String submit(String url) throws Exception {
            if (inputs == null || inputs.size() == 0) {
                throw new NullPointerException("请至少添加一个要提交的表单数据。");
            }
    
            URL url1 = new URL(url);
            URLConnection urlConnection = url1.openConnection();
    
            HttpURLConnection c = (HttpURLConnection) urlConnection;
    
            // 设置请求方式、
            c.setRequestMethod("POST");
            c.setDoOutput(true);
            c.setDoInput(true);
    
            String boundary = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 6);
    
            // 设置header
            c.setRequestProperty("Content-Type", "multipart/form-data, boundary=" + boundary);
    
            // 进行传输。
            OutputStream outputStream = c.getOutputStream();
            for (Input in : inputs) {
    
                outputStream.write(("--" + boundary + Constant.LINE_SEPARATOR).getBytes(Constant.charset));
                in.submit(outputStream);
            }
    
            outputStream.write(("--" + boundary + "--").getBytes(Constant.charset));
            outputStream.flush();
    
            // 获取结果
            InputStream inputStream = c.getInputStream();
    
            // 读取结果字符串。
            InputStreamReader reader = new InputStreamReader(inputStream, Constant.charset);
            char[] chars = new char[256];
            int charsize=  0;
    
            StringBuilder result = new StringBuilder();
            while ((charsize = reader.read(chars))!= -1) {
                result.append(chars, 0, charsize);
            }
            c.disconnect();
    
            return result.toString();
        }
    }
    
    d、测试结果

    现在InputFileInputForm 类都已完成开发,不妨进行一下测试:

    // 构建一个表单
    Form form = new Form();
    
    // 加入一个普通的键值对。
    form.addInput(new Input("name", "Jack"));
    
    // 加入一个包含多个文件的input。
    form.addInput(new Input("fileData",
            new InputFile(new File("C:\\Users\\Micro\\Desktop\\file1.jpg")),
            new InputFile(new File("C:\\Users\\Micro\\Desktop\\file2.jpg"))
    ));
    
    // 提交到指定地址。
    String result = form.submit("https://www.microanswer.cn/test/uploadFile");
    
    // 打印结果
    System.out.println("提交结果:" + result);
    
    // 输出:提交结果:{"msg":"success","code":200,"data":null}
    
    

    完美通过。

    e、附加类

    在上述几个类中,使用了一个常量类,其代码如下:

    public class Constant {
    
        /**
         * 表单中上传文件,使用的换行必须是 \r\n。
         */
        public static final String LINE_SEPARATOR = "\r\n";
    
        /**
         * 表单中字符串的编码。
         */
        public static final Charset charset = StandardCharsets.UTF_8;
    
    }
    

    五、后台接收表单文件

    本文主要讲了Java后台如何实现获取表单内的数据,下面将分别对spring后台和原生的servlet后台进行示例。

    1、Spring后台

    如果你的后台使用了Spring框架,那么你就幸运了,你可以十分方便的拿到表单上传上来的文件。只需要通过一些简单的注解就可以完成,下面示列了一个典型的Controller接口方法,用于获取表单上传上来的文件:

    @RequestMapping("/uploadFile")
    public Object uploadFileTest(
            @RequestParam(value = "fileData") MultipartFile multipartFile,
            HttpServletRequest request
    ) throws Exception {
        // 即可拿到已上传的文件内容。如果你不处理,这个文件就会在本次请求结束时被删除
        InputStream in = multipartFile.getInputStream();
    
        return Util.buildReturnJson(WebApplication.Code.SUCCESS, "success", null);
    }
    

    这个方法针对input里只有一个文件时非常方便。如果某个input里包含了多个文件,这个方法似乎没法获取到更多的文件。(如果这个方法能的法话,还请大佬评论指正。)

    当你希望获取到某个input下的所有上传的文件时。你可以用下面的方法:

    @RequestMapping("/uploadFile")
    public Object uploadFileTest(HttpServletRequest request) throws Exception {
    
        // 使用 spring 提供的内置工具
        StandardMultipartHttpServletRequest r = new StandardMultipartHttpServletRequest(request);
    
        // 可以直接获取到input里如果是text类型的值。
        String name = r.getParameter("name");
    
        // 可以通过这样的方式获取到某个input下的所有文件。
        // 这里就获取了input的name为files时,提交上来的所有文件。
        List<MultipartFile> files = r.getMultiFileMap().get("files");
    
        return Util.buildReturnJson(WebApplication.Code.SUCCESS, "success", null);
    }
    

    2、servlet 实现

    在servlet中获取表单上传的文件相对比spring里复杂,但其实spring只是把servlet的封装了一下。咱们java后台的表单解析功能是自带就有的。下面给出一个示例:

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 获取表单的所有 part 内容。
        // 一个 text 的input 是一个 part
        // 一个 file 的input 里可能有多个part(因为可以多选文件嘛),这些part的name都是 input的name。
        Collection<Part> parts = request.getParts();
        Iterator<Part> iterator = parts.iterator();
        while (iterator.hasNext()) {
            // 获取到表单里的每个part。
            Part part = iterator.next();
            part.getName(); // 对应了 input 的 name 属性。
            part.getSubmittedFileName(); // 对应了 input=file 时 提交的文件的真实名字。(当input不是file类型时此方法返回的null)
            part.getInputStream();  // 对应了 input=file 是文件时的文件输入流。input=text时,其value值也通过此流读取。
            // todo 实现自己的业务。
        }
    }
    

    六、总结

    无论使用什么方式进行文件上传,只要对HTTP协议有一定的了解,都可以轻松完成各种需求的开发。为什么要针对表单进行模拟来上传文件,因为这是大多数服务器的上传文件方式,许多服务端也都默认支持解析表单文件,因此客户端的上传也许迎合服务器所支持的使用表单进行文件上传。

    如果你还对HTTP协议不是很了解,可通过这篇文章进行入门:《【HTTP,Java】认识HTTP协议,在互联网世界里翱翔》。

    展开全文
  • 文件上传接口设计

    千次阅读 2019-09-01 19:14:58
    18年初跳槽来到新公司,在开发中惊奇的发现公司项目中的文件上传功能竟然存在多个接口,他们为每个需要文件上传的业务实现了接口,仔细看了下这些上传文件代码,他们主要区别在于每个业务存储的文件类型、文件大小、...

    1.问题描述

    18年初跳槽来到新公司,在开发中惊奇的发现公司项目中的文件上传功能竟然存在多个接口,他们为每个需要文件上传的业务实现了接口,仔细看了下这些上传文件代码,他们主要区别在于每个业务存储的文件类型、文件大小、是否要压缩需要限制或者是文件路径需要指定不同。这几个特性完全可以配置化定义规则(上家公司就是这么做的,配置化实现公用文件上传接口是一个正确的选择)。究其原因可能大家害怕更改之前的接口会照成bug,索性自己重写一个上传接口(反正是复制粘贴过来改下内容吧)。
    下面我们进入主题,如何实现一个根据配置化定义规则的文件上传设计

    2.接口定义

    后端服务我们直接使用springboot2.X框架,文件上传相关配置信息将入数据库,这样可支持动态修改。测试用例我提供一个html页面,或者你可以直接使用postman。更详细的内容处理请看代码(实现很简单的)
    API信息:
    接口地址:http://127.0.0.1:8080/uploadFile
    请求格式:post | multipart/form-data;

    参数名必填/类型说明
    bizType是/String不同业务对应不同的类型,根据此类型查找文件上传的配置
    bizId否/Long业务id,可以通过该id找到对于的业务信息
    isShrink否(默认false)/boolean是否需要压缩代码为了简介,暂未实现支持是否压缩
    file文件信息,支持多文件上传

    返回列表 JSON: [FileInfo]

    3.数据库设计

    文件信息配置表 file_conf , 不同业务对应不同的配置,这里主要考虑了文件类型、文件大小、文件存储路径等。(甚至可以扩展出存储目标服务器、文件有效期等规则)

    字段类型约束说明
    idint(11)必填自增主键id
    bize_typevarchar(20)必填业务类型,不同业务不同的类型
    file_type_limitvarchar(200)非必填允许上传的文件类型(mine-type标准),为空时不限制类型
    file_size_limitvarchar(20)非必填允许上传的文件大小(kb),为空时不限制大小
    pathvarchar(50)必填服务器存储文件的路径
    descriptionvarchar(100)非必填描述,如描述该业务类型对应的文件上传业务功能的业务表
    resource_realmvarchar (100)必填外部访问文件资源相对根路径
    enabledtinyint(4)必填是否可用(默认1可用,0禁用),用于禁止某个业务上传文件的功能
    creat_timedatetime必填创建时间
    last_update_timedatetime非必填最近修改时间

    文件信表 file_info ,主要作用保存文件存储的相关信息,方便某些业务需要统计上传文件或删除文件时需要

    字段类型约束说明
    idint(11)必填自增主键id
    bize_typevarchar(20)必填业务类型
    bize_idint(11)非必填业务id
    original_namevarchar (255)必填文件原名称
    new_namevarchar(50)必填(唯一)文件新名称(随机码
    file_typevarchar(20)必填文件类型
    file_sizevarchar(20)必填文件大小(kb)
    file_pathvarchar(200)必填文件服务器存储绝对路径
    relative_pathvarchar(200)必填文件相对路径,域名+此字段为该资源的请求地址
    creat_timedatetime必填创建时间
    last_update_timedatetime非必填最近修改时间
    del_flagtinyint(1)必填逻辑删除(默认0正常,1文件已被物理删除)

    4.主要代码实现

    4.1.实体类信息

    对应数据库表结构建立对应实体类`

    /**
     * 文件配置信息</p>
     * table: file_conf
     * @author lilee
     * @version 1.0.0
     * @date 2018/12/20 14:55
     */
    public class FileConf {
        private Long id;  // 主键ID
        private String bizType;  // 上传服务类型
        private String fileTypeLimit; // 文件类型(mine-type标准),为空不限制上传类型
        private String fileSizeLimit; //(kb)文件限制大小,为空不限制上传大小(但要满足框架支持的上传文件大小)
        private String path; // 服务器文件夹路径
        private String description;  // 描述
        private String resourceRealm; // 访问资源路径
        private Boolean enabled; // 是否可用(默认1可用,0禁用)
        private Date createTime;  // 创建时间
        private Date lastUpdateTime;  // 最后修改时间
       
        // setter & getter
    }
    
    /**
     * 文件信息</p>
     * table: file_info
     * @author lilee
     * @version 1.0.0
     * @date 2018/12/20 14:55
     */
    public class FileInfo {
        private Long id;  // 主键ID
        private String originalName;  // 文件原名称
        private String newName; // 文件新名称
        private String fileType;  // 文件类型(image/jpg, image/png, video/mp4, xsl,doc等)
        private String fileSize; // 文件大小(kb)
        private String filePath;  // 文件服务器存储路径
        private String relativePath;  // 文件相对路径
        private Long bizId;  // 业务ID
        private String bizType;  // 上传服务类型(业务类型)
        private Date createTime;  // 创建时间
        private Date lastUpdateTime;  // 最后修改时间
        private Boolean delFlag; // 数据删除标记0=正常,1=文件已物理删除
        
        // setter & getter
    

    4.2.数据访问dao

    dao层,本实例为了简单,并未提供数据库操作相关代码,该模块需要用户根据自己项目架构自己实现。
    当前文件存储到/home/data下

    @Repository
    public class FileConfDao {
        // 根据业务类型bizType获取该业务的配置信息
        public FileConf selectByBizType(String bizType) {
            // todo 为了简单,未正真的对数据库操作 
            // FileConf fileConf = dbdao.findByBizType(bizType);
            FileConf fileConf = new FileConf();
            fileConf.setBizType(bizType);
            fileConf.setPath("/home/data");
            fileConf.setResourceRealm("/res");
            fileConf.setEnabled(true);
            return fileConf;
        }
        // 存储文件的信息
         public FileInfo insert(FileInfo fileInfo) {
           //  dbdao.insert(fileInfo)
            return fileInfo;
        }
    }
    

    4.3.文件上传核心service

    该模块实现文件上传的核心,主要包括对文件参数信息验证,对文件上传是否符合业务的配置验证、对配置规则的验证

    /**
     * TODO 文件上传service
     *
     * @author lilee
     * @version 1.0.0
     * @date 2018/12/20 14:55
     */
    @Service
    public class FileUploadService {
        @Resource
        private FileConfDao fileConfDao;
        // @Resource
        // private FileInfoDao fileInfoDao;
        protected static Logger log = LoggerFactory.getLogger(FileUploadService.class);
        /**
         * 文件上传
         * @param mpfList  文件信息集
         * @param bizType 业务类型(必传)
         * @param bizId   业务id
         * @param extraPath  额外的路径,首部和结尾不能带斜杠'/'
         * @return
         */
        public List<FileInfo> uploadFile(List<MultipartFile> mpfList, String bizType, Long bizId, String extraPath) {
            // 验证数据begin
            // 获取对应业务文件配置信息
            FileConf fileConf = this.fileConfDao.selectByBizType(bizType);
            if(fileConf == null){
                log.info("file conf is null");  // 打印文件配置信息
                return null;
            }
            // 验证文件信息是否符合配置信息
            if (!validateFileInfo(mpfList, fileConf)) {
                // 验证失败
                log.info("fileInfo is error");  // 打印文件配置信息
                return null;
            }
            // 信息验证end
            List<FileInfo> files = new ArrayList<>();
            FileInfo fileInfo = null;
            String path = fileConf.getPath();  // 文件存储的目录
            // 获取相对路径,由file_conf、额外路径
            String relativePath = fileConf.getResourceRealm() + "/"
                    + (StringUtils.isEmpty(extraPath) ? "" : extraPath + "/");
            // 验证服务器存储路径是否存在,若不存在,则新建文件夹
            File serFile = new File(path + relativePath);
            if (!serFile.exists()) {
                serFile.mkdirs();
            }
            // 循环上传文件
            for (MultipartFile mpf : mpfList) {
                String originalFileName = mpf.getOriginalFilename(); // 获取源文件名
                // 生成新文件名
                String newFileName = "F" + UUID.randomUUID().toString().replace("-", "").toUpperCase()
                        + originalFileName.substring(originalFileName.lastIndexOf("."));
                // 组装数据
                fileInfo = new FileInfo();
                fileInfo.setOriginalName(originalFileName);
                fileInfo.setFileSize(String.valueOf(mpf.getSize() / 1024)); // 单位(kb)
                fileInfo.setFileType(mpf.getContentType());     // 文件类型
                fileInfo.setNewName(newFileName);                        // 文件新名字
                fileInfo.setRelativePath(relativePath + newFileName);    // 文件相对路径
                fileInfo.setFilePath(path + relativePath + newFileName); // 文件物理路径
                fileInfo.setBizType(bizType);
                fileInfo.setBizId(bizId);
                fileInfo.setDelFlag(false);
                // 存储文件并记录到数据库
                try {
                    FileCopyUtils.copy(mpf.getBytes(), new FileOutputStream(fileInfo.getFilePath()));
                    fileConfDao.insert(fileInfo); 
                } catch (IOException e) {
                    log.error("upload file error!", e);
                    return null;
                }
                files.add(fileInfo);
            }
            return files;
        }
        private boolean validateFileInfo(List<MultipartFile> mpfList, FileConf fileConf) {
            if (mpfList == null || fileConf == null) { return false; }
            for (MultipartFile mpf : mpfList) {
                // 验证文件大小是否超出配置大小
                if (!StringUtils.isEmpty(fileConf.getFileSizeLimit()) && mpf.getSize() / 1024 > Integer.parseInt(fileConf.getFileSizeLimit())) {
                    return false;
                }
                // 验证文件类型是否符合文件配置的要求
                if (!StringUtils.isEmpty(fileConf.getFileTypeLimit()) && fileConf.getFileTypeLimit().indexOf(mpf.getContentType()) < 0) {
                    return false;
                }
            }
            return true;
        }
    }
    

    4.4.controller层

    ****简单controller****

    @RestController
    public class FileUploadController {
        @Resource
        private FileUploadService fileUploadService;
        /**
         * 文件上传接口
         * @param request  
         * @param bizType 业务类型(必传)
         * @param bizId   业务id
         * @param extraPath  额外的路径,首部和结尾不能带斜杠'/'
         * @return
         */
        @RequestMapping(value ="/uploadFile", method = RequestMethod.POST)
        public List<FileInfo> uploadFile(MultipartHttpServletRequest request, String bizType, Long bizId, String extraPath) {
            int count = 0;
            List<FileInfo> result = this.fileUploadService.uploadFile(request.getMultiFileMap().get("fileData"), bizType, bizId, extraPath);
            return result;
        }
    }
    

    4.5.html测试代码

    使用postman测试更简单,记得考虑跨越问题
    引用博客https://www.cnblogs.com/tianyuchen/p/5594641.html

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
        <title>fileUpload Test</title>
        <script type="text/javascript">
            var xhr;
            var ot;//
            var oloaded;
            //上传文件方法
            function upladFile() {
                var url = "http://127.0.0.1:8080/uploadFile"; // 接收上传文件的后台地址 
                
                var form = new FormData(); // FormData 对象
                var fileObj = document.getElementById("file").files; // js 获取文件对象
                if (fileObj != null) {
                    for (var i=0; i< fileObj.length; i++) {
                        form.append('fileData', fileObj[i], fileObj[i].name);
                    }
                }
                xhr = new XMLHttpRequest();  // XMLHttpRequest 对象
                xhr.open("post", url, true); //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
                xhr.onload = uploadComplete; //请求完成(成功)
                xhr.onerror =  uploadFailed; //请求失败
                xhr.upload.onprogress = progressFunction;        //【上传进度调用方法实现】
                xhr.upload.onloadstart = function(){            //上传开始执行方法(初始)
                    ot = new Date().getTime();                   //设置上传开始时间
                    oloaded = 0;                                //设置上传开始时,以上传的文件大小为0
                };
                xhr.send(form); 
            }
            //上传进度实现方法,上传过程中会频繁调用该方法
            function progressFunction(evt) {
                 var progressBar = document.getElementById("progressBar");
                 var percentageDiv = document.getElementById("percentage");
                 // event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
                 if (evt.lengthComputable) {        
                     progressBar.max = evt.total;
                     progressBar.value = evt.loaded;
                     percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
                 }
                
                var time = document.getElementById("time");
                var nt = new Date().getTime();//获取当前时间
                var pertime = (nt-ot) / 1000; //计算出上次调用该方法时到现在的时间差,单位为s
                ot = new Date().getTime(); //重新赋值时间,用于下次计算
                
                var perload = evt.loaded - oloaded; //计算该分段上传的文件大小,单位b       
                oloaded = evt.loaded;//重新赋值已上传文件大小,用以下次计算
            
                //上传速度计算
                var speed = perload / pertime; //单位b/s
                var bspeed = speed;
                var units = 'b/s';//单位名称
                if(speed/1024 > 1){
                    speed /= 1024;
                    units = 'k/s';
                }
                if(speed/1024 > 1){
                    speed /= 1024;
                    units = 'M/s';
                }
                speed = speed.toFixed(1);
                //剩余时间
                var resttime = ((evt.total - evt.loaded) / bspeed).toFixed(1);
                time.innerHTML = ',速度:' + speed + units + ',剩余时间:' + resttime + 's';
                if(bspeed == 0)
                time.innerHTML = '上传已取消';
            }
            //上传成功响应
            function uploadComplete(evt) {
                alert("上传成功!");
            }
            //上传失败
            function uploadFailed(evt) {
                alert("上传失败!");
            }
             //取消上传
            function cancleUploadFile(){
                xhr.abort();
            }
        </script>
    </head>
    <body>
        <progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
        <span id="percentage"></span><span id="time"></span>
        <br /><br />
        <input type="file" id="file" name="myfile" multiple="multiple"/>
        <input type="button" onclick="upladFile()" value="上传(多选)" />
        <input type="button" onclick="cancleUploadFile()" value="取消" />
    </body>
    </html>
    

    跨域问题解决

        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurerAdapter() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
                            .allowedMethods("PUT", "DELETE","GET","POST")
                            .allowedHeaders("*")
                            .exposedHeaders("access-control-allow-headers",
                                    "access-control-allow-methods",
                                    "access-control-allow-origin",
                                    "access-control-max-age",
                                    "X-Frame-Options")
                            .allowCredentials(false).maxAge(3600);
                }
            };
        }
    

    4.最后

    综上一个根据配置规则定义的公用上传文件设计便完成了,这样我们项目所有和上传文件相关的业务都可以使用该接口,只需要简单的在数据库中配置下和业务相关的信息便可。当然可能有些业务还需要更细的定制,如要指定存储服务器,是否需要压缩,一定时间内需要删除文件等等,那你只需在该设计上添加新的配置字段,然后实现对该配置字段的支持就行了。



    、﹗∕
    — 〇 -
    ╱︱ ヽ
    但行好事、莫问前程!
    >.freerme、我是lilee[https://blog.csdn.net/freerme]
    _________________ *_*______
    ____ ____ ____
    展开全文
  • 功能实现1、axios二次封装2、 单文件上传FROM-DATA,先选文件上传3、 单文件上传BASE64,只能上传小于100K的png或jpg图片文件4、多文件上传FORM-DATA5、多文件拖拽上传FORM-DATA6、大文件切片上传,断点续传FORM-...
  • 之前写过一遍《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》,其中谈到了如何使用WinHttp接口实现Http的Get、Post和文件上传功能。后来发现蛮多人关注该技术的实现,于是我决定重新设计框架结构,梳理这块...
  • 文件上传功能测试点整理

    千次阅读 2019-10-14 14:34:35
    最近有任务中包含了上传功能的测试,总结下文件上传的...假设限制上传文件最大为X: 指定文件类型的文件小于X,允许上传 指定文件类型的文件等于X,不允许上传 指定文件类型的文件大于X,不允许上传并给出合理提示 ...
  • java上传超大文件解决方案

    千次阅读 2019-11-14 11:24:54
    用JAVA实现文件上传及显示进度信息 ... ... 一.文件上传基础描述: ... 各种WEB框架中,对于浏览器上传文件的请求,都有自己的处理对象负责对HttpMultiPart协议内容进行解析,并供开发人员调用请求的表单内容。...
  • MediaWiki使用MSUpload扩展上传文件功能

    千次阅读 2018-01-16 11:27:46
    MediaWiki是一个比较流行的wiki百科软件,部署使用都比较简单,容易上手。...mediawiki默认上传文件功能比较单一,支持文件类型少,大小受限制,操作不方便,需要先上传文件,再插入页面。而且一次只能有一个文件上
  • 分布式文件上传服务架构设计

    千次阅读 2017-11-20 23:35:07
    背景由于某业务需要,需要对文件上传服务进行一次架构调整,初步考虑几...就近原则:通过离用户最近的区域节点上传文件,通过CDN节点下载文件 初步架构逻辑架构图如图所示: 重点功能模块说明: - 上传调度服务
  • 如何实现文件上传功能

    千次阅读 2019-06-17 21:47:03
    开发工具与关键技术: VS MVC基础 ...这就是我今天的分享内容上传文件功能的实现。(下面的代码来源于老师) 上传文件我们都知道点击上传按钮,再选择要上传的文件之类资料,确定即可上传,哪在这...
  • Laravel 文件上传功能实现

    千次阅读 2017-12-18 10:01:39
    以Laravel 5.2.45 框架为主,进行文件上传功能的实现如下: 实现步骤:(1). 配置文件修改 打开 config/filesystems.php 文件 在 ‘disks’ 数组中添加如下代码 //自定义 'uploads' => [ 'driver' => 'local...
  • 文件上传功能测试用例

    万次阅读 多人点赞 2018-04-29 17:15:32
    https://www.cnblogs.com/puresoul/p/4584169.html前做了一个项目很多功能文件上传有关,所以总结了下这块功能的测试案例: 序号测试案例名称测试案例描述步骤描述预期结果说明1附件上传-文件命名检查-符合文件...
  • 上传大文件(100G)的解决方案

    千次阅读 2019-12-24 16:25:49
    但是在很多情况下,平台运营方并没有大文件上传和断点续传的开发经验,往往在网上找一些简单的PHP或者Java程序来实现基本的上传功能,然而在实际使用中会发现,这些基于脚本语言实现的上传功能模块性能很弱,一是不...
  • HTTP文件上传是做Web开发时的常见功能,例如上传图片、上传影片等。实现HTTP文件上传也比较简单,用任何Web端的脚本都可以轻松实现,例如PHP、JSP都有现成的函数或者类来调用。但笔者最近在做项目时遇到了一个问题...
  • 如何实现文件上传功能

    千次阅读 2018-04-25 11:10:07
    如何用servlet如何实现文件上传: 一、用servlet如何实现文件上传: 1.需要先获取你把上传文件放到哪里(也就是你的存储路径) 2.如果你需要上传的不只是一个文件的话,需要先定一个Part集合来得到你要上传的集合...
  • WPF+WCF大文件上传控件

    热门讨论 2012-01-02 12:01:48
    WPF+WCF做的大文件上传控件,在WPF客户端把大文件分割成二进制数组,每次往服务器上写100K,可断点续传。页面做的比较丑,没认真设计,只为实现功能。另外还有一个Silverlight+WCF版的。
  • 上传文件功能测试用例

    千次阅读 2019-09-05 11:32:10
  • 功能描述:在设计论坛时用户要发帖,用户会向帖子中插入图片,插入图片的流程是:用户先上传图片到服务器,然后将服务器中图片的路径添加到帖子中。那么有一个问题,用户会将要发布的图片上传到服务器的文件夹,然后...
  • 在 Spring Boot 项目中实现文件下载功能 后,项目需要进一步实现 Excel 文件上传功能,已供后端代码读取 Excel 中单元格的数据。本文的代码在下载功能的基础之上继续扩展。 (二)代码 2.1 后端代码 控制层 @...
  • 文件上传大小限制

    千次阅读 2018-05-16 09:27:25
    修改PHP上传文件大小限制的方法1. 一般的文件上传,除非文件很小.就像一个5M的文件,很可能要超过一分钟才能上传完.但在php中,默认的该页最久执行时间为 30 秒.就是说超过30秒,该脚本就停止执行.这就导致出现 无法打开...
  • 1.表单部分  允许用户上传文件,在HTML表单的声明中要...表单选项MAX_FILE_SIZE隐藏域用于限制上传文件大小,它必须放在文件表单元素前面,单位为字节。  如:  复制代码代码如下:         2.处
  • 服务端的安全性,不因上传文件功能导致JVM内存溢出影响其他功能使用;最大限度利用网络上行带宽,提高上传速度;二、 设计分析对于文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是...
  • 之前仿造uploadify写了一个HTML5版的文件上传插件,没看过的朋友可以点此先看一下~得到了不少朋友的好评,我自己也用在了项目中,不论是用户头像上传,还是各种媒体文件上传,以及各种个性的业务需求,都能得到...
  • 文件上传功能的安全考虑

    千次阅读 2008-10-25 08:49:00
    文件上传功能的安全考虑你的服务器在开通HTTP上传文件功能之前,一件很重要的事情你必须考虑:安全,因为不当的设计或配置,将使你的服务器很容易受到攻击. 例如:早期的PHP上传文件脚本及Jsp上传文件的脚本是不...
  • 但是在很多情况下,平台运营方并没有大文件上传和断点续传的开发经验,往往在网上找一些简单的PHP或者Java程序来实现基本的上传功能,然而在实际使用中会发现,这些基于脚本语言实现的上传功能模块性能很弱,一是不...
  • 文件上传漏洞

    万次阅读 多人点赞 2019-03-04 00:27:45
    文件上传漏洞是指由于程序员未对上传文件进行严格的验证和过滤,而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。这里上传文件可以是木马,病毒,恶意脚本或者WebShell等。这种攻击方式是...
  • 使用WinHttp接口实现HTTP协议Get、Post和文件上传功能

    万次阅读 热门讨论 2014-04-14 21:16:30
    我实现了一个最新版本的接口,详见《实现HTTP协议Get、Post和文件上传功能——使用WinHttp接口实现》。还有基于libcurl实现的版本《实现HTTP协议Get、Post和文件上传功能——使用libcurl接口实现》。以下是原博文: ...
  • Android大文件上传秒传之MD5篇

    千次阅读 2016-11-09 21:47:17
    前言现在越来越多的应用开始有上传大文件的需求,以及秒传,续传功能。由于最近学习大文件分隔上传,以及秒传的实现,给予分享的这种精神,我想将自己的学习过程,以及遇到的问题做一个总结,希望对有这方面需求的小...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 158,974
精华内容 63,589
关键字:

上传大文件功能如何设计