2019-05-25 21:49:31 +0000   |     spring java gradle controller mvc   |   Viewed times   |    

M-V-C三者的关系

一个Http请求过来之后,DispatcherServlet根据请求的信息找到对应的Controller。假设有数据层的情况,Controller从数据层查询数据之后(或许再进行计算)得到结果放在Model里,然后跳转到指定View。这时View就能从Model里拿到数据结果进行渲染。

5.2 Basic Controller

这一节的例子是个玩具。没有数据层实体类,测试时也不必真的启动服务器。例子中的代码只是简单配置了DispatcherServlet,然后创建了2个Controller来响应2中不同的请求。具体结构如下,

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── ciaoshen
    │   │           └── sia4
    │   │               └── ch05
    │   │                   └── basic_controller_52
    │   │                       └── spittr
    │   │                           ├── Spittle.java
    │   │                           ├── config
    │   │                           │   ├── RootConfig.java
    │   │                           │   ├── SpittrWebAppInitializer.java
    │   │                           │   └── WebConfig.java
    │   │                           ├── data
    │   │                           │   └── SpittleRepository.java
    │   │                           └── web
    │   │                               ├── HomeController.java
    │   │                               └── SpittleController.java
    │   └── resources
    │       └── static
    │           └── basic_controller_52
    │               └── spittr
    │                   ├── home.jsp
    │                   └── spittles.jsp
    └── test
        └── java
            └── com
                └── ciaoshen
                    └── sia4
                        └── ch05
                            └── basic_controller_52
                                └── spittr
                                    └── HomeControllerTest.java

配置DispatcherServlet

DispatcherServlet是每个应用默认的唯一入口。要为它配置一个专属上下文,配置类需要继承AbstractAnnotationConfigDispatcherServletInitializer初始化工具类。 初始化DispatcherServlet对应的上下文的同时,还初始化了另一个全局上下文。那这两个上下文有什么区别的?

DispatcherServletContextLoaderListener分别对应不同的上下文

先要明确一点:一个Spring应用可以有多个上下文。原则上是一个主从的关系,

  1. 最多只能有一个“根”上下文
  2. 可以有多个“子”上下文
  3. 且任何一个子上下文可以访问根上下文的信息,反之则不行

ContextLoaderListener负责初始化的是那个唯一的“根”上下文。而DispatcherServlet可以有多个,每个对应一个单独的子上下文。

为什么说是“原则上”?因为“根”上下文不是必须的。完全可以把所有配置都集中到DispatcherServlet对应的子上下文。只有当有多个子应用,且有多个DispatcherServlet并且之间需要共享某些数据时才考虑创建“根”上下文。

传统的用XML配置中,无论ContextLoaderListener还是DispatcherServlet都可以在web.xml里配置。而书上是用一个继承了AbstractAnnotationConfigDispatcherServletInitializer类的配置类做这件事。而且把配置类拆分成了RootConfigWebConfig两个配置类,分别对应“根”上下文和“子”上下文。

WebMvcConfigurerAdapter类被弃用

WebMvcConfigurerAdapterWebMvcConfigurer接口的基本实现。继承WebMvcConfigurerAdapter就只需要重写部分有改动的方法即可。从Java 8开始接口允许可以有default默认实现,所以WebMvcConfigurerAdapter整个类被弃用。直接实现WebMvcConfigurer接口即可,效果一样。

@Controller标记的控制器类

DispatcherServlet的配置类WebConfig中开启@EnableWebMvc,应用就开启了对Spring MVC的支持。/web包下的HomeControllerSpittleController负责对Http请求的调度。比如用户浏览器中访问http://localhost:8080/spittles,视图路径解析器会把/spittles解析成/WEB-INF/views/spittles.jsp,最终spittles()函数中的Model参数会被传递给spittles.jsp页面。Model实例中附带spittle列表数据就可以被显示在spittles.jsp的页面上。

/**
 * 对 http://localhost:8080/spittles 发起GET请求时调用此方法
 * ViewResolver会把 "spittles" 解析成 "/WEB-INF/views/spittles.jsp"
 * 带有SpittleRepository实例的Mode对象会被传递给 spittles.jsp 页面
 */
@RequestMapping(method=GET)
public String spittles(Model model) {
    model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
    return "spittles";
}

MockMvc测试

单元测试类HomeControllerTest类用MockMvc模拟发出HTTP请求,这样测试就不需要启动服务器。实际测试中大部分组件都是模拟的,只有Controller是真实的。

5.3 pass parameters

例子沿用5.2节,做了点小改动,将SpittleController中查询当前所有spittle列表的函数,改为根据id查询单个spittle。

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── ciaoshen
    │   │           └── sia4
    │   │               └── ch05
    │   │                   └── with_parameter_53
    │   │                       └── spittr
    │   │                           ├── Spittle.java
    │   │                           ├── config
    │   │                           │   ├── RootConfig.java
    │   │                           │   ├── SpittrWebAppInitializer.java
    │   │                           │   └── WebConfig.java
    │   │                           ├── data
    │   │                           │   └── SpittleRepository.java
    │   │                           └── web
    │   │                               ├── HomeController.java
    │   │                               └── SpittleController.java
    │   └── resources
    │       └── static
    │           └── with_parameter_53
    │               └── spittr
    │                   ├── home.jsp
    │                   └── spittle.jsp
    └── test
        └── java
            └── com
                └── ciaoshen
                    └── sia4
                        └── ch05
                            └── with_parameter_53
                                └── spittr
                                    └── HomeControllerTest.java

首先SpittleController变了。通过在@RequestMapping里加入value属性给参数赋值。然后spittle()函数中通过@PathVariable拿到参数。

其次,拿到要查询的ID,SpittleRepository要加一个findOne()接口方法,用来根据ID查询单条信息。

spittle.jsp页面也要改成显示单条信息。

最后再修改HomeControllerTest测试类,模拟查询单条消息的场景。

5.4 提交表单

主要是SpitterController这个控制器。/spitter/registerGET请求对应registerForm.jsp页面,让用户填写注册表单。提交表单到/spitter/registerPOST请求储存用户信息,并跳转到/spitter/${username},这里${username}是刚注册的用户名,对应视图为profile.jsp

测试类SpitterControllerTest中照例数据层SpitterRepository和HTTP请求都是mockito模拟的。不需要启动服务器,也不需要考虑数据库。只考察控制器能不能根据发来的模拟HTTP请求做出正确的调度。

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── ciaoshen
    │   │           └── sia4
    │   │               └── ch05
    │   │                   └── post_form_54
    │   │                       └── spittr
    │   │                           ├── Spitter.java
    │   │                           ├── config
    │   │                           │   ├── RootConfig.java
    │   │                           │   ├── SpittrWebAppInitializer.java
    │   │                           │   └── WebConfig.java
    │   │                           ├── data
    │   │                           │   └── SpitterRepository.java
    │   │                           └── web
    │   │                               └── SpitterController.java
    │   └── resources
    │       └── static
    │           └── post_form_54
    │               └── spittr
    │                   ├── profile.jsp
    │                   └── registerForm.jsp
    └── test
        └── java
            └── com
                └── ciaoshen
                    └── sia4
                        └── ch05
                            └── post_form_54
                                └── spittr
                                    └── SpitterControllerTest.java

测试中最后一个断言是错的,因为根据表单提交之后,SpitterControllerprocessRegistration(Spitter spitter)函数拿到的Spitter实例是MockMvc创建的,不是测试类中创建的Spitter unsaved

@Test
public void shouldShowRegistration() throws Exception {
    SpitterRepository mockRepository = mock(SpitterRepository.class);
    Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer");
    Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer");
    when(mockRepository.save(unsaved)).thenReturn(saved);

    SpitterController controller = new SpitterController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(post("/spitter/register")
        .param("firstName", "Jack")
        .param("lastName", "Bauer")
        .param("username", "jbauer")
        .param("password", "24hours"))
        .andExpect(redirectedUrl("/spitter/jbauer"));

    /**
     * 这个测试通不过
     * processRegistration(Spitter spitter)函数拿到的参数是mockMvc根据表单提交的参数构造的
     * 和unsave不是一个对象
     */
    verify(mockRepository, atLeastOnce()).save(unsaved);
}

参考文献