构建Spring Web应用程序
学习内容
映射请求到Spring控制器
透明地绑定表单参数
校验表单提交
简介
状态管理、工作流以及验证都是需要解决的重要特性,HTTP协议的无状态性决定了这些问题都不那么容易解决。
Spring的Web框架就是为了解决这些关注点而设计的。Spring MVC基于模型-视图-控制器模式(Model-View-Controller,MVC)实现,他能够帮你构建像Spring框架那样灵活和松耦合的Web应用程序。
Spring MVC起步
Spring将请求在调度Servlet、处理器映射(handler mapping)、控制器以及视图解析器(view resolver)之间移动。
1、跟踪Spring MVC的请求
流程详解:
在请求离开浏览器时<1>,会带有用户所请求内容的信息,至少会包含请求的URL。但是还可能带有其他的信息。例如用户提交的表单信息。
Spring MVC所有的请求都会通过一个前端控制器(front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。在Spring MVC中,DispatcherServlet就是前端控制器。
DispatcherServlet的任务是将请求发送给Spring MVC控制器。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet会查询一个或多个处理器映射(handler mapping)<2>(处理器映射会根据请求所携带的URL信息来进行决策)来确定将请求发送给那个控制器。
一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器<3>。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理)
控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(Model)。不过仅仅给用户返回原始的信息是不够的--这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。
控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。他接下来会将请求连同模型和视图名发送回DispatcherServlet<4>。
这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不会直接标示某个特定的JSP。实际上,他甚至并不能确定视图就是JSP。相反,他仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器(view resolver)<5>来将逻辑视图名匹配为一个特定的视图实现,他可能是也可能不是JSP。
既然DispatcherServlet已经知道由那个视图渲染结果,那请求的任务基本上也就完成了。他的最后一站是视图的实现(可能是JSP)<6>,在这里他交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码)。
2、搭建Spring MVC
配置DispatcherServlet
DispatcherServlet是Spring MVC的核心。在这里请求会第一次接触到框架,他要负责将请求路由到其他的组件之中。
Servlet3规范之前和Spring3.1之前:DispatcherServlet这样的Servlet会配置在web.xml文件中,这个文件会放到应用的WAR包里面。
Servlet3规范之后和Spring3.1之后:可以使用Java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。
配置DispatcherServlet代码如下:
package spittr.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import spittr.web.WebConfig;
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() {
return new String[] { "/" };
}
}
配置DispatcherServlet代码分析:
AbstractAnnotationConfigDispatcherServletInitializer剖析
扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动地配置DispatcherServlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用他来配置Servlet容器。Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给他们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer。SpitterWebInitializer扩展了AbstractAnnotationConfigDispatcherServletInitializer,因此当部署到Servlet3.0容器中的时候,容器会自动发现他,并用它来配置Servlet上下文。
第一个方法getServletMappings()
他会将一个或多个路径映射到DispatcherServlet上,本例中映射的是“/”,他表示会是应用的默认Servlet。他会处理进入应用的所有请求。
两个应用上下文之间的关系
当DispatcherServlet启动的时候,他会创建Spring应用上下文,并加载配置文件或配置类中所声明的bean。getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类中的bean。
但是在Spring Web应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由ContextLoaderListener创建的。我们希望DispatcherServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射,而ContextLoaderListener要加载应用中的其他bean。这些bean通常是驱动应用后端的中间层和数据层组件。
实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListener。getServletConfigClasses()方法返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的Bean。getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean。
AbstractAnnotationConfigDispatcherServletInitializer来配置DispatcherServlet是传统web.xml方式的替代方案。只能部署到支持Servlet3.0的服务器中。
3、启用Spring MVC
XML配置:可以使用<mvc:annotation-driven>启动注解驱动的Spring MVC。
Java配置:@EnableWebMvc注解类
package org.hhc.spittr.config;
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接口,他以这样的方式来解析视图。
没有启动组件扫描。
WebConfig最小但可用Spring MVC配置
package org.hhc.spittr.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
@ComponentScan("org.hhc.spittr.web")
public class Webconfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
通过调用DefaultServletHandlerConfigurer的enable()方法,我们要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此请求。
RootConfig配置比较简单,使用@ComponentScan注解,我们就有很多机会用非Web的组件来充实完善RootConfig。
编写基本的控制器
1、编写控制器
package org.hhc.spittr.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class HomeController {
@RequestMapping(method =GET)
public String home(Modelmodel) {
return "home";
}
}
@RequestMapping注解:value代表路径;method属性细化了可处理的HTTP方法。
@Controller注解:他基于@Component注解。
2、测试控制器
package org.hhc.spittr.web;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
public class HomeControllerTest {
@Test
public void testHomePage()throws Exception {
HomeController controller =new HomeController();
MockMvc mockMvc =standaloneSetup(controller).build();
mockMvc.perform(get("/")).andExpect(
view().name("home"));
}
}
3、定义类级别的请求处理
使用的还是@RequestMapping
4、传递模型数据到视图中
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private SpittleRepositoryrepository;
@Autowired
public SpittleController(SpittleRepositoryrepository) {
this.repository =repository;
}
/**
* @param model:map的key根据对象类型推断确定:spittleList
* @return
*/
@RequestMapping(method=RequestMethod.GET)
public Stringspittles(Model model) {
model.addAttribute(repository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
/**
* @param model:指定key
* @return
*/
@RequestMapping(method = RequestMethod.GET)
public Stringspittles(Model model) {
model.addAttribute("spittleList",
repository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
/**
* @param model:如果希望使用非Spring类型的话,可以用Map来代替Model
* @return
*/
@RequestMapping(method = RequestMethod.GET)
public String spittles(Map model) {
model.put("spittleList",
repository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
/**
* 处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的key会根据类型推断得出
* 逻辑视图的名称将会根据请求路径推断得出。本例中视图名为:spittles
* @return
*/
@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles() {
return repository.findSpittles(Long.MAX_VALUE, 20);
}
}
上面选择哪种方式来编写spittles()方法,所达到的结果都是相同的。模型中会存储一个spittle列表,key为spittleList,然后这个列表会发送到名为spittles的视图中。按照我们配置的视图解析器,视图的JSP将会是”WEB-INF/views/spittles.jsp”。
接受请求的输入
Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
查询参数
表单参数
路径变量
1、处理查询参数
@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(@RequestParam(value="max", defaultValue="20")long max,
@RequestParam(value="count", defaultValue="20")int count) {
return repository.findSpittles(max,count);
}
2、通过路径参数接受输入
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String showSpittle(@PathVariable("spittleId")long spittleId, Model model){
return "spittle";
}
当占位符的名称与方法的参数名相同时,才可以简写:
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String showSpittle(@PathVariable long spittleId, Model model) {
return "spittle";
}
处理表单
Web应用允许用户填充表单并将数据提交回应用中,通过这种方式实现与用户的交互。
使用表单分为两个方面:展现表单和处理用户通过表单提交的数据。
1、校验表单
@RequestMapping(value="/register", method=POST)
public String processRegistration(
@Valid Spitterspitter,
Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" +spitter.getUsername();
}
public class Spitter {
private Longid;
@NotNull
@Size(min=5, max=16)
private Stringusername;
@NotNull
@Size(min=5, max=25)
private Stringpassword;
@NotNull
@Size(min=2, max=30)
private StringfirstName;
@NotNull
@Size(min=2, max=30)
private StringlastName;
@NotNull
private Stringemail;
}