springmvc-day03
第一章 拦截器
1.概念
1.1 使用场景
1.1.1 生活中坐地铁的场景
为了提高乘车效率,在乘客进入站台前统一检票:
1.1.2 程序中的校验登录场景
在程序中,使用拦截器在请求到达具体 handler 方法前,统一执行检测。
1.2 拦截器与过滤器的对比
1.2.1 相同点
三要素相同
- 拦截(配置拦截路径):必须先把请求拦住,才能执行后续操作
- 过滤(根据某种规则/业务逻辑进行筛选):拦截器或过滤器存在的意义就是对请求进行统一处理
- 放行(满足规则/筛选条件,就让你访问你想访问的资源):对请求执行了必要操作后,放请求过去,让它访问原本想要访问的资源
1.2.2 不同点
- 工作平台不同
- 过滤器工作在 Servlet 容器中
- 拦截器工作在 SpringMVC 的基础上
- 拦截的范围
- 过滤器:能够拦截到的最大范围是整个 Web 应用
- 拦截器:能够拦截到的最大范围是整个 SpringMVC 负责的请求(handler方法、view-controller跳转页面、default-servlet-handler处理的静态资源)
- IOC 容器支持
- 过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
- 拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持
2. 具体使用
使用步骤:
1 编写拦截器
(1). 编写一个类实现HandlerInterceptor接口
(2). 重写HandlerInterceptor接口的三个方法:
① 在处理器处理请求之前执行preHandle(),该方法的返回值表示是否放行
② 在处理器处理请求之后执行postHandle()
③ 在视图渲染完毕(整个请求过程完成)之后执行afterCompletion()2 配置拦截器的拦截路径(在spring的配置文件)
mvc:interceptors
mvc:interceptor
<mvc:mapping path=“/hello/*”/>
</mvc:interceptor>
</mvc:interceptors>
2.1 导入依赖
<dependencies>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
</dependencies>
2.2 创建spring-web.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.oracle"></context:component-scan>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler></mvc:default-servlet-handler>
<!-- Thymeleaf视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<!-- 视图前缀 -->
<property name="prefix" value="/WEB-INF/templates/"/>
<!-- 视图后缀 -->
<property name="suffix" value=".html"/>
<!--模板类型-->
<property name="templateMode" value="HTML5"/>
<!--模板的字符编码-->
<property name="characterEncoding" value="UTF-8" />
</bean>
</property>
</bean>
</property>
</bean>
</beans>
2.3 web.xml文件添加映射器和解决字符乱码问题
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<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-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--CharacterEncodingFilter-->
<!-- 配置过滤器解决 POST 请求的字符乱码问题 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!-- encoding参数指定要使用的字符集名称 -->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!-- 请求强制编码 -->
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<!-- 响应强制编码 -->
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
2.4 创建拦截器类
package com.atguigu.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Demo01Interceptor implements HandlerInterceptor {
// 在处理请求的目标方法前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("Demo01Interceptor的preHandle方法执行了...");
// 返回true放行 返回false不放行
return true;
}
// 在目标方法之后,渲染视图之前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("Demo01Interceptor的postHandle方法执行了...");
}
// 在渲染视图之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("Demo01Interceptor的afterCompletion方法执行了...");
}
}
单个拦截器执行顺序:
- preHandle() 方法
- 目标 handler 方法
- postHandle() 方法
- 渲染视图
- afterCompletion() 方法
2.5 注册拦截器
2.5.1 默认拦截全部请求
<!-- 注册拦截器 -->
<mvc:interceptors>
<!-- 直接通过内部 bean 配置的拦截器默认拦截全部请求(SpringMVC 范围内) -->
<bean class="com.atguigu.interceptor.Demo01Interceptor"/>
</mvc:interceptors>
2.5.2 配置拦截路径
2.5.2.1 精确匹配
<!-- 具体配置拦截器可以指定拦截的请求地址 -->
<mvc:interceptor>
<!-- 精确匹配 -->
<mvc:mapping path="/hello/sayHello"/>
<bean class="com.atguigu.interceptor.Demo01Interceptor"/>
</mvc:interceptor>
2.5.2.2 模糊匹配:匹配单层路径
<mvc:interceptor>
<!-- /*匹配路径中的一层 -->
<mvc:mapping path="/hello/*"/>
<bean class="com.atguigu.interceptor.Demo01Interceptor"/>
</mvc:interceptor>
2.5.2.3 模糊匹配:匹配多层路径
<mvc:interceptor>
<!--模糊匹配多级目录-->
<mvc:mapping path="/hello/**"/>
<!--排除-->
<mvc:exclude-mapping path="/hello/sayHello"/>
<bean class="com.atguigu.interceptor.Demo01Interceptor"/>
</mvc:interceptor>
2.6 多个拦截器执行顺序
- preHandle()方法:和配置的顺序一样
- 目标handler方法
- postHandle()方法:和配置的顺序相反
- 渲染视图
- afterCompletion()方法:和配置的顺序相反
第二章 类型转换
SpringMVC 将『把请求参数注入到 POJO 对象』这个操作称为**『数据绑定』**,英文单词是 binding。数据类型的转换和格式化就发生在数据绑定的过程中。 类型转换和格式化是密不可分的两个过程,很多带格式的数据必须明确指定格式之后才可以进行类型转换。最典型的就是日期类型。
1. 自动类型转换
HTTP 协议是一个无类型的协议,我们在服务器端接收到请求参数等形式的数据时,本质上都是字符串类型。请看 javax.servlet.ServletRequest 接口中获取全部请求参数的方法:
public Map<String, String[]> getParameterMap();
而我们在实体类当中需要的类型是非常丰富的。对此,SpringMVC 对基本数据类型提供了自动的类型转换。例如:请求参数传入“100”字符串,我们实体类中需要的是 Integer 类型,那么 SpringMVC 会自动将字符串转换为 Integer 类型注入实体类。
类型转换的种类:
类型转换的种类:
1 自动类型转换(SpringMVC会根据你接收请求参数的类型,进行自动转换)
2 手动类型转换(如果在类型转换的时候,有一些特殊的要求,我们可以通过手动进行转换)
(1). 使用SpringMVC提供的某些注解,对特定的类型进行手动转换
① DateTimeFormate
② NumberFormate注解
(2). 编写自定义的类型转换器
① 创建一个类型转换器类实现Converter接口,该接口有俩泛型(S,T)
2.手动类型转换(日期和数值类型转换)
2.1 先导入依赖
<dependencies>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
2.2 创建Product实体类
package com.atguigu.pojo;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import java.util.Date;
/**
* SpringMVC提供了一些注解,可以让我们进行一些手动类型转换
* 1. DateTimeFormat注解:可以对日期时间类型进行转换
* 2. NumberFormat注解:可以对数值类型进行转换
*/
@Data
public class Product {
// 可以根据yyyy-MM-dd格式转换成Date类型显示
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date productDate;
// 可以根据#,#格式显示数字
@NumberFormat(pattern = "#,#")
private Double productPrice;
}
2.3 创建HelloController类
package com.atguigu.controller;
import com.atguigu.pojo.Product;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/demo01")
public String sayHello(Product product){
System.out.println("product = " + product);
return "target";
}
}
2.4 创建index.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<form th:action="@{/hello/demo01}">
生产日期:<input type="text" name="productDate"> <br>
产品价格:<input type="text" name="productPrice"> <br>
<button>提交</button>
</form>
</body>
</html>
2.5 数据绑定失败后处理方式
2.5.1 默认结果
2.4.2 BindingResult 接口
BindingResult 接口和它的父接口 Errors 中定义了很多和数据绑定相关的方法,如果在数据绑定过程中发生了错误,那么通过这个接口类型的对象就可以获取到相关错误信息。
2.4.3 重构 handler 方法
package com.atguigu.controller;
import com.atguigu.pojo.Product;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/demo01")
public String sayHello(Product product, BindingResult bindingResult, Model model){
// 在该方法中声明bindingResult,SpringMVC就不会自己处理绑定错误了
ArrayList<String> filedNameList = new ArrayList<>();
// 判断是否有绑定错误
if (bindingResult.hasErrors()){
// 如果有绑定错误
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
// 获取出现绑定错误的字段名
String fieldName = fieldError.getField();
// 获取字段绑定错误的信息
String defaultMessage = fieldError.getDefaultMessage();
System.out.println(fieldName + ":" + defaultMessage);
filedNameList.add(fieldName);
}
model.addAttribute("errFieldNames", filedNameList);
return "bind-error";
}
System.out.println("product = " + product);
return "target";
}
}
2.4.4 在页面上显示错误消息
页面是vind-error.html,放在Thymeleaf前后缀控制范围之内
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>数据绑定错误</title>
</head>
<body>
<h1>传入的参数有问题!</h1>
<ul th:each="fieldName:${errFieldNames}">
<li th:text="${filedName}"></li>
</ul>
</body>
</html>
3.自定义类型转换器
现在我们希望通过一个文本框输入约定格式的字符串,然后转换为我们需要的类型,所以必须通过自定义类型转换器来实现,否则 SpringMVC 无法识别。
3.1 创建自定义类型转换器类
实现接口:org.springframework.core.convert.converter.Converter<S,T>
泛型 S:源类型(本例中是 String 类型)
泛型 T:目标类型(本例中是 Date类型)
package com.atguigu.comverter;
import org.springframework.core.convert.converter.Converter;
import java.util.Date;
public class AtguiguDateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
// source就是要进行转换的那个字符串,例如"1992-03-03"或者"1992/03/03"
String[] strs = source.split("-");
// 如果strs的长度大于1,那说明客户端传的是-格式
if (strs.length > 1){
int year = Integer.parseInt(strs[0]);
int month = Integer.parseInt(strs[1]);
int day = Integer.parseInt(strs[2]);
return new Date(year, month, day);
}
strs = source.split("/");
if (strs.length > 1){
int year = Integer.parseInt(strs[0]);
int month = Integer.parseInt(strs[1]);
int day = Integer.parseInt(strs[2]);
return new Date(year, month, day);
}
throw new RuntimeException("传入的参数只能是-或者/格式");
}
}
3.2 在springmvc配置文件中注册类型转换器
<!-- 在 mvc:annotation-driven 中注册 FormattingConversionServiceFactoryBean -->
<mvc:annotation-driven conversion-service="formattingConversionService"></mvc:annotation-driven>
<!-- 配置类型转换器-->
<!-- 在 FormattingConversionServiceFactoryBean 中注册自定义类型转换器 -->
<bean id="formattingConversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- 在 converters 属性中指定自定义类型转换器 -->
<property name="converters">
<list>
<bean class="com.atguigu.converter.AtguiguDateConverter"></bean>
</list>
</property>
</bean>
配置好自定义类型转换器后不管是/格式还是-格式都可以转成Date类型显示
第三章 数据校验
在 Web 应用三层架构体系中,表述层负责接收浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的数据隔绝在业务逻辑层之外。
1. 数据校验概述
JSR 303 是 Java 为 Bean 数据合法性校验提供的标准,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
注解 | 规则 |
---|---|
@Null | 标注值必须为 null |
@NotNull | 标注值不可为 null,但是可以为空字符串 |
@AssertTrue | 标注值必须为 true |
@AssertFalse | 标注值必须为 false |
@Min(value) | 标注值必须大于或等于 value |
@Max(value) | 标注值必须小于或等于 value |
@DecimalMin(value) | 标注值必须大于或等于 value |
@DecimalMax(value) | 标注值必须小于或等于 value |
@Size(max,min) | 标注值大小必须在 max 和 min 限定的范围内 |
@Digits(integer,fratction) | 标注值值必须是一个数字,并且指定最大的整数位数和最大的小数位数 |
@Past | 标注值只能用于日期型,且必须是过去的日期 |
@Future | 标注值只能用于日期型,且必须是将来的日期 |
@Pattern(value) | 标注值必须符合指定的正则表达式 |
JSR 303 只是一套标准,需要提供其实现才可以使用。Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:
注解 | 规则 |
---|---|
标注值必须是格式正确的 Email 地址 | |
@Length | 标注值字符串大小必须在指定的范围内 |
@NotEmpty | 标注值字符串不能是空字符串 |
@Range | 标注值必须在指定的范围内 |
Spring 4.0 版本已经拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC 中,可直接通过注解驱动 mvc:annotation-driven 的方式进行数据校验。Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean中。Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的实现者的jar包放到类路径下。
配置 mvc:annotation-driven 后,SpringMVC 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Validated 注解即可让 SpringMVC 在完成数据绑定后执行数据校验的工作。
2. 具体操作
前提:1. springmvc环境 2. Tomcat8及以上版本
2.1 引入依赖
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.2.0.Final</version>
</dependency>
2.2 应用校验规则
2.2.1 给要进行校验的字段加上校验规则注解
package com.atguigu.pojo;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import org.springframework.context.annotation.Bean;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Pattern;
@Data
public class User {
@Null
private Integer id;
@NotNull
@Length(min = 6, max = 10)
private String username;
@NotNull
@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$")
private String phone;
@NotNull
@Email
private String email;
@NotNull
@Range(min = 18, max = 80)
private Integer age;
}
2.2.2 给处理器方法的形参加上Validated
package com.atguigu.controller;
import com.atguigu.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/add")
public String add(@Validated User user){
return "target";
}
@RequestMapping("/update")
public String update(User user){
System.out.println(user);
return "target";
}
}
2.3 测试效果
2.2.3.1 测试add方法 一个参数都没填
校验生效了 所以报400错误
2.2.3.2 测试update方法 一个参数都没填
能够成功访问页面 因为update方法中参数中没有加@Validated注解
2.2.3.3 测试add方法 参数全部填写正确
能够正常访问页面了
2.4 显示友好的错误提示
2.4.1 重构UserController方法
@RequestMapping("/add")
public String add(@Validated User user, BindingResult bindingResult, Model model){
List<String > fieldNameList = new ArrayList();
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String fieldName = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
fieldNameList.add(fieldName);
}
model.addAttribute("errorFieldNames", fieldNameList);
return "bind-error";
}
System.out.println("user add = " + user);
return "target";
}
2.4.2 创建错误信息页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>数据绑定错误</title>
</head>
<body>
<h1>传入的参数有问题!</h1>
<ul th:each="fieldName:${errorFieldNames}">
<li th:text="${fieldName}"></li>
</ul>
</body>
</html>
2.4.3 显示效果
2.5 分组校验
如果想要使用update方法时候id必须为null 使用add方法的时候id必须不能为null的时候该如何处理呢
使用分组校验解决
2.5.1 创建分组的接口
add分组接口
package com.atguigu.group;
public interface AddGroup {
}
update分组接口
package com.atguigu.group;
public interface UpdateGroup {
}
2.5.2 重构User实体类校验规则
package com.atguigu.pojo;
import com.atguigu.group.AddGroup;
import com.atguigu.group.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import org.springframework.context.annotation.Bean;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Pattern;
@Data
public class User {
@Null(groups = AddGroup.class)
@NotNull(groups = UpdateGroup.class)
private Integer id;
@NotNull(groups = {AddGroup.class,UpdateGroup.class})
@Length(min = 6,max = 10, groups = {AddGroup.class,UpdateGroup.class})
private String username;
@NotNull(groups = {AddGroup.class,UpdateGroup.class})
@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",
groups = {AddGroup.class,UpdateGroup.class})
private String phone;
@NotNull(groups = {AddGroup.class,UpdateGroup.class})
@Email(groups = {AddGroup.class,UpdateGroup.class})
private String email;
@NotNull(groups = {AddGroup.class,UpdateGroup.class})
@Range(min = 18, max = 80, groups = {AddGroup.class,UpdateGroup.class})
private Integer age;
}
2.5.3 重构UserController方法
package com.atguigu.controller;
import com.atguigu.group.AddGroup;
import com.atguigu.group.UpdateGroup;
import com.atguigu.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/add")
// add方法使用AddGroup分组的校验
public String add(@Validated(AddGroup.class) User user, BindingResult bindingResult, Model model){
List<String > fieldNameList = new ArrayList();
if (bindingResult.hasErrors()){
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String fieldName = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
fieldNameList.add(fieldName);
}
model.addAttribute("errorFieldNames", fieldNameList);
return "bind-error";
}
System.out.println("user add = " + user);
return "target";
}
@RequestMapping("/update")
// update方法使用UpdateGroup分组的校验
public String update(@Validated(UpdateGroup.class) User user){
System.out.println("user update = " + user);
return "target";
}
}
2.5.4 测试效果
2.5.4.1 add方法传入id参数 不能通过校验
2.5.4.1 update方法传入id参数 通过校验
第四章 异常映射
1. 为什么需要异常映射
一个项目中会包含很多个模块,各个模块需要分工完成。如果张三负责的模块按照 A 方案处理异常,李四负责的模块按照 B 方法处理异常……各个模块处理异常的思路、代码、命名细节都不一样,那么就会让整个项目非常混乱。
异常映射可以将异常类型和某个具体的视图关联起来,建立映射关系。好处是可以通过 SpringMVC 框架来帮助我们管理异常。
- 声明式管理异常:在配置文件中指定异常类型和视图之间的对应关系。在配置文件或注解类中统一管理。
- 编程式管理异常:需要我们自己手动 try … catch … 捕获异常,然后再手动跳转到某个页面。
2. 异常映射的优势
- 使用声明式代替编程式来实现异常管理
- 让异常控制和核心业务解耦,二者各自维护,结构性更好
- 整个项目层面使用同一套规则来管理异常
- 整个项目代码风格更加统一、简洁
- 便于团队成员之间的彼此协作
3. 基于 XML 的异常映射
3.1 XML配置
SpringMVC 会根据异常映射信息,在捕获到指定异常对象后,将异常对象存入请求域,然后转发到和异常类型关联的视图。
<!--配置异常处理-->
<bean id="exceptionResolver"
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 配置异常映射关系 -->
<property name="exceptionMappings">
<props>
<!-- key属性:指定异常类型 -->
<!-- 文本标签体:和异常类型对应的逻辑视图 -->
<prop key="java.lang.ArithmeticException">error-arith</prop>
<prop key="java.lang.ClassNotFoundException">error-class</prop>
<prop key="java.lang.RuntimeException">error-runtime</prop>
</props>
</property>
</bean>
3.2 异常范围
如果在配置文件中,发现有多个匹配的异常类型,那么 SpringMVC 会采纳范围上最接近的异常映射关系。
<prop key="java.lang.ArithmeticException">error-arith</prop>
<prop key="java.lang.RuntimeException">error-runtime</prop>
4. 基于注解的异常映射
4.1 创建异常处理器类
4.2 异常处理器加入IOC容器
4.2.1 包扫描
<!--1.包扫描-->
<context:component-scan base-package="com.atguigu"/>
4.2.2 给异常处理器类标记注解和处理异常的方法
package com.atguigu.handler;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.List;
@ControllerAdvice(basePackages = "com.atguigu.controller")
public class AtguiguExceptionHandler {
// @ExceptionHandler注解:标记异常处理方法
// value属性:指定匹配的异常类型 value可以不写
// 异常类型的形参:SpringMVC 捕获到的异常对象
@ExceptionHandler(RuntimeException.class)
public String handlerRuntimeException(Exception e){
// 记录异常信息
System.out.println("记录日志:" + e);
return "error";
}
// 标记匹配的异常类型是数据绑定异常类型
@ExceptionHandler(BindException.class)
public String handlerBindingException(BindException e){
// 记录异常信息
List<FieldError> fieldErrors = e.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
System.out.println("记录日志:" + fieldError.getField() + "字段:" + fieldError.getDefaultMessage());
}
// 同步请求跳转报错页面
return "error";
}
}
4.3 处理器方法
package com.atguigu.controller;
import com.atguigu.pojo.Product;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/sayHello")
public String sayHello(){
System.out.println("hello world");
int num = 10 / 0;
return "target";
}
public String sayXixi(@Validated Product product){
System.out.println("product = " + product);
return "target";
}
}
当同一个异常类型在基于 XML 和注解的配置中都能够找到对应的映射,那么以注解为准。
4.4 测试效果 测试sayHello和sayXixi方法报错是否使用了异常处理器
5. 区分请求类型
5.1 为什么要区分请求类型
异常处理机制和拦截器机制都面临这样的问题:
5.2 判断依据
查看请求消息头中是否包含 Ajax 请求独有的特征:
- Accept 请求消息头:包含 application/json
- X-Requested-With 请求消息头:包含 XMLHttpRequest
两个条件满足一个即可。
5.3 重构异常处理器兼容两种请求的处理方法
package com.atguigu.handler;
import com.atguigu.result.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@ControllerAdvice(basePackages = "com.atguigu.controller")
public class AtguiguExceptionHandler {
// @ExceptionHandler注解:标记异常处理方法
// value属性:指定匹配的异常类型 value可以不写
// 异常类型的形参:SpringMVC 捕获到的异常对象
@ExceptionHandler(RuntimeException.class)
public String handlerRuntimeException(HttpServletRequest request, HttpServletResponse response, Exception e) throws IOException {
// 记录异常信息
System.out.println("记录日志:" + e);
// 判断是同步请求还是异步请求
if (request.getHeader("accept").contains("application/json")){
// 异步请求
// 将result对象转成json响应给客户端
response.getWriter().write(new ObjectMapper().writeValueAsString(Result.fail()));
return null;
}
// 同步请求 跳转到错误页面
return "error";
}
// 标记匹配的异常类型是数据绑定异常类型
@ExceptionHandler(BindException.class)
public String handlerBindingException(HttpServletRequest request,HttpServletResponse response,BindException e) throws IOException {
// 记录异常信息
List<FieldError> fieldErrors = e.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
System.out.println("记录日志:" + fieldError.getField() + "字段:" + fieldError.getDefaultMessage());
}
// 判断是同步请求还是异步请求
if (request.getHeader("accept").contains("application/json")){
// 异步请求
// 将result对象转成json响应给客户端
response.getWriter().write(new ObjectMapper().writeValueAsString(Result.fail()));
return null;
}
// 同步请求跳转报错页面
return "error";
}
}
5.4 测试效果
5.4.1 sayHello方法测试同步请求
5.4.2 sayHello方法测试异步请求 accept设为application/json
5.4.3 sayXixi方法测试同步请求
5.4.4 sayXixi方法测试异步请求 accept设为application/json
第五章 文件上传
1.前端表单
需要满足的要求:
- 第一点:请求方式必须是 POST
- 第二点:请求体的编码方式必须是 multipart/form-data(通过 form 标签的 enctype 属性设置)
- 第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>文件上传</h1>
<form enctype="multipart/form-data" th:action="@{/file/upload}" method="post">
请选择你想上传的文件:<input type="file" name="icon"> <br>
请输入文件描述:<input type="text" name="desc"> <br>
<button>提交</button>
</form>
</body>
</html>
2.SpringMVC环境
2.1 映入依赖
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
2.2 配置
在 SpringMVC 的配置文件中加入 multipart 类型数据的解析器:
<!--配置文件上传的解析器-->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
<property name="defaultEncoding" value="UTF-8"/>
<!--指定上传文件的最大体积: 8M-->
<property name="maxUploadSize" value="8388608"/>
3 创建FileUtil工具类
package com.atguigu.utils;
import java.util.Calendar;
import java.util.Random;
import java.util.UUID;
public class FileUtil {
/**
* 随机生成唯一的文件名
* @param originalFileName
* @return
*/
public static String getUUIDName(String originalFileName){
//1. 获取文件名的后缀
String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
//2. 使用UUID生成一个唯一的字符串
String str = UUID.randomUUID().toString().replaceAll("-","");
//返回唯一的文件名
return str + suffix;
}
/**
* 生成随机目录的方法
* @param dirPathLevel
* @param dirPathNameSize
* @return
*/
public static String getRandomDirPath(int dirPathLevel,int dirPathNameSize){
StringBuilder dirPath = new StringBuilder();
//我们随机生成的目录就从这个字符串中随机取俩字母
String source = "abcde0123456789";
Random random = new Random();
for (int j=0;j < dirPathLevel;j++) {
for (int i = 0; i < dirPathNameSize; i++) {
//循环两次,每次取出一个字符作为目录名
int index = random.nextInt(source.length());
dirPath.append(source.charAt(index));
}
dirPath.append("/");
}
return dirPath.toString();
}
/**
* 按照日期生成随机目录:我们每天上传的内容放到一个目录中
* @return
*/
public static String getDateDirPath(){
StringBuilder dirPath = new StringBuilder();
//获取当前时间:获取当前的年、月、日就可以生成目录路径
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
dirPath.append(year+"/");
//获取到的月是0-11,而真正的月是1-12
int month = calendar.get(Calendar.MONTH) + 1;
if (month < 10){
dirPath.append("0"+month+"/");
}else {
dirPath.append(month+"/");
}
//获取日
int day = calendar.get(Calendar.DAY_OF_MONTH);
if(day < 10){
dirPath.append("0" + day + "/");
}else {
dirPath.append(day + "/");
}
return dirPath.toString();
}
public static void main(String[] args) {
System.out.println(getDateDirPath());
}
}
4 创建FileController处理类接收数据
package com.atguigu.controller;
import com.atguigu.utils.FileUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
@Controller
@RequestMapping("/file")
public class FileController {
@Autowired
private ServletContext servletContext;
@RequestMapping("/upload")
public String upload(@RequestParam("desc") String desc,
@RequestParam("icon")MultipartFile multipartFile) throws IOException {
System.out.println("desc = " + desc);
System.out.println("multipartFile = " + multipartFile);
// 将客户端上传的文件保存起来
// 1.获取上传时候的文件名
String originalFilename = multipartFile.getOriginalFilename();
// 文件上传问题1:文件重名问题
// 2.我们要针对每一个上传的文件给其一个唯一的文件名,但是要保证后缀不变(UUID)
String uuidName = FileUtil.getUUIDName(originalFilename);
// 文件上传问题2:所有文件都上传同一个目录,不好管理 最少弄两级目录(随机生成或者按照日期生成)
String randomDirPath = FileUtil.getRandomDirPath(2, 2);
// 方式一:将其保存到当前电脑的D:\\upload中
// 创建表示目录的file对象
/*
File dirFile = new File("D:\\upload");
// 判断是否存在,如果不存在则创建这个目录
if (!dirFile.exists()){
dirFile.mkdirs();
}
// 将该文件转存到目录中
multipartFile.transferTo(new File(dirFile,originalFilename));
*/
// 方式二:使用servletContext获取项目部署路径
// 动态获取项目部署路径中的upload文件夹
String dirPath = servletContext.getRealPath(randomDirPath);
File dirFile = new File(dirPath);
// 判断是否存在,如果不存在则创建这个目录
if (!dirFile.exists()){
dirFile.mkdirs();
}
// 将该文件转存到目录中
multipartFile.transferTo(new File(dirFile,originalFilename));
return "target";
}
}
5 测试效果
上传文件
上传到部署目录中了
6. MultipartFile接口介绍
6.1 底层原理
6.2 三种去向
6.2.1 本地转存
6.2.1.3 缺陷
- Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
- 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
- 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。
- 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。
6.2.2 文件服务器
6.2.2.1 优势
- 不受 Web 应用重新部署影响
- 在应用服务器集群环境下不会导致数据不一致
- 针对文件读写进行专门的优化,性能有保障
- 能够实现动态扩容
5.2.2.2 常见的文件服务器类型
- 第三方平台:
- 阿里的 OSS 对象存储服务
- 七牛云
- 自己搭建服务器:FastDFS等
5.2.3 上传到其他模块(了解)
这种情况肯定出现在分布式架构中,常规业务功能不会这么做,采用这个方案的一定的特殊情况。
第六章 文件下载
1. 原始形态
使用链接地址指向要下载的文件。此时浏览器会尽可能解析对应的文件,只要是能够在浏览器窗口展示的,就都会直接显示,而不是提示下载。
<a href="download/hello.atguigu">下载</a><br/>
<a href="download/tank.jpg">下载</a><br/>
<a href="download/chapter04.zip">下载</a><br/>
上面例子中,只有 chapter04.zip 文件是直接提示下载的,其他两个都是直接显示。
2.明确要求浏览器提示下载
2.1 前端代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>文件上传</h1>
<form enctype="multipart/form-data" th:action="@{/file/upload}" method="post">
请选择你想上传的文件:<input type="file" name="icon"> <br>
请输入文件描述:<input type="text" name="desc"> <br>
<button>提交</button>
</form>
<h1>文件下载</h1>
<a th:href="@{/file/download(fileName='abc.7z')}">下载abc.7z</a> <br>
<a th:href="@{/file/download(fileName='demo.txt')}">下载demo.txt</a> <br>
<a th:href="@{/file/download(fileName='timg.jpg')}">下载timg.jpg</a> <br>
</body>
</html>
2.2 处理器方法
@RequestMapping("/download")
public ResponseEntity download(@RequestParam("fileName") String fileName) throws IOException {
// 1.使用字节输入流读取要下载的文件
InputStream is = servletContext.getResourceAsStream("static/download/" + fileName);
// 2.将字节输入流中的所有字节读到byte[]
// is.available() 获取字节输入流中的字节数
byte[] buffer = new byte[is.available()];
is.read(buffer);
// 3.响应:响应行、头、体
HttpHeaders headers = new HttpHeaders();
//Content-Disposition:响应头是指示客户端下载内容
headers.add("content-disposition", "attachment;filename=" + fileName);
// 获取要下载的那个文件的媒体类型(mime-type)
String mimeType = servletContext.getMimeType(fileName);
headers.add("content-type", mimeType);
// 4.使用输出流输出
return new ResponseEntity(buffer, headers, HttpStatus.OK);
}
3. 典型应用场景举例
我们目前实现的是一个较为简单的下载,可以用在下面的一些场合:
- 零星小文件下载
- 将系统内部的数据导出为 Excel、PDF 等格式,然后以下载的方式返回给用户