文章目录
SpringMVC
关注微信公众号:
回复 springMVC
,获取详细介绍文章
传统 JavaEE 项目中,对于不同的请求,我们都要去写一个 Servlet,一旦请求增多,Servlet 的量就会十分巨大(比如说对用户的增删改查,我们就需要去写4个servlet),有没有办法,可以将对同一个类的操作,写在一个类(控制器)里呢?这,就需要用到我们的 SpringMVC 了。
在看这个文档之前,我强烈建议各位,去看一下我之前写的一个简易的 MVC 框架,就是模仿 SpringMVC框架来编写的,但是十分的轻量级,可以帮助我们很好的取了解 SpringMVC 的执行流程,和实现方式
一、快速开始
入门案例
在去学习 SpringMVC 的知识之前,我们先去尝试配置一下SpringMVC
1、创建 Maven 格式的 web 项目
这里的步骤我就跳过了
2、编写 Spring 和 SpringMVC 的配置文件
之所以要将二者区分开来,是因为 SpringMVC 设定为专门控制 controller,而 Spring 用来管理其他对象。这样可以让我们的项目结构更加清晰
配置文件内容如下:
spring:
<?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:aop="http://www.springframework.org/schema/aop"
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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--spring所要管理的部分-->
<context:component-scan base-package="top.faroz.service,top.faroz.dao"/>
</beans>
springMVC:
<?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
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--mvc所要管理的部分-->
<context:component-scan base-package="top.faroz.controller"/>
<!--开启注解驱动,说明 top.faroz.controller 中的注解,都是有用的-->
<mvc:annotation-driven/>
</beans>
3、编写 web.xml
虽然我们上面写了 spring 和 springMVC 的配置文件,但是没有去实例化二者的容器。根据 JavaEE 的生命周期可知,在项目开启的时候,Tomcat 会去创建 Servlet ,并且,监听器也会在项目开启后,开始工作。
所以,我们就要用到 监听器,和JavaEE的生命周期,去创建 Spring 和 SpringMVC 的容器
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">
<!--1.spring配置 相当于我们之前去 new ClassPathXmlApplicationContext()-->
<!--1.1. spring 配置文件的位置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--1.2. 监听器,这样当web容器开启是时候,就会去注册我们的spring容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--2.springmvc配置 配置中央控制器-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<!--指定要被创建的中央控制器的位置-->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--要指定 springmvc 的配置文件位置-->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!--在 tomcat 开启的时候,就创建该servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<!--为 中央控制器 配置访问路径-->
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
4、配置视图解析器
视图解析器的作用,在于,告诉我们的 SpringMVC,该去访问什么类型的视图(html jsp…)
<!--视图解析器,告诉我们的 SpringMVC,该去访问什么类型的视图(html jsp...)-->
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
如果我们最后,所有的视图位置,在一个叫 jsp 的文件夹下
那我们的视图解析器配置,就要修改为:
5、测试
编写 controller 代码,service、dao 代码,进行测试
controller 代码如下:
最外层要加上 @Controller 注解,告诉Spring,这是应该被托管的对象
调用的 Service 对象,也要加上 @Resource 注解,保证依赖注入
@Controller
public class UserController {
@Resource
private UserService userService;
@RequestMapping("add.do")
public ModelAndView add() {
System.out.println("执行 add.do");
userService.getAll();
ModelAndView mv = new ModelAndView();
/**
* 相当于 request.setAttribute(..)
*/
mv.addObject("user",new User(1,"haha",22));
/**
* 会经过 springMVC的视图解析器,转换成我们的物理路径
* 相当于 request.getRequestDispatcher("index.html").forword..
*/
mv.setViewName("index");
return mv;
}
}
idex.jsp如下:
我们在页面上,显示从后端接口传来的参数
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首页</title>
</head>
<body>
<h1>--index ${user.name}</h1>
</body>
</html>
测试结果如下:
开启服务器,访问 add.do
案例解析
当 Spring 和 SpringMVC 同时出现,我们的项目中将存在两个容器,一个是 Spring 容器,另一个是 SpringMVC 容器,Spring 容器通过 ContextLoaderListener 来加载,SpringMVC 容器则通过 DispatcherServlet 来加载,这两个容器不一样:
1.Spring容器中不能扫描所有Bean嘛?
不可以。当用户发送的请求达到服务端后,会寻找前端控制器DispatcherServlet去处理,只在
SpringMVC容器中找,所以Controller必须在SpringMVC容器中扫描。
2.SpringMVC容器中可以扫描所有Bean嘛?
可以。可以在SpringMVC容器中扫描所有Bean。但是实际开发中一般不会这么做,原因如下:
(1)为了方便配置文件的管理
(2)未来在 Spring+SpringMVC+Mybatis组合中,要写的配置内容很多,一般都会根据功能分开编写
二、SpringMVC 执行流程
三、SpringMVC 的组件
四、@RequestMapping
作用范围
打开源码,我们来看一下:
使用方式
了解了上面的作用范围,我们就知道,可以在类最外层,限定请求的对象了
限定请求方法
可以使用 @RequestMapping
的 Method 属性,指定请求方法
@RequestMapping(value = "/add.do" , method = RequestMethod.GET) //指定使用 get 方法,可以请求
但我们常营的方法,还是对于不同请求类型,使用不同注解
@PostMapping
@GetMapping
@PutMapping
@DeleteMapping
...
五、url-pattern解析
为什么不能用 /
我们之前在配置的时候,访问路径后面是加上 .do
的
但是,我们也看到有项目,使用 /
作为访问路径,它们是经过配置的
如果不配置,直接使用 /
,那就回出现静态资源无法访问的问题。
为什么会这样?这是因为,如果使用 /
,就是默认对所有请求进行拦截,然后走 DispatcherServlet
,
假如说我们想访问一个图片:
/image/cat.jpg
这个时候,因为 DispatcherServlet
对所有请求,都会去拦截,所以,/image/cat.jpg
也会被拦截,去找寻后端接口,但是我们知道,后端不可能有 /image/cat.jpg
的接口,所以,最后的结果,就是访问失败
如果要用 / ,该怎么配置
这里,在 SpringMVC 的配置文件里,我们可以配置不走 DispatcherServlet 的请求路径
将 web.xml中,DispatcherServlet 的请求路径,修改为 /:
测试:
我们在前端页面中,添加图片的访问连接
然后去访问后端接口
可以看到,图片可以显示,对于静态资源的请求,没有走 DispatcherServlet
==注意:==不可以将整个 web 目录,都放进静态资源忽略路径中,不然,我们的所有页面,就都对用户公开了,用户就可以通过 url ,肆意的去访问我们的所有页面了,这是绝对不允许的
对于页面的跳转,我们只允许交给 Controller 去处理
六、处理方法的参数
在 JavaEE 中,我们获取前端传来的参数,一般只有 req.getParams()
这种方式,但是在 SpringMVC 中,为我们封装了多种多样的简便的参数获取方式。
1、参数一个个获取
前端:
<h1>1. 参数一个个获取</h1>
<form action="/user/pass1" method="post">
输入姓名:<input type="text" name="name"> <br/>
输入年龄:<input type="text" name="age"> <br/>
<button type="submit">提交</button>
</form>
后端:
@RequestMapping("/pass1")
public ModelAndView getParam(String name,String age) {
System.out.println(name+" "+age);
ModelAndView mv = new ModelAndView();
mv.setViewName("success");
return mv;
}
演示:
注意:
表单中的name,必须要和参数名称一样,不然会获取不到
如果参数不一致,该怎么办?
这里,就需要用到 @RequestParam 注解
@RequestMapping("/pass1")
public ModelAndView getParam(@RequestParam("name")String userName,@RequestParam("age") String userage) {
System.out.println(userName+" "+userage);
ModelAndView mv = new ModelAndView();
mv.setViewName("success");
return mv;
}
如果出现参数不一致的情况,可能会报错,这不是我们所希望的,我们可以设置 required = false ,当参数不一致的时候,不会报错,而是接受一个 空值
@RequestMapping("/pass1")
public ModelAndView getParam(@RequestParam(value = "name" ,required = false)String userName,
@RequestParam(value = "age",required = false) String userage) {
System.out.println(userName+" "+userage);
ModelAndView mv = new ModelAndView();
mv.setViewName("success");
return mv;
}
2、对象获取
前端:
<h1>2. 使用对象传递</h1>
<form action="/user/pass2" method="post">
输入姓名:<input type="text" name="name"> <br/>
输入年龄:<input type="text" name="age"> <br/>
<button type="submit">提交</button>
</form>
后端:
@RequestMapping("/pass2")
public ModelAndView getParam2(User user) {
System.out.println(user);
ModelAndView mv = new ModelAndView();
mv.setViewName("success");
return mv;
}
这里也要注意,前端 name 的值,必须要和后端的对象的属性名,保持一致
3、HttpServletRequest 获取
当然,SpringMVC 任然支持使用传统方式,获取参数的值
这里,要注意一点,SpringMVC ,不是单对 req 会填上参数的,如果参数中还包含,之前的取值方式,照样会取到值
4、URL地址传参(restful)
所谓的 url 地址传值,其实,就是 restful 风格
url:
/user/pass3/admin/123
后端:
@RequestMapping("/pass3/{name}/{age}")
public ModelAndView getParam3(@PathVariable("name") String name,@PathVariable("age") String age) {
ModelAndView mv = new ModelAndView();
System.out.println(name+" "+age);
mv.setViewName("success");
return mv;
}
结果:
5、获取日期类型
其实,就相当于,我们用注解,做了一个 SimpleDateFormat
前端:
<h1>5. 传输日期类型</h1>
<form action="/user/pass5" method="post">
输入日期:<input type="text" name="date"> <br/>
<button type="submit">提交</button>
</form>
后端:
@RequestMapping("/pass5")
public ModelAndView getParam4(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date ) {
ModelAndView mv = new ModelAndView();
System.out.println("获取的日期为:"+date);
mv.setViewName("success");
return mv;
}
当然,后端也支持多注解
@RequestMapping("/pass5")
public ModelAndView getParam4(@RequestParam("d") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date ) {
ModelAndView mv = new ModelAndView();
System.out.println("获取的日期为:"+date);
mv.setViewName("success");
return mv;
}
结果:
6、获取数组类型
和第一种方式类似,这里不再演示
7、获取集合类型
需要我们去封装一个类,类中包含集合,才可以传入
七、中文乱码问题
在传输中文字符的时候,会出现乱码问题
Spring 包中,已经有对字符类型进行设定的过滤器,我们去注册就行了
在 web.xml 中,注册 Spring 所提供的过滤器:
在注册之前,我们来看一下,我们需要对什么参数进行注册
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<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>
通过上面的设定以后,Tomcat 在启动后,就会将这个 filter ,实例化到 tomcat 的容器中
测试:
这个时候,前端传来中文,后端接收就不会出现乱码了
八、处理方法的返回值
使用@Controller 注解的处理器的处理器方法,其返回值常用的有四种类型:
1、ModelAndView
2、String
3、返回自定义类型对象
4、无返回值 void
咱们根据实际的业务选择不同的返回值。
1、ModelAndView
之前也演示过,相当于进行了 转发
2、String
如果返回的是 String 类型,就会将这个 Spring 类型,走页面解析器,生成访问的后端页面路径
但是,这不意味着,这就是重定向了,其实,走的方法还是转发,只是不能像 ModelAndView 那样,显示的传值
我们还是可以借助 HttpServletRequest,在 req 或者 session 中放值的
3、返回自定义类型对象
这种方式,必须要注意三点
1、要加上 @ResponseBody ,将返回的结果转换为 json 类型
2、前端必须使用 ajax 接受数据
3、必须加上 json 类型的自动转换依赖(jackson)
导入 maven 依赖:
<!--jackson核心-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<!--jackson数据绑定-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
后端:
一定要加上 @ResponseBody 注解,不然不会转换为 json 格式的
@ResponseBody //将返回值,转换成 json 类型
@RequestMapping("/json")
public User getUserJson() {
User user = new User();
user.setId(123);
user.setName("DIO");
user.setAge(321);
return user;
}
测试:
我们直接去访问 /user/json
接口
发现,成功将后端接口的数据,以 json 格式,取出来了
这种方式,是前后端项目所惯用的开发方式,各位读者要牢记
当然,还有返回 List Set 数据类型的,其实,就是返回不同类型的 json 格式,这里就不细讲了
4、void
表示我们的返回方式,不希望交给 SpringMVC 来处理,而是使用 JavaEE 的原生处理方式
使用 void ,我们的参数中,就必须用到 HttpServletRequest,和 HttpServletResponse 了
@RequestMapping("/haha")
public void getHaha(HttpServletRequest req, HttpServletResponse resp) {
/**
* 下面,就是 原生 javaEE 的开发方式
* 可以搞转发,重定向等
*/
}
九、页面导航的方式
页面导航分为两种:
1、转发
2、重定向
springMVC有以下两种方式实现页面的转发或重定向:
1、返回字符串
2、使用ModelAndView
在SpringMVC中两种导航进行页面导航的时候使用不同的前缀指定转发还是重定向
前缀:
转发: forward:url 默认
重定向: redirect:url
SpringMVC 默认使用转发
注意: 当使用前缀以后,视图解析器就失效了,必须手动写上跳转的页面的全路径!!
1、ModelAndView 实现
重定向
转发是默认的,我们就来讲一下怎么重定向
其实,只要在 setViewName 的时候,加上 redirect: 前缀,就 OK 了
但是别忘了,当使用前缀以后,视图解析器就失效了,必须手动写上跳转的页面的全路径!!
@RequestMapping("/pass5")
public ModelAndView getParam4(@RequestParam("d") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date ) {
ModelAndView mv = new ModelAndView();
System.out.println("获取的日期为:"+date);
/**
* 注意,一旦使用了页面跳转的前缀,视图解析器就会失效
* 必须写上页面的全路径
*/
mv.setViewName("redirect:/jsp/success.jsp");
return mv;
}
测试:
点击提交,url 发生改变
说明,是服务器发送了一个新的访问路径,让客户端去访问这个新的访问路径,即发生了重定向
如果为 ModelAndView 设定参数,并且还执行重定向,那么,会把参数,以url的形式,贴到 url 后面
@RequestMapping("/pass5")
public ModelAndView getParam4(@RequestParam("d") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date ) {
ModelAndView mv = new ModelAndView();
System.out.println("获取的日期为:"+date);
/**
* 注意,一旦使用了页面跳转的前缀,视图解析器就会失效
* 必须写上页面的全路径
*/
mv.addObject("name","haha");
mv.addObject("age","123");
mv.setViewName("redirect:/jsp/success.jsp");
return mv;
}
转发
SpringMVC 默认就是使用转发,为什么我这里还要写转发?
这是因为,我们可能会有转发到另一个后端接口的需求,如果使用默认的转发方式,是铁定会走视图解析器的,这样,我们就没法转发到其它接口了
如果想要访问其他接口,还是必须要用到 forward:
前缀,使得视图解析器失效,从而,才能访问其他后端接口
2、String 实现
加上前缀即可,这里不再赘述
十、异常处理
异常处理,使用到的注解,是 ExceptionHandler
其 Value 值,是要被捕获的、异常的类对象
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* Exceptions handled by the annotated method. If empty, will default to any
* exceptions listed in the method argument list.
*/
Class<? extends Throwable>[] value() default {};
}
在介绍异常处理之前,我们先去自定义异常
1、控制器内异常处理
我们可以在单个 Controller 中,写一个异常处理方法
@ExceptionHandler({UserException.class, UserLoginException.class, UserAccessException.class})
public ModelAndView handelError(Exception e) {
ModelAndView mv = new ModelAndView();
if (e instanceof UserLoginException) {
mv.addObject("msg",e.getMessage());
//登录失败,返回跳转页面
mv.setViewName("login");
} else if (e instanceof UserAccessException) {
mv.addObject("msg",e.getMessage());
//访问失败,返回首页
mv.setViewName("index");
} else {
mv.addObject("msg",e.getMessage());
//剩余的异常,返回到一个统一的异常界面
mv.setViewName("err");
}
return mv;
}
这样,如果控制器中哪部分抛出异常,都会被我们的异常处理方法捕获,并依据对应的异常,做出处理
我们对登录接口,做一个登录判断:
如果 service 层返回一个 false,就抛出登录失败异常
@PostMapping("/login")
public ModelAndView login(User user) throws UserLoginException {
boolean res = userService.login(user);
if (!res) {
//登录失败,抛出异常
throw new UserLoginException();
}
ModelAndView mv = new ModelAndView();
mv.setViewName("success");
return mv;
}
2、全局异常处理
在每个控制器中,都去写一个异常处理方法,比较麻烦
我们可以将这个异常处理方法的等级提高,用它来处理所有接口的异常
使用注解@ControllerAdvice,就是“控制器增强”,是给控制器对象增强功能的。使用 @ControllerAdvice 修饰的类中可以使用@ExceptionHandler。
当使用@RequestMapping 注解修饰的方法抛出异常时,会执行@ControllerAdvice 修饰的类中的异常 处理方法。
@ControllerAdvice 注解所在的类需要进行包扫描,否则无法创建对象。
<context:component-scan base-package="com.kkb.exceptions"/>
@ControllerAdvice
public class ExceptionHandlerController {
@ExceptionHandler({UserException.class, UserLoginException.class, UserAccessException.class})
public ModelAndView handelUserError(Exception e) {
ModelAndView mv = new ModelAndView();
if (e instanceof UserLoginException) {
mv.addObject("msg",e.getMessage());
//登录失败,重定向到登录页面
mv.setViewName("redirect:/jsp/login.jsp");
} else if (e instanceof UserAccessException) {
mv.addObject("msg",e.getMessage());
//访问失败,返回首页
mv.setViewName("index");
} else {
mv.addObject("msg",e.getMessage());
//剩余的异常,返回到一个统一的异常界面
mv.setViewName("err");
}
return mv;
}
/**
* 用来处理所有异常
* (即没有被捕获的所有异常)
* @return
*/
@ExceptionHandler({Exception.class})
public String handleAllError(Exception e) {
//...
return "error";
}
}
十一、拦截器
拦截器介绍
SpringMVC 中的 拦截器( Interceptor)是非常重要的,它的主要作用是拦截指定的用户请求,并进行相应的预处理与后处理。 通过使用拦截器,我们可以对用户访问进行限制,比如,已经登录的用户,可以去访问某些接口,而未登录的用户,就无法去访问这些后端接口。
拦截的时间点在“处理器映射器HandlerMapping根据用户提交的请求映射出了所要执行的处理器类,并且也找到了要执行该处理器类的处理器适配器,在处理器适配器HandlerAdaptor执行处理器之前”。
在处理器映射器映射出所要执行的处理器类时,已经将拦截器与处理器组合为了一个处理器执行链 HandlerExecutionChain,并返回给了前端控制器。
自定义拦截器,需要实现 HandlerInterceptor 接口。而该接口中含有三个方法:
preHandle(request,response, Object handler):
该方法在处理器方法执行之前执行。其返回值为boolean,若为true,则紧接着会执行处理器方法,且会将afterCompletion()方法放入到一 个专门的方法栈中等待执行。
postHandle(request,response, Object handler,modelAndView):
该方法在处理器方法执行之后执行。处理器方法若最终未被执行,则该方法不会执行。由于该方法是在处理器方法执行完后执行,且该方法参数中 包含 ModelAndView,所以该方法可以修改处理器方法的处理结果 数据,且可以修改跳转方向。
afterCompletion(request,response, Object handler, Exception ex):
当 preHandle()方法返回true时,会将该方法放到专门的方法栈中,等到对请求进行响应的所工作完成之后才执行该方法。即该方法是在前端 控制器渲染(数据填充)了响应页面之后执行的,此时对 ModelAndView再操作也对响应无济于事。
afterCompletion最后执行的方法,清除资源,例如在Controller方法中加入数据
拦截器的使用实例
1、编写拦截器
需要实现 HandleInterceptor 接口
public class LoginInterceptor implements HandlerInterceptor {
/**
* 在被拦截的接口之前执行
* 使用场景:登录验证
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("----preHandle");
return true;
}
/**
* 在接口执行结束之后,执行,可以修改接口的 ModelAndView 返回值
* 使用场景:日记记录,记录登录的时间 ip
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
System.out.println("----postHandle");
}
/**
* 使用场景:全局资源的操作
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
System.out.println("----afterCompletion");
}
}
2、springmvc.xml 配置文件中,注册拦截器
<!--配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--拦截的路径-->
<!--设置,只对登录进行拦截-->
<mvc:mapping path="/user/login"/>
<bean id="loginInterceptor" class="top.faroz.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
<!--如果有多个拦截器的时候: preHandle:按照配置前后顺序执行 postHandle:按照配置前后逆序执行 afterCompletion:按照配置前后逆序执行-->
3、测试
访问登录界面,执行登录操作
控制台输出如下
可以很明显的看到,接口是在 preHandle 之后执行的
十二、文章上传与下载
spring 是有带文件上传的类CommonsMultipartResolver
的,只是没有默认装载
我们来执行以下文件上传与下载的流程
1、文件上传
快速开始
1、导入 Maven 依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
2、装载 Spring 中带的文件上传类
id 一定要写 multipartResolver ,不能写其他的,这是因为,引用它的类,对它的指向,使用的是 CommonsMultipartResolver 的接口,即 multipartResolver,所以,不能写其他的。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>
3、编写前端
这里要注意两点:
- 表单的上传方式,必须为 post
- 必须要加上 enctype=“multipart/form-data”
<form action="/file/upload" method="post" enctype="multipart/form-data">
请选择上传的文件:<input type="file" name="file"> <br/>
<button type="submit">上传</button>
</form>
4、在静态资源目录下,新建图片的存储位置
注意: 在新建这个文件夹后,一定要在文件夹里放一个文件,不然,编译的时候,不会将这个空文件夹包含进去的!
5、编写后端
@RequestMapping("/upload")
public String upload(@RequestParam("myFile") MultipartFile file, HttpServletRequest req) {
System.out.println("进入文件上传接口");
/**
* 获取原始文件名
*/
String originalFilename = file.getOriginalFilename();
/**
* 要为上传的文件,进行重命名
* 将文件的后缀保留,文件名,改为 UUID
*/
String newName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
System.out.println("新生成的文件名为:"+newName);
/**
* 获取 web 资源目录
* 我们要将图片,存储到 /uploadFile 上去
*/
String path = req.getServletContext().getRealPath("/uploadFile")+"/"+newName;
System.out.println("获取的新路径为:"+path);
try {
file.transferTo(new File(path));
} catch (IOException e) {
e.printStackTrace();
}
return "uploadSuccess";
}
/**
* SpringMVC 不建议我们直接 访问 jsp 文件
* 所以我们可以专门写一个后端接口
* 跳转到上传页面
* @return
*/
@RequestMapping("/toupload")
public String toUpload() {
return "upload";
}
6、测试
我们通过访问 /file/toupload
接口,先跳转到文件上传页面
然后选择文件,点击上传
成功以后,在 target 目录下,可以看到被上传的文件
指定文件大小和类型
1、文件大小指定
CommonsMultipartResolver
中,有对文件上传的信息指定,我们可以在 springmvc 的配置文件中,去进行配置
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--文件上传的大小限制 以字节B为单位 5m :1024*1024*5 1M=1024KB 1KB=1024B-->
<property name="maxUploadSize" value="5242880"/>
<property name="defaultEncoding" value="utf-8"/>
</bean>
2、使用拦截器
使用拦截器,我们可以对文件,做更加细化的拦截(比如文件后缀的判断,文件的合法性判断等)
1)、编写拦截器
public class FileInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean flag = true;
/**
* 只有当请求类型,是和文件相关的时候
* 才会去对请求进行处理
*/
if (request instanceof MultipartHttpServletRequest) {
MultipartHttpServletRequest req = (MultipartHttpServletRequest) request;
Map<String, MultipartFile> fileMap = req.getFileMap();
//遍历文件 key 集合,获取单个文件,对单个文件进行处理
for (String key : fileMap.keySet()) {
MultipartFile file = fileMap.get(key);
String originalFilename = file.getOriginalFilename();
//获取问价后缀
String hz = originalFilename.substring(originalFilename.lastIndexOf("."));
System.out.println("获取的问价后缀为:"+hz);
if (!hz.toLowerCase().equals(".jpg") && !hz.toLowerCase().equals(".jpeg")) {
flag=false;
request.setAttribute("msg","文件后缀不合法!");
request.getRequestDispatcher("上传页面").forward(request,response);
}
}
}
return flag;
}
}
2)、在 springmvc.xml
中,配置拦截器
<mvc:interceptors>
<!--登录拦截器-->
<mvc:interceptor>
<!--拦截的路径-->
<!--设置,只对登录进行拦截-->
<mvc:mapping path="/user/login"/>
<bean id="loginInterceptor" class="top.faroz.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
<!--文件上传拦截器-->
<mvc:interceptor>
<mvc:mapping path="/file/**"/>
<bean id="fileInterceptor" class="top.faroz.interceptor.FileInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
3、执行测试
选择一个 .txt 文件上传
跳转到失败页面
2、文件下载
1)、编写前端
<form action="/file/download" method="post">
<p>下载 5f418527-0b29-4f29-8691-989b5bdad13d.jpg 文件</p>
<button type="submit">下载</button>
</form>
2)、编写后端接口
要读懂下面这段代码,需要对 http 协议,有所了解
下面这段是下载的死代码,要用的时候,直接复制过去,就好了
@RequestMapping("/download")
public ResponseEntity<byte[]> download(HttpServletRequest request) throws IOException {
//获取要下载的文件的路径
String path = request.getServletContext().getRealPath("/uploadFile") + "/" + "5f418527-0b29-4f29-8691-989b5bdad13d.jpg";
//创建响应头的信息
HttpHeaders headers = new HttpHeaders();
//标记以流的方式作出响应
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
//以附件的形式响应给用户
headers.setContentDispositionFormData("attachment",
URLEncoder.encode("5f418527-0b29-4f29-8691-989b5bdad13d.jpg","utf-8"));
//根据路径,创建文件对象
File file = new File(path);
//创建返回体
ResponseEntity<byte[]> resp = new ResponseEntity<>(FileUtils.readFileToByteArray(file),/*返回的文件信息*/
headers, /*响应头*/
HttpStatus.CREATED); /*响应状态*/
return resp;
}
3)、进行测试
进入到下载页面
点击下载,弹出下载框
十三、Restful 风格
这里先讲一下 Restful 的概念和一些既定原则,实例会在后面统一讲解
1、Restful 介绍
原来的 URL 的写法:
http://localhost:8080/getExpress.do?id=1
http://localhost:8080/saveExpress.do
http://localhost:8080/updateExpress.do
http://localhost:8080/deleteExpress.do?id=1
改用 Restful 风格后:
GET /expresses # 查询所有的快递信息列表
GET /express/1006 # 查询一个快递信息
POST /express # 此案件一个快递信息
PUT /express/1006 # 更新一个快递信息(全部更新)
PATCH /express/1006 #更新一个快递信息(部分更新)
DELETE /express/1006 #删除一个快递信息
2、Restful API 设计原则
动词通常就是五种 HTTP 方法,对应 CRUD 操作。
GET:读取(Read)
POST:新建(Create)
PUT:更新(Update)
PATCH:更新(Update),通常是部分更新
DELETE:删除(Delete)
PS:
1、根据 HTTP 规范,动词一律大写。
2、一些代理只支持POST和GET方法, 为了使用这些有限方法支持RESTful API,需要一种办法覆盖http原来的方法。使用订制的HTTP头 X- HTTP-Method-Override 来覆盖POST 方法.
一下方式是不推荐的:
以下这些URL都是不推荐的,因为带上了动词,不是推荐写法。
/getAllExpresses
/getExpress
/createExpress
/deleteAllExpress
要避免出现多级 URL:
如果资源中有多级分类,也不建议写出多级的URL。
例如要获取球队中某个队员,有人可能这么写: GET /team/1001/player/1005。这种写法的语义不够明确,所以推荐使用查询字符串做后缀,改写为 GET /team/1001?player=1005。
再例如查询所有的还未取出的快递,你该如何编写呢?
GET /expresses/statu # 不推荐
GET /expresses?statu=false # 推荐
3、HTTP 状态码
五类状态码分别如下:
1xx:相关信息
2xx:操作成功
3xx:重定向
4xx:客户端错误
5xx:服务器错误
PS:API 不需要1xx状态码,所以这个类别直接忽略。
1)、状态码 2xx
GET: 200 OK 表示一切正常
POST: 201 Created 表示新的资源已经成功创建 PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content 表示资源已经成功删除
2)、状态码 3xx
我们只需要关注一下304状态码就可以了
304 : Not Modified 客户端使用缓存数据
3)、状态码 4xx
4xx
状态码表示客户端错误。
400 Bad Request:服务器不理解客户端的请求,未做任何处理。
401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证。
403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。
404 Not Found:所请求的资源不存在,或不可用。
405 Method Not Allowed:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。
410 Gone:所请求的资源已从这个地址转移,不再可用。
415 Unsupported Media Type:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是 客户端要求返回 XML 格式。
422 Unprocessable Entity :客户端上传的附件无法处理,导致请求失败。
429 Too Many Requests:客户端的请求次数超过限额。
4)、状态码 5xx
5xx
状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。
500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。
503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。
4、服务器响应
服务器返回的信息一般不推荐纯文本,而是建议大家选择JSON 对象,因为这样才能返回标准的结构化数据。
所以,服务器回应的 HTTP 头的 Content-Type 属性要设为 application/json 。客户端请求时,也要 明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的 ACCEPT 属性也要设成application/json 。 当发生错误的时候,除了返回状态码之外,也要返回错误信息。所以我们可以自己封装要返回的信息。