Spring MVC 檔案上傳、Restful、表單校驗框架

Juno3550發表於2021-12-28


檔案上傳

上傳檔案過程分析

image


SpringMVC 的檔案上傳技術:MultipartResolver 介面

  • MultipartResolver 介面定義了檔案上傳過程中的相關操作,並對通用性操作進行了封裝。
  • MultipartResolver 介面底層實現類 CommonsMultipartResovler。
  • CommonsMultipartResovler 並未自主實現檔案上傳下載對應的功能,而是呼叫了 apache 的檔案上傳下載元件。

SpringMVC 檔案上傳實現

  1. Maven 依賴:
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>
  1. 頁面表單:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="/fileupload" method="post" enctype="multipart/form-data">
        上傳檔案: <input type="file" name="file"/><br/>
        <input type="submit" value="上傳"/>
    </form>
</body>
</html>
  1. SpringMVC 配置:
<bean id="multipartResolver"
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>
  1. 控制器:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;

@Controller
public class FileUploadController {

    // 引數中定義 MultipartFile 引數,用於接收頁面提交的 type=file 型別的表單(要求表單中的name名稱與方法入參名相同)
    @RequestMapping(value="/fileupload")
    public String fileupload(MultipartFile file, HttpServletRequest request) throws IOException {
        //設定儲存的路徑
        String realPath = request.getServletContext().getRealPath("/images");
        file.transferTo(new File(realPath, "file.png"));  // 將上傳的檔案儲存到伺服器
        return "page.jsp";
    }
}

image


檔案上傳常見問題

  1. 檔案命名問題
  2. 檔名過長問題
  3. 檔案儲存路徑
  4. 重名問題
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;

@Controller
public class FileUploadController {

    @RequestMapping(value="/fileupload")
    public String fileupload(MultipartFile file, MultipartFile file1,
                             MultipartFile file2, HttpServletRequest request) throws IOException {
        // MultipartFile引數中封裝了上傳的檔案的相關資訊
        // System.out.println(file.getSize());
        // System.out.println(file.getBytes().length);
        // System.out.println(file.getContentType());
        // System.out.println(file.getName());
        // System.out.println(file.getOriginalFilename());
        // System.out.println(file.isEmpty());

        // 首先判斷是否是空檔案,也就是儲存空間佔用為0的檔案
        if(!file.isEmpty()) {
            // 如果大小在範圍要求內則正常處理;否則丟擲自定義異常告知使用者(未實現)
            // 獲取原始上傳的檔名,可以作為當前檔案的真實名稱儲存到資料庫中備用
            String fileName = file.getOriginalFilename();
            // 設定儲存的路徑
            String realPath = request.getServletContext().getRealPath("/images");
            // 儲存檔案的方法,指定儲存的位置和檔名即可,通常檔名使用隨機生成策略產生,避免檔名衝突問題
            // String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();  // UUID 隨機數
            file.transferTo(new File(realPath, file.getOriginalFilename()));
        }
        // 測試一次性上傳多個檔案
        if(!file1.isEmpty()) {
            String fileName = file1.getOriginalFilename();
            //可以根據需要,對不同種類的檔案做不同的儲存路徑的區分,修改對應的儲存位置即可
            String realPath = request.getServletContext().getRealPath("/images");
            file1.transferTo(new File(realPath, file1.getOriginalFilename()));
        }
        if(!file2.isEmpty()) {
            String fileName = file2.getOriginalFilename();
            String realPath = request.getServletContext().getRealPath("/images");
            file2.transferTo(new File(realPath, file2.getOriginalFilename()));
        }
        return "page.jsp";
    }
}

Restful

Restful 簡介

Rest(REpresentational State Transfer)是一種網路資源的訪問風格,定義了網路資源的訪問方式。

而 Restful 則是按照 Rest 風格訪問網路資源。

優點:

  • 隱藏資源的訪問行為,通過地址無法得知做的是何種操作。
  • 書寫簡化。

Rest 行為常用約定方式

注意:上述行為是約定方式,約定不是規範,可以打破,所以稱 Rest 風格,而不是 Rest 規範。

Restful開發入門

  1. 頁面表單:
    <!-- 切換請求路徑為restful風格 -->
    <!-- GET請求通過位址列可以傳送,也可以通過設定form的請求方式提交 -->
    <!-- POST請求必須通過form的請求方式提交 -->
    <form action="/user/1" method="post">
        <!-- 當新增了 name 為 _method 的隱藏域時,可以通過設定該隱藏域的值,修改請求的提交方式,切換為 PUT 請求或 DELETE 請求,但是 form 表單的提交方式 method 屬性必須填寫 post -->
        <!-- 該配置需要配合 HiddenHttpMethodFilter 過濾器使用,單獨使用無效,請注意檢查 web.xml 中是否配置了對應過濾器 -->
        <!-- 使用隱藏域提交請求型別,引數名稱固定為"_method",必須配合提交型別 method=post 使用 -->
        <input type="hidden" name="_method" value="PUT"/>  <!-- value或="DELETE" -->
        <input type="submit"/>
    </form>
  1. 開啟 SpringMVC 對 Restful 風格的訪問支援過濾器,即可通過頁面表單提交 PUT 與 DELETE 請求:
    <!-- 配置攔截器,解析請求中的引數_method,否則無法發起PUT請求與DELETE請求,配合頁面表單使用 -->
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <servlet-name>DispatcherServlet</servlet-name>
    </filter-mapping>
    
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:spring-mvc.xml</param-value>
        </init-param>
    </servlet>

表單校驗框架

表單校驗框架介紹

表單校驗分類

  • 校驗位置:
    • 客戶端校驗
    • 服務端校驗
  • 校驗內容與對應方式:
    • 格式校驗
      • 客戶端:使用 JS 技術,利用正規表示式校驗
      • 服務端:使用校驗框架
    • 邏輯校驗
      • 客戶端:使用 ajax 傳送要校驗的資料,在服務端完成邏輯校驗,返回校驗結果
      • 服務端:接收到完整的請求後,在執行業務操作前,完成邏輯校驗

表單校驗規則

  • 長度:例如使用者名稱長度,評論字元數量
  • 非法字元:例如使用者名稱組成
  • 資料格式:例如 Email 格式、IP 地址格式
  • 邊界值:例如轉賬金額上限,年齡上下限
  • 重複性:例如使用者名稱是否重複

表單校驗框架

  • JSR(Java Specification Requests):Java 規範提案

    • JSR 303:提供 bean 屬性相關校驗規則
  • Hibernate 框架中包含一套獨立的校驗框架 hibernate-validator

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>

注意:

  • tomcat7:搭配 hibernate-validator 版本 5...Final
  • tomcat8.5 及以上:搭配 hibernate-validator 版本 6...Final

快速入門

  1. 頁面表單:
<form action="/addemployee" method="post">
    員工姓名:<input type="text" name="name"><span style="color:red">${name}</span><br/>
    員工年齡:<input type="text" name="age"><span style="color:red">${age}</span><br/>
    <input type="submit" value="提交">
</form>
  1. 設定校驗規則:

    • 名稱:@NotNull
    • 型別:屬性註解 等
    • 位置:實體類屬性上方
    • 作用:設定當前屬性校驗規則
    • 範例:
      每個校驗規則所攜帶的引數不同,根據校驗規則進行相應的調整
      具體的校驗規則檢視對應的校驗框架進行獲取
import javax.validation.constraints.NotBlank;

public class Employee {
	
    @NotBlank(message="姓名不能為空")
    private String name;  // 員工姓名
    private Integer age;  // 員工年齡

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  1. 開啟校驗,並獲取校驗錯誤資訊:

    • 名稱:@Valid、@Validated
    • 型別:形參註解
    • 位置:處理器類中的實體類型別的方法形參前方
    • 作用:設定對當前實體類型別引數進行校驗
    • 範例:
import com.bean.Employee;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@Controller
public class EmployeeController {

    // 使用 @Valid 開啟校驗(使用 @Validated 也可以開啟校驗)
    // Errors 物件用於封裝校驗結果,如果不滿足校驗規則,對應的校驗結果封裝到該物件中,包含校驗的屬性名和校驗不通過返回的訊息
    @RequestMapping(value="/addemployee")
    public String addEmployee(@Valid Employee employee, Errors errors, Model model) {
        System.out.println(employee);
        // 判定Errors物件中是否存在未通過校驗的欄位
        if(errors.hasErrors()){
            // 獲取所有未通過校驗規則的資訊
            for(FieldError error : errors.getFieldErrors()){
                // 將校驗結果資訊新增到Model物件中,用於頁面顯示
                // 實際開發中無需這樣設定,返回json資料即可
                model.addAttribute(error.getField(), error.getDefaultMessage());
            }
            // 當出現未通過校驗的欄位時,跳轉頁面到原始頁面,進行資料回顯
            return "employee.jsp";
        }
        return "success.jsp";
    }

}

  1. 示例效果:提交表單並返回校驗結果

image

image


多規則校驗

  • 同一個屬性可以新增多個校驗器:
@NotNull(message = "請輸入您的年齡")
@Max(value = 60, message = "年齡最大值不允許超過60歲")
@Min(value = 18, message = "年齡最小值不允許低於18歲")
private Integer age;  // 員工年齡
  • 3 種判定空校驗器的區別:

image


巢狀校驗

  • 名稱:@Valid
  • 型別:屬性註解
  • 位置:實體類中的引用型別屬性上方
  • 作用:設定當前應用型別屬性中的屬性開啟校驗
  • 範例:
public class Employee {
    // 實體類中的引用型別通過標註 @Valid 註解,設定開啟當前引用型別欄位中的屬性參與校驗
    @Valid
    private Address address;
}
  • 注意:開啟巢狀校驗後,被校驗物件內部需要新增對應的校驗規則。

分組校驗

同一個模組,根據執行的業務不同,需要校驗的屬性也會有不同,如新增使用者和修改使用者時的校驗規則不同。

因此,需要對不同種類的屬性進行分組,在校驗時可以指定參與校驗的欄位所屬的組類別:

// 定義組(通用)
public interface GroupOne {
}
// 為屬性設定所屬組,可以設定多個
@NotEmpty(message = "姓名不能為空", groups = {GroupOne.class})
private String name;  // 員工姓名
// 開啟組校驗
public String addEmployee(@Validated({GroupOne.class}) Employee employee){
}

綜合案例

  • 頁面表單:employee.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="/addemployee" method="post">
        員工姓名:<input type="text" name="name"><span style="color:red">${name}</span><br/>
        員工年齡:<input type="text" name="age"><span style="color:red">${age}</span><br/>
        <!-- 注意,引用型別的校驗未通過資訊不是通過物件進行封裝的,而是直接使用"物件名.屬性名"的格式作為整體屬性字串進行儲存的,因此需要使用以下獲取方法。
        這和使用者的屬性傳遞方式有關,不具有通用性,僅適用於本案例 -->
        省名:<input type="text" name="address.provinceName"><span style="color:red">${requestScope['address.provinceName']}</span><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>
  • 實體類:Employee.java
package com.bean;

import com.group.GroupA;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class Employee {

    // 設定校驗器,設定校驗不通過對應的訊息,設定所參與的校驗組
    @NotBlank(message="姓名不能為空", groups = {GroupA.class})
    private String name;  // 員工姓名

    // 一個屬性可以新增多個校驗器
    @NotNull(message = "請輸入您的年齡", groups = {GroupA.class})
    @Max(value = 60, message = "年齡最大值不允許超過60歲")
    @Min(value = 18, message = "年齡最小值不允許低於18歲")
    private Integer age;  // 員工年齡

    // 實體類中的引用型別通過標註 @Valid 註解,設定開啟當前引用型別欄位中的屬性參與校驗
    @Valid
    private Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }
}
  • 實體類:Address.java
package com.bean;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

// 巢狀校驗的實體中,對每個屬性正常新增校驗規則即可
public class Address {

    @NotBlank(message = "請輸入省份名稱")
    private String provinceName;  // 省份名稱

    @NotBlank(message = "請輸入城市名稱")
    private String cityName;  // 城市名稱

    @NotBlank(message = "請輸入詳細地址")
    private String detail;  // 詳細住址

    @NotBlank(message = "請輸入郵政編碼")
    @Size(max = 6, min = 6, message = "郵政編碼由6位組成")
    private String zipCode;  // 郵政編碼

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        return "Address{" +
                "provinceName='" + provinceName + '\'' +
                ", cityName='" + cityName + '\'' +
                ", detail='" + detail + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}
  • 分組介面:GroupA.java
package com.group;

public interface GroupA {
}
  • 控制層:EmployeeController.java
package com.controller;

import com.bean.Employee;
import com.group.GroupA;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;
import java.util.List;

@Controller
public class EmployeeController {

    // 應用GroupA的分組校驗規則
    @RequestMapping(value="/addemployee")
    // 使用@Valid開啟校驗,使用@Validated也可以開啟校驗
    // Errors物件用於封裝校驗結果,如果不滿足校驗規則,對應的校驗結果封裝到該物件中,包含校驗的屬性名和校驗不通過返回的訊息
    public String addEmployee(@Validated({GroupA.class}) Employee employee, Errors errors, Model model) {
        System.out.println(employee);
        // 判定Errors物件中是否存在未通過校驗的欄位
        if(errors.hasErrors()){
            // 獲取所有未通過校驗規則的資訊
            List<FieldError> fieldErrors = errors.getFieldErrors();
            System.out.println(fieldErrors.size());
            for(FieldError error : fieldErrors){
                System.out.println(error.getField());
                System.out.println(error.getDefaultMessage());
                //將校驗結果資訊新增到Model物件中,用於頁面顯示,後期實際開發中無需這樣設定,返回json資料即可
                model.addAttribute(error.getField(),error.getDefaultMessage());
            }
            // 當出現未通過校驗的欄位時,跳轉頁面到原始頁面,進行資料回顯
            return "employee.jsp";
        }
        return "success.jsp";
    }

    // 不區分校驗分組,即全部規則均校驗
    @RequestMapping(value="/addemployee2")
    public String addEmployee2(@Valid Employee employee, Errors errors, Model model) {
        System.out.println(employee);
        if(errors.hasErrors()){
            for(FieldError error : errors.getFieldErrors()){
                model.addAttribute(error.getField(), error.getDefaultMessage());
            }
            return "employee.jsp";
        }
        return "success.jsp";
    }
}

實用校驗範例

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;

// 實用的校驗範例,僅供參考
public class Employee implements Serializable {

    private String id;  // 員工ID

    private String code;  // 員工編號

    @NotBlank(message = "員工名稱不能為空")
    private String name;  // 員工姓名

    @NotNull(message = "員工年齡不能為空")
    @Max(value = 60,message = "員工年齡不能超過60歲")
    @Min(value = 18,message = "員工年裡不能小於18歲")
    private Integer age;  // 員工年齡

    @NotNull(message = "員工生日不能為空")
    @Past(message = "員工生日要求必須是在當前日期之前")
    private Date birthday;  // 員工生日

    @NotBlank(message = "請選擇員工性別")
    private String gender;  // 員工性別

    @NotEmpty(message = "請輸入員工郵箱")
    @Email(regexp = "@", message = "郵箱必須包含@符號")
    private String email;  // 員工郵箱

    @NotBlank(message = "請輸入員工電話")
    @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "手機號不正確")
    private String telephone;  // 員工電話

    @NotBlank(message = "請選擇員工類別")
    private String type;  // 員工型別:正式工為1,臨時工為2

    @Valid  // 表示需要巢狀驗證
    private Address address;  // 員工住址

    // 省略各 getter、setter
}

相關文章