《Spring 实战》笔记5:构建 Spring Web 应用程序

Spring MVC 基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够帮你构建像Spring框架那样灵活和松耦合的 Web 应用程序。

Spring MVC 起步

Spring 将请求在调度 Servlet、处理器映射(Handler Mappering)、控制器以及视图解析器(View resolver)之间移动,每一个 Spring MVC 中的组件都有特定的目的,并且也没那么复杂。

让我们看一下,请求是如何从客户端发起,经过 Spring MVC 中的组件,最终返回到客户端

跟踪 Spring MVC 的请求

每当用户在 Web 浏览器中点击链接或提交表单的时候,请求就开始工作了。请求是一个十分繁忙的家伙,从离开浏览器开始到获取响应返回,它会经历很多站,在每站都会留下一些信息,同时也会带上一些信息。下图展示了请求使用 Spring MVC 所经历的所有站点。

一路上请求会将信息带到很多站点,并生产期望的结果

Spring MVC 流程图
Spring MVC 流程图2

Spring MVC 流程图

  1. 用户向服务器发送请求,请求被 Spring 前端控制 Servelt DispatcherServlet 捕获;
  2. DispatcherServlet 对请求 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返回;
  3. DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 preHandler(...) 方法)
  4. 提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler(Controller)。 在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:
    • HttpMessageConveter:将请求消息(如 JSON、XML 等数据)转换成一个对象,将对象转换为指定的响应信息
    • 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等
    • 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
    • 数据验证:验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中
  5. Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象;
  6. 根据返回的 ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver)返回给 DispatcherServlet
  7. ViewResolver 结合 ModelView,来渲染视图
  8. 将渲染结果返回给客户端。

搭建 Spring MVC

配置 DispatcherServlet

DispatcherServlet 是 Spring MVC 的核心。在这里请求会第一次接触到框架,它要负责将请求路由到其他的组件之中。

按照传统的方式,像 DispatcherServlet 这样的 Servlet 会配置在 web.xml 文件中

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>*.form</url-pattern>
</servlet-mapping>

Servlet 3 规范和 Spring 3.1 的功能增强,可以使用 Java 将 DispatcherServlet 配置在 Servlet 容器中

public class SpittrWebAppInitializer
            extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected String[] getServletMappings() {  // 将 DispatcherServlet 映射到 "/"
        return new String[] { "/" };
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }
}

要理解程序是如何工作的,我们可能只需要知道扩展 AbstractAnnotationConfigDispatcherServletInitializer 的任意类都会自动地配置 DispatcherServlet 和 Spring 应用上下文,Spring 的应用上下文会位于应用程序的 Servlet 上下文之中。

AbstractAnnotationConfigDispatcherServletInitializer 剖析

在 Servlet 3.0 环境中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer 接口的类,如果能发现的话,就会用它来配置 Servlet 容器。

Spring 提供了这个接口的实现,名为 SpringServletContainerInitializer ,这个类反过来又会查找实现 WebApplicationInitializer 的类并将配置的任务交给它们来完成。Spring 3.2 引入了一个便利的 WebApplicationInitializer 基础实现,也就是 AbstractAnnotationConfigDispatcherServletInitializer 。因为我们的 SpittrWebAppInitializer 扩展了 AbstractAnnotationConfigDispatcherServletInitializer (同时也就实现了 WebApplicationInitializer ),因此当部署到 Servlet 3.0 容器中的时候,容器会自动发现它,并用它来配置 Servlet 上下文。

SpittrWebAppInitializer 重写了三个方法。

  • 第一个方法是 getServletMappings(),它会将一个或多个路径映射到 DispatcherServlet 上。在本例中,它映射的是 "/" ,这表示它会是应用的默认 Servlet。它会处理进入应用的所有请求。

为了理解其他的两个方法,我们首先要理解 DispatcherServlet 和一个 Servlet 监听器(也就是 ContextLoaderListener )的关系。

两个应用上下文之间的故事

DispatcherServlet 启动的时候,它会创建 Spring 应用上下文,并加载配置文件或配置类中所声明的 bean。在上面程序的 getServletConfigClasses() 方法中,我们要求 DispatcherServlet 加载应用上下文时,使用定义在 WebConfig 配置类(使用 Java 配置)中的 bean。

但是在 Spring Web 应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由 ContextLoaderListener 创建的。

我们希望 DispatcherServlet 加载包含 Web 组件的 bean,如控制器、视图解析器以及处理器映射,而 ContextLoaderListener 要加载应用中的其他 bean。这些 bean 通常是驱动应用后端的中间层和数据层组件。

实际上, AbstractAnnotationConfigDispatcherServletInitializer 会同时创建 DispatcherServletContextLoaderListenergetServletConfigClasses() 方法返回的带有 @Configuration 注解的类将会用来定义 DispatcherServlet 应用上下文中的 bean。getRootConfigClasses() 方法返回的带有 @Configuration 注解的类将会用来配置 ContextLoaderListener 创建的应用上下文中的 bean。

在本例中,根配置定义在 RootConfig 中, DispatcherServlet 的配置声明在 WebConfig 中。稍后我们将会看到这两个类的内容。

需要注意的是,通过 AbstractAnnotationConfigDispatcherServletInitializer 来配置 DispatcherServlet 是传统 web.xml 方式的替代方案。

启用 Spring MVC

以前,Spring 是使用 XML 进行配置的,你可以使用 <mvc:annotation-driven> 启用注解驱动的 Spring MVC。

基于 Java 进行配置,我们所能创建的最简单的 Spring MVC 配置就是一个带有 @EnableWebMvc 注解的类:

@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        // 要求 DispatcherServlet 将对静态资源的请求转发到 Servlet 容器中默认的 Servlet 上
        configurer.enable();
    }
}
@Configuration
@ComponentScan(basePackages = {"spittr"},
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
        })
public class RootConfig {
}

编写基本的控制器

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)   // 处理对 "/" 的 Get 请求
    public String home() {
        return "home";  // 视图名为 home
        // DispatcherServlet 会要求视图解析器将这个逻辑名称解析为实际的视图。
        // 由于配置 InternalResourceViewResolver 的方式,视图名 "home" 将会解析为 "WEB-INF/views/home.jsp" 路径的 JSP。
    }
}

@Controller 是一个构造型(stereotype)的注解,它基于 @Component 注解。在这里,它的目的就是辅助实现组件扫描。因为 HomeController 带有 @Controller 注解,因此组件扫描器会自动找到 HomeController,并将其声明为 Spring 应用上下文中的一个 bean。

带有 @RequestMapping 注解的方法,它的 value 属性指定了这个方法所要处理的请求路径,method 属性细化了它所处理的 HTTP 方法。

测试控制器

public class HomeControllerTest {
    @Test
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        // 搭建 MockMvc
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();

        mockMvc.perform(MockMvcRequestBuilders.get("/")) // 对 “/” 执行 GET 请求,
                .andExpect(MockMvcResultMatchers.view().name("home"));// 预期得到 home 视图
    }
}

SpringMVC Test

定义类级别的请求处理

传递模型数据到视图中

@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
    model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); // 将 spittle 添加到视图
    // 和上一行等效
    // model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));

    return "spittles";  // 返回视图名
}

Model 实际上就是一个 Map(也就是key-value对的集合),它会传递给视图,这样数据就能渲染到客户端了。当调用 addAttribute() 方法并且不指定 key 的时候,那么 key 会根据值的对象类型推断确定。在本例中,因为它是一个 List<Spittle>,因此,键将会推断为 spittleList

@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles() {
    return spittleRepository.findSpittles(Long.MAX_VALUE, 20));
}

它并没有返回视图名称,也没有显式地设定模型,这个方法返回的是 Spittle 列表。当处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的 key 会根据其类型推断得出(在本例中,也就是 spittleList)。

而逻辑视图的名称将会根据请求路径推断得出。因为这个方法处理针对 "/spittles" 的 GET 请求,因此视图的名称将会是 spittles(去掉开头的斜线)。

接受请求的输入

Spring MVC 允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:

  • 查询参数(Query Parameter)。
  • 表单参数(Form Parameter)。
  • 路径变量(Path Variable)。

处理查询参数

// 注:原文中所写的这行是错误的,注解那块会提示 Attribute value must be constant
// private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);

private static final String MAX_LONG_AS_STRING = Long.MAX_VALUE + "";

@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
        @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
        @RequestParam(value="count", defaultValue="20") int count) {
    return spittleRepository.findSpittles(max, count);
}

通过路径参数接受输入

Spring MVC 允许我们在 @RequestMapping 路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。如果对 "/spittles/54321" 发送 GET 请求,那么将会把 "54321" 传递进来,作为 spittleId 的值。

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

需要注意的是,如果你想要重命名参数时,必须要同时修改占位符的名称,使其互相匹配。

处理表单

编写处理表单的控制器

校验表单

在 Spring MVC 中要使用 Java 校验 API 的话,并不需要什么额外的配置。只要保证在类路径下包含这个 Java API 的实现即可,比如 Hibernate Validator

Java 校验 API 所提供的校验注解

  • @AssertFalse 所注解的元素必须是 Boolean 类型,并且值为 false
  • @AssertTrue 所注解的元素必须是 Boolean 类型,并且值为 true
  • @DecimalMax 所注解的元素必须是数字,并且它的值要小于或等于给定的 BigDecimalString 值
  • @DecimalMin 所注解的元素必须是数字,并且它的值要大于或等于给定的 BigDecimalString 值
  • @Digits 所注解的元素必须是数字,并且它的值必须有指定的位数
  • @Future 所注解的元素的值必须是一个将来的日期
  • @Max 所注解的元素必须是数字,并且它的值要小于或等于给定的值
  • @Min 所注解的元素必须是数字,并且它的值要大于或等于给定的值
  • @NotNull 所注解元素的值必须不能为 null
  • @Null 所注解元素的值必须为 null
  • @Past 所注解的元素的值必须是一个已过去的日期
  • @Pattern 所注解的元素的值必须匹配给定的正则表达式
  • @Size 所注解的元素的值必须是 String 、集合或数组,并且它的长度要符合给定的范围
public class Spitter {
    private Long id;

    @NotNull
    @Size(min=5, max=16)
    private String username;  // 非空,5 到 16 个字符

    @NotNull
    @Size(min=5, max=25)
    private String password;

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

    @NotNull
    @Size(min=2, max=30)
    private String lastName;
    ...
}

启用校验功能

@RequestMapping(value="/register", method=POST)
public String processRegistration(@Valid Spitter spitter, Errors errors) {
    if (errors.hasErrors()) {
        return "registerForm";
    }
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

Errors 参数要紧跟在带有 @Valid 注解的参数后面,@Valid 注解所标注的就是要检验的参数


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Spring 实战》笔记5:构建 Spring Web 应用程序

文章字数:3k

本文作者:Bin

发布时间:2019-07-13, 16:35:24

最后更新:2019-08-06, 20:24:32

原始链接:http://coolview.github.io/2019/07/13/Spring/%E3%80%8ASpring%20%E5%AE%9E%E6%88%98%E3%80%8B%E7%AC%94%E8%AE%B05%EF%BC%9A%E6%9E%84%E5%BB%BA%20Spring%20Web%20%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录