构建SpringWeb应用程序

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/denghj618/article/details/81811859

构建Spring Web 应用程序

本章主要内容包括:

  • 映射请求到Spring控制器
  • 透明地绑定表单参数
  • 校验表单提交

状态管理、工作流以及验证都是Web的应用程序需要解决的重要特性。HTTP协议的无状态性决定了这些问题都不是那么容易解决。Spring的Web框架就是为了解决这些关注点而设计的。Spring MVC 基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够帮你构建像Spring框架那样灵活和松耦合的Web应用程序。在本章中,我们主要介绍Spring MVC Web框架,并使用新的Spring MVC注解来构建处理各种Web请求、参数和表单输入的控制器。在深入介绍Spring MVC之前,先总体介绍一下Spring MVC,并建立起Spring MVC运行的基本配置。

1 Spring MVC起步

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

1.1 跟踪Spring MVC的请求

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

在请求离开浏览器时,会带有用户所请求内容的信息,至少会包含请求的URL。但是还有可能带有其他的信息,例如用户提交的表单信息。
请求流程的第一站是Spring的DispatcherServlet。与大多数基于Java的Web框架一样,Spring MVC所有的请求都会通过一个前端控制器(front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。在Spring MVC中,DispatcherServlet就是前端控制器。
DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet会查询一个或多个处理器映射(handler mapping)来确定请求的下一站在哪里。处理器映射会根据请求所携带的URL信息来进行决策。
一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理)。
控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅返回给用户原始的信息是不够的–这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。
控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连通模型和视图名发送回DispatcherServlet。
这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确定视图就是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器(view resolver)来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。
既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是JSP),在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码)。

1.2 搭建Spring MVC

基于图1.1,看上去我们需要配置很多的组成部分。幸好,借助于最近几个Spring新版本的功能增强,开始使用Spring MVC变得非常简单了。现在,我们要使用最简单的方式来配置Spring MVC:所要实现的功能仅限于运行我们所创建的控制器。

配置DispatcherServlet

DispatcherServlet是Spring MVC的核心。在这里请求会第一次接触到框架,它要负责将请求路由到其他的组件之中。
按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中,这个文件会放到应用的WAR包里面。当然,这是配置DispatcherServlet的方法之一。但是,借助于Servlet3规范和Spring3.1的功能增强,这种方式已经不是唯一的方案了,这也不是我们本章所使用的配置方法。
我们会使用Java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。如下展示了所需要的Java类:

package com.godman.spittr.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

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

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

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

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

AbstractAnnotationConfigDispatcherServletInitializer剖析

在Servlet3.0环境中,环境会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它类配置Servlet容器。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。
SpringServletContainerInitializer实现代码如下:

package org.springframework.web;

import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;

@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public SpringServletContainerInitializer() {
    }

    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = new LinkedList();
        Iterator var4;
        if (webAppInitializerClasses != null) {
            var4 = webAppInitializerClasses.iterator();

            while(var4.hasNext()) {
                Class<?> waiClass = (Class)var4.next();
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)waiClass.newInstance());
                    } catch (Throwable var7) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        } else {
            servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
            AnnotationAwareOrderComparator.sort(initializers);
            var4 = initializers.iterator();

            while(var4.hasNext()) {
                WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
                initializer.onStartup(servletContext);
            }

        }
    }
}

在它的onStartup()方法中,将配置的任务交给了WebApplicationInitializer实现类。WebApplicationInitializer接口的实现类继承关系如下:

Spring3.2引入了一个便利的WebApplicationInitializer基础实现,它就是AbstractAnnotationConfigDispatcherServletInitializer。因为我们的SpittrWebAppInitializer扩展了AbstractAnnotationConfigDispatcherServletInitializer(同时也就实现了WebApplicationInitializer),因此当部署到Servlet3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。
尽管它的名字很长,但是AbstractAnnotationConfigDispatcherServletInitializer使用起来非常简便。SpittrWebAppInitializer只需要重写三个方法。第一个方法是getServletMappings(),它会将一个或多个路径映射到DispatcherServlet上。在本例中,它映射的是”/”,这表示它会是应用的默认Servlet。它会处理进入应用的所有请求。
为了理解其他的两个方法,我们首先需要理解DispatcherServlet和一个Servlet监听器(也就是ContextLoaderListener)的关系。

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

当DispatcherServlet启动的时候,它会创建Spring应用上下文,并加载配置文件或者配置类中所声明的bean。在以上代码SpittrWebAppInitializer中,通过getServletConfigClasses()方法,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类(使用Java配置)中的bean。
但是在Spring Web应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由ContextLoaderListener创建的。我们希望DispatcherServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射,而ContextLoaderListener要加载应用中的其他bean。这些bean通常是驱动应用后端的中间层和数据层组件。
实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListener。
getRootConfigClasses()方法返回带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean
在本例中,根配置定义在RootConfig中,DispatcherServlet的配置声明在WebConfig中。稍后会看到这两个类的内容。
需要注意的是,通过AbstractAnnotationConfigDispatcherServletInitializer来配置DispatcherServlet是传统web.xml方式的替代方案。如果你愿意的话,可以同时包含web.xml和AbstractAnnotationConfigDispatcherServletInitializer,但这其实没有什么必要。
另外,如果按照这个方式配置DispatcherServlet,而不是使用web.xml的话,那唯一问题在于它只能部署到支持Servlet3.0的服务器中才能正常工作。

启动Spring MVC

我们有多种方式来配置DispatcherServlet,与之类似,启用Spring MVC组件的方式也不仅一种。以前,Spring是使用XML进行配置的,可以使用<mvc:annotation-driven>启用注解驱动的Spring MVC。我们会在之后的章节讨论Spring MVC配置可选项的时候,再讨论<mvc:annotation-driven>。不过,现在我们会让Spring MVC的搭建过程尽可能简单并基于Java进行配置。我们所能创建的最简单的Spring MVC配置就是一个带有@EnableWebMvc注解的类:

package com.godman.spittr.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
public class WebConfig {
}

这样的确可以运行起来:

但是还有不少问题需要解决:

  • 没有配置视图解析器。如果这样的话,Spring默认会使用BeanNameViewResolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,它以这样的方式来解析视图。
  • 没有启用组件扫描。这样的结果就是,Spring只能找到显示声明在配置类中的控制器。
  • 这样配置的话,DispatcherServlet会映射为应用的默认Servlet,所以他会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这可能并不是你想要的结果)。

因此,我们需要在WebConfig这个最小的Spring MVC配置上再加一些内容,从而让它变得真正有用。

package com.godman.spittr.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("com.godman.spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        //配置JSP视图解析器
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        //配置静态资源处理
        configurer.enable();
    }
}

在以上代码中第一件需要注意的事情是WebConfig现在添加了@ComponentScan注解,因此将会扫描com.godman.spittr.web包来查找组件。稍后将会看到,我们所编写的控制器都会带有@Controller注解,这会使其成为组件扫描时的候选bean。因此,我们不需要在配置类中显示声明任何的控制器。
紧接着,我们添加了一个ViewResolver bean。更具体地将,是InternalResourceViewResolver。我们将会在之后的章节中详细讨论视图解析器,在这里只需要知道它会查找JSP文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀(例如,名为home的视图将会解析为/WEB-INF/views/home.jsp)。
最后,新的WebConfig类还扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling方法。通过调用DefaultServletHandlerConfigurer的enable()方法,我们要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。
WebConfig已经就绪,那RootConfig呢?因为本章聚焦于Web开发,而Web相关的配置通过DispatcherServlet创建的上下文都已经配置好了,因此RootConfig相对简单。

package com.godman.spittr.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan(basePackages = {"com.godman.spittr"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
public class RootConfig {
}

在这里需要注意的是RootConfig使用了@ComponentScan注解。@ComponentScan注解中的basePackages标识扫描基础包,excludeFilters表示过滤不用加入Spring容器的类,另外还有与excludeFilters对应的includeFilters,表示加入扫描路径下没有但满足Filter过滤条件的类。现在我们基本已经可以使用Spring MVC构建Web应用了。

1.3 Spittr应用简介

为了实现在线社交功能,我们将要构建一个简单的微博应用。在很多方面,我们构建的应用与最早的微博应用Twitter很类似。在这个构成中,我们会添加一些小的变化。当然,我们要使用Spring技术来构建这个应用。Spittr应用有两个基本的领域概念:Spitter(应用的用户)和Spittle(用户发布的简短状态更新)。当我们在接下来的内容中完善spittr应用的功能时,将会介绍这两个领域概念。在本章中,我们将会构建应用的web层,创建展现spittr的控制器以及处理用户注册成为spittr的表单。
舞台已经搭建完成,我们已经配置了DispatcherServlet,启用了基本的SpringMVC组件并确定了目标应用。让我们进入本章的核心内容:使用Spring MVC控制器处理Web请求。

2 编写基本的控制器

在SpringMVC中,控制器只是方法上添加了@RequestMapping注解的类,这个注解声明了它们所要处理的请求。开始的时候,尽可能简单,假设控制器类要处理对“/”的请求,并渲染应用的首页。如下代码所示:

package com.godman.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(){
        return "home";
    }
}

首先注意到的第一件事就是HomeController使用了@Controller注解。很显然这个注解是用来声明控制器的,但实际上这个注解对SpringMVC本身的影响并不大。@Controller是一个构造型(stereotype)的注解,它基于@Component注解。在这里,它的目的就是辅助实现组件扫描。因为HomeController带有@Controller注解,因此组件扫描器会自动找到HomeController,并将其声明为Spring应用上下文中的一个bean。其实,这里也可以将@Controller注解替换为@Component注解,它所实现的效果是一样的,只是在表意性上可能差一些,无法确认HomeController是什么组件类型。
HomeController唯一的一个方法就是home(),带有@RequestMapping注解。它的value属性指定了这个方法所要处理的请求路径,method属性细化了它所要处理的HTTP方法。在本例中,当收到对”/”的HTTP get 请求时,就会调用home()方法。
这个方法并没有做太多的事情:它返回一个String类型的”home”。这个String将会被Spring MVC解读为要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。在本例中,”home”将会被解析为”/WEB-INF/views/home.jsp”。这个页面内容如下所示:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spitter</title>
</head>
<body>
    <h1>Welcome to Spittr</h1>
</body>
</html>

其页面展示效果如下:

2.1 测试控制器

下面让我们来测试一下HomeController。代码如下:

@Test
public void testHome2(){
    HomeController controller = new HomeController();
    Assert.assertEquals("home", controller.home());
}

结果如下:

这个测试方法很简单,但是它只测试了home()方法中会发生什么。在测试中直接调用home()方法,并断言返回包含”home”值的String。它完全没有站在Spring MVC控制器的视角进行测试。这个测试没有断言当接收到针对”/”的GET请求时会调用home()方法。因为它返回的值就是”home”,所以并没有真正判断home是视图的名称。
不过从Spring3.2开始,我们可以按照控制器的方式来测试SpringMVC中的控制器了,而不仅仅是作为POJO进行测试。Spring现在包含了一种mock Spring MVC并针对控制器执行HTTP请求的机制。这样的话,在测试控制器的时候就没有必要再启动web服务器和web浏览器了。此种方式的测试代码如下所示:

@Test
public void testHome() throws Exception {
    HomeController controller = new HomeController();
    //搭建MockMvc
    MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    //对"/"执行GET请求,预期得到home视图
    mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.view().name("home"));
}

测试结果如下所示:

尽管新版本的测试只比之前多了几行代码,但是它更加完整地测试了HomeController。这次我们不是直接调用home()方法并测试它的返回值,而是发起了对”/”的GET的请求,并断言结果视图的名称为home。它首先传递一个HomeController实例到MockMvcBuilders.standaloneSetup()并调用build()来构建MockMvc实例。然后它使用MockMvc实例来执行针对”/”的GET请求并设置期望得到的视图名称。

2.2 处理类级别的请求处理

现在我们已经为HomeController编写了测试代码,那么我们可以做一些重构,并通过测试来保证不会对功能造成什么破坏。我们可以做的一件事情就是拆分@RequestMapping,并将其路径映射部分放到类级别上。代码如下所示:

package com.godman.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @RequestMapping(method = RequestMethod.GET)
    public String home(){
        return "home";
    }
}

在这个新版本中,路径现在被转移到类级别的@RequestMapping注解上,而HTTP方法依然映射在方法级别上。当控制器在类级别上添加@RequestMapping注解时,这个注解会应用到这个控制器的所有处理器方法上。处理器方法上的@RequestMapping注解会对类级别上的@RequestMapping注解的声明进行补充。现在我们来测试一下,看看结果有没有发生变化

从结果来看,没有改变任何功能,只是将代码换了个地方。但是HomeController还是做和以前一样的事情。当我们在修改@RequestMapping时,还可以对HomeController做另外的一个变更。@RequestMapping的value属性能够接受一个String类型的数组。到目前为止我们给它设置的都是一个String类型的”/”。但是我们还可以将它映射到对”homePage”的请求上,只需要将类级别的@RequestMapping改为如下所示:

@Controller
@RequestMapping(value = {"/", "/homePage"})
public class HomeController {

    @RequestMapping(method = RequestMethod.GET)
    public String home(){
        return "home";
    }
}

测试代码同步更改为:

@Test
public void testHome() throws Exception {
    HomeController controller = new HomeController();
    //搭建MockMvc
    MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    //对"/"执行GET请求,预期得到home视图
    mockMvc.perform(MockMvcRequestBuilders.get("/homePage")).andExpect(MockMvcResultMatchers.view().name("home"));
}

测试结果为:

2.3 传递模型数据到视图中

在Spittr应用中,我们需要一个页面展示最近提交的Spittle列表。因此,我们需要一个新的方法来处理这个页面。
首先,需要定义一个数据访问的Reposity。为了实现解耦以及避免陷入数据库访问的细节中,我们将Reposity定义为一个接口。此时,我们只需要一个能够获取Spittle列表的Reponsitory,如下所示的功能就已经足够了:

package com.godman.spittr.data;

import com.godman.spittr.vo.Spittle;

import java.util.List;

public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
}

其实现类代码如下:

package com.godman.spittr.data.impl;

import com.godman.spittr.data.SpittleRepository;
import com.godman.spittr.vo.Spittle;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Component
public class SpittleRepositoryImpl implements SpittleRepository {
    @Override
    public List<Spittle> findSpittles(long max, int count) {
        List<Spittle> spittles = new ArrayList<>();
        for (int i=0; i<count; i++){
            spittles.add(new Spittle(UUID.randomUUID().getLeastSignificantBits(), "Spittle" + i, new Date(), Math.random()*100, Math.random()*100));
        }
        return spittles;
    }
}

findSpittles()方法接受两个参数。其中max参数代表所返回的Spittle中,Spittle ID属性的最大值,而count参数表明要返回多少个Spittle对象。Spittle对象的属性包括消息内容、时间戳以及发布时对应的经纬度:

package com.godman.spittr.vo;

import java.util.Date;

public class Spittle {
    private Long id;
    private String message;
    private Date time;
    private Double latitude;
    private Double longitude;

    public Spittle(String message, Date time) {
        this.message = message;
        this.time = time;
    }

    public Spittle(Long id, String message, Date time, Double latitude, Double longitude) {
        this.id = id;
        this.message = message;
        this.time = time;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public Long getId() {
        return id;
    }

    public String getMessage() {
        return message;
    }

    public Date getTime() {
        return time;
    }

    public Double getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

}

下面我们来编写新的控制器:

package com.godman.spittr.web;

import com.godman.spittr.data.SpittleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/spittles")
public class SpittleController {

    private SpittleRepository spittleRepository;

    @Autowired
    public SpittleController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String spittles(Model model){
        //将spittle添加到模型中
        model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        //返回视图名
        return "spittles";
    }
}

我们可以看到SpittleController有一个构造器,这个构造器使用了@Autowired注解,用来注入SpittleRepository。这个SpittleRepository随后又调用findSpittles()方法用来获取最新的spittle列表。需要注意的是,我们在spittles()方法中给定了一个Model作为参数。这样,spittles()方法就能将Repository中获取到的Spittle列表填充到模型中。Model实际上就是一个Map(也就是key-value对的集合),它会传递给视图,这样数据就能渲染到客户端了。当调用addAttribute()方法并且不指定key的时候,那么key会根据值的对象类型推断确定。在本例中,因为它是一个List,因此,键值将会推断为spittleList。
spittle()方法所做的最后一件事情就是返回spittles作为视图的名字,这个视图会渲染模型。
如果你希望显示声明模型的key的话,那也可以进行指定:

@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model){
    //将spittle添加到模型中
    model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
    //返回视图名
    return "spittles";
}

如果你希望使用非Spring类型的话,那么可以使用java.util.Map来替换Model。如下所示:

@RequestMapping(method = RequestMethod.GET)
public String spittles(Map model){
    //将spittle添加到模型中
    model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
    //返回视图名
    return "spittles";
}

还可以使用下面的方案进行替换:

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

这个版本与其他的版本有些差别。它并没有返回视图名称,也没有显示的设置模型,这个方法返回的是Spittle列表。当处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的key会根据其类型推断得出(在本例中,也就是spittleList)。而逻辑视图的名称会根据请求路径推断得出。因为这个方法处理针对”/spittles”的GET请求,因此视图的名称将会是spittles(去掉开头的斜线)。
不管采用哪种方式来编写spittles()方法,所达成的结果都是相同的。模型中会存储一个Spittle列表,key为spittleList,然后这个列表会发送到名为spittles的视图中。按照我们配置InternalResourceViewResolver的方式,视图的JSP将会是”/WEB-INF/views/spittles.jsp”。
现在,数据已经放到了模型中,在JSP中该如何访问它呢?实际上,当视图是JSP的时候,模型数据会作为请求属性放到请求(request)之中。因此,在spittles.jsp文件中可以使用JSTL的<c:forEach>标签渲染spittle列表:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>spittles</title>
</head>
<body>
    <c:forEach items="${spittleList}" var="spittle">
        <li id="spittle_<c:out value="spittle.id"/> ">
            <h3><c:out value="${spittle.message}"/></h3>
            <h5>
                <span><c:out value="${spittle.time}"/></span>
                <br/>
                <span>
                    (<c:out value="${spittle.latitude}"/>, <c:out value="${spittle.longitude}"/>)
                </span>
            </h5>
        </li>
    </c:forEach>
</body>
</html>

其展示效果如下:

3 接受请求的输入

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

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

下面将会看到如何编写控制器处理这些不同机制的输入。作为开始,我们先看一下如何处理带有查询参数的请求。这也是客户端往服务器端发送数据时最简单和最直接的方式。

3.1 处理查询参数

假设我们现在要查看某一页的Spittle列表,这个列表会按照最新的Spittle在前的方式进行排序。因此,下一页中第一条的ID肯定会早于当前页最后一条的ID。所以,为了显示下一页的Spittle,我们需要将一个Spittle的ID传入进来,这个ID恰好要小于前一页最后一条Spittle的ID,另外还可以传递一个参数来确定要展现的Spittle数量。为了实现这个分页的功能,我们所编写的处理器方法要接受如下的参数:

  • before参数(表明结果中所有Spittle的ID均应该在这个值之前)
  • count参数(表明在结果中要包含的Spittle数量)

为了实现这个功能,我们将之前SpittleController的spittles()方法替换为使用before和count参数的新spittles()方法。

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

SpittleController中处理器方法要同时处理有参数和没有参数的场景,我们需要对其进行修改,让它能够接受参数。同时,如果这些参数不在请求中,就用默认值Long.MAX_VALUE和20。@RequestParam注解的defaultValue属性可以完成这个任务:

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

3.2 通过路径参数接受输入

假设我们需要根据指定的ID来展现某一个Spittle记录。其中一种方案就是编写处理器方法,通过使用@RequestParam注解,让它来接受ID作为查询参数:

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

这个处理器方法将会处理形如”/spittles/show?spittle_id=12345”这样的请求。尽管这也可以正常工作,但是从面向资源的角度来看这并不理想。在理想情况下,要识别的资源(Spittle)应该通过URL路标进行标示,而不是通过查询参数。对”/spittles/12345”发起GET请求要优先于”/spittles/show?spittle_id=12345”发起请求。前者能够识别出要查询的资源,而后者描述的是带有参数的一个操作–本质上是通过HTTP发去的RPC。
到目前为止,在我们编写的控制器中,所有的方法都映射到了(通过@RequestMapping)静态定义好的路径上。如果现在需要响应”/spittles/12345”这样的请求,我们编写的@RequestMapping要包含变量的部分,这部分代表了Spittle ID。为了实现这种路径变量,Spring MVC 允许我们在@RequestMapping路径中添加占位符。占位符的名称要用大括号(”{“和”}”)括起来。路径中的其他部分要与所处理的请求完全匹配,但是占位符可以是任意的值。

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

我们可以看到,spittle()方法的spittleId参数上添加了@PathVariable(“spittleId”)注解,这表明在请求路径中,不管占位符部分的值是什么都会传递给处理器方法的spittled参数中。如果对”/spittles/12345”发送GET请求,那么将会把”12345”传递进来,作为spittleId的值。
需要注意的是:在样例中spittleId这个词出现了好几次:先是在@RequestMapping的路径中,然后作为@PathVariable属性的值,最后又作为方法的参数名称。因为方法的参数名碰巧与占位符的名称相同,因此我们可以去掉@PathVariable中的value属性:

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

如果@PathVariable注解中没有value属性的话,它会假设占位符的名称与方法的参数名相同。这能够让代码稍微简洁一些,因为不必重复写占位符的名称了。但需要注意的是,如果你想要重命名参数时,必须要同时修改占位符的名称,使其互相匹配。

4 处理表单

使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。在Spittr应用中,我们需要有个表单让新用户进行注册。

package com.godman.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/spitter")
public class SpitterController {

    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String showRegistrationrForm(){
        return "registerForm";
    }
}

这个方法要处理的是针对”/spitter/register”的GET请求。按照我们配置InternalResourceViewResolver的方式,这意味着将会使用”/WEB-INF/views/registerForm.jsp”这个JSP来渲染注册表单。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Register</title>
</head>
<body>
    <form method="post">
        First Name: <input type="text" name="firstName"/> <br/>
        Last Name: <input type="text" name="lastName"/> <br/>
        Username: <input type="text" name="username"/> <br/>
        Password: <input type="password" name="password"> <br/>
        <input type="submit" value="Register">
    </form>
</body>
</html>

需要注意的是这个form表单并没有设置action属性。在这种情况之下,当表单提交时,它会提交到与展现时相同的URL路径上。也就是说它会提交到”/spitter/register”上。这意味着需要在服务器处理该HTTP POST请求。我们需要在SpitterController中再添加一个方法来处理这个表单提交:

package com.godman.spittr.web;

import com.godman.spittr.data.SpitterRepository;
import com.godman.spittr.vo.Spitter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/spitter")
public class SpitterController {

    private SpitterRepository spitterRepository;

    @Autowired
    public SpitterController(SpitterRepository spitterRepository){
        this.spitterRepository = spitterRepository;
    }

    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String showRegistrationrForm(){
        return "registerForm";
    }

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

我们之前创建的showRegistrationrForm()方法仍然在,不过请注意新创建的processRegistration()方法,它接受一个Spitter对象作为参数。这个对象有firstName,lastName,username,password属性,这些属性将会使用请求中同名的参数进行填充。
processRegistration方法最后会返回一个Stirng类型用来指定视图。但是这个视图格式和以前我们看到的视图有所不同。这里不仅返回了视图的名称供视图解析器查找目标视图,而且返回的值还带有重定向的格式。当InternalResourceViewResolver看到视图格式中的”redirect:”前缀时,它就知道要将其解析为重定向的规则而不是视图的名称。在本例中,它会重定向到用户基本信息的页面。例如,如果Spitter.username的属性值是”jbauer”,那么视图将会重定向到”/spitter/jbauer”。需要注意的是,除了”redirect:”,InternalResourceViewResolver还能识别”forward:”前缀。当它飞行爱你视图格式中以”forward:”作为前缀时,请求将会前往(forward)指定的URL路径,而不再是重定向。
我们再在SpitterController增加一个处理器方法,用来处理针对基本信息页面的请求。

@RequestMapping(value="/{username}", method = RequestMethod.GET)
public String showSpitterProfile(@PathVariable String username, Model model){
    Spitter spitter = spitterRepository.findByUsername(username);
    model.addAttribute(spitter);
    return "profile";
}

这个方法将会返回逻辑视图”profile”,解析为”/WEB-INF/views/profiles.jsp”

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Profile</title>
</head>
<body>
    <h2>Your Profile</h2>
    <c:out value="${spitter.username}"/> <br/>
    <c:out value="${spitter.firstName}"/> <br/>
    <c:out value="${spitter.lastName}"/>
</body>
</html>

其最终页面展示结果为:

4.2 校验表单

如果用户在提交表单时,username或password文本域为空的话,将会导致在新建Spitter对象中,username或password为空。我们需要阻止这种情况的发生,有一个好的办法就是限制这些输入域的长度,保持它们的值在它的合理长度范围内,避免这些输入域的误用。
有种处理校验的方式非常的初级,那就是在processRegistration()方法中添加代码来检查值的合法性,如果值不合法的话,就将注册表单重新显示给客户。但是这样会因此添加一些额外的if判断语句,导致语意混淆。与其让校验逻辑弄乱我们的处理器方法,还不如使用Spring对Java校验API(Java Validation API,又称JSR-303)的支持。从Spring3.0开始,在Spring MVC中提供了对Java校验API的支持。在Spring MVC中要使用Java校验API的话,并不需要什么额外的配置。只要保证在类路径下包含这个Java API的实现即可,比如Hibernate Validator。
Java校验API定义了多个注解,这些注解可以放到属性上,从而限制这些属性的值。所有的注解都位于javax.validation.constraints包中。下表列出了这些校验注解:

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

除上表的注解外,Java校验API的实现可能还会提供额外的校验注解。同时,也可以定义自己的限制条件。请考虑添加到Spitter域上的限制条件,似乎需要使用@NotNull和@Size注解:

package com.godman.spittr.vo;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class Spitter {
    @NotNull(message = "firstName不允许为空")
    @Size(min = 2, max = 30)
    private String firstName;

    @NotNull(message = "lastName不允许为空")
    @Size(min = 5, max = 25)
    private String lastName;

    @NotNull(message = "username不允许为空")
    @Size(min = 5, max = 25)
    private String username;

    @NotNull(message = "password不允许为空")
    @Size(min = 5, max = 25)
    private String password;
    ......
}

我们已经为Spitter的所有属性添加了@NotNull注解,以确保它们的值不会是null。同时添加了@Size注解以限制它们的长度在给定的最大值和最小值之间。接下来需要修改processRegistration()方法来启动校验功能。

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

在这里,Spitter参数添加了@Valid注解,这会告诉Spring,需要确保这个对象满足校验限制。在Spitter属性上添加校验限制并不能阻止表单的提交。即便用户没有填写某个域或者某个域给定的值超出了最大长度,processRegistration()方法依然会被调用。这样,我们就需要处理校验的错误,就像processRegistration()所示例。
如果有校验出现错误的话,那么这些错误可以通过Errors对象进行访问,现在这个对象已作为processRegistration()方法的参数。(很重要一点需要注意,Errors参数要紧跟在带有@Valid注解的参数后面,@Valid注解所标注的就是要校验的参数。)processRegistration()方法所要做的第一件事情就是调用Errors.hasErrors()开检查是否有错误。如果有错误的话,Errors.hasErrors()将会返回到registerForm,也就是注册表单视图。这能够让用户的浏览器重新回到注册表单页面,所以他们能够修正错误然后重新提交。现在,会显示空的表单,但是在下一章,我们将在表单中显示最初提交的值并将校验错误反馈给用户。

猜你喜欢

转载自blog.csdn.net/denghj618/article/details/81811859