SpringMvc -- 开发手册

本片博客记录SpringMvc带给了我们什么?,图解运行流程,以及开发中,常用的注解

SpringMvc基于servlet设计,用于处理用户的请求,基于方法级别拦截,参数通过入参传递,处理器设计为单例模式,和Spring无缝整合, 分离了 控制器,模型对象,让它们更容易被定制

主要组成部分:

  • DispatcherServlet:

前端控制器:所有的请求,都会首先被他拦截到,统一给请求分发处理的Handler

  • HandlerMapping

处理器映射器: 识别控制器中的注解,目的是找到具体的和Url对应的处理方法

  • HanderAdapter

处理器适配器,实例化控制器Controller,调用具体的方法,处理用户发来的请求

  • Controller

控制器, 用来处理用户的请求,返回ModelAndView给前端控制器

  • ViewResolver

视图解析器: 解析视图(ModeAndView), 把ModelAndView里面的逻辑视图编程一个正真的View对象,并把Model从ModelAndView中取出来

宏观上看,DispatcherServilet是整个web应用的控制器, 微观上,controller是单个http请求处理的控制器

@RequestMapping()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

String name() default "";

@AliasFor("path")
String[] value() default {};

@AliasFor("value")
String[] path() default {};

// 映射请求方式(get post put delete)
RequestMethod[] method() default {};
//
String[] params() default {};
// 映射请求参数
String[] headers() default {};

String[] consumes() default {};

String[] produces() default {};

}
  • 支持标注在类上和方法上
    • 最常用标注在方法上,用于映射处理前端发送过来的URL
    • 如果我们同时把它俩加载类和方法上, 那么请求路径就是两者的路径使用 / 分隔开
1
2
3
4
5
6
 //         映射url                   映射方法为POST                     指定必须包含username  并且age!=10
@RequestMapping(value = "sayHello", method = RequestMethod.POST, params = {"username", "age!=10"})
public String sayHello() {
System.out.println("来到了sayHello");
return "success";
}

@RequestMapping 路径支持通配符

  • ? 一个字符
  • *任意字符
  • ** 多层路径
1
2
3
4
5
6
7
8
9
/**
* 测试 ant风格的占位符
* @return
*/
@RequestMapping("/textAntPath/*/text")
public String textAntPath() {
System.out.println("v");
return "success";
}

@PathVariable 与 @RequestParam()

  • @PathVariable 映射URL绑定的占位符(一般在我们添加在方法上的注解上通过{XXX} 占位 )
  • @RequestParam取出来的是请求参数,发出的url格式如下:
1
2
3
4
5
6

/**
* 1. 假如:@RequestParam("password") url中并不存在的话, 报错400
* 1.1, 设置@RequestParam的required=false
* 2. 假如只有第一个:@RequestParam("username")String username,String password ( password同样会被装配进去值)
*

SpringMvc的REST风格

浏览器的form表单,只是支持GET POST请求,而Delete put的方法,是不支持的, 我们使用rest风格的提交表单时, 会被SpringMvc的 HiddenHttpMethodFilter拦截下来,过滤处理成标准的http方法,从而支持了 delete put

修改配置文件 web.xml

1
2
3
4
5
6
7
8
9
<!--配置  HiddenHttpMethodFilter   开启RestFul风格  -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<br>
<a href="text01/1">测试get请求</a>
<br>
<form action="text02" method="post">
<input type="text" name="id">
<input type="submit" value="点击发送post请求">
</form>
<br>
<form action="text03/8" method="post">
<input type="hidden" name="_method" value="PUT">
<input type="submit" value="点击发送put请求">
</form>
<br>
<form action="text04/7" method="post">

<%-- 我们需要添加隐藏域, name="_method" 不能不写,Spring会拿到它的信息,从而将post转换成 delete --%>
<input type="hidden" name="_method" value="delete">
<input type="submit" value="点击发送delete请求">
</form>

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequestMapping(value="text01/{id}",method = RequestMethod.GET)
public String text01(@PathVariable("id")Integer id){
System.out.println("获取id=="+id+"的信息");
return "success";
}

@RequestMapping(value = "text02",method = RequestMethod.POST)
public String text02(@RequestParam Integer id){
System.out.println("新增id=="+id+"的信息");
return "success";
}

@RequestMapping(value = "text03/{id}",method = RequestMethod.PUT)
public String text03(@PathVariable("id") Integer id){
System.out.println("修改id=="+id+"的信息");
return "success";
}

@RequestMapping(value = "text04/{id}",method = RequestMethod.DELETE)
public String text04(@PathVariable("id") Integer id){
System.out.println("删除id=="+id+"的信息");
return "success";
}

@RequestHeader()

映射请求头的信息到入参位置,不同浏览器的请求头的细节可能是不同的

1
2
3
4
@RequestMapping("/text")
public void text(@RequestHeader(value="Accept-Langnage") String s1){
//
}

@CookieValue()

很常用,在微服务的安全验证模块,安全中心会给满足条件的用户办法token, 存放到浏览器的cookie里面, 用户再次访问就会携带cookie,我们通过这个注解取出cookie的值,进行安全验证

1
2
3
4
5
@RequestMapping("textCookieValue")
public String textCookieValue(@CookieValue("JSESSIONID")String jessionId){
System.out.println("cookieValue=="+jessionId);
return "success";
}

POJO绑定请求参数

很多时候前端提交的表单对应着我们将要持久化对象,那么使用pojo绑定参数无疑是一件超赞的事

springMvc支持 按照请求参数名和pojo属性进行自定匹配,自动为该对象填充属性,支持级联属性

JSP

1
2
3
4
5
6
7
8
9
10

<form action="textPojo">
用户名: <input type="text" name="username">
密码: <input type="text" name="password">
邮箱: <input type="text" name="email">
<%-- 级联属性 --%>
省: <input type="text" name="adress.province">
市: <input type="text" name="adress.city">
<input type="submit" value="提交">
</form>

pojo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Person {
String username;
String password;
String email;
Adress adress;

}

@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Adress {

String province;
String city;

}

控制器

springmvc 会自定为我们的入参绑定上前端表单上的数据

1
2
3
4
5
@RequestMapping("textPojo")
public String textPojo(Person person){
System.out.println(person);
return "success";
}

支持Servlet原生API

1
2
3
4
@RequestMapping("textServletAPi")
public String textServletApi(HttpServletRequest request, HttpServletResponse response) {
return "success";
}

处理模型数据

ModelAndView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* springMvc会把ModelAndView的Model放入request域对象中
* @return
*/
@RequestMapping("textModelAndView")
public ModelAndView textModelAndView(){
String viewName="success";
ModelAndView modelAndView = new ModelAndView();
// 设置视图名
modelAndView.setViewName(viewName);
// 添加模型数据
modelAndView.addObject("日期",new Date());
return modelAndView;
}

Map

说白了,就是方法的入参位置可以添加一个 Map或者Model类型的参数,mvc会把隐藏的模型引用传递给这个入参, 从而是开发者可以通过这个入参访问模型中的所有数据,同是可以添加新数据

1
2
3
4
5
6
@RequestMapping("textMap")
public String textMap(Map<String, Object> map) {
map.put("names", Arrays.asList("zhangsan", "lisi"));
return "success";

}

@SessionAttributes

如果我们希望多个请求之间共享某个模型属性数据,那么我们使用@SeesionAttribute,她会把我们存放到作用域中的信息备份到Session中

1
2
3
4
5
6
7
8
9
10
11
// 她会把我们存放到 作用域中的数据,备份到Session
@SessionAttributes("user",types = String.class)
public class HelloController {

@RequestMapping("textSessionAttribute")
public String textSeesionAttribute(Map<String,Object> map){
Adress adress = new Adress("山东","XXX");
Person zhangsan = new Person("zhangsan", "234234", "4646@qq.com", adress);
map.put("user",zhangsan); // 放入请求域
return "success";
}

在前几个低版本的SpringMvc中版本中,如果本类标记上了@SessionAttribute但是却没有标记@ModelAttribute的方法,服务器报错500;

@ModelAttribute

这个注解可以帮我解决这样一种情况, 更新操作,很多时候,我们只是针对表中的其中几个字段进行更新, 另外一些字段不需要更新(比如插入时间),那怎么做? 如果自己new对象的话,前端的数据绑定到我们new的对象上,插入时间就会空着,这时已更新,原来的插入时间就会被覆盖,于是我们不new ,通常使用@ModelAttribute注解标注方法上,先查询数据库,得到有插入时间字段的对象

控制器(标注在方法上):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 添加上这个注解的方法,会被SpringMvc拦截, 所有的方法在调用前都会先执行这个方法,把查询出来的信息放到作用域
// 这样SpringMvc把前端传递过来的信息,赋值给 作用域里面user -- 狸猫换太子,
// 用户得到的就是被 狸猫
@ModelAttribute
public void textModelAttribute(
@RequestParam(value = "id",required = false) Integer id,
Map<String,Object> map){

// 判断,如果id不为空, 就表示用户想修改信息
// 于是,我们的任务就是 把查数据库,把用户的信息查出来,通过Map放到作用域里面, 谁用,谁取
if(null!=id){
//模拟查库
System.out.println("查询数据库,获取user信息");
Person person = new Person("1", "lisi", "999", "8989@qq.com", new Adress("山西", "北京"));

// 注意点: 在ModelAttribute修饰的方法中,放入作用域的 key 为 类名首字母小写
map.put("person",person);
}

}

@RequestMapping("textModelAttribute2")
public String textModelAttribute2(Person person) // 这里的名字也要和作用域里面的名字相同
{
System.out.println("修改=="+person);
return "success";
}

控制器: (标注在参数上)

1
2
3
4
5
6
7
8
9
10
11
12
**
* 1. 这里的名字随便, 但是上面那个方法的的key 尽量就是类名首字母小写
*
* 2. 使用 添加在入参上 @ModelAttribute(value="XXX") , 则名字随便
*
*@ModelAttribute
* @param user
* @return
*/
@RequestMapping("textModelAttribute2")
public String textModelAttribute2(@ModelAttribute(value = "abc")Person user)
{

自定义视图:

第一步: 实现View接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HelloView implements View {
/**
* 返回内容类型
* @return
*/
@Override
public String getContentType() {
return "text/html";
}

/**
* 渲染视图
* @param map
* @param httpServletRequest
* @param httpServletResponse
* @throws Exception
*/
@Override
public void render(Map<String, ?> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
httpServletResponse.getWriter().print("helloViewTime"+new Date());
}
}

第二步: 配置视图解析器

1
2
3
4
5
6
<!-- 配置视图解析器 BeanNameViewResolver 解析器: 使用视图的名字解析视图 (所以我们需要把我们的视图添加进IOC)-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<!-- 到现在为止,我们就有了两个视图解析器,需要指定优先顺序-->
<!-- 常用的放在后边,我们的放前边 order越小,优先级越高-->
<property name="order" value="200"></property>
</bean>

测试

1
2
3
4
5
6
  @RequestMapping("textView")
public String textView(){
System.out.println("textView");
// 自定义的视图类名小写
return "helloView";
}

重定向

控制器返回的字符串被当作逻辑视图名称处理

如果返回的字符串中带有forward:或者redirect: 会被SpringMvc当作指示符特殊处理,后面的字符串当作url路径

1
2
3
4
5
@RequestMapping("textRedirect")
public String textRedirect(){
System.out.println("测试重定向!!!");
return "redirect:/index.jsp";
}

数据校验:

当我们添加<mvc:annotation-driven/>配置,SpringMvc会自动为我们做如下处理

  • RequestMappingHandlerMapping
  • RequestMappingHanderAdapter
  • ExceptionHandlerExceptionResolver 这三个bean
  • 支持使用ConversionService对表单参数进行类型转换
  • 支持使用@NumberFormatannptation @DateTimeFormat 注解完成数据类型格式化
  • 支持使用@Valid 注解对JavaBean 实例惊醒jsr303 验证
  • 支持使用@RequestBody和@ResponseBady // 处理ajax

常使用如下连个注解对bean进行校验

1
2
3
4
@DateTimeFormat(pattern="yyyy-MM-dd")
Date birth;
@NumberFormat(pattern = "#,###,###.#") // #表示数数字
Float salary;

返回Json

@ResponseBody

拦截器

  • 实现自己的拦截器实现HandlerInterceptor接口

重写他的三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
preHandle()
作用: 对用户请求request进行处理
调用时机: 在处理器处理请求之前被调用
返回:
true: 如果还需要调用其他拦截器或者是业务处理器
f 何组件处理请求


postHandle()
作用: 对用户请求request进行处理
调用时机: 业务处理器处理完请求之后,DispatcherServlet向客户端相应之前


aferCompletion()

作用: 进行资源清理工作
调用时机: 在DispatcherServlet向客户端响应数据之后调用

配置文件:

1
2
3
4
<!-- 配置拦截器 -->
<mvc:interceptors>
<bean class="com.changwu.interceptor.FirstInterceptor"/>
</mvc:interceptors>
  • 配置拦截指定请求路径的拦截器
1
2
3
4
5
6
7
8
9
10
<!-- 配置拦截器 -->
<mvc:interceptors>
<bean class="com.changwu.interceptor.FirstInterceptor"/>

// 配置 专门针对某个求情的拦截器
<mvc:interceptor>
<mvc:mapping path="/textView"/>
<bean class="com.changwu.interceptor.SecondInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
  • 多个拦截器的执行顺序
  1. 第一个拦截器的firstInterceptor返回flase,其他拦截器不执行,目标方法不执行
  2. 第一个拦截器的firstInterceptor返回true,第二个拦截器的firstInterceptor返回false,目标方法不执行,但是第一个拦截器的afterCompletion方法会执行,回收资源

异常处理–ExceptionHandlerExceptionResolver

SpringMvc 使用HandlerExceptionResolver处理异常

ExceptionHandlerExceptionResolver
主要处理Handler中使用 @ExceptionHandler注解定义的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. 控制器: 出现异常
@RequestMapping("textExceptionHandlerExceptionResolver")
public String textExceptionHandlerExceptionResolver(@RequestParam("i")int i){
System.out.println("result=="+10/i);
return "success";
}

2. 标注有@ExceptionHandler注解的方法 会处理异常
/**
* 捕获数学异常
* 把异常带到页面"ModelAndView
*/
@ExceptionHandler(value=ArithmeticException.class)
public ModelAndView handleArithmeticException(Exception e){
// 通过ModelAndView 把错误信息带到页面
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("errorMsg",e);
System.out.println("异常信息:"+e);
return modelAndView;
}

@ExceptionHandler定义的方法优先级问题,例如发生的是 空指针异常,但是声明异常是 运行时异常和异常, 这时就会报 离空指针异常比较近的 运行时异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 假如出现了数学异常, 它会优先使用下面的第一个异常

@RequestMapping("textExceptionHandlerExceptionResolver")
public String textExceptionHandlerExceptionResolver(@RequestParam("i")int i){
System.out.println("result=="+10/i);
return "success";
}

/**
* 捕获数学异常
* 把异常带到页面"ModelAndView
*
*/

@ExceptionHandler(value=ArithmeticException.class)
public ModelAndView handleArithmeticException(Exception e){
// 通过ModelAndView 把错误信息带到页面
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("errorMsg",e);
System.out.println("异常信息:"+e);
return modelAndView;
}

@ExceptionHandler(value=RuntimeException.class)
public ModelAndView handleRuntimeException(Exception e){
// 通过ModelAndView 把错误信息带到页面
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("errorMsg",e);
System.out.println("{异常信息}:"+e);
return modelAndView;
}

ExceptionHandlerExceptionResolver 内部找不到@ExceptionHandler 注解的话,就会找 @ControllerAdvice 中的 @ExceptionHandler注解方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Author: Changwu
* @Date: 2019/4/14 19:04
*/
@ControllerAdvice
public class HandException {

@ExceptionHandler(value=ArithmeticException.class)
public ModelAndView handleArithmeticException(Exception e){
// 通过ModelAndView 把错误信息带到页面
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("errorMsg",e);
System.out.println("异常信息:"+e);
return modelAndView;
}
}

异常处理– ResponseStatusExceptionResolver

通过@ResponseStatus(value="异常信息",code="错误状态码")注解,定制返回给前端的异常信息以及状态码

异常处理 – SimpleMappingExceptionResolver

xml文件中进行配置, 指定出现什么异常,跳往哪个页面

1
2
3
4
5
6
7
8
9
<!-- 配置SimpleMappingExceptionResolver 来映射异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!-- 统一匹配的异常的全类名 跳往的异常页面 -->
<prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
</props>
</property>
</bean>
  • 异常信息会自动存储在 作用域 通过${requestScope.exception} 可以取出来