Spring MVC 文件上传、Restful、表单校验框架
文件上传
上传文件过程分析:
SpringMVC 的文件上传技术:MultipartResolver 接口
- MultipartResolver 接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装。
- MultipartResolver 接口底层实现类 CommonsMultipartResovler。
- CommonsMultipartResovler 并未自主实现文件上传下载对应的功能,而是调用了 apache 的文件上传下载组件。
SpringMVC 文件上传实现:
- 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>
- 页面表单:
<%@ 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>
- SpringMVC 配置:
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>
- 控制器:
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";
}
}
文件上传常见问题:
- 文件命名问题
- 文件名过长问题
- 文件保存路径
- 重名问题
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)是一种网络资源的访问风格,定义了网络资源的访问方式。
- 传统风格访问路径
- Rest 风格访问路径
而 Restful 则是按照 Rest 风格访问网络资源。
优点:
- 隐藏资源的访问行为,通过地址无法得知做的是何种操作。
- 书写简化。
Rest 行为常用约定方式
- GET(查询) http://localhost/user/1 GET
- POST(保存) http://localhost/user POST
- PUT(更新) http://localhost/user PUT
- DELETE(删除) http://localhost/user DELETE
注意:上述行为是约定方式,约定不是规范,可以打破,所以称 Rest 风格,而不是 Rest 规范。
Restful开发入门
- 页面表单:
<!-- 切换请求路径为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>
- 开启 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>
- 控制层:
package com.controller;
import org.springframework.web.bind.annotation.*;
// 设置rest风格的控制器
@RestController // 等于 @Controller + @ResponseBody
// 设置公共访问路径,配合下方访问路径使用
@RequestMapping("/user/")
public class RestfulController {
/**
// rest风格访问路径完整书写方式
@RequestMapping("/user/{id}")
// 使用 @PathVariable 注解获取路径上配置的具名变量,该配置可以使用多次
public String restLocation(@PathVariable Integer id){
System.out.println("restful is running ....");
return "success.jsp";
}
// rest风格访问路径简化书写方式,配合类注解@RequestMapping使用
@RequestMapping("{id}")
public String restLocation2(@PathVariable Integer id){
System.out.println("restful is running ....get:"+id);
return "success.jsp";
}
*/
// 接收GET请求配置方式
// @RequestMapping(value = "{id}", method = RequestMethod.GET)
// 接收GET请求简化配置方式
@GetMapping("{id}")
public String get(@PathVariable Integer id){
System.out.println("restful is running ....get:"+id);
return "success.jsp";
}
// 接收POST请求配置方式
// @RequestMapping(value = "{id}", method = RequestMethod.POST)
// 接收POST请求简化配置方式
@PostMapping("{id}")
public String post(@PathVariable Integer id){
System.out.println("restful is running ....post:"+id);
return "success.jsp";
}
// 接收PUT请求简化配置方式
// @RequestMapping(value = "{id}", method = RequestMethod.PUT)
// 接收PUT请求简化配置方式
@PutMapping("{id}")
public String put(@PathVariable Integer id){
System.out.println("restful is running ....put:"+id);
return "success.jsp";
}
// 接收DELETE请求简化配置方式
// @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
// 接收DELETE请求简化配置方式
@DeleteMapping("{id}")
public String delete(@PathVariable Integer id){
System.out.println("restful is running ....delete:"+id);
return "success.jsp";
}
}
表单校验框架
表单校验框架介绍
表单校验分类:
- 校验位置:
- 客户端校验
- 服务端校验
- 校验内容与对应方式:
- 格式校验
- 客户端:使用 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
快速入门
- 页面表单:
<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>
-
设置校验规则:
- 名称:@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 +
'}';
}
}
-
开启校验,并获取校验错误信息:
- 名称:@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";
}
}
- 示例效果:提交表单并返回校验结果
多规则校验
- 同一个属性可以添加多个校验器:
@NotNull(message = "请输入您的年龄")
@Max(value = 60, message = "年龄最大值不允许超过60岁")
@Min(value = 18, message = "年龄最小值不允许低于18岁")
private Integer age; // 员工年龄
- 3 种判定空校验器的区别:
嵌套校验
- 名称:@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
}