《Spring实战》学习笔记-第五章:构建Spring web应用

<div id="article_content" class="article_content tracking-ad" data-mod="popu_307" data-dsm="post">


<blockquote>
<p>之前一直在看《<a href="http://lib.csdn.net/base/javaee" class="replace_word" title="Java EE知识库" target="_blank" style="color:#df3434; font-weight:bold;">spring</a>实战》第三版,看到第五章时发现很多东西已经过时被废弃了,于是现在开始读《Spring实战》第四版了,章节安排与之前不同了,里面应用的应该是最新的技术。</p>
</blockquote>
<p>本章中,将会接触到Spring MVC基础,以及如何编写控制器来处理web请求,如何通明地绑定请求参数到业务对象上,同时还可以提供数据校验和错误处理的功能。</p>
<h1><a name="t0" target="_blank"></a>Spring MVC初探</h1>
<h2><a name="t1" target="_blank"></a>跟踪Spring MVC请求</h2>
<div class="image-package imagebubble"><img src="http://hoxis-github-io.qiniudn.com/160118-spring-in-action-5.1.png" class="imagebubble-image" alt=""><br>
<div class="image-caption">请求会由DispatcherServlet分配给控制器(根据处理器映射来确定),在控制器完成处理后,接着请求会被发送给一个视图来呈现结果</div>
</div>
<p>在请求离开浏览器时,会带有用户所请求内容的信息,例如请求的URL、用户提交的表单信息。</p>
<p>请求旅程的第一站是Spring的DispatcherServlet。Spring MVC所有的请求都会通过一个前端控制器Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。在Spring MVC中,DispatcherServlet 就是前端控制器。</p>
<p>DispatcherServlet的任务是将请求发送给Spring MVC<strong>控制器</strong>。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器, Dispatcher Servlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet会查询一个或多个处理器映射来确定请求的下一站在哪里。<strong>处理器映射</strong>会根据请求所携带的URL信息来进行决策。</p>
<p>一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器。到达了控制器,请求会卸下其负载(用户提交的信息)并等待控制器处理这些信息(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给个或多个服务对象)。</p>
<p>控制器在完成逻辑处理后通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为<strong>模型</strong>(Model)。不过仅仅给用户返回原始的信息是不够的--这些信息需要以用户友好的方式进行格式化,一般是HTML。所以,信息需要发送给—个<strong>视图</strong>(View),通常会是JSP。</p>
<p>控制器所做的最后一件事是<strong>将模型数据打包</strong>,并且标示出用于渲染输出的视图名称。它接下来会将请求连同模型和视图名称发送回DispatcherServlet。</p>
<p>这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名称并不直接表示某个特定的JSP。它仅仅传递了一个逻辑名,这个名字将会用来查找用来产生结果的真正视图。DispatcherServlet将会使用<strong>视图解析器</strong>来将逻辑视图名匹配为一个特定的视图实现。</p>
<p>既然DispatcherServlet已经知道由哪个视图渲染结果,那么请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是JSP),在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,并通过这个输出将响应对象传递给客户端。</p>
<h2><a name="t2" target="_blank"></a>搭建Spring MVC</h2>
<h3><a name="t3" target="_blank"></a>配置DispatcherServlet</h3>
<p>DispatcherServlet是Spring MVC的核心,它负责将请求分发到其他各个组件。</p>
<p>在旧版本中,DispatcherServlet之类的servlet一般在<code>web.xml</code>文件中配置,该文件一般会打包进最后的war包种;但是Spring3引入了注解,我们在这一章将展示如何基于注解配置Spring MVC。</p>
<p><strong>注意:</strong><br>
在使用maven构建web工程时,由于缺少web.xml文件,可能会出现<code>web.xml is missing and &lt;failOnMissingWebXml&gt; is set to true</code>这样的错误,那么可以通过在pom.xml文件中添加如下配置来避免这种错误:</p>
<pre class="hljs xml" name="code"><code class="xml"><span class="hljs-tag">&lt;<span class="hljs-title">build</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">plugins</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">plugin</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">groupId</span>&gt;</span>org.apache.maven.plugins<span class="hljs-tag">&lt;/<span class="hljs-title">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">artifactId</span>&gt;</span>maven-war-plugin<span class="hljs-tag">&lt;/<span class="hljs-title">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">version</span>&gt;</span>2.6<span class="hljs-tag">&lt;/<span class="hljs-title">version</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">configuration</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-title">failOnMissingWebXml</span>&gt;</span>false<span class="hljs-tag">&lt;/<span class="hljs-title">failOnMissingWebXml</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-title">configuration</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-title">plugin</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">plugins</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">build</span>&gt;</span></code></pre>
<p>既然不适用<code>web.xml</code>文件,你需要在servlet容器中使用<a href="http://lib.csdn.net/base/javase" class="replace_word" title="Java SE知识库" target="_blank" style="color:#df3434; font-weight:bold;">Java</a>配置DispatcherServlet,具体的代码列举如下:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.config;


<span class="hljs-keyword">import</span> org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpittrWebAppInitializer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AbstractAnnotationConfigDispatcherServletInitializer</span> </span>{


    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> String[] getServletMappings() {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> String[] { <span class="hljs-string">"/"</span> };
    }


    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> Class&lt;?&gt;[] getRootConfigClasses() {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Class&lt;?&gt;[] { RootConfig.class };
    }


    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> Class&lt;?&gt;[] getServletConfigClasses() {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Class&lt;?&gt;[] { WebConfig.class };
    }


}</code></pre>
<p>任意继承自<code>AbstractAnnotationConfigDispatcherServletInitializer</code>的类都会被自动用来配置DispatcherServlet,这个类负责<strong>配置DispatcherServlet</strong>、<strong>初始化Spring MVC容器和Spring容器</strong>。</p>
<p>SpittrWebAppInitializer重写了三个方法,<code>getRootConfigClasses()</code>方法用于获取Spring应用容器的配置文件,这里我们给定预先定义的RootConfig.class;<code>getServletConfigClasses()</code>负责获取SpringMVC应用容器,这里传入预先定义好的WebConfig.class;<code>getServletMappings()</code>方法负责指定需要由DispatcherServlet映射的路径,这里给定的是"/",意思是由DispatcherServlet处理所有向该应用发起的请求。</p>
<h3><a name="t4" target="_blank"></a>两种应用上下文</h3>
<p>当DispatcherServlet启动时,会创建一个Spring应用上下文并且会加载配置文件中声明的bean,通过<code>getServletConfigClasses()</code>方法,DispatcherServlet会加载<code>WebConfig</code>配置类中所配置的bean。</p>
<p>在Spring web应用中,通常还有另外一种应用上下文:<code>ContextLoaderListener</code>。</p>
<p>DispatcherServlet用来加载web组件bean,如控制器(controllers)、视图解析器(view resolvers)以及处理器映射(handler mappings)等。而ContextLoaderListener则用来加载应用中的其他bean,如运行在应用后台的中间层和数据层组件。</p>
<p>AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListener。<code>getServletConfigClasses()</code>方法返回的<code>@Configuration</code>类会定义DispatcherServlet应用上下文的bean。同时,<code>getRootConfigClasses()</code>返回的<code>@Configuration</code>类用来配置ContextLoaderListener上下文创建的bean。</p>
<p>相对于传统的<code>web.xml</code>文件配置的方式,通过AbstractAnnotationConfigDispatcherServletInitializer来配置DispatcherServlet是一种替代方案。需要注意的是,这种配置只适用于<strong>Servlet 3.0</strong>,例如Apache Tomcat 7或者更高。</p>
<h3><a name="t5" target="_blank"></a>激活Spring MVC</h3>
<p>正如有多种方式可以配置DispatcherServlet,激活Spring MVC组件也有不止一种方法。一般的,都会通过XML配置文件的方式来配置Spring,例如可以通过<code>&lt;mvc:annotation-driven&gt;</code>来激活基于注解的Spring MVC。
</p>
<p>最简单的配置Spring MVC的一种方式是通过<code>@EnableWebMvc</code>注解:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.config;


<span class="hljs-keyword">import</span> org.springframework.context.annotation.Configuration;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.config.annotation.EnableWebMvc;


<span class="hljs-annotation">@Configuration</span>
<span class="hljs-annotation">@EnableWebMvc</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebConfig</span> </span>{


}</code></pre>
<p><code>@Configuration</code>表示这是Java配置类;<code>@EnableWebMvc</code>注解用于启动Spring MVC特性。</p>
<p>这样就可以激活Spring MVC了,但是还有其他一些问题:</p>
<ul>
<li>没有配置视图解析器(view resolvers),这种情况下,Spring会默认使用<code>BeanNameViewResolver</code>,它会通过寻找那些与视图id匹配的bean以及实现了View接口的类进行视图解析;</li><li>没有激活组件扫描:这样Spring会寻找配置中明确声明的任意控制器;</li><li>DispatcherServlet会处理所有的请求,包括静态资源请求,如图片和样式(这些往往不是我们想要的)。</li></ul>
<p>因此,需要为WebConfig增加一些配置:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.config;


<span class="hljs-keyword">import</span> org.springframework.context.annotation.Bean;
<span class="hljs-keyword">import</span> org.springframework.context.annotation.ComponentScan;
<span class="hljs-keyword">import</span> org.springframework.context.annotation.Configuration;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.ViewResolver;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.config.annotation.EnableWebMvc;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.view.InternalResourceViewResolver;


<span class="hljs-annotation">@Configuration</span>
<span class="hljs-annotation">@EnableWebMvc</span>
<span class="hljs-annotation">@ComponentScan</span>(<span class="hljs-string">"spitter.web"</span>) <span class="hljs-comment">// 激活Spring MVC</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebConfig</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">WebMvcConfigurerAdapter</span> </span>{


    <span class="hljs-comment">// 配置一个JSP视图解析器</span>
    <span class="hljs-annotation">@Bean</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> ViewResolver <span class="hljs-title">viewResolver</span><span class="hljs-params">()</span> </span>{
        InternalResourceViewResolver resolver = <span class="hljs-keyword">new</span> InternalResourceViewResolver();
        resolver.setPrefix(<span class="hljs-string">"/WEB_INF/views/"</span>);
        resolver.setSuffix(<span class="hljs-string">".jsp"</span>);
        resolver.setExposeContextBeansAsAttributes(<span class="hljs-keyword">true</span>);
        <span class="hljs-keyword">return</span> resolver;
    }


    <span class="hljs-annotation">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">configureDefaultServletHandling</span><span class="hljs-params">(DefaultServletHandlerConfigurer configurer)</span> </span>{
        configurer.enable();
    }
}</code></pre>
<p>首先需要注意的是,<code>WebConfig</code>使用了<code>@ComponentScan</code>注解,因此会在<code>spitter.web</code>包下扫描寻找组件,这些组件包括使用<code>@Controller</code>进行注解的控制器。这样就不再需要在配置类中显式地声明其他控制器。</p>
<p>接下来,添加了一个<code>ViewResolver</code>bean,即<code>InternalResourceViewResolver</code>。它通过匹配符合设置的前缀和后缀的视图来用来寻找对应的JSP文件,比如视图home会被解析为/WEB-INF/views/home.jsp。这里的三个函数的含义依次是:<code>setPrefix()</code>方法用于设置视图路径的前缀;<code>setSuffix()</code>用于设置视图路径的后缀,即如果给定一个逻辑视图名称——"home",则会被解析成"/WEB-INF/views/home.jsp";
<code>setExposeContextBeansAsAttributes(true)</code>使得可以在JSP页面中通过${}访问容器中的bean。</p>
<p>然后,<code>WebConfig</code>继承自<code>WebMvcConfigurerAdapter</code>,并且重写了<code>configureDefaultServletHandling()</code>方法,通过调用<code>enable()</code>方法从而可以让DispatcherServlet将静态资源的请求转发给默认的servlet。</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.config;


<span class="hljs-keyword">import</span> org.springframework.context.annotation.ComponentScan;
<span class="hljs-keyword">import</span> org.springframework.context.annotation.ComponentScan.Filter;
<span class="hljs-keyword">import</span> org.springframework.context.annotation.Configuration;
<span class="hljs-keyword">import</span> org.springframework.context.annotation.FilterType;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.config.annotation.EnableWebMvc;


<span class="hljs-annotation">@Configuration</span>
<span class="hljs-annotation">@ComponentScan</span>(basePackages = { <span class="hljs-string">"spitter"</span> }, excludeFilters = {
        <span class="hljs-annotation">@Filter</span>(type = FilterType.ANNOTATION, value = EnableWebMvc.class) })
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RootConfig</span> </span>{


}</code></pre>
<p>需要注意的一点是,RootConfig 使用了<code>@ComponentScan</code>注解。</p>
<h2><a name="t6" target="_blank"></a>Spittr应用介绍</h2>
<p>这一章要用的例子应用,从Twitter获取了一些灵感,因此最开始叫Spitter;然后又借鉴了最近比较流行的网站Flickr,因此我们也把e去掉,最终形成Spittr这个名字。这也有利于区分领域名称(类似于twitter,这里用spring实现,因此叫spitter)和应用名称。</p>
<p>Spittr类似于Twitter,用户可以通过它添加一些推文。Spittr有两个重要的概念:<em>spitter</em>(应用的用户)和<em>spittle</em>(用户发布简单状态)。本章将会构建该应用的web层、创建用于展示spittle的控制器以及用户注册流程。</p>
<h1><a name="t7" target="_blank"></a>编写简单的控制器</h1>
<p>Spring MVC中,控制器仅仅是拥有<code>@RequestMapping</code>注解方法的类,从而可以声明它们可以处理何种请求。</p>
<p>在开始之前,我们先假设一个控制器,它可以处理匹配<code>/</code>的请求并会跳转到主页面。</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.web;


<span class="hljs-keyword">import</span> org.springframework.stereotype.Controller;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestMethod;


<span class="hljs-annotation">@Controller</span> <span class="hljs-comment">// 声明一个控制器</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeController</span> </span>{


    <span class="hljs-annotation">@RequestMapping</span>(value = <span class="hljs-string">"/"</span>, method = RequestMethod.GET) <span class="hljs-comment">// 处理GET请求</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">home</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"home"</span>;
    }


}</code></pre>
<p><code>@Controller</code>是一个构造型注解,它基于<code>@Component</code>,组件扫描器会自动地将HomeController声明为Spring上下文的一个bean。</p>
<p>home()方法采用了<code>@RequestMapping</code>注解,属性<code>value</code>指定了该方法处理的请求路径,<code>method</code>方法指定了可以处理的HTTP方法。这种情况下,当一个来自<code>/</code>的GET方法请求时,就会调用home()方法。</p>
<p>home()方法仅仅返回了一个"home"的String值,Spring MVC会对这个String值进行解析并跳转到指定的视图上。<code>DispatcherServlet</code>则会请求视图解析器将这个逻辑视图解析到真实视图上。</p>
<p>我们已经配置了InternalResourceViewResolver,“home”视图会被解析到<code>/WEB-INF/views/home.jsp</code>。</p>
<pre class="hljs xml" name="code"><code class="xml"><span class="hljs-tag">&lt;<span class="hljs-title">%@</span> <span class="hljs-attribute">page</span> <span class="hljs-attribute">language</span>=<span class="hljs-value">"java"</span> <span class="hljs-attribute">contentType</span>=<span class="hljs-value">"text/html; charset=UTF-8"</span>
    <span class="hljs-attribute">pageEncoding</span>=<span class="hljs-value">"UTF-8"</span><span class="hljs-value">%</span>&gt;</span>


<span class="hljs-tag">&lt;<span class="hljs-title">%@</span> <span class="hljs-attribute">taglib</span> <span class="hljs-attribute">prefix</span>=<span class="hljs-value">"c"</span> <span class="hljs-attribute">uri</span>=<span class="hljs-value">"http://java.sun.com/jsp/jstl/core"</span><span class="hljs-value">%</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">%@</span> <span class="hljs-attribute">taglib</span> <span class="hljs-attribute">prefix</span>=<span class="hljs-value">"spring"</span> <span class="hljs-attribute">uri</span>=<span class="hljs-value">"http://www.springframework.org/tags"</span><span class="hljs-value">%</span>&gt;</span>


<span class="hljs-tag">&lt;<span class="hljs-title">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">meta</span> <span class="hljs-attribute">charset</span>=<span class="hljs-value">"utf-8"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">title</span>&gt;</span>Spittr<span class="hljs-tag">&lt;/<span class="hljs-title">title</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">link</span> <span class="hljs-attribute">rel</span>=<span class="hljs-value">"stylesheet"</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"text/css"</span>
    <span class="hljs-attribute">href</span>=<span class="hljs-value">"&lt;c:url value="</span>/<span class="hljs-attribute">resources</span>/<span class="hljs-attribute">style.css</span>" /&gt;</span>"&gt;
<span class="hljs-tag">&lt;/<span class="hljs-title">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">h1</span>&gt;</span>Welcome to Spittr<span class="hljs-tag">&lt;/<span class="hljs-title">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">a</span> <span class="hljs-attribute">href</span>=<span class="hljs-value">"&lt;c:url value="</span>/<span class="hljs-attribute">spittles</span>" /&gt;</span>"&gt;Spittles<span class="hljs-tag">&lt;/<span class="hljs-title">a</span>&gt;</span> |
    <span class="hljs-tag">&lt;<span class="hljs-title">a</span> <span class="hljs-attribute">href</span>=<span class="hljs-value">"&lt;c:url value="</span>/<span class="hljs-attribute">spitter</span>/<span class="hljs-attribute">register</span>" /&gt;</span>"&gt;Register<span class="hljs-tag">&lt;/<span class="hljs-title">a</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">html</span>&gt;</span></code></pre>
<p>下面对HomeController进行<a href="http://lib.csdn.net/base/softwaretest" class="replace_word" title="软件测试知识库" target="_blank" style="color:#df3434; font-weight:bold;">测试</a>。</p>
<h2><a name="t8" target="_blank"></a>测试控制器</h2>
<p>一般的web测试需要将工程发布到一个web容器中,启动后才能观察运行结果。<br>
如:<br>
</p>
<div class="image-package imagebubble"><img src="http://hoxis-github-io.qiniudn.com/160118-spring-in-action-homepage.png" class="imagebubble-image" alt=""><br>
<div class="image-caption">主页</div>
</div>
<p>从另外的角度来看,HomeController其实是一个简单的POJO对象,那么可以使用下面的方法对其进行测试:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.web;


<span class="hljs-keyword">import</span> org.junit.Test;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.MockMvc;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.result.MockMvcResultMatchers;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.setup.MockMvcBuilders;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeControllerTest</span> </span>{


    <span class="hljs-annotation">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testHomePage</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        HomeController controller = <span class="hljs-keyword">new</span> HomeController();
        <span class="hljs-comment">// 设置MockMvc</span>
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
        mockMvc.perform(MockMvcRequestBuilders.get(<span class="hljs-string">"/"</span>)).andExpect(MockMvcResultMatchers.view().name(<span class="hljs-string">"home"</span>));
    }


}</code></pre>
<p>相对于直接调用home()方法测试它的返回值,上面的测试中发起了一个来自<code>/</code>的 GET 请求,并且对其结果视图进行断言。将HomeController的实例传送给<code>MockMvcBuilders.standaloneSetup</code>,并且调用<code>build()</code>方法来创建一个<code>MockMvc</code>实例。然后,使用<code>MockMvc</code>实例产生了一个GET请求,并且设置了视图的期望。</p>
<h2><a name="t9" target="_blank"></a>定义类层级的请求处理</h2>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.web;


<span class="hljs-keyword">import</span> org.springframework.stereotype.Controller;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestMethod;


<span class="hljs-annotation">@Controller</span> <span class="hljs-comment">// 声明一个控制器</span>
<span class="hljs-annotation">@RequestMapping</span>(<span class="hljs-string">"/"</span>) <span class="hljs-comment">// 控制器匹配路径</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeController</span> </span>{


    <span class="hljs-annotation">@RequestMapping</span>(method = RequestMethod.GET) <span class="hljs-comment">// 处理GET请求</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">home</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"home"</span>;<span class="hljs-comment">// 视图名称</span>
    }


}</code></pre>
<p>在这个新版的HomeController中,将请求匹配路径移到了类层级,HTTP方法的匹配仍处在方法层级。当有控制类中有一个类层级的<code>@RequestMapping</code>,该类中所有的用<code>@RequestMapping</code>注解的处理方法共同组成了类层级的<code>@RequestMapping</code>。</p>
<p><code>@RequestMapping</code>的value属性接受String数组,那么就可以使用如下配置:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@Controller</span> <span class="hljs-comment">// 声明一个控制器</span>
<span class="hljs-annotation">@RequestMapping</span>(<span class="hljs-string">"/"</span>, <span class="hljs-string">"/homepage"</span>) <span class="hljs-comment">// 控制器匹配路径</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeController</span> </span>{
...
}</code></pre>
<p>这种情况下,home()方法就可以处理来自<code>/</code>和<code>/homepage</code>的GET请求。</p>
<h2><a name="t10" target="_blank"></a>将model数据传送给视图</h2>
<p>在Spittr应用中,需要一个页面,用来显示最近提交的spittle清单。首先需要定义一个数据访问的仓库,用来抓取spittle:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.data;


<span class="hljs-keyword">import</span> java.util.List;
<span class="hljs-keyword">import</span> spittr.Spittle;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">SpittleRepository</span> </span>{
    <span class="hljs-comment">/**
     * <span class="hljs-doctag">@param</span> max
     *            待返回的最大的Spittle ID
     * <span class="hljs-doctag">@param</span> count
     *            返回Spittle对象的个数
     * <span class="hljs-doctag">@return</span>
     */</span>
    <span class="hljs-function">List&lt;Spittle&gt; <span class="hljs-title">findSpittles</span><span class="hljs-params">(<span class="hljs-keyword">long</span> max, <span class="hljs-keyword">int</span> count)</span></span>;
}</code></pre>
<p>如果要获取最近的20个Spittle对象,那么只需调用这样调用:<br>
<code>List&lt;Spittle&gt; recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);</code></p>
<p>下面对Spittle进行定义:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr;


<span class="hljs-keyword">import</span> java.util.Date;


<span class="hljs-keyword">import</span> org.apache.commons.lang3.builder.EqualsBuilder;
<span class="hljs-keyword">import</span> org.apache.commons.lang3.builder.HashCodeBuilder;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Spittle</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Long id;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String message;<span class="hljs-comment">// 消息</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Date time;<span class="hljs-comment">// 时间戳</span>
    <span class="hljs-keyword">private</span> Double latitude;
    <span class="hljs-keyword">private</span> Double longitude;


    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Spittle</span><span class="hljs-params">(String message, Date time)</span> </span>{
        <span class="hljs-keyword">this</span>(message, time, <span class="hljs-keyword">null</span>, <span class="hljs-keyword">null</span>);
    }


    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Spittle</span><span class="hljs-params">(String message, Date time, Double latitude, Double longitude)</span> </span>{
        <span class="hljs-keyword">this</span>.id = <span class="hljs-keyword">null</span>;
        <span class="hljs-keyword">this</span>.message = message;
        <span class="hljs-keyword">this</span>.time = time;
        <span class="hljs-keyword">this</span>.latitude = latitude;
        <span class="hljs-keyword">this</span>.longitude = longitude;
    }


    <span class="hljs-annotation">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">equals</span><span class="hljs-params">(Object that)</span> </span>{
        <span class="hljs-keyword">return</span> EqualsBuilder.reflectionEquals(<span class="hljs-keyword">this</span>, that, <span class="hljs-string">"id"</span>, <span class="hljs-string">"time"</span>);
    }


    <span class="hljs-annotation">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">hashCode</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> HashCodeBuilder.reflectionHashCode(<span class="hljs-keyword">this</span>, <span class="hljs-string">"id"</span>, <span class="hljs-string">"time"</span>);
    }


    <span class="hljs-comment">//getters and setters</span>


}</code></pre>
<p>Spittle对象中现在包含信息、时间戳、位置这几个属性。</p>
<p>下面利用Spring的<code>MockMvc</code>来断言新的控制器的行为是否正确:</p>
<pre class="hljs java" name="code"><code class="java"></code></pre>
<p>上面的测试通过创建一个SpittleRepository接口的mock实现,该实现会通过findSpittles()方法返回一个包含20个Spittle对象的集合。然后将这个bean注入到SpittleController实例中,并设置MockMvc使用该实例。</p>
<p>不同于HomeControllerTest,该测试使用了<code>setSingleView()</code>,发起一个<code>/spittles</code>的GET请求,并断言视图是否为spittles以及model是否含有一个spittleList的属性值。</p>
<p>当然,现在运行这个测试代码肯定是会出错的,因为还没有SpittleController。</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.web;


<span class="hljs-keyword">import</span> java.util.ArrayList;
<span class="hljs-keyword">import</span> java.util.Date;
<span class="hljs-keyword">import</span> java.util.List;


<span class="hljs-keyword">import</span> org.hamcrest.core.IsCollectionContaining;
<span class="hljs-keyword">import</span> org.junit.Test;
<span class="hljs-keyword">import</span> org.mockito.Mockito;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.MockMvc;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.result.MockMvcResultMatchers;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.setup.MockMvcBuilders;
<span class="hljs-keyword">import</span> org.springframework.web.servlet.view.InternalResourceView;


<span class="hljs-keyword">import</span> spittr.Spittle;
<span class="hljs-keyword">import</span> spittr.data.SpittleRepository;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpittleControllerTest</span> </span>{


    <span class="hljs-annotation">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">shouldShowRecentSpittles</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        List&lt;Spittle&gt; expectedSpittles = createSpittleList(<span class="hljs-number">20</span>);
        SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
        Mockito.when(mockRepository.findSpittles(Long.MAX_VALUE, <span class="hljs-number">20</span>)).thenReturn(expectedSpittles);


        SpittleController controller = <span class="hljs-keyword">new</span> SpittleController(mockRepository);
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setSingleView(<span class="hljs-keyword">new</span> InternalResourceView(<span class="hljs-string">"/WEB_INF/views/spittles.jsp"</span>)).build();


        <span class="hljs-comment">// 调用MockMvc.perform(RequestBuilder requestBuilder)发起一个http请求,然后将得到ResultActions</span>
        mockMvc.perform(MockMvcRequestBuilders.get(<span class="hljs-string">"/spittles"</span>))<span class="hljs-comment">// 添加验证断言来判断执行请求后的结果是否是预期的;</span>
                .andExpect(MockMvcResultMatchers.view().name(<span class="hljs-string">"spittles"</span>))<span class="hljs-comment">// view():得到视图验证器;</span>
                <span class="hljs-comment">// 得到相应的***ResultMatchers后,接着再调用其相应的API得到ResultMatcher,</span>
                <span class="hljs-comment">// 如ModelResultMatchers.attributeExists(final String... names)判断Model属性是否存在。</span>
                .andExpect(MockMvcResultMatchers.model().attributeExists(<span class="hljs-string">"spittleList"</span>))<span class="hljs-comment">// model():得到模型验证器;</span>
                .andExpect(MockMvcResultMatchers.model().attribute(<span class="hljs-string">"spittleList"</span>, IsCollectionContaining.hasItems(expectedSpittles.toArray())));
    }


    <span class="hljs-function"><span class="hljs-keyword">private</span> List&lt;Spittle&gt; <span class="hljs-title">createSpittleList</span><span class="hljs-params">(<span class="hljs-keyword">int</span> count)</span> </span>{
        List&lt;Spittle&gt; spittles = <span class="hljs-keyword">new</span> ArrayList&lt;Spittle&gt;();
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; count; i++) {
            spittles.add(<span class="hljs-keyword">new</span> Spittle(<span class="hljs-string">"Spittle "</span>, <span class="hljs-keyword">new</span> Date()));
        }
        <span class="hljs-keyword">return</span> spittles;
    }


}</code></pre>
<p>SpittleController中,使用@Autowired注解注入了spittleRepository属性。</p>
<p>需要注意的是<code>spittles()</code>方法使用了Model(<em>控制器和视图之间传递的数据</em>)作为入参,Model本质上是一个map,它会被传送至view,因此数据可以提供给客户端。如果在调用<code>addAttribute()</code>方法时没有指定key,那么就会从传入的对象中获取,比如代码中传入的参数属性是List&lt;Spittle&gt;,那么key就是spittleList。最后,该方法返回spittles作为传动给model的视图名称。</p>
<p>也可以显示的指定key:</p>
<pre class="hljs java" name="code"><code class="java">model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, <span class="hljs-number">20</span>));</code></pre>
<p>也可以直接采用map的方式:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@RequestMapping</span>(method = RequestMethod.GET)
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">spittles</span><span class="hljs-params">(Map model)</span> </span>{
    <span class="hljs-comment">// 将spittles添加到model中</span>
    model.put(<span class="hljs-string">"spittles"</span>, spittleRepository.findSpittles(Long.MAX_VALUE, <span class="hljs-number">20</span>));


    <span class="hljs-comment">// 返回视图名称</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"spittles"</span>;
}</code></pre>
<p>不管采用何种方式实现spittles()方法,结果都是一样的。一个Spittle对象集合会存储在model中,并分配到名为spittles的view中,根据测试方法中的配置,该视图就是/WEB-INF/views/spittles.jsp。</p>
<p>现在model已经有数据了,那么JSP页面中如何获取数据呢?当视图是一个JSP页面时,model数据会作为请求属性被拷贝到请求中,因此可以通过JSTL(JavaServer Pages Standard Tag Library)<code>&lt;c:forEach&gt;</code>来获取:</p>
<pre class="hljs xml" name="code"><code class="xml"><span class="hljs-tag">&lt;<span class="hljs-title">c:forEach</span> <span class="hljs-attribute">items</span>=<span class="hljs-value">"${spittleList}"</span> <span class="hljs-attribute">var</span>=<span class="hljs-value">"spittle"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">li</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"spittle_&lt;c:out value="</span><span class="hljs-value">spittle.id"</span>/&gt;</span>"&gt;
        <span class="hljs-tag">&lt;<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"spittleMessage"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">c:out</span> <span class="hljs-attribute">value</span>=<span class="hljs-value">"${spittle.message}"</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-title">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"spittleTime"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-title">c:out</span> <span class="hljs-attribute">value</span>=<span class="hljs-value">"${spittle.time}"</span> /&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">span</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"spittleLocation"</span>&gt;</span> (<span class="hljs-tag">&lt;<span class="hljs-title">c:out</span>
                    <span class="hljs-attribute">value</span>=<span class="hljs-value">"${spittle.latitude}"</span> /&gt;</span>, <span class="hljs-tag">&lt;<span class="hljs-title">c:out</span>
                    <span class="hljs-attribute">value</span>=<span class="hljs-value">"${spittle.longitude}"</span> /&gt;</span>)
            <span class="hljs-tag">&lt;/<span class="hljs-title">span</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-title">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">li</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">c:forEach</span>&gt;</span></code></pre>
<p>下面对SpittleController进行扩展,让它可以处理一些输入。</p>
<h1><a name="t11" target="_blank"></a>接受输入请求</h1>
<p>Spring MVC提供了如下方式供客户端传递数据到控制器处理方法:</p>
<ul>
<li>Query parameters</li><li>Form parameters</li><li>Path variables</li></ul>
<h2><a name="t12" target="_blank"></a>处理查询参数:@RequestParam</h2>
<p>Spittr应用的一个需求就是要对spittle列表分页展示,但是SpittleController仅仅展示最近的spittle。如果要让用户可以每次得到一页的spittle记录,那么就需要让用户可以通过某种方式将他们想看的spittle记录的参数传递到后台。</p>
<p>在浏览spittle时,如果想要查看下一页的spittle,那么就需要传递比当前页的最后一个spittle的id小一位的id,也可以传递想要展示的spittle的数量。</p>
<p>为了实现分页,需要编写一个控制器满足:</p>
<ul>
<li><code>before</code>参数,结果中的spittle的id都要在这个参数之前;</li><li><code>count</code>参数,结果中要包含的spittle的个数</li></ul>
<p>下面我们对上面的<code>spittles()</code>方法进行小小的改动,让它可以使用before和count参数。首先对测试方法进行改动:</p>
<pre class="hljs java" name="code"><code class="java">    <span class="hljs-annotation">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">shouldShowRecentSpittles</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        List&lt;Spittle&gt; expectedSpittles = createSpittleList(<span class="hljs-number">20</span>);
        SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
        Mockito.when(mockRepository.findSpittles(<span class="hljs-number">238900</span>, <span class="hljs-number">50</span>)).thenReturn(expectedSpittles);


        SpittleController controller = <span class="hljs-keyword">new</span> SpittleController(mockRepository);
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setSingleView(<span class="hljs-keyword">new</span> InternalResourceView(<span class="hljs-string">"/WEB_INF/views/spittles.jsp"</span>)).build();


        <span class="hljs-comment">// 调用MockMvc.perform(RequestBuilder requestBuilder)发起一个http请求,然后将得到ResultActions</span>
        mockMvc.perform(MockMvcRequestBuilders.get(<span class="hljs-string">"/spittles?max=238900&amp;count=50"</span>))<span class="hljs-comment">// 添加验证断言来判断执行请求后的结果是否是预期的;</span>
                .andExpect(MockMvcResultMatchers.view().name(<span class="hljs-string">"spittles"</span>))<span class="hljs-comment">// view():得到视图验证器;</span>
                <span class="hljs-comment">// 得到相应的***ResultMatchers后,接着再调用其相应的API得到ResultMatcher,</span>
                <span class="hljs-comment">// 如ModelResultMatchers.attributeExists(final String... names)判断Model属性是否存在。</span>
                .andExpect(MockMvcResultMatchers.model().attributeExists(<span class="hljs-string">"spittleList"</span>))<span class="hljs-comment">// model():得到模型验证器;</span>
                .andExpect(MockMvcResultMatchers.model().attribute(<span class="hljs-string">"spittleList"</span>, IsCollectionContaining.hasItems(expectedSpittles.toArray())));
    }</code></pre>
<p>这个测试方法的主要改动就是它发起的GET请求传递了两个参数:max和count。对<code>spittles()</code>进行修改:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@RequestMapping</span>(method=RequestMethod.GET)
<span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;Spittle&gt; <span class="hljs-title">spittles</span><span class="hljs-params">(
        @RequestParam(value=<span class="hljs-string">"max"</span>, defaultValue=MAX_LONG_AS_STRING)</span> <span class="hljs-keyword">long</span> max, 
        @<span class="hljs-title">RequestParam</span><span class="hljs-params">(value=<span class="hljs-string">"count"</span>, defaultValue=<span class="hljs-string">"20"</span>)</span> <span class="hljs-keyword">int</span> count) </span>{
    <span class="hljs-keyword">return</span> spittleRepository.findSpittles(max, count);
}</code></pre>
<p>这种情况下,如果没有max参数没有指定,那么就会使用默认的设置。由于查询参数是String类型的,因此<code>defaultValue</code>属性值也需要设置为String类型,需要对Long.MAX_VALUE进行设置:<br>
<code>private static final String MAX_LONG_AS_STRING = "9223372036854775807";</code></p>
<p>虽然,这里defaultValue的属性为String类型,当运行到函数时,将会根据函数的参数类型进行转换。</p>
<p>查询参数是请求中传送信息给控制器的最常用方式,另外一种流行的方式就是将参数作为请求路径的一部分。</p>
<h2><a name="t13" target="_blank"></a>通过路径参数传递数据:@PathVariable</h2>
<p>假设现在应用需要展示单独的一篇Spittle,那么就需要一个id作为查询参数,对应的处理方法可以是:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@RequestMapping</span>(value=<span class="hljs-string">"show"</span>, method=RequestMethod.GET)
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">showSpittle</span><span class="hljs-params">(
        @RequestParam(<span class="hljs-string">"spittle_id"</span>)</span> <span class="hljs-keyword">long</span> spittleId,
        Model model
        ) </span>{
    model.addAttribute(spittleRepository.findOne(spittleId));
    <span class="hljs-keyword">return</span> <span class="hljs-string">"spittle"</span>;
}</code></pre>
<p>这个handler方法将会处理形如<code>/spittles/show?spittle_id=12345</code>的请求,但是这并不符合资源导向的观点。理想情况下,应该使用URL路径对资源进行区分,而不是查询参数,即应该使用<code>/spittles/12345</code>这种形式。</p>
<p>为了实现资源导向的控制器,我们先在测试中获得这个需求(使用了静态引入):</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@Test</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">testSpittle</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
    Spittle expectedSpittle = <span class="hljs-keyword">new</span> Spittle(<span class="hljs-string">"Hello"</span>, <span class="hljs-keyword">new</span> Date());
    SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
    when(mockRepository.findOne(<span class="hljs-number">12345</span>)).thenReturn(expectedSpittle);


    SpittleController controller = <span class="hljs-keyword">new</span> SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller).build();


    mockMvc.perform(get(<span class="hljs-string">"/spittles/12345"</span>))
    .andExpect(view().name(<span class="hljs-string">"spittle"</span>))
    .andExpect(model().attributeExists(<span class="hljs-string">"spittle"</span>))
    .andExpect(model().attribute(<span class="hljs-string">"spittle"</span>, expectedSpittle));
}</code></pre>
<p>该测试中发起了一个<code>/spittles/12345</code>的GET请求,并且对其返回结果视图进行断言。</p>
<p>为了满足路径参数,Spring MVC允许在<code>@RequestMapping</code>路径中使用占位符(需要用大括号包围),下面是使用占位符来接受一个id作为路径的一部分:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@RequestMapping</span>(value=<span class="hljs-string">"/{spittleId}"</span>, method=RequestMethod.GET)
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">spittle</span><span class="hljs-params">(
        @PathVariable(<span class="hljs-string">"spittleId"</span>)</span> <span class="hljs-keyword">long</span> spittleId,
        Model model
        ) </span>{
    model.addAttribute(spittleRepository.findOne(spittleId));
    <span class="hljs-keyword">return</span> <span class="hljs-string">"spittle"</span>;
}</code></pre>
<p>spittle()方法的spittleId入参使用了<code>@PathVariable("spittleId")</code>注解,表明请求中占位符位置的值都会被传送到handler的spittleId参数。<em>@RequestMapping中value属性的占位符必须和@PathVariable包裹的参数一致</em>。如果@PathVariable中没有给定参数,那么将默认使用入参的册数参数名。即可以使用下面的方法:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-annotation">@RequestMapping</span>(value=<span class="hljs-string">"/{spittleId}"</span>, method=RequestMethod.GET)
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">spittle</span><span class="hljs-params">(
        @PathVariable <span class="hljs-keyword">long</span> spittleId,
        Model model
        )</span> </span>{
    model.addAttribute(spittleRepository.findOne(spittleId));
    <span class="hljs-keyword">return</span> <span class="hljs-string">"spittle"</span>;
}</code></pre>
<p>spittle()方法会将接收的参数值传递给spittleRepository的findOne()方法并查找到一个Spittle,并将其放置到model中,model的key值会是spittle,接下来就可以在视图中引用这个Spittle:</p>
<pre class="hljs xml" name="code"><code class="xml"><span class="hljs-tag">&lt;<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"spittleView"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">div</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"spittleMessage"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">c:out</span> <span class="hljs-attribute">value</span>=<span class="hljs-value">"${spittle.message}"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">span</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"spittleTime"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-title">c:out</span> <span class="hljs-attribute">value</span>=<span class="hljs-value">"${spittle.time}"</span> /&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">span</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">div</span>&gt;</span></code></pre>
<p>查询参数和路径参数可以处理一些少量的请求数据,但是当请求数据过大时,它们就不再适用,下面就来讲解一下如何处理表单数据。</p>
<h1><a name="t14" target="_blank"></a>处理表单</h1>
<p>Web应用不仅仅是将内容推送给用户,它同时也会让用户填写表单并将数据提交给应用。</p>
<p>对于表单有两种处理方式:展示表单以及处理用户提交的表单数据。在Spittr中,需要提供一个供新用户进行注册的表单。</p>
<p><code>SpitterController</code>:展示用户注册表单</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.web;


<span class="hljs-keyword">import</span> org.springframework.stereotype.Controller;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestMethod;


<span class="hljs-annotation">@Controller</span>
<span class="hljs-annotation">@RequestMapping</span>(<span class="hljs-string">"/spitter"</span>)
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpitterController</span> </span>{


    <span class="hljs-comment">// 处理来自/spitter/register的get请求</span>
    <span class="hljs-annotation">@RequestMapping</span>(value = <span class="hljs-string">"/register"</span>, method = RequestMethod.GET)
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">showRegistrationForm</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"registerForm"</span>;
    }


}</code></pre>
<p><code>showRegistrationForm</code>方法的<code>@RequestMapping</code>注解,以及类级别的注解<code>@RequestMapping</code>,表明了这个方法会处理来自/spitter/register的get请求,该方法仅仅返回了一个名为registerForm的逻辑视图。根据之前在<code>InternalResourceViewResolver</code>中的配置,这个逻辑视图会导向到<code>/WEB-INF/views/registerForm.jsp</code>该界面。</p>
<p>对应的测试方法:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">package</span> spittr.web;


<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.springframework.test.web.servlet.setup.MockMvcBuilders.*;


<span class="hljs-keyword">import</span> org.junit.Test;
<span class="hljs-keyword">import</span> org.springframework.test.web.servlet.MockMvc;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpitterControllerTest</span> </span>{


    <span class="hljs-annotation">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">shouldShowRegistration</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        SpitterController controller = <span class="hljs-keyword">new</span> SpitterController();
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(get(<span class="hljs-string">"/spitter/register"</span>)).andExpect(view().name(<span class="hljs-string">"registerForm"</span>));
    }
}</code></pre>
<p>也可以通过启动项目访问界面的方式验证:</p>
<pre class="hljs xml" name="code"><code class="xml"><span class="hljs-tag">&lt;<span class="hljs-title">%@</span> <span class="hljs-attribute">taglib</span> <span class="hljs-attribute">uri</span>=<span class="hljs-value">"http://java.sun.com/jsp/jstl/core"</span> <span class="hljs-attribute">prefix</span>=<span class="hljs-value">"c"</span> %&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">%@</span> <span class="hljs-attribute">page</span> <span class="hljs-attribute">session</span>=<span class="hljs-value">"false"</span> %&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">title</span>&gt;</span>Spitter<span class="hljs-tag">&lt;/<span class="hljs-title">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">link</span> <span class="hljs-attribute">rel</span>=<span class="hljs-value">"stylesheet"</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"text/css"</span> <span class="hljs-attribute">href</span>=<span class="hljs-value">"&lt;c:url value="</span>/<span class="hljs-attribute">resources</span>/<span class="hljs-attribute">style.css</span>" /&gt;</span>" &gt;
  <span class="hljs-tag">&lt;/<span class="hljs-title">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">h1</span>&gt;</span>Register<span class="hljs-tag">&lt;/<span class="hljs-title">h1</span>&gt;</span>


    <span class="hljs-tag">&lt;<span class="hljs-title">form</span> <span class="hljs-attribute">method</span>=<span class="hljs-value">"POST"</span>&gt;</span>
      First Name: <span class="hljs-tag">&lt;<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"text"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"firstName"</span> /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-title">br</span>/&gt;</span>
      Last Name: <span class="hljs-tag">&lt;<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"text"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"lastName"</span> /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-title">br</span>/&gt;</span>
      Username: <span class="hljs-tag">&lt;<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"text"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"username"</span> /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-title">br</span>/&gt;</span>
      Password: <span class="hljs-tag">&lt;<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"password"</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"password"</span> /&gt;</span><span class="hljs-tag">&lt;<span class="hljs-title">br</span>/&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-title">input</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"submit"</span> <span class="hljs-attribute">value</span>=<span class="hljs-value">"Register"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">form</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-title">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">html</span>&gt;</span></code></pre>
<div class="image-package imagebubble"><img src="http://hoxis-github-io.qiniudn.com/160215-spring-in-action-rigister.png" class="imagebubble-image" alt=""><br>
<div class="image-caption">该界面提供了用户注册的功能</div>
</div>
<p>接下来需要对提交的表单进行处理。</p>
<h2><a name="t15" target="_blank"></a>编写表单处理控制器</h2>
<p>在处理POST请求时,控制器需要接受表单数据并且将这些数据存储为一个Spitter对象。为了避免重复提交,应该重定向到一个新的界面:用户信息页。在处理post请求时,一个聪明的做法就是在处理完成后发送一个重定向的请求,从而可以避免重复提交。</p>
<p>下面来实现控制器方法,从而可以处理注册请求。</p>
<pre class="hljs java" name="code"><code class="java">    <span class="hljs-keyword">private</span> SpitterRepository spitterRepository;


    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SpitterController</span><span class="hljs-params">()</span> </span>{
    }


    <span class="hljs-comment">// 注入SpitterRepository</span>
    <span class="hljs-annotation">@Autowired</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SpitterController</span><span class="hljs-params">(SpitterRepository spitterRepository)</span> </span>{
        <span class="hljs-keyword">this</span>.spitterRepository = spitterRepository;
    }


    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">processRegistration</span><span class="hljs-params">(Spitter spitter)</span> </span>{
        <span class="hljs-comment">// 保存Spitter</span>
        spitterRepository.save(spitter);
        <span class="hljs-comment">// 重定向到新的页面</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"redirect:/spitter/"</span> + spitter.getUsername();
    }
`</code></pre>
<p>processRegistration方法使用Spitter对象作为入参,该对象的属性会从请求中填充。该方法中调用了spitterRepository的save方法对Spitter对象进行存储。最后返回了一个带有<code>redirect:</code>的字符串。</p>
<p>当InternalResourceViewResolver遇到<code>redirect:</code>时,它会自动地将其当做一个重定向请求,从而可以重定向到用户详情页面,如/spitter/xiaoming。</p>
<p>同时,InternalResourceViewResolver也可以识别前缀<code>forward:</code>,这种情况下,请求会被转向到给定的URL地址。</p>
<p>下面需要编写处理处理用户详情页面的方法:</p>
<pre class="hljs java" name="code"><code class="java">    <span class="hljs-annotation">@RequestMapping</span>(value = <span class="hljs-string">"/{username}"</span>, method = RequestMethod.GET)
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">showSpitterProfile</span><span class="hljs-params">(@PathVariable(<span class="hljs-string">"username"</span>)</span> String username, Model model) </span>{
        Spitter spitter = spitterRepository.findByUsername(username);
        model.addAttribute(spitter);
        <span class="hljs-keyword">return</span> <span class="hljs-string">"profile"</span>;
    }</code></pre>
<h2><a name="t16" target="_blank"></a>参数校验</h2>
<p>从Spring3.0开始,Spring支持Java校验api,从而可以从而可以不需要添加其他配置,仅仅需要有一个<a href="http://lib.csdn.net/base/java" class="replace_word" title="Java 知识库" target="_blank" style="color:#df3434; font-weight:bold;">Java </a>API 的实现,如<a href="http://lib.csdn.net/base/javaee" class="replace_word" title="Java EE知识库" target="_blank" style="color:#df3434; font-weight:bold;">hibernate</a> Validator。</p>
<p>Java Validation API定义了许多注解,可以使用这些注解来约束参数的值,所有的注解都在包<code>javax.validation.constraints</code>中。</p>
<table>
<thead>
<tr>
<th>注解</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>@AssertFalse(@AssertTrue)</td>
<td>对象必须是布尔类型,并且必须为false(true)</td>
</tr>
<tr>
<td>@DecimalMax(value)、@DecimalMin(value)</td>
<td>限制对象必须是一个数字,其值不大于(不小于)指定的BigDecimalString值</td>
</tr>
<tr>
<td>@Digits(integer,fraction)</td>
<td>对象必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction</td>
</tr>
<tr>
<td>@Future</td>
<td>必须是一个将来的日期</td>
</tr>
<tr>
<td>@Max(value)、@Min(value)</td>
<td>必须为一个不大于(不小于)指定值的数字</td>
</tr>
<tr>
<td>@NotNull</td>
<td>限制对象不能为空</td>
</tr>
<tr>
<td>@Null</td>
<td>限制对象必须为空</td>
</tr>
<tr>
<td>@Past</td>
<td>必须是一个过去的日期</td>
</tr>
<tr>
<td>@Pattern(value)</td>
<td>必须符合指定的正则表达式</td>
</tr>
<tr>
<td>@Size(min,max)</td>
<td>限制字符长度必须在min到max之间</td>
</tr>
</tbody>
</table>
<p>使用示例:</p>
<pre class="hljs java" name="code"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Spitter</span> </span>{


    <span class="hljs-keyword">private</span> Long id;


    <span class="hljs-annotation">@NotNull</span>
    <span class="hljs-annotation">@Size</span>(min = <span class="hljs-number">5</span>, max = <span class="hljs-number">16</span>)
    <span class="hljs-keyword">private</span> String username;


    <span class="hljs-annotation">@NotNull</span>
    <span class="hljs-annotation">@Size</span>(min = <span class="hljs-number">5</span>, max = <span class="hljs-number">25</span>)
    <span class="hljs-keyword">private</span> String password;


    <span class="hljs-annotation">@NotNull</span>
    <span class="hljs-annotation">@Size</span>(min = <span class="hljs-number">2</span>, max = <span class="hljs-number">30</span>)
    <span class="hljs-keyword">private</span> String firstName;


    ...</code></pre>
<p>既然已经对Spitter的参数添加了约束,那么就需要改动processRegistration方法来应用校验:</p>
<pre class="hljs java" name="code"><code class="java">    <span class="hljs-annotation">@RequestMapping</span>(value = <span class="hljs-string">"/register"</span>, method = RequestMethod.POST)
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">processRegistration</span><span class="hljs-params">(@Valid Spitter spitter, Errors errors)</span> </span>{
        <span class="hljs-comment">// 若校验中出现错误,那么就返回到注册界面</span>
        <span class="hljs-keyword">if</span> (errors.hasErrors()) {
            <span class="hljs-keyword">return</span> <span class="hljs-string">"registerForm"</span>;
        }
        <span class="hljs-comment">// 保存Spitter</span>
        spitterRepository.save(spitter);
        <span class="hljs-comment">// 重定向到新的页面</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"redirect:/spitter/"</span> + spitter.getUsername();
    }</code></pre>
<h1><a name="t17" target="_blank"></a>总结</h1>
<p>这一章比较适合Spring MVC的入门学习资料。涵盖了Spring MVC处理web请求的处理过程、如何写简单的控制器和控制器方法来处理Http请求、如何使用mockito框架测试控制器方法。</p>
<p>基于Spring MVC的应用有三种方式读取数据:查询参数、路径参数和表单输入。本章用两节介绍了这些内容,并给出了类似错误处理和参数验证等关键知识点。</p>
<p>由于缺少真正的入库操作,因此本章节的一些方法不能真正的运作。</p>
<p>在接下来的章节中,我们会对Spring视图进行深入了解,对如何在JSP页面中使用Spring标签库进行展开。</p>
<br>
<br>
<div>文/hoxis(简书作者)<br>
原文链接:http://www.jianshu.com/p/74357110e4cc<br>
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。</div>
   
</div>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值