构建Spring Web应用程序

  1. 映射请求到Spring控制器
  2. 透明地绑定表单参数
  3. 校验表单提交

Spring MVC 基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,帮助构建灵活和松耦合的Web应用程序

Spring MVC起步

Spring MVC 请求流程

Spring MVC框架将请求在调度Servler、处理器映射(handler mapping)、控制器以及视图解析器(view resolver)之间移动

clipboard.png

1:请求访问前端控制器(DispatcherServlet);
2:DispatcherServlet查询一个或多个处理器映射(handler mapping),通过请求路径确定控制器(Controller);
3:DispatcherServlet将请求发送给控制器(Controller);
4:控制器对请求进行处理返回模型和视图名(ModelAndView);
5:DispatcherServlet查询视图解析器(view resolver),通过视图名匹配一个特定的视图实现;
6:DispatcherServlet将模型发送给视图;
7:视图通过模型数据渲染输出,通过响应对象传递给客户端;

搭建Spring MVC

配置DisPatcherServlet

DisPatcherServlet是Spring MVC的核心。负责将请求路由到指定控制器处理。
传统方式,DispathcherServlet会配置在web.xml文件中;
借助于Servlet 3规范和Spring3.1功能增强,可实现Java配置DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class DemoWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected String[] getServletMappings() {
return new String[]{ "/" }; //将DispatcherServlet 映射到 "/"
}

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{ RootConfig.class }; //指定Spring配置类
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{ WebConfig.class }; //指定Spring MVC配置类
}
}

原理:
1:在Serlvet 3环境中,Servlet容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,用来配置Servlet容器;
2:Spring通过SpringServletContainerInitializer实现了该接口,SpringServletContainerInitializer又会在容器中查找实现WebApplicationInitializer接口的类并将配置任务交给它们来完成;
3:Spring3.2引入一个便利的WebApplicationInitializer基础实现,即AbstractAnnotationConfigDispatcherServletIni-tializer,通过继承该类,当部署到Servler3.0容器中时,容器就会自动发现它,并用它来配置Servlet上下文。

继承AbstractAnnotationConfigDispatcherServletInitializer需重写三个方法:
1:getServletMappings() 设置DispatcherServlet映射、

在讲另外两个方法之前,先了解一下Dispatcher和一个Servler监听器(ContextLoaderListener)的关系
关于两个应用上下文的关系
DispatcherServlet启动的时候,会创建Spring应用上下文容器,并加载配置文件或配置类中所声明的bean。
Spring Web应用中,通常还有会另外一个应用上下文,由ContextLoaderListener创建。

我们通常希望DispathcerServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射。
而ContextLoaderListener加载应用中其他bean,这些bean通常是加载驱动应用后端的中间层和数据层组件。

2:getServletConfigClasses() 返回@Configuration注解类并定义DispatcherServlet应用上下文中的bean。
3:getRootConfigClasses() 返回@Configuration注解类并定义ContextLoaderListener应用上下文中的bean。

启用Spring MVC

完成配置DispatcherServlet后,需启用Spring MVC。

传统方式:使用XML配置,使用mvn:annotation-driven启用注解驱动的Spring MVC
使用Java配置:@EnableWebMvc注解 启用Spring MVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package demo;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "demo.web") // 控制器、视图解析器以及处理器映射。
public void WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver () { //设置视图解析器
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}

@Override //继承自WebMvcConfigurerAdapter 用于配置静态资源的处理
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}

package demo;
@Configuration
@ComponentScan(basePackages = "demo", // 加载驱动应用后端的中间层和数据层组件
excludeFilters = {
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
}
)
public class RootConfig {

}

编写基本的控制器(Controller)

1
2
3
4
5
6
7
8
9
10
package demo.web;

@Controller //声明为一个控制器,与@Component作用相同,语义不同,更具有可读性
public class HomeController {

@RequestMapping(value = "/", method = RequestMethod.GET) // 处理对"/"的GET请求
public String home() {
return "home"; //视图名为home
}
}

基于以上配置,我们可以使用一个简单的Spring MVC访问页面,
为助于读者理解,提供一份开发部署流程,仅供参考:
1 配置DispatcherServlet,编写DemoWebAppInitializer(src.main.java.demo包)
2 开启Spring MVC,编写WebConfig 和 RootConfig (src.main.java.demo包)
3 编写控制器,HomeController(src.main.java.demo.web包)
4 编写页面,home.jsp(src.main.webapp.WEB-INF.views)
4 打war包(此过程应该会提示缺少web.xml,可在WEB-INF下添加web.xml 或 配置maven-war-plugin插件忽略web.xml)
5 部署war包至tomcat即可(注意Tomcat版本。理论上至少需要支持Servlet 3的Tomcat容器)

测试控制器

1
2
3
4
5
6
7
8
9
10
11
//关于断言
import static org.junit.Assert.assertEquals; //此处通过静态引入assertEquals静态方法
import org.junit.Test;
import demo.web.HomeController;
public class HomeControllerTest {

@Test
public void testHomePage() throws Exception {
HelloController controller = new HelloController();
assertEquals("home",controller.home()); //通过断言判断controller.home()返回的值是否为"home",不是则报错。
}

上述测试仅调用home(),并断言返回包含”home”值的String,而不是站在Spring MVC控制器的视角进行测试。
正确的测试应该是发送”/“的GET请求会调用home()方法,并真正判断”home”是视图的名称。
Spring3.2开始支持第控制器视角的测试。通过mock Spring MVC并针对控制器执行HTTP请求的测试机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.*;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import demo.web.HomeController;
public class HomeControllerTest {

@Test
public void testHomePage() throws Exception {
HelloController controller = new HelloController();

MockMvc mockMvc = standaloneSetup(controller).build(); //构建MockMvc

mockMvc.perform(get("/")) // 执行 "/"GET请求
.andExpect(view().name("home")); // 预期得到home视图
}

请求映射处理

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("demo") \\当控制器在类级别上添加该注解,这个注解会应用到控制器的所有处理器方法上。
public void DemoController {

@RequestMapping("demo") \\此方法路径为\demo\demo
public String demo () {
return "demo";
}
}

@RequestMapping({“/“,”/demo”}) 支持多个路径映射

传递模型数据到视图

有三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//方式1 Model(本质是一个Map,即 key-value 对的集合)
public String demo(Model model) {
//未指定key时,key依据值类型推断得出(List<String>:stringList,String:string等)
model.addAttribute(createList());
return "demo";
}

//方式2 Map
public String demo(Map model) {
model.put("list",createList());
return "demo";
}

//方式3 此方式即没返回视图名称,也没显式设定模型
public List<String> demo() {
//(视图名称由请求路径决定,返回的对象会添加到模型中,key由其类型推断,类似方式1)
return createList();
}

public List<String> createList() {
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
return list;
}

接受请求的输入

Spring MVC允许以多种方式传递参数
1:查询参数(Query Parameter)
2:表单参数(Form Parameter)
3:路径变量(Path Variable)

处理查询参数

1基本类型 2 包装类型 3 java bean
默认以参数名接参
@RequestParam
value、name 设置参数名(与请求参数名匹配)
required 设置参数是否必传
defaultValue 设置默认值

通过路径参数接受输入

1 设置路径参数占位符
@RequestMapping(“/book/{id}”)

2 接受路径参数
public String getBook(@PathVariable Long id) {…}

@PathVariable
value、name 设置参数名(与路径参数名匹配)
required 设置参数是否必传

传输少量数据时,查询参数 和 路径参数都很适合
传递很多数据时,通常是表单提交的数据

处理表单

接受表单数据

表单的数据往往较多,可使用java Bean接受参数,Spring MVC会将请求参数中与bean属性同名的进行赋值,并返回bean实例。

检验表单数据

避免校验逻辑弄乱处理器代码,可使用Spring对Java校验API(Java Validation API,又称JSR-303)的支持。
Spring3.0开始,Spring MVC中提供了对Java校验API的支持。只需导入该Java API的实现即可,如Hibernate Validator

将注解定义到Java Bean字段属性上,在接受参数处对待检验的参数添加 @Valid 即可

@AssertFalse 所注解的元素必须为Boolean类型,且值为false
@AssertTrue 所注解的元素必须为Boolean类型,且值为true
@DecimalMax 所注解的元素必须为数字,且值要小于或等于给定的BigDecimalString的值
@DecimalMin 所注解的元素必须为数字,且值要大于或等于给定的BigDecimalString的值
@Max 所注解的元素必须为数字,并且它的值要小于或等于给定的值
@Min 所注解的元素必须为数字,并且它的值要大于或等于给定的值
@Digits 所注解的元素必须为数字,且值必须有指定的位数
@Future 所注解的元素必须为一个将来的日期
@Past 所注解元素必须为一个已过去的日期
@NotNull 所注解元素的值必须不能为null
@Null 所注解元素的值必须为null
@Pattern 所注解元素必须匹配给定的正则表达式
@Size 所注解的元素的值必须是String、集合或数组,并且它的长度要符合给定的范围

校验出错时,可以通过Errors对象进行访问,此对象已作为processRegistration()方法参数。(需注意,Errors参数要紧跟在带有@Valid注解的参数后面)
processRegistration()方法所做的第一件事就是调用Errors.hasErrors()来检查是否有错误。例如:

1
2
3
4
5
6
7
8
9
10
11
//...表示省略
@RequestMapping(...)
public String processRegistration(
@valid Spitter spitter,
Errors errors){
if(errors.hasErrors()){
return "registerForm";
}
spitterRepository.save(spitter);
return ...;
}

forward 和 redirect

总结一句话:转发是服务器行为,重定向是客户端行为。

转发过程(forward):客户浏览器发送http请求–》web服务器接受此请求–》调用内部的一个方法在容器内部完成请求处理和转发动作—-》将目标资源发送给客户;在这里,转发的路径必须是同一个web容器下的url,其不能转向到其他的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。

重定向过程(redirect):客户浏览器发送http请求–àweb服务器接受后发送302状态码响应及对应新的location给客户浏览器–》客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址—-》服务器根据此请求寻找资源并发送给客户。在这里location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的。