step1)请求旅程的第一站是spring 的 DispatchServlet: 与大多数java web 框架一样,spring mvc所有请求都会通过一个前端控制器 servlet,而DispatchServlet就是前端控制器;step2)接着,DispatchServlet的任务是将请求发送给 spring mvc控制器。因为应用程序中有很多控制器,在发送之前DispatchServlet需要查询一个或多个控制器映射来确定请求的下一站在哪里;处理器映射会根据所携带的URL信息来进行决策;step3)DispatchServlet会将请求发送给选中的控制器;到了控制器 , 请求会卸下其负载(用于提交的info),并耐心等待控制器处理这些 info;step4)处理器完成逻辑处理后,会产生一些info,这些info 需要返回给用户并在浏览器上显示。这些info 被称为 模型;(干货——模型的定义);这些info 需要以用户友好的方式进行格式化,一般会是 HTML,所以,info需要发送给一个视图(view),通常会是 JSP;控制器所做的最后一件事情是将模型数据打包,并标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送回 DispatchServlet;(总结:这样,控制器就不会与特定的视图相耦合了,传递给DispatchServlet的视图名并不直接表示某个特定的JSP,。实际上,它仅仅传递了一个逻辑名称,这个名字将会用来产生结果的真正视图)(干货——引入了逻辑名称,由控制器传递给DispatchServlet,前者还传递了模型(响应info的打包))step5)DispatchServlet将会使用视图解析器来将逻辑视图名匹配为一个特定的视图实现,可能是也可能不是JSP;step6)通过控制器传递过来的逻辑名称,DispatchServlet知道由哪个视图渲染结果。DispatchServlet交付模型数据到某个视图的实现,请求的任务完成了;step7)视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端;
<span style="font-family:SimSun;font-size:18px;">public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { RootConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { // 指定配置类. return new Class<?>[] { WebConfig.class }; } @Override protected String[] getServletMappings() { // 将DispatcherServlet映射到 "/" return new String[] { "/" }; } }</span>
<span style="font-family:SimSun;font-size:18px;">public abstract class AbstractAnnotationConfigDispatcherServletInitializer extends AbstractDispatcherServletInitializer { // org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer @Override protected WebApplicationContext createRootApplicationContext() { Class<?>[] configClasses = getRootConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext(); rootAppContext.register(configClasses); return rootAppContext; } else { return null; } } @Override protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext(); Class<?>[] configClasses = getServletConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { servletAppContext.register(configClasses); } return servletAppContext; } protected abstract Class<?>[] getRootConfigClasses(); protected abstract Class<?>[] getServletConfigClasses(); }</span>
3.1)spring 提供了这个接口的实现,名为 SpringServletContainerInitializer:这个类反过来又会查找实现 WebApplicationInitializer 的类并将配置的任务交给它们来完成;
method1)getServletMappings方法:它会将一个或多个路径映射到 DispatcherServlet上;在本例中,它映射的是“/“,这表示它会是 应用的默认 servlet,它会处理进入应用的所有请求;(为了理解其他两个方法,首先要理解 DispatcherServlet和一个 servlet 监听器的关系)
3.1)getServletConfigClasses()方法:返回的带有 @Configuration注解的类将会用来定义 DispatcherServlet应用上下文中的bean。3.2)getRootConfigClasses()方法:返回的带有@Configuration注解的类将会用来配置 ContextLoaderListener 创建的应用上下文中的bean;
<span style="font-family:SimSun;font-size:18px;">@Configuration @EnableWebMvc public class WebConfig { }</span>
2.1)以上代码的确能够启用 spring mvc,但还有不少问题要解决:(problems)
problem1)没有配置视图解析器:这样的话,spring默认会使用 BeanNameViewResolver,这个视图解析器会查找ID 与 视图名称匹配的bean,并且查找的bean 要实现 View 接口,它以这样的方式来解析视图;problem2)没有启用组件扫描:这样的话,spring 只能找到显式声明在配置类中的控制器;problem3)这样配置的话,这样配置的话,DispatcherServlet 会映射为 应用的默认servlet:所以它会处理所有的请求,包括对静态资源的请求,如图片等;(大多数cases下,这不是你想要的效果)
2.2)修改WebConfig这个spring mvc配置类, 修改后的内容为:<span style="font-family:SimSun;font-size:18px;">@Configuration @EnableWebMvc //启用spring mvc. @ComponentScan("com.spring.chapter5.spittr.web") // 启用组件扫描. public class WebConfig extends WebMvcConfigurerAdapter { @Bean public ViewResolver viewResolver() { // 配置JSP视图解析器. InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); return resolver; } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { // 配置静态资源处理. configurer.enable(); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // TODO Auto-generated method stub super.addResourceHandlers(registry); } }</span>对以上代码的分析(Analysis):新的 WebConfig 扩展了WebMvcConfigurerAdapter类,重写了其 configureDefaultServletHandling()方法,通过调用 DefaultServletHandlerConfigurer .enable()方法,我们要求 DispatcherServlet 将对静态资源的请求转发到 Servlet容器中默认的Servlet上,而不是使用 DispatcherServlet 本身来处理此类请求;
@Configuration @ComponentScan(basePackages={"com.spring.chapter5.spittr"}, excludeFilters={ @Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class) }) public class RootConfig { }
<span style="font-family:SimSun;font-size:18px;">@Controller // 声明一个控制器 @RequestMapping("/") public class HomeController { @RequestMapping(method = GET) // 处理对 ”/“ 的get请求; public String home(Model model) { return "home"; //视图名为home } }</span>
A1)@Controller注解:是一个构造型注解,它基于 @Component注解。在这里,它的目的是 辅助实现组件扫描,因为 HomeController 带有 @Controller注解,因此组件扫描器会自动找到 HomeController,并将其声明为 spring应用上下文中的一个 bean;(干货——@Controller注解的作用)A2)@RequestMapping注解:它的value属性指定了这个方法所要处理的请求路径,method细化了所要处理的HTTP方法;
<span style="font-family:SimSun;font-size:18px;"><%@ 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></span>
public class HomeControllerTest { @Test public void testHomePage() throws Exception { HomeController controller = new HomeController(); String result = controller.home(null); System.out.println(result); } }
<span style="font-family:SimSun;font-size:18px;"> //拆分前 @Controller public class HomeController { @RequestMapping(value="/", method = GET) public String home(Model model) { return "home"; } } //拆分后 @Controller @RequestMapping("/") public class HomeController { @RequestMapping(method = GET) public String home(Model model) { return "home"; } }</span>
<span style="font-family:SimSun;font-size:18px;">@Controller @RequestMapping({"/" ,"/homepage"}) // 设置多个映射路径. public class HomeController() { ...... }</span>
step1)首先,需要定义一个数据访问的 Repository(能够获取 Spittle列表的 Repository);<span style="font-family:SimSun;font-size:18px;">public interface SpittleRepository { List<Spittle> findRecentSpittles(); List<Spittle> findSpittles(long max, int count); Spittle findOne(long id); void save(Spittle spittle); } // 为了获取最新的20个 Spittle 对象,我们可以这样调用 findSpittles(long max, int count): // List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);</span>step2)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; } public long getId() { return id; } public String getMessage() { return message; } public Date getTime() { return time; } public Double getLongitude() { return longitude; } public Double getLatitude() { return latitude; } @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that, "id", "time"); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, "id", "time"); } }对以上代码的分析(Analysis):使用了 Apache Common Lang 包来实现了 equals()方法 和 hashCode()方法;(downloading from apache commons lang)step3)测试 SpittleController处理针对 "/spittles" 的GET请求:(使用 spring 的 MockMvc 来断言新的处理器方法中你所期望的行为)@Test public void shouldShowRecentSpittles() 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); 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; }
step4)SpittleController:在模型中放入最新的 Spittle列表@Controller @RequestMapping("/spittles") public class SpittleController { private static final String MAX_LONG_AS_STRING = "9223372036854775807"; private SpittleRepository spittleRepository; @Autowired public SpittleController(SpittleRepository spittleRepository) { this.spittleRepository = spittleRepository; } @RequestMapping(method = RequestMethod.GET) public String spittles(Model model) { // Model实际上就是一个Map,他会传递给视图,这样就能渲染到client. model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); return "spittles"; } }对上述代码的分析(Analysis):A1)当调用 addAttribute()方法而不指定key时,那么key 会根据值的对象类型推断确定。因为它是一个List<Spittle>,所以推断key == spittleList。
A2)如果你想显示指定模型的key的话,可以这样指定:@RequestMapping(method = RequestMethod.GET) public String spittles(Model model) { model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20)); return "spittles"; }A3)我们还可以将该方法改写为:当处理器方法像这样返回对象或集合时,这个值会被放到模型中,且模型key会被推断为 spittleList;且逻辑视图的名称将会根据请求路径推断得出,因为请求路径是 "/spittles" 的 GET请求,所以视图名称会是 spittles(去掉斜线即可);
@RequestMapping(method=RequestMethod.GET) public List<Spittle> spittles() { return spittleRepository.findSpittles(Long.MAX_VALUE, 20)); }
Attention)不管选择哪种方式编写 spittles()方法,所达到的效果是一样的。
A1)模型中会存储一个Spittle列表,key 为 spitleList,然后该列表会被发送到 spittles的视图中;
A2)按照 InternalResourceViewResolver, 视图解析器的配置,视图的JSP将是 /WEB-INF/views/spittles.jsp;该jsp的源码如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="s" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <html> <head> <title>Spitter</title> <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" > </head> <body> <div class="spittleForm"> <h1>Spit it out...</h1> <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> </div> <div class="listTitle"> <h1>Recent Spittles</h1> <ul class="spittleList"> <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> </ul> <c:if test="${fn:length(spittleList) gt 20}"> <hr /> <s:url value="/spittles?count=${nextCount}" var="more_url" /> <a href="${more_url}">Show more</a> </c:if> </div> </body> </html>
【3】接收请求的输入
parameter1)before参数:表明查询出的id 要在这个值之前;parameter2)count参数:表明每页的 Spittle 数量;<span style="font-family:SimSun;font-size:18px;">@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())));</span>
对以上代码的分析(Analysis):上述方法的关键区别在于它针对 "/spittles" 发送 GET 请求,同时还传入了 max 和 count参数;<span style="font-family:SimSun;font-size:18px;">@RequestMapping(method=RequestMethod.GET) public List<Spittle> spittles( @RequestParam("max") long max, @RequestParam("count") int count) { return spittleRepository.findSpittles(max, count); }</span><span style="font-family:SimSun;font-size:18px;">@RequestMapping(method = RequestMethod.GET) public List<Spittle> spittles( @RequestParam("max") long max, @RequestParam("count") int count) { return spittleRepository.findSpittles(max, count); }</span>
<span style="font-family:SimSun;font-size:18px;">@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); }</span>4)请求中的查询参数是往控制器中传递信息的常用手段。另外一种方式很流行,就是将传递参数作为请求路径的一部分;
<span style="font-family:SimSun;font-size:18px;">@RequestMapping(value = "/show", method = RequestMethod.GET) public String showSpittle(@RequestParam("spittle_id") long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }</span>
<span style="font-family:SimSun;font-size:18px;">@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")) .andExpect(model().attributeExists("spittle")) .andExpect(model().attribute("spittle", expectedSpittle)); }</span>
<span style="font-family:SimSun;font-size:18px;">@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET) public String spittle(@PathVariable("spittleId") long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }</span>
<span style="font-family:SimSun;font-size:18px;">// 因为 方法的参数名与占位符的名称相同,所以可以去掉 @PathVariable 中的value属性; // 如果@PathVariable 中 没有 value属性,它会假设占位符的名称与方法的参数名相同; @RequestMapping(value = "/{spittleId}", method = RequestMethod.GET) public String spittle(@PathVariable long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }</span>
<span style="font-family:SimSun;font-size:18px;">@Controller @RequestMapping("/spitter") public class SpitterController { @RequestMapping(value = "/register", method = GET) // 这意味 这将会使用 /WEB-INF/views/registerForm.jsp 这个JSP 来渲染注册表单; public String showRegistrationForm() { return "registerForm"; } }</span>
<span style="font-family:SimSun;font-size:18px;">public void shouldShowRegistration() throws Exception { SpitterController controller = new SpitterController(); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(get("/spitter/register")).andExpect( view().name("registerForm")); }</span>
<span style="font-family:SimSun;font-size:18px;">@Test public void shouldProcessRegistration() throws Exception { SpitterRepository mockRepository = mock(SpitterRepository.class); Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer"); Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer"); when(mockRepository.save(unsaved)).thenReturn(saved); SpitterController controller = new SpitterController(mockRepository); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform( post("/spitter/register").param("firstName", "Jack") .param("lastName", "Bauer").param("username", "jbauer") .param("password", "24hours")).andExpect( redirectedUrl("/spitter/jbauer")); verify(mockRepository, atLeastOnce()).save(unsaved); }</span>
<span style="font-family:SimSun;font-size:18px;">@Controller @RequestMapping("/spitter") public class SpitterController { private SpitterRepository spitterRepository; @Autowired public SpitterController(SpitterRepository spitterRepository) { this.spitterRepository = spitterRepository; } @RequestMapping(value = "/register", method = GET) public String showRegistrationForm() { return "registerForm"; } @RequestMapping(value = "/register", method = POST) public String processRegistration(Spitter spitter) { spitterRepository.save(spitter); return "redirect:/spitter/" + spitter.getUsername(); // 当InternalResourceViewResolver 视图解析器看到 "redirect:" 前缀时,它就知道要将其解析为重定向的规则,而不是视图名称; } }</span>
<span style="font-family:SimSun;font-size:18px;">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; ... }</span>
<span style="font-family:SimSun;font-size:18px;">@RequestMapping(value = "/register", method = POST) public String processRegistration(@Valid Spitter spitter, Errors errors) { if (errors.hasErrors()) { return "registerForm"; } spitterRepository.save(spitter); return "redirect:/spitter/" + spitter.getUsername(); }</span>
A1)@Valid注解:告知spring, 需要确保这个对象满足校验限制;A2)在 Spiter属性上添加校验限制并不能阻止表单提交。即便用户没有填写某个域或者某个域所给定的值超过了最大长度,processRegistration方法依然会被调用;