若是你有幸能看到。css
谈一些我的感觉html
系统面临的挑战:状态管理、工做流、以及验证都是须要解决的重要特性。HTTP协议的无状态决定了这些问题都不是那么容易解决。前端
Spring的Web框架就是为了帮你解决这些关注点而设计的。Spring MVC基于模型-视图-控制器(Model-View-Controller MVC)模式实现的,他可以帮你构建向Spring框架那样灵活和松耦合的Web应用程序。java
在本章中,将会介绍Spring MVC Web框架,并使用新的Spring MVC注解来构建处理各类Web请求、参数、和表单输入的控制器。git
Spring将请求在调度Servlet、处理器映射(Handler Mappering)、控制器以及视图解析器(View resolver)之间移动,每个Spring MVC中的组件都有特定的目的,而且也没那么复杂。github
让咱们看一下,请求是如何从客户端发起,通过Spring MVC中的组件,最终返回到客户端web
每当用户在Web浏览器中点击连接或提交表单的时候,请求就开始工做了。请求是一个十分繁忙的家伙,从离开浏览器开始到获取响应返回,它会经历不少站,在每站都会留下一些信息,同时也会带上一些信息。正则表达式
Spring工做流程描述原文在这里spring
DispatcherServlet
对请求URL进行解析,获得请求资源标识符(URI)。而后根据该URI,调用HandlerMapping得到该Handler配置的全部相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain
对象的形式返回;DispatcherServlet
根据得到的Handler,选择一个合适的HandlerAdapter。(附注:若是成功得到HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)图片参考这里sql
Spring工做流程描述
HandlerMapping
以及HandlerAdapter
来处理Handler?HandlerAdapter
可能会被用于处理多种Handler。一、请求旅程的第一站是Spring的DispatcherServlet
。与大多数基于Java的Web框架同样,Spring MVC全部的请求都会经过一个前端控制器(front contrller)Servlet.前端控制器是经常使用Web应用程序模式。在这里一个单实例的Servlet将请求委托给应用的其余组件来执行实际的处理。在Spring MVC中,DisPatcherServlet就是前端控制器。
二、DisPactcher的任务是将请求发送Spring MVC控制器(controller).控制器是一个用于处理请求的Spring组件。在典型的应用中可能会有多个控制器,DispatcherServlet
须要知道应该将请求发送给那个哪一个控制器。因此Dispactcher以会查询一个或 多个处理器映射(Handler mapping),来肯定请求的下一站在哪里。处理映射器根据请求携带的 URL信息来进行决策。
三、一旦选择了合适的控制器,DispatcherServlet
会将请求发送给选中的控制器。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器 自己只是处理不多,甚至不处理工做,而是将业务逻辑委托给一个或多个服务器对象进行处理)
四、控制器在完成处理逻辑后,一般会产生一些信息。这些 信息须要返回给 用户,并在浏览器上显示。这些信息被称为模型(Model),不过仅仅给用户返回原始的信息是不够的----这些信息须要以用户友好的方式进行格式化,通常会是HTML。因此,信息须要发送一个视图(View),一般会是JSP。
五、 控制器作的最后一件事就是将模型打包,而且表示出用于渲染输出的视图名。它接下来会将请求连同模型和视图发送回DispatcherServlet。
六、这样,控制器就不会与特定的视图相耦合*传递给控制器的视图名并不直接表示某个特定的jsp。实际上,它甚至并不能肯定视图就是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器(View resolver),来将逻辑视图名称匹配为一个特定的视图实现,他可能也可能不是JSP
七、虽然DispatcherServlet
已经知道了哪一个驶入渲染结果、那请求的任务基本上也就完成了,它的最后一站是试图的实现。在这里它交付给模型数据。请求的任务就结束了。视图将使用模型数据渲染输出。这个输出经过响应对象传递给客户端(不会像听上去那样硬编码)
能够看到,请求要通过不少步骤,最终才能造成返回给客户端的响应,大多数的 步骤都是在Spirng框架内部完成的。
借助于最近几个Spring新特性的功能加强,开始使用SpringMVC变得很是简单了。使用最简单的方式配置Spring MVC;所要实现的功能仅限于运行咱们所建立的控制器。
配置DisPatcherServlet
DispatcherServlet
是Spirng MVC的核心,在这里请求会第一次接触到框架,它要负责将请求路由到其余组件之中。
按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml中。这个文件会放到应用的war包中。固然这是配置DispatcherServlet
方法之一。借助于Servlet 3规范和Spring 3.1 的功能加强,这种方式已经不是惟一的方案来。
咱们会使用Java将DispatcherServlet
配置在Servlet容器中。而不会在使用web.xml文件
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected String[] getServletMappings() { //将DispatcherServlet映射到“/” return new String[]{"/"}; } @Override protected Class<?>[] getRootConfigClasses() { return new Class<?> [] {RootConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?> [] { WebConfig.class}; } }
咱们只须要知道扩展AbstractAnnotationConfigDispatcherServletInitializer
的任意类都会自动的配置Dispatcherservlet和Spring应用上下文,Spirng的应用上下文会位于应用程序的Servlet上下文之中
在Servlet3.0环境中,容器会在类路径中 查找实现javax.servlet.ServletContainerInitialzer
接口的类,若是能发现的话,就会用它来配置Servlet容器。
Spring提供了这个接口的实现名为SpringServletContainnerInitialzer
,这个类反过来又会查找实现WebApplicationInitialzer
的类,并将配置的任务交给他们来完成。Spring 3.2引入了一个遍历的WebApplicationInitialzer
基础实现也就是AbstractAnnotationConfigDispatcherServletInitializer
由于咱们的Spittr-WebApplicationInitialzer
扩展了AbstractAnnotationConfigDispatcherServletInitializer
,(同时也就实现了WebApplicationInitialzer
),所以当部署Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文
第一个方法getServletMappings()
,它会将一个或多个路径映射到DispatcherServlet
上,在本示例中,它映射的是“/”,表示它是应用默认的Servlet,它会处理应用的全部请求。
为了理解其余两个方法,咱们首先须要理解DispatcherServlet
和一个Servlet监听器(也就是ContextLoaderListener)的关系。
当DispatcherServlet
启动的时候,它会建立应用上下文,并加载配置文件或配置类中声明的bean。在上面那个程序中的getServletConfigClasses()
方法中,咱们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类(使用Java配置)中的bean
但在Spring Web应用中,一般还会有另一个应用上下文。另外这个就是由ContextLoaderListener
建立.
咱们但愿DispatcherServlet
加载包含Web组件的bean,如控制器,视图解析器,以及处理器映射,而ContextLoaderListener
要加载应用中的其余bean。这些bean一般 是驱动应用后端的中间层和数据层组件。
实际上AbstractAnnotationConfigDispatcherServletInitializer
会同时建立DispatcherServlet
和ContextLoaderListener
。getServletConfigClasses()
方法会返回带有@Configuration
注解的类将会用来定义DispatcherSerle应用上下文中的bean,getRootConfigClasses()
会返回带有@Configuration
注解的类将会用来配置ContextLoaderListener
建立的应用上下文。
若是有必要两个能够同时存在,wex.xml和 AbstractAnnotationConfigDispatcherServletInitializer
,但其实没有必要。
若是按照这种方式配置DispatcherServlet,而不是使用Web.xml的话,那么惟一的问题在于它能部署到支持Servlet3.0的服务器上才能够正常工做,如Tomcat7或更高版本,Servlet3.0规范在2009年12月份就发布了,
若是没有支持Servlet3.0,那别无选择了,只能使用web.xml配置类。
启用Spring MVC
咱们有多种方式来启动DispatcherServlet,与之相似,启用Spring MVC组件的方式也不止一种,之前Spring是XMl进行配置的,你能够选择<mvc:annotation-driver>启用注解驱动的Spring MVC。
在第七章的时候会介绍<mvc:annotaion-driver>,如今会让Spring MVC搭建的过程尽量简单,并基于Java进行配置。
咱们所能建立最简单的Spring MVC配置就是一个带有@EnableWebMvc注解的类
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @EnableWebMvc public class WebConfig { }
这能够运行起来,它的确可以启用Spring MVC,但还有很多问题要解决。
一、没有配置视图解析器,若是这样的话,Spring默认会使用BeanNameView-Resolver,这个视图解析器会查找ID与视图名称匹配的bean,而且查找的bean要实现View接口,它以这样的方式来解析视图。
二、没有启用组件扫描。这样的结果就是,Spirng只能找到显示声明在配置类中的控制器。
三、这样配置的话,DispatcherServlet会映射为默认的Servlet,因此他会处理全部的请求,包括对静态资源的请求,如图片 和样式表(在大多数状况下,这可能并非你想要的结果)。
所以咱们须要在WebConfig这个最小的Spring MVC配置上再加一些内容,从而让他变得真正实用。
@Configuration @EnableWebMvc //启用Spring MVC @ComponentScan("com.guo.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 //咱们要求DispatcherServlet将静态资源的请求转发到Servlet容器中默认的Servlet上, //而不是使用DispatcherServlet原本来处理此类请求。 public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { //配置静态资源的处理 configurer.enable(); } }
第一件须要注意的是WebConfig如今添加了@ComponentScan注解,此时将会扫描com.guo.spittr.web
包来查找组件。稍后你会看到,咱们编写的控制器将会带有@Controller注解,这会使其成为组件扫描时的候选bean。所以,咱们不须要在配置类中显示声明任何的控制器。
接下来,咱们添加了一个ViewResolver bean,更具体的将是InternalResourceViewResolver
。将会在第6章更为详细的讨论视图解析器。咱们只须要知道他会去查找jsp文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀。(例如:名为home的视图会被解析为/WEB-INF/views/home.jsp)
最后新的WebConfig类还扩展里WebMvcConfigurerAdapter
并重写了其configureDefaultServletHandling()
方法,经过调用DefaultServletHandlerConfigurer
的enable()方法,咱们要求DispatcherServlet将静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet原本来处理此类请求。
WebConfig已经就绪,那么RootConfig呢?由于本章聚焦于Web开发,而Web相关的配置经过DisPatcherServlet建立的应用上下文都已经配好了,所以如今的RootConfig相对很简单:
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * Created by guo on 23/2/2018. */ @Configuration @ComponentScan(basePackages = {"com.guo.spittr"}, excludeFilters = { @Filter(type = FilterType.ANNOTATION,value = EnableWebMvc.class)}) public class RootConfig { }
惟一须要注意的是RootConfig使用了@ComponentScan注解,这样的话,咱们就有不少机会用非Web的组件来完善RootConfig。
为了实如今线社交的功能,咱们将要构造一个简单的微博(microblogging)应用,在不少方面,咱们所构建的应用于最先的微博应用Twitter很相似,在这个过程当中,咱们会添加一些小的变化。固然咱们使用Spirng技术来构建这个应用。
由于从Twitter借鉴了灵感并经过Spring来进行实现,因此它就有了一个名字:Spitter。
Spittr应用有两个基本的领域概念:Spitter(应用的用户)和Spittle(用户发布的简短状态更新)。当咱们在书中完善Spittr应用的功能时,将会介绍这两个概念。在本章中,咱们会构建应用的Web层,建立展示Spittle的控制器以及处理用户注册为Spitter的表单。
舞台已经搭建完成了,咱们已经配置了DispatcherServlet,启用了基本的Spring MVC组件,并肯定了目标应用。让咱们进入本章的核心内容:使用Spring MVC 控制器处理Web请求。
在SpringMVC中,控制器只是在方法上添加了@RequestMapping注解的类,这个注解声明了他们所要处理的请求。
开始的时候,咱们尽量简单,假设控制器类要处理对/的请求,并对渲染应用的首页。
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; /** * Created by guo on 24/2/2018. * 首页控制器 */ @Controller public class HomeController { @RequestMapping(value = "/",method = RequestMethod.GET) //处理对“/”的Get请求 public String home() { return "home"; //视图名为home } }
写完测试了下,好使,
你可能注意到第一件事就是HomeController带有@Controller注解,很显然这个注解是用来声明控制器的,但实际上这个注解对Spirng MVC 自己影响不大。
@Controller是一个构造型(stereotype)的注解。它基于@Component注解。在这里,它的目的就是辅助实现组件扫描。由于homeController带有@Controller注解,所以组件扫描器会自动去找到HomeController,并将其声明为Spring应用上下文中的bean。
Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { String value() default ""; }
其实你可让HomeController带有@Component注解,它所实现的效果是同样的。可是在表意性上可能差一些,没法肯定HomeController是什么组件类型。
HomeController惟一的一个方法,也就是Home方法,带有@RequestMapping注解,他的Value属性指定了这个方法所要处理的请求路径,method属性细化了它所能处理的HTTP方法,在本例中,当收到对‘/’的HTTP GET请求时,就会调用home方法。
home()方法其实并无作太多的事情,它返回一个String类型的“home”,这个String将会被Spring MVC 解读为要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。
鉴于咱们配置InternalResourceViewResolver
的方式,视图名“home”将会被解析为“/WEB-INF/views/home.jsp”
Spittr应用的首页,定义为一个简单的JSP
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ page session="false" %> <html> <head> <title>Spitter</title> <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" > </head> <body> <h1>Welcome to Spitter</h1> <a href="<c:url value="/spittles" />">Spittles</a> | <a href="<c:url value="/spitter/register" />">Register</a> </body> </html>
测试控制器最直接的办法多是构建并部署应用,而后经过浏览器对其进行访问,可是自动化测试可能会给你更快的反馈和更一致的独立结果,因此,让咱们编写一个针对HomeController的测试
编写一个简单的类来测试HomoController。
import static org.junit.Assert.*; import org.junit.Test; public class HomeControllerTest { @Test public void testHomePage() throws Exception { HomeController controller = new HomeController(); assertEquals("home",controller.home()); } }
在测试中会直接调用home()方法,并断言返回包含 "home"值的String类型。它彻底没有站在Spring MVC控制器的视角进行测试。这个测试没有断言当接收到针对“/”的GET请求时会调用home()方法。由于它返回的值就是“home”,因此没有真正判断home是试图的名称。
不过从Spring 3.2开始,咱们能够按照控制器的方式进行测试Spring MVC中的控制器了。而不只仅是POJO进行测试。Spring如今包含了一种mock Spirng MVC 并针对控制器执行 HTTP请求的机制。这样的话,在测试控制器的时候,就没有必要在启动Web服务器和Web浏览器了。
为了阐述如何测试Spirng MVC 容器,咱们重写了HomeControllerTest并使用Spring MVC 中新的测试特性。
import org.junit.Test; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * Created by guo on 24/2/2018. */ public class HomeControllerTest1 { @Test //你们在测试的时候注意静态导入的方法 public void testHomePage() throws Exception { HomeController controller = new HomeController(); MockMvc mockMvc = standaloneSetup(controller).build(); //搭建MockMvc mockMvc.perform(get("/")) //对“/”执行GET请求, .andExpect(view().name("home")); //预期获得home视图 } }
此次咱们不是直接调用home方法并测试它的返回值,而是发起了对"/"的请求,并断言结果视图的名称为home,它首先传递一个HomeController实例到MockMvcBuilders.strandaloneSetup()并调用build()来构建MockMvc实例,而后它使用MockMvc实例执行针对“/”的GET请求,并设置 指望获得的视图名称。
如今,已经为HomeController编写了测试,那么咱们能够作一些重构。并经过测试来保证不会对功能形成什么破坏。咱们能够作的就是拆分@RequestMapping,并将其路径映射部分放到类级别上
@Controller @RequestMapping("/") public class HomeController { @RequestMapping(method = RequestMethod.GET) //处理对“/”的Get请求 public String home() { return "home"; //视图名为home } }
在这个新版本的HomeController中,路径被转移到类级别的@RequestMapping上,而HTTP方法依然映射在方法级别上。当控制器在类级别上添加@RequestMapping注解时,这个注解会应用到控制器的全部处理器方法上,处理器方法上的@RequestMapping注解会对类级别上的@RequestMapping的声明进行补充。
就HomeController而言,这里只有一个控制器方法,与类级别的@RequestMapping合并以后,这个方法的@RequestMapping代表home()将会处理对 “/”路径的GET请求。
有了测试,因此能够确保在这个过程当中,没有对原有的功能形成破坏。
当咱们修改@RequestMapping时,还能够对HomeController作另外一个变动。@RequestMapping的value接受一个String类型的数组。到目前为止,咱们给它设置的都是一个String类型的‘/’。可是,咱们还能够将它映射到对“/Homepage”的请求,只须要将类级别的@RequestMapping改动下
@Controller @RequestMapping({"/","/Homepage"}) public class HomeController { ... }
如今,HomeController的home()方法能够被映射到对“/”和“/homepage”的GET请求上。
到目前为止,就编写超级简单的控制器来讲,HomeController已是一个不错的样例了,可是大多数的控制器并非那么简单。在Spring应用中,咱们须要有一个页面展现最近提交的Spittle列表。所以,咱们须要有一个新的方法来处理这个页面。
首先须要定义一个数据访问的Repository,为了实现解耦以及避免陷入数据库访问的细节中,咱们将Repository定义为一个接口,并在稍后实现它(第十章),此时,咱们只须要一个可以获取Spittle列表的Repository,
package com.guo.spittr.data; import com.guo.spittr.Spittle; import java.util.List; /** * Created by guo on 24/2/2018. */ public interface SpittleRepository { List<Spittle> finfSpittles(long max, int count); }
findSpittles()方法接受两个参数,其中max参数表明所返回的Spittle中,Spittle ID属性的最大值,而count参数代表要返回多少个Spittle对象,为了得到最新的20个Spittle对象,咱们能够这样调用方法。
List<Spittle> recent = SpittleRepository.findSpittles(long.MAX_VALUE(),20)
它的属性包括消息内容,时间戳,以及Spittle发布时对应的经纬度。
public class Spittle { private final Long id; private final String message; private final Date time; private Double latitude; private Double longitude; public Spittle(String message, Date time) { this(null, message, time, null, null); } public Spittle(Long id, String message, Date time, Double longitude, Double latitude) { this.id = id; this.message = message; this.time = time; this.longitude = longitude; this.latitude = latitude; } //Getter和Setter略 @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that, "id", "time"); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, "id", "time"); }
须要注意的是,咱们使用Apache Common Lang包来实现equals()和hashCode()方法,这些方法除了常规的做用之外,当咱们为控制器的处理器方法编写测试时,它们也是有用的。
既然咱们说到了测试,那么咱们继续讨论这个话题,并为新的控制器方法编写测试,
@Test public void houldShowRecentSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(20); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findSpittles(Long.MAX_VALUE, 20)) .thenReturn(expectedSpittles); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")) .build(); mockMvc.perform(get("/spittles")) .andExpect(view().name("spittles")) .andExpect(model().attributeExists("spittleList")) .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); } /.................佩服老外,测试代码一大堆,省略了好多,好好研究下,..................../ private List<Spittle> createSpittleList(int count) { List<Spittle> spittles = new ArrayList<Spittle>(); for (int i=0; i < count; i++) { spittles.add(new Spittle("Spittle " + i, new Date())); } return spittles; } }
测试首先会建立SpittleRepository接口的mock实现,这个实现会从他的findSpittles()方法中返回20个Spittle对象,而后将这个Repository注入到一个新的SpittleController实例中,而后建立MockMvc并使用这个控制器。
须要注意的是这个测试在MockMvc构造器上调用了setSingleView().这样的话,mock框架就不用解析控制器中的视图名了。在不少场景中,其实不必这么作,可是对于这个控制器方法,视图和请求路径很是类似,这样按照默认的驶入解析规则,MockMvc就会发生失败,由于没法区分视图路径和控制器的路径,在这个测试中,构建InternalResourceViewResolver时所设置的路径是可有可无的,但咱们将其设置为InternalResourceViewResolver
一致。
这个测试对“/spittles”发起Get请求,而后断言视图的名称为spittles而且模型中包含名为spittleList的属性,在spittleList中包含预期的内容。
固然若是此时运行测试的话,它将会失败。他不是运行失败,而是编译的时候就失败,这是由于咱们还没编写SpittleController。
@Controller @RequestMapping("/spittles") public class SpittleController { private SpittleRepository spittleRepository; @Autowired public SpittleController(SpittleRepository spittleRepository) { //注入SpittleRepository this.spittleRepository = spittleRepository; } @RequestMapping(method = RequestMethod.GET) public String spittles(Model model) { model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE,20)); // 将spittle添加到视图 return "spittles"; // 返回视图名 } }
咱们能够看到SpittleController有一个构造器,这个构造器使用@Autowired注解,用来注入SpittleRepository。这个SpittleRepository随后又在spittls()方法中,用来获取最新的spittle列表。
须要注意的是咱们在spittles()方法中给定了一个Model做为参数。这样,spittles()方法就能够将Repository中获取到的Spittle列表填充到模型中,Model实际上就是一个Map(也就是key-value的集合)它会传递给视图,这样数据就能渲染到客户端了。当调用addAttribute()方法而且指定key的时候,那么key会根据值的对象类型来推断肯定。
sittles()方法最后一件事是返回spittles做为视图的名字,这个视图会渲染模型。
若是你但愿显示模型的key的话,也能够指定,
@RequestMapping(method = RequestMethod.GET) public String spittles(Model model) { model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE,20)); // 将spittle添加到视图 return "spittles"; // 返回视图名 }
若是你但愿使用非Spring类型的话,那么可使用java.util.Map来代替Model
@RequestMapping(method = RequestMethod.GET) public String spittles(Map model) { model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE,20)); // 将spittle添加到视图 return "spittles"; // 返回视图名 }
既然咱们如今提到了各类可替代方案,那下面还有另一种方式来编写spittles()方法
@RequestMapping(method = RequestMethod.GET) public List<String> spittles() { return spittleRepository.findSpittles(Long.MAX_VALUE,20)); }
这个并无返回值,也没有显示的设定模型,这个方法返回的是Spittle列表。。当处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的key会根据其类型推断得出。在本示例中也就是(spittleList)
逻辑视图的名称也会根据请求的路径推断得出。由于这个方法处理针对“/spittles”的GET请求,所以视图的名称将会是spittles,(去掉开头的线。)
无论使用哪一种方式来编写spittles()方法,所达成的结果都是相同的。模型会存储一个Spittle列表,ket为spittleList,而后这个列表会发送到名为spittles的视图中。视图的jsp会是“/WEB-INF/views/spittles.jsp”
如今数据已经放到了模型中,在JSP中该如何访问它呢?实际上,当视图是JSP的时候,模型数据会做为请求属性放入到请求之中(Request) ,所以在spittles.jsp文件中可使用JSTL(JavaServer Pages Standard Tag Library) 的<c:forEach>标签渲染spittle列表。
<c:forEach items="${spittleList}" var="spittle" > <li id="spittle_<c:out value="spittle.id"/>"> <div class="spittleMessage"><c:out value="${spittle.message}" /></div> <div> <span class="spittleTime"><c:out value="${spittle.time}" /></span> <span class="spittleLocation">(<c:out value="${spittle.latitude}" />, <c:out value="${spittle.longitude}" />)</span> </div> </li> </c:forEach>
尽管SpittleController很简单,可是它依然比homeController更进一步,不过,SpittleController和HomeController都没有处理任何形式的输入。如今,让咱们扩展SpittleContorller,让它从客户端接受一些输入。
Spring MVC 容许以多种方法将客户端中的数据传送到控制器的处理器方法中
做为开始,先来看下如何处理带有查询参数的请求,这也是客户端往服务器发送数据时,最简单和最直接的方法。
在Spittr应用中,可能须要处理的一件事就是展示分页的Spittle列表,若是你想让用户每次查看某一页的Spittle历史,那么就须要提供一种方式让用户传递参数进来,进而肯定展示那些Spittle列表。
为了实现这个分页功能,咱们编写的处理方法要接受两个参数
为了实现这个功能,咱们将程序修改成spittles()方法替换为使用before参数和count参数的新spittles()方法。
首先添加一个测试,这个测试反映了xinspittles()方法的功能
@Test public void shouldShowPagedSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(50); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findSpittles(238900, 50)) .thenReturn(expectedSpittles); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")) .build(); mockMvc.perform(get("/spittles?max=238900&count=50")) .andExpect(view().name("spittles")) .andExpect(model().attributeExists("spittleList")) .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); }
这个测试方法关键点在于同时传入了max和count参数,它测试了这些参数存在时的处理方法,而另外一个则测试了没有这些参数的情景。
在这个测试以后,咱们就能确保无论控制器发生了什么样的变化,它都可以处理这两种类型的请求。
@RequestMapping(method = RequestMethod.GET) public List<Spittle> spittles( @RequestParam(value = "max") long max, @RequestParam(value = "count") int count) { return spittleRepository.findSpittles(max, count); }
SittleController中的处理器方法同时要处理有参数和没参数的场景,那咱们须要对其进行修改,让它能接受参数。同时若是这些参数在请求中不存在的话,就是用默认值Long.MAX_VALUE和20.@RequestParam注解的defaultValue属性能够完成这个任务。
@RequestMapping(method=RequestMethod.GET) public List<Spittle> spittles( @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max, @RequestParam(value="count", defaultValue="20") int count) { return spittleRepository.findSpittles(max, count); }
如今若是max若是没有参数指定的话,它将会是Long的最大值。
由于查询参数都是String 类型 ,所以defaultValue属性须要String类型,
private static final String MAX_LONG_AS_STRING = long.toString(Long.MAX.VALUE)
请求中的查询参数是往控制器中传递信息的经常使用手段。另一种方式就是将传递的参数做为请求路径的一部分。
假设咱们的应用程序须要根据给定的ID来展示某一个Spittle记录。其中一种方案就是编写处理器方法,经过使用@RequestParam注解,让它接受ID做为查询参数。
@RequestMapping(value="/show",method = RequestMethod.GET) public String showSpittle( @RequestParam("spittle_id") long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }
在理想状况下,要识别资源应用应该经过URL路径来标识,而不是经过查询参数。对“/spittles/12345”发起请求要优于对“/spittles/show?spittle_id=12345”发起的请求。前者能识别出要查询的资源,然后者描述的是带有参数的一个操做——本质上是经过HTTP发起的RPC。
既然已经以面向资源的控制器做为目标,那咱们将这个需求转化为一个测试。
@Test public void testSpittle() throws Exception { Spittle expectedSpittle = new Spittle("Hello", new Date()); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findOne(12345)).thenReturn(expectedSpittle); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(get("/spittles/12345")) .andExpect(view().name("spittle")) //断言图片的名称为spittle .andExpect(model().attributeExists("spittle")) //预期Spittle放到了模型之中 .andExpect(model().attribute("spittle", expectedSpittle)); }
这个测试构建了一个mockRepository,一个控制器和MockMvc
到目前为止,咱们所编写的控制器,全部的方法都映射到了静态定义好的路径上,还须要包含变量部分
为了实现这种路径变量,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"; }
@PathVariable("spittleId") 代表在请求路径中,无论占位符部分的值是什么都会传递给处理器方法的showSpittle参数中。
也能够去掉这个value的值,由于方法的参数碰巧与占位符的名称相同。
@RequestMapping(value="/{spittleId}",method = RequestMethod.GET) public String showSpittle(@PathVariable long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }
若是传递请求中少许的数据,那查询参数和路径变量是合适的,但一般咱们还须要传递不少的数据,(表单数据),那么查询显得有些笨拙和受限制了。
Web应用的功能不局限于为用户推送内容,大多数的应用容许用户填充表单,并将数据提交回应用中,经过这种方式实现与用户的交互。
使用表单分为两个方面:展示表单以及处理用户经过表单提交的数据。在Spittr应用中,咱们须要有个表单让用户进行注册,SitterController是一个新的控制器,目前只有一个请求处理的方法来展示注册表单。
@Controller @RequestMapping("/spitter") public class SpitterController { //处理对“/spitter/register” @RequestMapping(value = "/register",method = RequestMethod.GET) public String showRegistrationForm() { return "registerForm"; } }
测试展示表单的控制器方法(老外每次都测试)
@Test public void shouldShowRegistration() throws Exception { SpitterController controller = new SpitterController(); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(get("/spitter/register")) .andExpect(view().name("registerForm")); } }
这个JSP必须包含一个HTML<form>标签,
<form method="POST" name="spittleForm"> <input type="hidden" name="latitude"> <input type="hidden" name="longitude"> <textarea name="message" cols="80" rows="5"></textarea><br/> <input type="submit" value="Add" /> </form>
须要注意的是这里的<form>标签中并无设置action属性。在这种状况下,当表单体提交的时,它会提交到与展示时相同的URL路径上,它会提交到“/spitter/reqister”上。
这意味着须要在服务器端编写该HTTP POST请求。
当处理注册表单的POST请求时,控制器须要接受表单数据,并将表单数据保存为Spitter对象。最后为了防止重复提交(用户刷新页面),应该将浏览器重定向到新建立用户的基本信息页面。
@Test public void shouldProcessRegistration() throws Exception { SpitterRepository mockRepository = mock(SpitterRepository.class); Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); 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") .param("email", "jbauer@ctu.gov")) .andExpect(redirectedUrl("/spitter/jbauer")); verify(mockRepository, atLeastOnce()).save(unsaved); }
但愿你们也能够学会这样方式
在构建完SpitterRepository的mock实现以及所要执行的控制器和MockNvc以后,shouldProcessRegistration()对“/spitter/register”发起了一个POST请求,做为请求的一部分,用户信息以参数的形式放到request中,从而模拟提交的表单。
/** * Created by guo on 24/2/2018. */ @Controller @RequestMapping("/spitter") public class SpitterController { private SpitterRepository spitterRepository; @Autowired public SpitterController(SpitterRepository spitterRepository) { //注入SpiterRepository this.spitterRepository = spitterRepository; } @RequestMapping(value = "/register", method = RequestMethod.GET) public String showRegistrationForm() { return "registerForm"; } @RequestMapping(value = "/register",method = RequestMethod.POST) public String procesRegistration(Spitter spitter) { spitterRepository.save(spitter); //保存Spitter return "redirect:/spitter/" + spitter.getUsername(); //重定向到基本信息页面 } }
返回一个String类型,用来指定视图。可是这个视图格式和之前有所不一样。这里不只返回了视图的名称供视图解析器查找目标视图,并且返回的值还带有重定向的格式return "redirect:/spitter/"
当看到视图格式中有“redirect:”前缀时,它就知道要将其解析为重定向的规则,而不是试图的名称。在本例中,它将会重定向到基本信息的页面。
须要注意的是除了能够“redirect”还能够识别“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"; }
spitterRepository经过用户获取一个Spitter对象,showSpitterProfile()方法获得这个对象并将其添加到模型中,而后返回profile。也就是基本信息页面的逻辑视图。
<body> <h1>Your Profile</h1> <c:out value="${spitter.username}" /><br/> <c:out value="${spitter.firstName}" /> <c:out value="${spitter.lastName}" /><br/> <c:out value="${spitter.email}" /> </body>
注意:这里使用H2数据库,太有用了。
@Configuration public class DataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("schema.sql") .build(); } @Bean public JdbcOperations jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }
若是表单中没有发送username或password,会发生什么状况呢?或者名字太长,由会怎么样?,接下来,让咱们看一下为表单添加校验,而从避免数据呈现不一致性。
若是用户在提交表单的时候,username和password为空的话,那么将会致使在新建Spitter对象中,username和password是空的String。若是不处理,将会出项安全问题。
同时咱们应该阻止用户提交空的名字。限制这些输入的长度。
从Spring 3.0 开始,在Spring MVC中提供了java校验的API的支持。只须要在类路径下包含这个JavaAPI的实现便可。好比Hibernate validator.
Java校验API定义了多个注解,这些注解能够用在属性上,从而限制这些属性的值。
public class Spitter { private Long id; @NotNull @Size(min=5, max=16) private String username; @NotNull @Size(min=5, max=25) private String password; @NotNull @Size(min=2, max=30) private String firstName; @NotNull @Size(min=2, max=30) private String lastName; @NotNull @Email private String email; 忽略其余方法。 }
@RequestMapping(value="/register", method=POST) //老外喜欢静态导入特性 public String processRegistration( @Valid Spitter spitter, //校验Spitter输入 Errors errors) { if (errors.hasErrors()) { return "registerForm"; //若是校验出现错误,则从新返回表单 } spitterRepository.save(spitter); return "redirect:/spitter/" + spitter.getUsername(); }
Spitter参数添加了@Valid注解,这会告诉Spring,须要确保这个对象知足校验限制。
若是表单出错的话,那么这些错误能够经过Errors进行反问。
很重要一点须要注意的是:Errors参数要紧跟在带有Valid注解参数的后面。@Valid注解所标注的就是要校验的参数。
若是没有错误的话,Spitter对象将会经过Repository进行保存,控制器会像以前那样重定向到基本信息页面。
在本章中,咱们为编写应用程序的Web部分开来一个好头,能够看到Spring有一个强大而灵活的Web框架。借助于注解,Spring MVC 提供了近似于POJO的开发模式,这使得开发处理请求的控制器变得简单,同时也易于测试。
当编写控制器的处理方法时,Spring MVC及其灵活。归纳来说,若是你的处理器方法须要内容的话,只需将对应的对象做为参数,而他不须要的内容,则没有必要出如今参数列表中。这样,就为请求带来了无限的可能性,同时还能保持一种简单的编程模型。
尽管本章中不少内容都是关于控制器的请求处理的,可是渲染响应也一样重要,咱们经过使用JSP的方式,简单了解了如何为控制器编写视图,可是,就Spring MVC视图来讲,它并非本章所看到的简单JSP。
在接下来的第6章,咱们将会更深刻的学习Spring视图,包括如何在JSP中使用Spring标签库,还会学习如何借助于Apache Tiles为视图添加一致的结构。同时,还会了解Thymeleaf,这是一个颇有意思的JSP替代方法,Spring为其提供了内置的支持。
真的很是期待下一章,,,,,加油