Spring-MVC-JavaConfig

Spring In Action(第四版) chapter5读书笔记

搭建Spring MVC

配置DispatcherServlet

DispatcherServlet是SpringMVC的核心,负责将请求路由到其它组件

注:接下来将使用Java配置将DispatcherServlet配置到Servlet中,而不再使用web.xml文件

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
31
32
33
34
35
/**
* 扩展AbstractAnnotationConfigDispatcherServletInitializer
* 的任意类都会自动地配置DispatcherServlet和Spring上下文
* <p>
* Spring的上下文会位于应用程序的Servlet上下文中
*/
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfig.class};
}

/**
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebConfig.class};
}

/**
* 将一个或多个默认路径映射到DispatcherServlet上
*
* 行程多个url对应DispatcherServlet的多对一关系
* @return
*/
@Override
protected String[] getServletMappings() {
// 将DispatcherServlet映射到了根目录 '/'
return new String[]{"/"};
}
}

AbstractAnnotationConfigDispatcherServletInitializer剖析:

在Servlet 3.0环境中,容器会在类路径下查找实现javax.servlet.ServletContainerInitializer接口的类,如果发现的话,就会用实现类来配置Servlet容器。Spring提供了这个接口的实现类,名为org.springframework.web.SpringServletContainerInitializer,这个类反过来查找实现了org.springframework.web.context.WebApplicationContext接口的类并将任务分配给它们完成。

Spring 3.2引入了一个便捷的WebApplicationContext基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer

而上述编写的SpitterWebInitializer类扩展了AbstractAnnotationConfigDispatcherServletInitializer抽象类(同时也实现了WebApplicationContext接口),因而当部署到Servlet 3.0中的容器中,容器会自动发现它,并用来配置Servlet上下文。

getRootConfigClasses()/getServletConfigClasses()

对于这两个方法的理解,首先要理解DispatcherServlet和一个Servlet监听器(即ContextLoaderListener)的关系。

首先的区分SpringWeb项目中的两个应用上下文:

  • DispatcherServlet启动时会创建Spring的应用上下文,并加载配置文件或者配置类中声明的bean,该上下文加载包含Web组件的bean,如控制器、视图解析器以及处理器映射;
  • 在SpringWeb项目中,通常还会有另一个应用上下文,该上下文是由ContextLoaderListener所创建,该上下文加载其它bean(不同于Web组件),这些bean通常是驱动应用后端的中间层和数据层组件。

实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServletContextLoaderListener

getServletConfigClasses()方法返回带有@Configuration注解的类将会用于定义DispatcherServlet应用上下文的bean,而getRootConfigClasses()方法返回带有@Configuration注解的类将会用于定义ContextLoaderListener应用上下文的bean。

对于配置类的编写:

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
@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig implements WebMvcConfigurer {
/**
* 定义JSP视图解析器对象
* @return
*/
@Bean
public ViewResolver viewResolver() {
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();
}
}

@Configuration
@ComponentScan(basePackages = {"spittr"},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
})
public class RootConfig {
}

控制器的编写

1
2
3
4
5
6
7
@Controller
public class HomeController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home() {
return "home";
}
}

测试控制器

所用到jar包的maven依赖

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
<!-- 测试jar包依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>

测试类的编写,做法:发起了对/路径的GET请求,并断言结果视图的名称为’home’

1
2
3
4
5
6
7
8
9
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"));
}
}

传递模型数据到视图中

1、首先定义一个数据访问的Repository,数据来源随机数(照常理而言应来源于数据库,但此处重点在于传递),首先定义Repository接口及实现类:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 接口
public interface SpittleRepository {
List<Spittle> findSpittles(long max, int count);

List<Spittle> findSpittles(int s, int count);
}

// 实现类
/**
* 该注解会在spring容器扫描时创建该类对象,同@Component,但用于数据访问层
*/
@Repository
public class SpittleRepositoryImpl implements SpittleRepository {
private List<Spittle> spittleList;

// 无参构造方法,生成随机数据
public SpittleRepositoryImpl() {
this.spittleList = new ArrayList<>();
for (int i = 0; i < 200; i++) {
Map<String, String> stringStringMap = RandomLonLat.randomLonLat(0, 180, 0, 90);
this.spittleList.add(new Spittle(new Long(i), "hey man, i'm " + i + "!", new Date(),
Double.parseDouble(stringStringMap.get("J")), Double.parseDouble(stringStringMap.get("W"))));
}
}

@Override
public List<Spittle> findSpittles(long max, int count) {
return this.spittleList.subList(0, count);
}

/**
* 用于获取下标s开始往后count的评论数据
*/
@Override
public List<Spittle> findSpittles(int s, int count) {
int n = this.spittleList.size();
if (s > n) {
return null;
}
if (s + count > n) {
return this.spittleList.subList(s, n);
}
return this.spittleList.subList(s, s + count);
}
}

2、编写控制器,用于接收请求返回数据及视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
@RequestMapping("/spittles")
public class SpittleController {
// Spring自动注入之前编写并添加@Repository注解的数据访问对象
@Autowired
private SpittleRepository spittleRepository;

/**
* 表示返回的视图
* @return
*/
@RequestMapping(method = RequestMethod.GET)
public ModelAndView spittles() {
ModelAndView modelAndView = new ModelAndView();
// 设置视图名,由于设定了视图解析器,故直接写名称即可
modelAndView.setViewName("spittles");
// 设置返回的数据
modelAndView.addObject("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return modelAndView;
}
}

3、定义视图Jsp页面

注意:web.xml的版本问题,过低可能导致EL表达式无效

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
<%@ page import="spittr.Spittle" %>
<%@ page import="java.util.List" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Recent Spittles</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/style.css">
</head>
<body>
<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>
</div>
</body>
</html>

接收请求参数

SpringMVC允许以多种方式将客户端中的数据传送到控制器的处理控制方法中,包括:

  • 查询参数(Query Parameter)
  • 表单参数(Form Parameter)
  • 路径参数(Path Paramter)

查询参数(Query Parameter)

在之前的的控制类中编写下述控制方法,将请求参数写在控制方法的形参中,使用注解@RequestParm建立映射关系,注解参数value对应请求的参数名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param s
* @param count
* @return ModelAndView:表示本次请求的处理结果(数据、视图)
* Model:表示数据
* View:表示视图
*/
@RequestMapping(value = "/show", method = RequestMethod.GET)
public ModelAndView spittles(@RequestParam(value = "s", defaultValue = "0") int s,
@RequestParam(value = "count", defaultValue = "20") int count) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("spittles");
modelAndView.addObject("spittleList", spittleRepository.findSpittles(s, count));
return modelAndView;
}

路径参数(Path Paramter)

例:获取某一条评论并展示

解决方法:

学习此部分内容前,想到的做法是转化成查询参数,即root?spittleId=5,这样便可与查询参数一样进行查询,但这样的做法从面向资源的角度看是不合法的。

而SpringMVC提供了路径参数这一解决方法,即root/5,即可达到目的,而具体控制器方法的编写如下

1
2
3
4
5
6
7
8
9
10
11
/**
* @param spittleId 表示请求中的路径参数
* @return
*/
@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public ModelAndView spittle(@PathVariable(value = "spittleId") int spittleId) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("spittles");
modelAndView.addObject("spittle", spittleRepository.findSpittle(spittleId));
return modelAndView;
}

这里的@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)的value值用{}框起表示占位符,在root/5中代表5,而形参中@PathVariable(value = "spittleId")表示参数与这个占位符形成映射关系,即spittleId=5

对于少量的请求参数用上述两种方法较为合适,而对于较多参数则应选用表单。

处理表单

对于大量参数,使用表单提交更为合适

操作步骤:

1、首先编写jsp表单视图

注:若未给表单指定action(提交到的地址),表单会提交到与展现是相同的URL路径上。

2、编写实体类,类中的属性应与表单的name属性对应

3、编写数据访问对象,用于存储用户信息及获取用户信息,模拟数据库功能,使用List集合模拟

4、编写注册成功的用户信息展示jsp视图

5、编写控制器

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 注册控制器
*/
@Controller
@RequestMapping("/spitter")
public class SpitterController {

// 用户信息数据访问对象
@Autowired
private SpitterRepository spitterRepository;

/**
* 用于定位注册表单视图
*
* @return
*/
@RequestMapping(value = "/register", method = RequestMethod.GET)
public ModelAndView showRegistration() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("registerForm");
return modelAndView;
}

/**
* 用于处理注册事件
*
* @param spitter 将表单数据映射成实体类传入控制方法
* @return
*/
@RequestMapping(value = "/register", method = RequestMethod.POST)
public ModelAndView processRegistration(Spitter spitter) {
ModelAndView modelAndView = new ModelAndView();
boolean save = this.spitterRepository.save(spitter);
if (save) {
modelAndView.setViewName(new StringBuilder("redirect:/spitter/").append(spitter.getUsername()).toString());
}
return modelAndView;
}

/**
* 用户展示用户信息
* @param username
* @return
*/
@RequestMapping(value = "/{username}", method = RequestMethod.GET)
public ModelAndView showSpitterProfile(@PathVariable(value = "username") String username) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("user", this.spitterRepository.findByUsername(username));
modelAndView.setViewName("profile");
return modelAndView;
}
}

表单校验

一个坑点,对于导入的依赖包(我使用的是maven项目)

之前对应找到网上最新的依赖,导入后发现@NotNull等校验注解不起作用

1
2
3
4
5
6
7
8
9
10
11
<!-- 用于表单的校验 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.0.Final</version>
</dependency>

能de出bug全凭运气,欢乐谋篇博客贴的两个依赖(和上面比只有org.hibernate.validator的版本不一样),换完后bug解决了

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>

但是还是很迷:

1、org.hibernate.validator低版本时包含javax.validation包,使用低版本时包含org.hibernate.validator即可

2、org.hibernate.validator 7.0.0版本将javax.validation分离出去,但是相应注解失效,还是不懂为什么