tags: [Spring MVC]
categories: [技术笔记]


概述

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. public class MyWebApplicationInitializer implements WebApplicationInitializer {
  2. @Override
  3. public void onStartup(ServletContext servletCxt) {
  4. // Load Spring web application configuration (在这里加载Spring)
  5. AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
  6. // Create and register the DispatcherServlet
  7. DispatcherServlet servlet = new DispatcherServlet(ac);
  8. ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
  9. registration.setLoadOnStartup(1);
  10. registration.addMapping("/app/*");
  11. }
  12. }

当然,如果是实际使用,请使用 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. @RestController
  2. public class CategoryController {
  3. @Autowired CategoryDAO categoryDAO;
  4. @GetMapping("/category")
  5. public List<Category> listCategory(@RequestParam(value = "start", defaultValue = "0") int start,
  6. @RequestParam(value = "size", defaultValue = "5") int size){
  7. return new ArrayList<Category>();
  8. }
  9. @GetMapping("/category/{id}")
  10. public Category getCategory(@PathVariable("id") int id) {
  11. return new Category();
  12. }
  13. @PutMapping("/category")
  14. public void addCategory(@RequestBody Category category) throws Exception {
  15. System.out.println("springMVC接受到浏览器以JSON格式提交的数据:"+category);
  16. }
  17. @PostMapping("/category/{id}")
  18. public String updateCategory(@ModelAttribute Category c) throws Exception {
  19. return "done";
  20. }
  21. @DeleteMapping("/category/{id}")
  22. public String deleteCategory(@ModelAttribute Category c) throws Exception {
  23. return "done";
  24. }
  25. }

传统模版渲染的写法

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

  1. @Controller
  2. public class IndexController {
  3. // 为每一个 Controller 都执行一次这个,创建一个公共对象
  4. @ModelAttribute
  5. public Account addAccount(@RequestParam String number) {
  6. return accountManager.findAccount(number);
  7. }
  8. @GetMapping("/index1")
  9. public String index1(Model model ) { // 这里也可以是 ModelMap ,差不多
  10. model.addAttribute("result", "后台返回index1");
  11. return "result"; // 根据配置找到 /template/result.xxx 进行渲染
  12. }
  13. @GetMapping("/index2")
  14. public ModelAndView index2() {
  15. ModelAndView mv = new ModelAndView("result");
  16. mv.addObject("result", "后台返回index2");
  17. return mv;
  18. }
  19. // 跳转的写法
  20. @PostMapping("/files/{path}")
  21. public String upload(...) {
  22. // ...
  23. return "redirect:files/{path}";
  24. }
  25. }

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

读取 Seesion,Cookies 自动转成对象

  1. @Controller
  2. @SessionAttributes("pet") // 这种写法可以在不同页面中传递值
  3. public class EditPetForm {
  4. @GettMapping("redirectTest")
  5. public String redirectTest(Model model){
  6. model.addAttribute("pet",new Pet());
  7. return "redirect:indexView";
  8. }
  9. @GetMapping("indexView")
  10. public String handle(@ModelAttribute Pet pet, BindingResult errors, SessionStatus status) {
  11. if (errors.hasErrors) { // 如果有错误会传到 errors 里面
  12. // ...
  13. }
  14. System.out.println(pet);
  15. status.setComplete();// 删掉 Seesion
  16. // ...
  17. }
  18. }
  19. // 或者直接这么写,读别的地方写入的 Seesion,如果要控制 Seesion 则 入参 为 HttpSession
  20. @RequestMapping("/demo1")
  21. public String handle(@SessionAttribute("user") User user) { // 括号可选
  22. // ...
  23. }
  24. // 获取 Cookies 值
  25. @GetMapping("/demo2")
  26. public void handle(@CookieValue("JSESSIONID") String cookie) {
  27. //...
  28. }
  29. }

错误处理

@Controller
public class SimpleController {
    @ExceptionHandler
    public String handle(IOException ex) {
        // ...
    }
    @ExceptionHandler({FileSystemException.class, RemoteException.class})
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }

}

验证

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。

例如

public class Employee {

    private long id;
    private double salary;

    // standard constructors, getters, setters
}

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

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

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

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 储存实际的数据,在不同部分中流转。
Spring MVC 用法备忘和原理简析 - 图1

  1. 收到HTTP请求后,DispatcherServlet会查询HandlerMapping以调用相应的Controller。

  2. Controller 接受请求并根据使用的GET或POST等方法调用适当的服务方法。服务方法将基于定义的业务逻辑设置Model 数据。最后 Controller 将 View 名称、Model 返回给 DispatcherServlet。

  3. DispatcherServlet把视图名称发给 ViewResolver,返回一个已经写好的视图(JSP文件或者其他模版文件)

  4. DispatcherServlet 把 Model 和 View 结合,返回给用户

拦截器流程

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 方法

Spring MVC 用法备忘和原理简析 - 图2

Spring MVC 用法备忘和原理简析 - 图3

DispatcherServlet 中的代码节选:

//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);
// 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