一、Spring MVC起步
看一下请求从客户端发起,到Spring MVC中的组件,最后返回客户段的过程。
1. 跟踪Spring MVC的请求
① 请求离开浏览器,带着用户所请求内容的信息,包括URL,表单信息等等。请求首先到达前端控制器DispatcherServlet。
DispatcherServlet负责将请求发送给Spring MVC控制器(controller)——用于处理请求的Spring组件。
② 一个应用中可能有多个控制器, DispatcherServlet会查询一个或多个处理器映射来确定请求的下一站在哪。处理器映射会根据请求的URL来进行决策。
③选择合适的控制器后,DispatcherServlet就会将请求发给选中的控制器,并等待控制器处理用户提交的信息。
控制器在逻辑处理后,产生的信息需要返回给用户并显示在浏览器上,这些信息叫做模型(model)。
但仅仅返回这些原始信息是不够的,需要更好的方式进行格式化,一般是HTML,所以需要发送一个视图(view),通常是JSP。
④控制器做的最后一步是讲模型数据打包并标示用于渲染输出的视图名,然后将请求,模型,视图名一起发送给DispatcherServlet。
⑤DispatcherServlet使用视图解析器将逻辑视图名匹配成一个特定的视图实现(不一定是JSP)。
⑥DispatcherServlet知道视图实现后,交付模型数据。
⑦视图使用模型数据渲染输出,通过响应对象传递给客户端。
2. 搭建Spring MVC
我们做一个战绩查询页面的例子。首先配置一下DispatcherServlet。
public class RecordWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// 指定配置类
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {RootConfig.class};
}
// 指定配置类
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebConfig.class};
}
// 将DispatcherServlet映射到"/"
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
看一下上面代码的三个方法是干什么用的:
Spring引入WebApplicationInitializer的基础实现:AbstractAnnotationConfigDispatcherServletInitializer,部署到Servlet 3.0容器的时候,容器会自动发现它并用它配置Servlet上下文。
上面例子中第三个方法getServletMappings(),将一个或多个路径映射到DispatcherServlet上。我们映射到的是“/”上,表示他是默认Servlet。所有请求都会进入到这里并被处理。
DispatcherServlet启动之后会创建Spring应用上下文并且加载配置。getServletConfigClasses() 方法就是告诉DispatcherServlet创建Spring应用上下文时用哪个配置类。
Spring Web应用中通常还有另一个上下文,由ContextLoaderListener创建。getRootConfigClasses() 方法就是告诉ContextLoaderListener加载哪个配置类。
DispatcherServlet负责加载包含Web组件的bean,如控制器,视图解析器,处理器映射。ContextLoaderListener负责加载其他驱动应用后端的中间层和数据层组件bean。
DispatcherServlet和ContextLoaderListener会同时被AbstractAnnotationConfigDispatcherServletInitializer创建。
接下来看一下如何启用Spring MVC:
@Configuration
@EnableWebMvc
public class WebConfig {
}
使用@EnableWebMvc启动Spring MVC。接下来我们要解决这几个问题:
- 配置视图解析器。
- 启用组件扫描。
- 处理静态资源。DispatcherServlet会映射应用默认Servlet,它会处理所有请求,我们不想让它处理静态资源。
我们修改一下WebConfig:
@Configuration
@EnableWebMvc
@ComponentScan("chapterfive") // 扫描chapterfive包来查找组件
public class WebConfig extends WebMvcConfigurerAdapter {
// ViewResolver bean会查找JSP文件,并且在视图名称上加上特定的前缀后缀,比如名字叫index的视图就变成了/WEB-INF/views/index.jsp
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
// 继承WebMvcConfigurerAdapter并重写configureDefaultServletHandling()方法。目的是让DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不让它自身处理。
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
配置RootConfig:
@Configuration
@ComponentScan(basePackages = {"chapterfive"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
public class RootConfig {
}
二、编写基本的控制器
@Controller // 声明为控制器
public class IndexController {
@RequestMapping(value = "/", method = RequestMethod.GET) // 处理对"/"的GET请求
public String index() {
return "index"; // 视图名为index
}
}
@Controller注解基于@Component,所以组件扫描会自动找到IndexController,并声明为bean。方法返回的“index”将会被Spring MVC解读为要渲染的视图名。根据之前的配置,这个视图名会被解析为/WEB-INF/views/index.jsp。
1. 测试控制器
我们写一个index.jsp的页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>战绩查询</title>
</head>
<body>
<h1>欢迎使用战绩查询</h1>
<a href="<c:url value="/login" />">登录</a>|<a href="<c:url value="/regist" />">注册</a>
</body>
</html>
测试一下controller,要再maven里添加
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
</dependency>
否则运行时会报Error java: 无法访问javax.servlet.ServletException这个错误。
编写测试类:
public class IndexControllerTest {
@Test
public void testIndex() {
IndexController controller = new IndexController();
assertEquals("index", controller.index());
}
}
运行,测试通过。
我们再增加一个依赖然后改进测试类:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
public class IndexControllerTest {
@Test
public void testIndex() throws Exception {
IndexController controller = new IndexController();
MockMvc mockMvc = standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(view().name("index")); // 执行get请求,预期得到index视图
}
}
测试通过。
2. 定义类级别的请求处理
@Controller // 声明为控制器
@RequestMapping("/")
public class IndexController {
@RequestMapping(method = RequestMethod.GET) // 处理对"/"的GET请求
public String index() {
return "index"; // 视图名为index
}
}
路径转移到类级别上的@RequestMapping上,它会应用到控制器中的所有处理器方法上,在处理器方法上的@RequestMapping会对类级别的@RequestMapping声明进行补充。
@RequestMapping的value属性还可以接收数组:
@RequestMapping({"/", "/indexPage"})
3. 传递模型数据到视图中
我们在首页中显示一个列表。首先定义一个数据访问接口:
@Component
public interface RecordRepository {
// count表示返回多少数据
List<Record> getRecordList(int count);
}
实现类:
@Component
public class RecordRepositryImpl implements RecordRepository {
@Override
public List<Record> getRecordList(int count) {
List<Record> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
list.add(new Record("name" + i, true, new Date()));
}
return list;
}
public class Record {
private String heroName;
private boolean win;
private Date date;
public Record(String heroName, boolean win, Date date) {
this.heroName = heroName;
this.win = win;
this.date = date;
}
public String getHeroName() {
return heroName;
}
public void setHeroName(String heroName) {
this.heroName = heroName;
}
public boolean isWin() {
return win;
}
public void setWin(boolean win) {
this.win = win;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
controller中增加一个获取列表的方法:
@Controller // 声明为控制器
@RequestMapping("/")
public class IndexController {
@Autowired
private RecordRepository recordRepository;
@RequestMapping(method = RequestMethod.GET) // 处理对"/"的GET请求
public String index() {
return "index"; // 视图名为index
}
@RequestMapping(value = "/getRecordList",method = RequestMethod.GET) // 处理对"/"的GET请求
public String getRecordList(Model model) {
model.addAttribute("list",recordRepository.getRecordList(20));
return "index";
}
}
JSP中使用<c:forEach>标签渲染列表。
三、接受请求的输入
Spring MVC允许多种方式将客户端的数据传到控制器的处理器方法中:
- 查询参数(Query Parameter)
- 表单参数(Form Parameter)
- 路径变量(Path Variable)
1. 处理查询参数
比如我们想在获取列表的方法中,将查询数量改为参数接收:
@RequestMapping(value = "/getRecordList",method = RequestMethod.GET) // 处理对"/"的GET请求
public String getRecordList(@RequestParam(value = "count",defaultValue = "20") int count, Model model) {
model.addAttribute("list",recordRepository.getRecordList(count));
return "index";
}
尽管defaultValue属性的值是String类型,但绑定到count上时,会自动转换成int类型。
请求使用:/getRecordList?count=20
2. 通过路径参数接收
@RequestMapping(value = "/getRecordList/{count}",method = RequestMethod.GET) // 处理对"/"的GET请求
public String getRecordList(@PathVariable("count") int count, Model model) {
model.addAttribute("list",recordRepository.getRecordList(count));
return "index";
}
请求使用:/getRecordList/20