FormData上传多文件+DefaultMultipartHttpServletRequest保存文件

1,446 阅读4分钟

前言:项目需求中涉及到多图片上传的注册功能,项目中前端使用的是比较老的ionic1+angularJS,后端SpringMVC,开发途中的遇到的各种爬坑血泪史,在此分享出来给大家参考。

1.组建表单

因涉及到多图片上传以及加载本地图片,故采用H5的FormData表单对象

这里先粘出部分HTML代码作为示例,以下表单上传到后台的数据有两个文本以及三张图片

<ion-view ng-controller="regController as vm" title="用户注册">    <ion-content class="content-bg-color" has-supheader="false" overflow-scroll="true">        <form enctype="multipart/form-data" method="post" novalidate ng-submit="vm.save(userForm)" name='userForm' id='userForm'>            <label class="item item-input item-stacked-label">                <span class="input-label">账号:</span>                <input type="text" required placeholder="建议使用手机号" ng-model="vm.user.account" name='account'> </label>            <label class="item item-input item-stacked-label">                <span class="input-label">密码:</span>                <input type="password" required ng-pattern="^[a-zA-Z]\w{5,17}$" ng-maxlength="18" ng-minlength="6"                    ng-model="vm.user.password" name='password' placeholder="以字母开头,长度在6~18之间,只能包含字母、数字和下划线"> </label>
            <label class="item item-input item-stacked-label">                <span class="input-label">姓名:</span>                <input type="text" required ng-maxlength="6" ng-model="vm.user.name" placeholder="长度不得超过6个字" name='name' ></label>            <div class="item item-input item-stacked-label">                <span class="input-label">上传照片:</span>                <div class="row">                    <div class="col ">                        <label for="">1.周围环境照片                            <br>                            <a href="javascript:;" class="file">选择照片                                <input accept="image/*" type="file" name='img_environment'> </a>                        </label>                    </div>                </div>                <div class="row">                    <div class="col">                        <label for="">2.逆安装区域照片                            <br>                            <a href="javascript:;" class="file">选择照片                                <input accept="image/*" type="file" name='img_area'> </a>                        </label>                    </div>                </div>                <div class="row">                    <div class="col">                        <label for="">3.基础及配重图                            <br>                            <a href="javascript:;" class="file">选择照片                                <input accept="image/*" type="file" name='img_basics'> </a>                        </label>                    </div>                </div>            </div>            <div class="row">                <div class="col">                    <button class="button button-block button-positive" type="submit">完成</button>                </div>            </div>        </form>    </ion-content></ion-view>

这里需要留意的是所有<form>标签以及<input>标签,以下我会逐步讲解每个标签的属性

<form>:

  • enctype='multipart/form-data',是设置表单的MIME编码。默认情况,这个编码格式是application/x-www-form-urlencoded,不能用于文件上传;只有使用了multipart/form-data才能将文件以二进制的形式上传,从而实现多种类型的文件上传。
  • method="post",如果表单指定请求方式为post,则在使用XMLHttpRequest对象.open()方法请求后台时,该请求为post请求。
  • novalidate ,该属性规定当提交表单时不对其进行验证。由于本项目中用的是angularJS的表单验证故屏蔽了H5的表单验证。
  • ng-submit,指令用于在表单提交后执行指定函数

<input>:

  • accept="image/*",H5中允许上传图片的类型。
  • ng-pattern,确保输入匹配一个正则表达式。

2.组装FormData对象,准备上传

这里使用的是ng-submit方法save(userForm)传入表单对象进行表单验证

下面还是先粘出controller层的js代码,

function save(userForm){            if(userForm.$valid){//成功为 true 否则为 false                if(vm.user.img[0]!=undefined){                    if(vm.user.img[0].size===3){//如果三张图片都上传了                        var formdata = new FormData(document.getElementById('userForm'));//获取页面上的表单对象                        var countImgSize = formdata.get('img_environment').size+formdata.get('img_area').size+formdata.get('img_basics').size;                        if(countImgSize<1024*1024*10){//如果三张图片总大小小于10mb                            console.log('表单验证成功,正在准备上传用户信息');                            formdata.set('name',encodeURI(formdata.get('name')));//转码                            regService.reg(formdata).then(function(data){                                if (data.data.state=="success") {                                    $timeout(function () {                                        $ionicPopup.alert({                                            title: '注册状态',                                            template: data.data.mes                                        });                                    }, 500).then(function () {                                        $state.go('login');                                    });                                } else if(data.data.state=="faild"){                                    $ionicPopup.alert({                                        title: '注册状态',                                        template: data.data.mes                                        }).then(function (res) {                                    });                                }                            })                        }else{                            $ionicPopup.alert({                                title: '警告',                                template: '图片总大小不能超过10MB,当前图片总大小为:'+(countImgSize/1024/1024).toFixed(2)+'MB'                            });                        }                                            }else{                        $ionicPopup.alert({                            title: '图片没有上传完'                        });                    }                }else{                    $ionicPopup.alert({                        title: '请上传图片'                    });                }            }else{                if(userForm.account.$error.required){                    $ionicPopup.alert({                        title: '账号没填'                    });                }                if(userForm.password.$error.required){                    $ionicPopup.alert({                        title: '密码没填'                    });                }                if(userForm.password.$error.pattern){                    $ionicPopup.alert({                        title: '密码不符合填写规则'                    });                }                if(userForm.password.$error.maxlength){                    $ionicPopup.alert({                        title: '密码超出最大长度'                    });                }                if(userForm.password.$error.minlength){                    $ionicPopup.alert({                        title: '密码小于最小长度'                    });                }
                if(userForm.name.$error.maxlength){//如果姓名超出最大长度                    $ionicPopup.alert({                        title: '姓名超出最大长度'                    });                }                if(userForm.name.$error.required){//如果姓名没填                    $ionicPopup.alert({                        title: '用姓名没填'                    });                }            }          }

以上代码重点在第5-10行,这里主要详细讲解第8行:

因为表单使用multipart/form-data编码,

请求头里面内容类型为Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryW8oohHhEVIBMNJp9

不是utf-8格式的,故产生乱码,自己尝试通过改变请求头的Content-Type为multipart/form-data;utf8的方式解决乱码,但是由于post请求使用的是angularjs的$http服务,在尝试强制更改请求头的时候引起了跨域异常,百度各种后无果,所以只能使用以下笨方法:前端使用encodeURI(str)方法将formdata中的所有涉及中文的表单数据转码,后端则使用URLDecoder.decode(str, "UTF-8")的方式解码(注:"UTF-8"应为大写).

service层很简单,使用$http服务请求后台,post请求,传入表单对象

function reg(dataform) {           return $http.post(SYS_INFO.SERVER_PATH+'/userorder_app/regUser', dataform, {          transformRequest: angular.identity,          headers: {'Content-Type': undefined}      })      .success(complete)      .error(failed);
    }

这里要注意的是,因为是通过anjularjs的http请求来上传文件的,所以要让当前的request成为一个Multipart/form-data请求,anjularjs对于post和get请求默认的Content-Type header 是application/json。通过设置‘Content-Type’: undefined,这样浏览器不仅帮我们把Content-Type 设置为 multipart/form-data,还填充上当前的boundary,如果你手动设置为: ‘Content-Type’: multipart/form-data,后台会抛出异常:the current request boundary parameter is null。
ps:
通过设置 transformRequest: angular.identity ,anjularjs transformRequest function 将序列化我们的formdata object.

3.接收表单数据

后端使用的是springMVC,详细描述都在代码注释中

@RequestMapping(value = "/regUser", method = RequestMethod.POST)
@ResponseBody
public JSONObject addUser(HttpServletRequest request) throws IOException {
    // 先实例化一个文件解析器(暂且先叫解析器,DefaultMultipartHttpServletRequest翻译名叫‘默认的多个请求’)
    DefaultMultipartHttpServletRequest dmhsq = (DefaultMultipartHttpServletRequest) request;
    //这个userOrder是我需要组装并且要持久化保存的用户对象
    UserOrder userOrder = new UserOrder();
    //获取所有表单数据   以下两行代码就涵盖了所有表单中的所有数据
    Map map = dmhsq.getParameterMap();
    //获取所有文件
    Map<String, MultipartFile> fileMap = dmhsq.getFileMap();
    //上传图片
    //1.图片存储的路径
    StringBuffer pre_save_path= new StringBuffer();
    String path = System.getProperty("java.class.path");
    path = path.substring(0, path.lastIndexOf("jboss-modules.jar"));//应用服务器的根路劲
    for (String key : fileMap.keySet()) {
        MultipartFile m = fileMap.get(key);
        //2.图片名称
        String originalFilename = new String(m.getOriginalFilename().getBytes("ISO-8859-1"));
        //3.新的图片
        File newfile = new File(path + "upload" + "/" + ((String[]) map.get("orderId"))[0] + "/" + originalFilename);
a       //拼接需要保存的图片路劲以逗号隔开
        pre_save_path.append("/upload/" + ((String[]) map.get("orderId"))[0] + "/" + originalFilename+",");
        //4.保存图片的文件夹
        File folder = new File(path + "upload" + "/" + ((String[]) map.get("orderId"))[0] + "/");
        //5.看看文件夹存不存在 不存在就造一个
        if (!folder.isDirectory()) {
            folder.mkdirs();
        }
        //6.把新图片扔进去
        m.transferTo(newfile);
    }
    //以下的就是一些业务逻辑 需要注意的是中文解码的那一块
    userOrder.setVerifyPicture(pre_save_path.toString().substring(0,pre_save_path.toString().length()-1));//核实图片
    //上面图片上传完了 下面该到注册信息了
    //后端使用URLDecoder.decode(str, "UTF-8")的方式解码(注:"UTF-8"应为大写)
    String name = ((String[]) map.get("name"))[0];
    name = URLDecoder.decode(name, "UTF-8");
    userOrder.setName(name);//姓名

    userOrder.setUserAccount(((String[]) map.get("account"))[0]);//账号
    userOrder.setPassword(((String[]) map.get("password"))[0]);//密码
    //获取当前订单号的userorder对象
    UserOrder u = userOrderService.findUserOrder(userOrder.getOrderId());
    //查看这个账户存不存在
    Boolean isExist = userService.hasAccountExist(userOrder.getUserAccount());
    JSONObject returnObj = new JSONObject();
    //如果都没有 就新创建一个userorder
    if (u == null && !isExist) {
        userOrderService.createUserOrder(userOrder);
        returnObj.put("mes", "注册成功,即将为您跳转到登陆页面~");
        returnObj.put("state", "success");
    } else {
        returnObj.put("mes", "注册失败,当前订单已注册或账户已存在!");
        returnObj.put("state", "faild");
    }

    return returnObj;
}

这里的DefaultMultipartHttpServletRequest文件解析器是个好东西,通过它能获取到表单上的所有文本数据和MultipartFile对象,这里不多解释,可以百度详情