Spring实战第五章
1、Spring MVC处理请求的流程
请求从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息。图展示了请求使用Spring MVC所经历的所有站点。
1、请求离开浏览器时(1),会带有用户所请求内容的信息,至少会包含请求的URL。但是还可能带有其他的信息,例如用户提交的表单信息。
2、Spring MVC所有的请求都会通过DispatcherServlet这个前端控制器(front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件 来执行实际的处理。
3、DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet需要知道应该将请 求发送给哪个控制器。所以DispatcherServlet以会查询一个或多个处理器映射(handler mapping)(2) 来确定请求的下一站在哪里。处理器映射会根据请求所携带的URL信息来进行决策。
4、选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器(3)。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。
5、控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方式 进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。
6、控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送回DispatcherServlet (4)。
7、控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确定视图就是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产 生结果的真正视图。DispatcherServlet将会使用视图解析器(view resolver)(5)来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。
8、既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是JSP)(6) ,在这里它交付模型数据。请求的任务就完成了。
9、视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码)(7) 。
2、Spring MVC的搭建
使用Java将DispatcherServlet配置在Servlet容器中。
package spittr.config; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{ @Override protected Class<?>[] getRootConfigClasses() { // TODO Auto-generated method stub return new Class<?>[]{RootConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { //指定配置类 // TODO Auto-generated method stub return new Class<?>[]{WebConfig.class}; } @Override protected String[] getServletMappings() { //将DispatcherServlet映射到"/" // TODO Auto-generated method stub return new String[]{"/"}; } }
扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动地配置Dispatcher-Servlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。
扩展三个方法:
第一个方法是getServletMappings(),它会将一个或多个路径映射到DispatcherServlet上。在本例中,它映射的是“/”,这表示它会是应用的默认Servlet。它会处理进入应用的所有请求。
理解DispatcherServlet和一个Servlet监听器(也就是ContextLoaderListener)的关系。
第二个方法:getServletConfigClasses()方法:
当DispatcherServlet启动的时候,它会创建Spring应用上下文,并加载配置文件或配置类中所声明的bean。getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类(使用Java配置)中的bean。
但是在Spring Web应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由ContextLoaderListener创建的。我们希望DispatcherServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射,而ContextLoaderListener要加载应用中的其他bean。这些bean通常是驱动应用后端的中间层和数据层组件。
实际上:AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListener。GetServleConfigClasses()方法返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的bean。getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean。
在本例中,根配置定义在RootConfig中,DispatcherServlet的配置声明在WebConfig中。(AbstractAnnotationConfigDispatcherServletInitializer它只能部署到支持Servlet 3.0的服务器中才能正常工作)
2.1启动Spring MVC
创建的最简单的Spring MVC配置就是一个带有@EnableWebMvc注解的类。
如果一个类仅仅配置了@EnableWebMvc注解,还有不少问题要解决:
没有配置视图解析器。如果这样的话,Spring默认会使用BeanNameViewResolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,它以这样的方式来解析视 图。
没有启用组件扫描。这样的结果就是,Spring只能找到显式声明在配置类中的控制器。
这样配置的话,DispatcherServlet会映射为应用的默认Servlet,所以它会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这可能并不是你想要的效果)。
WebConfig类:
package spittr.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.InternalResourceViewResolver; @Configuration @EnableWebMvc //启动Spring MVC @ComponentScan("spittr.web") //启动组件扫描 public class WebConfig extends WebMvcConfigurerAdapter{ @Bean public ViewResolver viewResolver(){ //配置JSP视图解析器 InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views"); resolver.setSuffix(".jsp"); resolver.setExposeContextBeansAsAttributes(true); return resolver; } @Override public void configureDefaultServletHandling( //配置静态资源的处理 DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }
解释:1、@Component-Scan注解,因此将会扫描spitter.web包来查找组件
2、控制器带有@Controller注解,这会使其成为组件扫描时的候选bean。不用再显示配置
3、InternalResourceViewResolver。它会查找JSP文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀(例如,名为home的视图将会解析为/WEB-INF/views/home.jsp)。
4、类还扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调用DefaultServlet-HandlerConfigurer的enable()方法,我们要求DispatcherServlet将对静态 资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。
简答配置RootConfig
是RootConfig使用了@ComponentScan注解,利于日后完善类
package spittr.config; 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; @Configuration @ComponentScan(basePackages={"spitter"}, excludeFilters={@Filter(type=FilterType.ANNOTATION,value=EnableWebMvc.class)}) public class RootConfig { }
3、编写控制器
在Spring MVC中,控制器只是方法上添加了@RequestMapping注解的类,这个注解声明了它们所要处理的请求。
package spittr.web; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class HomeController { @RequestMapping(value="/" ,method= RequestMethod.GET) //处理对"/"的请求 public String home(){ return "home"; //视图为home } }
第一件事情就是HomeController带有@Controller注解。很显然这个注解是用来声明控制器。
HomeController是一个构造型(stereotype)的注解,它基于@Component注解。在这里,它的目的就是辅助实现组件扫描。因为HomeController带有@Controller注解,因此组件扫描器会自动找到HomeController,并将其声明为Spring应用上下文中的一个bean。其实,你也可以让HomeController带有@Component注解,它所实现的效果是一样的,但是在表意性上可能会差一些,无法确定HomeController是什么组件类型。
HomeController唯一的一个方法,也就是home()方法,带有@RequestMapping注解。它的value属性指定了这个方法所要处理的请求路径,method属性细化了它所处理的HTTP方法。它返回了一个String类型的“home”。这个String将会被Spring MVC解读为要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。
简单的home.jsp页面
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>Spittr</title> <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />"> </head> <body> <h1>Welcome to Spittr</h1> <a href="<c:url value="spittles" />" >Spittes </a> <a href="<c:url value="/spittle/register" />">Register</a> </body> </html>
测试控制器(不用启动服务器和浏览器)
从Spring 3.2开始,可以按照控制器的方式来测试Spring MVC中的控制器了,而不仅仅是作为POJO进行测试。Spring现在包含了一种mock Spring MVC并针对控制器执行HTTP请求的机制。这样的话,在测试控制器的时候,就没有必要再启动Web服务器和Web浏览器了。
import org.junit.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; public class HomeControllerTest { @Test public void testHomePage() throws Exception{ HomeController controller = new HomeController(); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.view().name("home")); } }
发起了对“/”的GET请求,并断言结果视图的名称为home。它首先传递一个HomeController实例到MockMvcBuilders.standaloneSetup()并调用build()来构建MockMvc实例。然后它使用MockMvc实例来执行针对“/”的GET请求并设置期望得到的视图名称。
3.1 类级别请求处理:
拆分@RequestMapping,并将其路径映射部分放到类级别上。
@Controller @RequestMapping("/") public class HomeController { @RequestMapping(method= RequestMethod.GET) //处理对"/"的请求 public String home(){ System.out.println("你好!!!"); return "home"; //视图为home } }
路径现在被转移到类级别的@RequestMapping上,而HTTP方法依然映射在方法级别上。当控制器在类级别上添加@RequestMapping注解时,这个注解会应用到控制器的所有处理器方法上。处理器方法上的@RequestMapping注解会对类级别上的@RequestMapping的声明进行补充。(类上和方法上的映射组合就可以完成全部的映射)
@RequestMapping的value属性能够接受一个String类型的数组。可给它设置的都是一个String类型的“/”,还可以将它映射到对“/homepage”的请求
@RequestMapping({"/","/homnepage"})
3.2传递模型数据到视图
@Controller @RequestMapping("/spittles") public class SpittleController { private SpittleReopsitory spittleReopsitory; @Autowired public SpittleController(SpittleReopsitory spittleReopsitory){ this.spittleReopsitory=spittleReopsitory; } @RequestMapping(method=RequestMethod.GET) public String spittles(Model model){ model.addAttribute(spittleReopsitory.findSpittles(Long.MAX_VALUE, 20)); return "spittles"; } }
在spittles()方法中给定了一个Model作为参数。这样,spittles()方法就能将Repository中获取到的Spittle列表填充到模型中。Model实际上就是一个Map(也就是key-value对的集合),它会传递给视图,这样数据就能渲染到客户端了。当调用addAttribute()方法并且不指定key的时候,那么key会根据值的对象类型推断确定。在本例中,因为它是一个List<Spittle>,因此,键将会推断为spittleList。
spittles()方法所做的最后一件事是返回spittles作为视图的名字,这个视图会渲染模型。
显式声明模型的key的话,那也尽可以进行指定。
@RequestMapping(method=RequestMethod.GET) public String spittles(Model model){ model.addAttribute("spittleList", spittleReopsitory.findSpittles(Long.MAX_VALUE, 20)); return "spittles"; }
希望使用非Spring类型的话,那么可以用java.util.Map来代替Model。
@RequestMapping(method=RequestMethod.GET) public String spittles(Map model){ model.put("spittleList", spittleReopsitory.findSpittles(Long.MAX_VALUE, 20)); return "spittles"; }
或奇怪的方法:
@RequestMapping(method=RequestMethod.GET) public List<Spittle> spittles(Map model){ return spittleReopsitory.findSpittles(Long.MAX_VALUE, 20)); }
它并没有返回视图名称,也没有显式地设定模型,这个方法返回的是Spittle列表。当处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的key会根据其类型推断得出(在本例中,也就是spittleList)。而逻辑视图的名称将会根据请求路径推断得出。因为这个方法处理针对“/spittles”的GET请求,因此视图的名称将会是spittles(去掉开头的斜线)。
模型中会存储一个Spittle列表,key为spittleList,然后这个列表会发送到名为spittles的视图中。按照我们配置InternalResourceViewResolver的方式,视图的JSP将会是“/WEB-INF/views/spittles.jsp”。现在,数据已经放到了模型中,在JSP中该如何访问它呢?实际上,当视图是JSP的时候,模型数据会作为请求属性放到请求(request)之中。
3.3接受请求的输入
Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
查询参数(Query Parameter)。
表单参数(Form Parameter)。
路径变量(Path Variable)。
3.3.1查询参数
处理此等查询参数“/spittles?max=238900&count=50”
@Controller @RequestMapping("/spittles") public class SpittleController { private static final String MAX_LONG_AS_STRING = "9223372036854775807"; private SpittleReopsitory spittleReopsitory; @Autowired public SpittleController(SpittleReopsitory spittleReopsitory){ this.spittleReopsitory=spittleReopsitory; } @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 spittleReopsitory.findSpittles(max,count); } }
解释:
@RequestParam接受查询参数max的数据,并且会把其注入到max中,虽然查询参数的置为字符串类型,但是其会自动的转化。
SpittleController中的处理器方法同时处理有参数和没有参数的场景,它能接受参数,同时,如果这些参数在请求中不存在的话,就使用默认值Long.MAX_VALUE和@RequestParam注解的defaultValue属性完成这项任务。
如果max参数没有指定的话,它将会是Long类型的最大值。因为查询参数都是String类型的,因此defaultValue属性需要String类型的值。因此,使用Long.MAX_VALUE是不行的。我们可以将Long.MAX_VALUE转换为名为MAX_LONG_-AS_STRING的String类型常量。尽管defaultValue属性给定的是String类型的值,但是当绑定到方法的max参数时,它会转换为Long类型。如果请求中没有count参数的话,count参数的默认值将会设置为请求中的查询参数是往控制器中传递信息的常用手段。
3.3.2通过路径参数接受输入
处理URL:“/spittles/12345”
@Controller @RequestMapping("/spittles") public class SpittleController { private static final String MAX_LONG_AS_STRING = "9223372036854775807"; private SpittleReopsitory spittleReopsitory; @Autowired public SpittleController(SpittleReopsitory spittleReopsitory){ this.spittleReopsitory=spittleReopsitory; } @RequestMapping(value="/{spittleId}",method=RequestMethod.GET) public String spittle( @PathVariable("spittleId") long spittleId, Model model){ model.addAttribute(spittleReopsitory.findOne(spittleId)); return "spittle"; } }
为了实现这种路径变量,Spring MVC允许在@RequestMapping路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。路径中的其他部分要与所处理的请求完全匹配,但是占位符部分可以是任意的值。处理器方法使用了占位符,将Spittle ID作为路径的一部分。
spittle()方法的spittleId参数上添加了@PathVariable("spittleId")注解,这表明在请求路径中,不管占位符部分的值是什么都会传递到处理器方法的spittleId参数中
当方法的参数名与占位符的名称相同,可以去掉@PathVariable中的value属性。
@RequestMapping(value="/{spittleId}",method=RequestMethod.GET) public String spittle(@PathVariable long spittleId,Model model){ model.addAttribute(spittleReopsitory.findOne(spittleId)); return "spittle"; }
spittle()方法会将参数传递到SpittleRepository的findOne()方法中,用来获取某个Spittle对象,然后将Spittle对象添加到模型中。模型的key将会是spittle,这是根据传递到addAttribute()方法中的类型推断得到的
3.3.4表单的处理
Spring MVC的控制器也为表单处理提供了良好的支持。使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。
SpitterController是一个新的控制器,目前只有一个请求处理的方法来展现注册表单。
@Controller
@RequestMapping("/spitter")
public class SpitterController {
@RequestMapping(value="/register", method=RequestMethod.GET) public String showRegistrationForm(){ return "registerForm"; }
}
showRegistrationForm()方法的@RequestMapping注解以及类级别上的@RequestMapping注解组合起来,声明了这个方法要处理的是针对“/spitter/register”的GET请求。
视图的名称为registerForm,所以JSP的名称需要是registerForm.jsp。
<%@ page language="java" import="java.util.*" pageEncoding="utf-8" session="false"%> <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>Spittr</title> </head> <body> <h1>Register</h1> <form method="post"> Frist Name:<input type="text" name="fristName" /><br> Last Name:<input type="text" name="lastName"/><br> Username:<input type="text" name="username"/><br> Password:<input type="password" name="password"/><br> <input type="submit" value="Register"/> </form> </body> </html>
注意的是:这里的<form>标签中并没有设置action属性。在这种情况下,当表单提交时,它会提交到与展现时相同的URL路径上。也就是说,它会提交到“/spitter/register”上。
当处理注册表单的POST请求时,控制器需要接受表单数据并将表单数据保存为Spitter对象。最后,为了防止重复提交(用户点击浏览器的刷新按钮有可能会发生这种情况),应该将浏览器重定向到新创建用户的基本信息页面。
在处理POST类型的请求时,在请求处理完成后,最好进行一下重定向,这样浏览器的刷新就不会重复提交表单了。在这个测试中,预期请求会重定向到“/spitter/jbauer”,也就是新建用户的基本信息页面。
@RequestMapping(value="/register",method=RequestMethod.POST)
public String processRegistration(Spitter spitter){
spitterRepository.save(spitter);
return "redirect:/spitter/" +spitter.getUsername();
}
processRegistration()方法做的最后一件事就是返回一个String类型,用来指定视图。但是这个视图格式和以前我们所看到的视图有所不同。这里不仅返回了视图的名称供视图解析器查找目标视图,而且返回的值还带有重定向的格式。当InternalResourceViewResolver看到视图格式中的“redirect:”前缀时,它就知道要将其解析为重定向的规则,而不是视图的名称。
注意的是,除了“redirect:”,InternalResourceViewResolver还能识别“forward:”前缀。当它发现视图格式中以“forward:”作为前缀时,请求将会前往(forward)指定的URL路径,而不再是重定向。
之后,通过shouldProcess-Registration()方法实现处理表单提交的控制器方法。
@RequestMapping(value="/{username}",method=RequestMethod.GET) public String showSpitterProfile(@PathVariable String username,Model model){ Spitter spitter = spitterRepository.findByUsername(username); model.addAttribute(spitter); return "profile"; }
注意新创建的processRegistration()方法,它接受一个Spitter对象作为参数。这个对象有firstName、lastName、username和password属性,这些属性将会使用请求中同名的参数进行填充(setter)。
3.3.4.1效验表单
使用Spring对Java校验API(Java Validation API,又称JSR-303)的支持。从Spring 3.0开始,在Spring MVC中提供了对Java校验API的支持。
Java校验API定义了多个注解,这些注解可以放到属性上,从而限制这些属性的值。所有的注解都位于javax.validation.constraints包中
注解 | 描述 |
@AssertFalse | 所注解的元素必须是Boolean类型,并且值为false |
@AssertTrue | 所注解的元素必须是Boolean类型,并且值为true |
@DecimalMax |
所注解的元素必须是数字,并且它的值要小于或等于给定的 |
@DecimalMin |
所注解的元素必须是数字,并且它的值要大于或等于给定的 |
@Digits | 所注解的元素必须是数字,并且它的值必须有指定的位数 |
@Future | 所注解的元素的值必须是一个将来的日期 |
@Max | 所注解的元素必须是数字,并且它的值要小于或等于给定的值 |
@Min | 所注解的元素必须是数字,并且它的值要大于或等于给定的值 |
@NotNull | 所注解元素的值必须不能为null |
@Null | 所注解元素的值必须为null |
@Past | 所注解的元素的值必须是一个已过去的日期 |
@Pattern | 所注解的元素的值必须匹配给定的正则表达式 |
@Size |
所注解的元素的值必须是String、集合或数组,并且它的长度要符 |
Java校验API的实现可能还会提供额外的校验注解。同时,也可以定义自己的限制条件。
将这些注解添加到Spitter的属性上。
@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;
为Spitter添加了校验注解,接下来需要修改processRegistration()方法来应用校验功能。
@RequestMapping(value="/register",method=RequestMethod.POST) public String processRegistration( @Valid Spitter spitter,Errors errors){ if(errors.hasErrors()){ return "registerForm"; } spitterRepository.save(spitter); return "redirect:/spitter/" +spitter.getUsername(); }
Spitter参数添加了@Valid注解,这会告知 Spring,需要确保这个对象满足校验限制。
在Spitter属性上添加校验限制并不能阻止表单提交。即便用户没有填写某个域或者某个域所给定的值超出了最大长度,processRegistration()方法依然会被调用。这样,我们就需要处理校验的错误,就像在processRegistration()方法中所看到的那样。
如果有校验出现错误的话,那么这些错误可以通过Errors对象进行访问,现在这个对象已作为processRegistration()方法的参数。(很重要一点需要注意,Errors参数要紧跟在带有@Valid注解的参数后面,@Valid注解所标注的就是要检验的参数。)processRegistration()方法所做的第一件事就是调用Errors.hasErrors()来检查是否有错误。
如果有错误的话,Errors.hasErrors()将会返回到registerForm,也就是注册表单的视图。这能够让用户的浏览器重新回到注册表单页面,所以他们能够修正错误,然后重新尝试提交。