概述

Spring MVC 的 正式名称叫做 Spring Web MVC ,SpringMVC 本质上是一个 Servlet 接口的一个实现,需要在 Servlet 配置文件,一般是 web.xml 中配置。它的核心是 DispatcherServlet,它一方面实现了 Servlet 接口,一方面依赖 Spring 进行 Bean 的寻找,处理请求,处理错误等等。

本文没有什么干货,以转载为主,主要是一些用法备忘。用的时候再查文档或者找一些最佳实践。

使用

免XML配置

本文都是直接使用 Spring-Boot 中的 SpringMVC 进行讲解

我这里是讲一下 Servlet 3.0+之后的 免XML配置的方法, 这也是 Spring-boot中为什么不用 XML 就可以配置的原因。
在 Servlet3.0+ 容器中(Tomcat7.0+),会自动寻找实现了 WebApplicationInitializer 接口的类,对容器的初始化配置。

最简单的配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyWebApplicationInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext servletCxt) {

// Load Spring web application configuration (在这里加载Spring)
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();

// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}

当然,如果是实际使用,请使用 Spring-Boot ,放弃传统的复杂 XML 配置 SSM 吧。

SpringMVC几种使用方式

常用的到一些注解:
类级:
@RestController:写在类上,自动给每个添加 @ResponseBody
@SessionAttributes:写在类上,规定一个共用的 Seesion 对象名

方法级:
@ModelAttribute: 如果写在方法上面,会在每一个 Controller 执行前先执行,并且作为一个公共对象给各个方法传入。
@GetMapping: 处理 get 请求,还有 Post Put Delete ,可以在这里定义 解析 URI 的参数
@ResponseBody返回值直接输出到返回值的Body中,如果返回值是类,会被 Spring 寻找合适的 Converter 转换成文本,一般会转成 Json

参数级:
@RequestParam: 解析 url 问号后面的参数,如果是用类接收,Spring会寻找合适的 Converter 转成你的目标类,否则是文本。
@SessionAttribute:把 Seesion 里面的对象传入
@CookieValue : 把 Cookies 里面的对象传入
@PathVariable():解析 path 参数 (uri)
@RequestBody: 输入的 RequestBody,如果是用类接收,Spring会寻找合适的 Converter 转成你的目标类,否则是文本。
@RequestAttribute :从已存在的对象(由ModelAttribute创建、拦截器插入的)获取对象
@ModelAttribute:
如果把 ModelAttribute 写在参数上面,使用时它会 首先查询 @ModelAttribute域(方法上面的) 有无绑定的该对象,若没有则查询@SessionAttributes 域上是否绑定了该对象,若没有则将URI中或者从URL参数的值按对应的名称绑定到该对象的各属性上。

RESTFul

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
@RestController
public class CategoryController {
@Autowired CategoryDAO categoryDAO;

@GetMapping("/category")
public List<Category> listCategory(@RequestParam(value = "start", defaultValue = "0") int start,
 @RequestParam(value = "size", defaultValue = "5") int size){
return new ArrayList<Category>();
}

@GetMapping("/category/{id}")
public Category getCategory(@PathVariable("id") int id) {
return new Category();
}
@PutMapping("/category")
public void addCategory(@RequestBody Category category) throws Exception {

System.out.println("springMVC接受到浏览器以JSON格式提交的数据:"+category);
}
@PostMapping("/category/{id}")
public String updateCategory(@ModelAttribute Category c) throws Exception {

return "done";
}

@DeleteMapping("/category/{id}")
public String deleteCategory(@ModelAttribute Category c) throws Exception {

return "done";
}
}

传统模版渲染的写法

Conroller控制的数据其实都是 Model,最后都会转成 ModelAndView ,接着 Spring 会找到 Template 文件把 model 插进去,这里展示几种不同的写法

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
@Controller
public class IndexController {
// 为每一个 Controller 都执行一次这个,创建一个公共对象
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountManager.findAccount(number);
}

@GetMapping("/index1")
public String index1(Model model ) { // 这里也可以是 ModelMap ,差不多
model.addAttribute("result", "后台返回index1");
return "result"; // 根据配置找到 /template/result.xxx 进行渲染
}
@GetMapping("/index2")
public ModelAndView index2() {
ModelAndView mv = new ModelAndView("result");
mv.addObject("result", "后台返回index2");
return mv;
}
// 跳转的写法
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
}

结合自动 Conver 获取 Seesion 和 Cookies 对象

读取 Seesion,Cookies 自动转成对象

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
@Controller
@SessionAttributes("pet") // 这种写法可以在不同页面中传递值
public class EditPetForm {

@GettMapping("redirectTest")
public String redirectTest(Model model){
model.addAttribute("pet",new Pet());
return "redirect:indexView";
}

@GetMapping("indexView")
public String handle(@ModelAttribute Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) { // 如果有错误会传到 errors 里面
// ...
}
System.out.println(pet);
status.setComplete();// 删掉 Seesion
// ...
}
}
// 或者直接这么写,读别的地方写入的 Seesion,如果要控制 Seesion 则 入参 为 HttpSession
@RequestMapping("/demo1")
public String handle(@SessionAttribute("user") User user) { // 括号可选
// ...
}
// 获取 Cookies 值
@GetMapping("/demo2")
public void handle(@CookieValue("JSESSIONID") String cookie) {
//...
}
}

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class SimpleController {
@ExceptionHandler
public String handle(IOException ex) {
// ...
}
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}

}

验证

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
public class PersonForm {

@NotNull
@Size(min=2, max=30)
private String name;

@NotNull
@Min(18)
private Integer age;

public String toString() {
return "Person(Name: " + this.name + ", Age: " + this.age + ")";
}
}
@Controller
public class WebController {
@GetMapping("/")
public String showForm(PersonForm personForm) {
return "form";
}

@PostMapping("/")
public String checkPersonInfo(@Valid PersonForm personForm, BindingResult bindingResult) {

if (bindingResult.hasErrors()) {
return "form";
}

return "redirect:/results";
}
}

自定义Converter

如果传入或者返回的值应该是 String,但是你却用一个对象来对应,这个时候就需要 Covert。默认情况下 ,返回值是 String (ResponseBody),传对象会自动转成 Json。

例如

1
2
3
4
5
6
7
public class Employee {

private long id;
private double salary;

// standard constructors, getters, setters
}

现在你想传入一个值做参数:?data=1,50000.00 可以自动转成上面的对象:

1
2
3
4
5
@GetMapping("/string-to-employee")
public ResponseEntity<Object> getStringToEmployee(
@RequestParam("data") Employee employee) {
return ResponseEntity.ok(employee);
}

在 Springboot 中,你只需要添加这样一个对象,Spring就会自动转换:

1
2
3
4
5
6
7
8
9
10
public class String2EmplyeeConverter implements Converter<String, Employee> {

@Override
public Employee convert(String s) {
String[] data = s.split(",");
return new Employee(
Long.parseLong(data[0]),
Double.parseDouble(data[1]));
}
}

上面主要是备查,其他可查官方文档:

原理

SpringMVC 流程图

MVC其实就是把控制器 Controller 和视图View 分开,并且用 Model 储存实际的数据,在不同部分中流转。

image.png | left | 397x238

  1. 收到HTTP请求后,DispatcherServlet会查询HandlerMapping以调用相应的Controller。
  2. Controller 接受请求并根据使用的GET或POST等方法调用适当的服务方法。服务方法将基于定义的业务逻辑设置Model 数据。最后 Controller 将 View 名称、Model 返回给 DispatcherServlet。
  3. DispatcherServlet把视图名称发给 ViewResolver,返回一个已经写好的视图(JSP文件或者其他模版文件)
  4. DispatcherServlet 把 Model 和 View 结合,返回给用户

拦截器流程

1
2
3
4
5
6
7
public interface HandlerInterceptor {  
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;

  void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}

下面的HandlerAdapter 会调用相应的 Controller 方法

image.png | left | 661x351

image.png | left | 661x351

DispatcherServlet 中的代码节选:

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
33
34
35
//doDispatch方法
//1、处理器拦截器的预处理(正序执行)
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
if (interceptors != null) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
//1.1、失败时触发afterCompletion的调用
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
return;
}
interceptorIndex = i;//1.2、记录当前预处理成功的索引
}
}
//2、处理器适配器调用我们的处理器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//当我们返回null或没有返回逻辑视图名时的默认视图名翻译(详解4.15.5 RequestToViewNameTranslator)
if (mv != null && !mv.hasView()) {
mv.setViewName(getDefaultViewName(request));
}
//3、处理器拦截器的后处理(逆序)
if (interceptors != null) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv);
}
}
//4、视图的渲染
if (mv != null && !mv.wasCleared()) {
render(mv, processedRequest, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
//5、触发整个请求处理完毕回调方法afterCompletion
triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// triggerAfterCompletion方法
private void triggerAfterCompletion(HandlerExecutionChain mappedHandler, int interceptorIndex,
HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception {
// 5、触发整个请求处理完毕回调方法afterCompletion (逆序从1.2中的预处理成功的索引处的拦截器执行)
if (mappedHandler != null) {
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
if (interceptors != null) {
for (int i = interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, mappedHandler.getHandler(), ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
}

参考资料

http://jinnianshilongnian.iteye.com/blog/1670856
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web