构建Spring Web应用程序
SpringMVC起步
追踪SpringMVC的请求过程
- 首先请求从浏览器发出,请求包含url,还有一些请求数据。 和其他java web框架一样,第一站是前端控制器servlet。请求到达前端控制器DispatcherServlet。
- DispatcherServlet调用处理器映射(handler mapping)查询(根据url)下一步应该调用的控制器(controller)。因为通常程序都会有多个控制器。
- 找到应该委托的控制器后,DispatcherServlet将请求发送给相应的控制器,请求把请求数据交给控制器,等待控制器返回结果。(但是设计良好的控制器通常很少或不会进行处理工作,而是业务逻辑委托给一个或多个service对象处理)。
- 控制器处理完后,把数据和逻辑视图名返回给DispatcherServlet。这个数据我们称为模型(model),而这个逻辑视图名不是真正的视图,甚至还不是完整的视图名称,也不知道是不是JSP。
- DispatcherServlet委托视图解析器(view resolver)将逻辑视图名转成真正的视图名,来找到视图。
- 接着把模型发送到视图中,渲染,把数据在视图中显示。
- 最后,在响应对象中,把视图返回给请求客户端。
搭建SpringMVC
我们使用注解@EnableWebMvc来启动SpringMVC。
@Configuration
@EnableWebMvc
public class WebConfig {
}
这个代码已经可以启动SpringMVC了,但是存在一些问题
- 没有配置视图解析器。如果没有配置视图解析器,那么Spring会默认使用BeanNameViewResolver视图解析器,这个视图解析器是先找bean id和视图名称匹配的bean,而这个bean要实现View。
- 没有进行组件扫描。那么SpringMVC能使用的控制器只有进行了显示配置的控制器。
- 表明所有的请求都到达DispatcherServlet前端控制器,即使是静态资源也是,有时候我们并不想这样。
下面我们来解决上面提到的问题,定义一个最小但是可用的SpringMVC配置。
package spittr.config;
@Configuration
@EnableWebMvc //启动SpringMVC
@ComponentScan("spittr.web") //启动组件扫描
public class WebConfig extends WebMvcConfigurerAdapter {
//配置视图解析器
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
viewResolver.setExposeContextBeansAsAttributes(true);
return viewResolver;
}
//配置静态资源的处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
WebConfig已经配置好了,接下来是RootConfig。
package spittr.config;
@Configuration
@ComponentScan(basePackages = {"spittr"},
excludeFilters = {
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
})
public class RootConfig {
}
编写基本的控制器
控制器
package spittr.web;
@Controller //声明这是控制器
public class HomeController {
@RequestMapping(value = "/", method = RequestMethod.GET) //接收url为/的get请求
public String home() {
return "home"; //视图名为home
}
}
- @Controller:声明这是控制器
- @RequestMapping(value = “/”, method = RequestMethod.GET):接收url为/的get请求
- value:url路径。如果只有value属性时,value可以省略。
- method:请求的方式,例如get、post。
JSP页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>spittr</title>
</head>
<body>
<h1>Welcome to Spittr</h1>
<a href="<c:url value="/spittles" />">spittles</a>
<a href="<c:url value="/spitter/register" />">Register</a>
</body>
</html>
定义类级别的请求处理
@RequestMapping注解可以定义在类上
package spittr.web;
@Controller //声明这是控制器
@RequestMapping("/") //接收url为/请求
public class HomeController {
@RequestMapping(method = RequestMethod.GET) //接收get请求
public String home() {
return "home"; //视图名为home
}
}
这个home方法只映射到"/",其实我们可以让一个方法映射到多个url中,例如让home()方法也映射到"/homepage"。
package spittr.web;
@Controller
@RequestMapping({"/", "homepage"})
public class HomeController {
@RequestMapping(method = RequestMethod.GET)
public String home() {
return "home";
}
}
传递模型数据到视图中
现在,我们要实现让模型数据在视图中显示,为此,我们需要定义获取模型数据的类、pojo类、处理/spittles的控制器和视图spittles.jsp。
package spittr;
public class Spittle {
private final Long id;
private final String message;
private final Date date;
private Double Longitude;
private Double latitude;
public Spittle(String message, Date date) {
this(message ,date, null, null);
}
public Spittle(String message, Date date, Double longitude, Double latitude) {
this.id = null;
this.message = message;
this.date = date;
Longitude = longitude;
this.latitude = latitude;
}
public Long getId() {
return id;
}
public String getMessage() {
return message;
}
public Date getDate() {
return date;
}
public Double getLongitude() {
return Longitude;
}
public void setLongitude(Double longitude) {
Longitude = longitude;
}
public Double getLatitude() {
return latitude;
}
public void setLatitude(Double latitude) {
this.latitude = latitude;
}
}
package spittr.data;
public interface SpittleRepository {
List<Spittle> getSpittleList(long max, int count);
}
max参数的含义是要查找的spittle的id的最大值,count是要查找记录数量,为了查询出最新的20条记录,我们可以这样调用:
spittleRepository.getSpittleList(Long.MAX_VALUE, 20);
package spittr.web;
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private SpittleRepository spittleRepository;
@Autowired
public void setSpittleRepository(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute(spittleRepository.getSpittleList(Long.MAX_VALUE, 20));
return "spittles";
}
}
在控制器方法中,接收了一个Model参数,其实这个Model参数就是一个Map(键值对)。model的addAttribute方法可以设置key也可以不设置key,如果不设置key,key的值会根据值的类型确定,例如这里的值的类型是List,那么可以就是spittleList。我们也可以设置key,效果和上面的一样,如下:
@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute("spittleList", spittleRepository.getSpittleList(Long.MAX_VALUE, 20));
return "spittles";
}
我们也不一定要用Spring的类型,也可以使用java.util.Map。
@RequestMapping(method = RequestMethod.GET)
public String spittles(Map model) {
model.addAttribute("spittleList", spittleRepository.getSpittleList(Long.MAX_VALUE, 20));
return "spittles";
}
甚至还有下面这种写法:
@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles() {
return spittleRepository.getSpittleList(Long.MAX_VALUE, 20);
}
这里我们既没有显示地把数据放到模型Model中,也没有返回逻辑视图名。但是,当控制器方法返回对象或集合时,会把对象或集合放到Model中,key根据值的类型确定。
而逻辑视图名根据url获取,例如这里的url是"/spittles",那么逻辑视图名就是"spittles"(去掉前面的/)。
最后,只剩下视图。SpringMVC会把模型数据作为请求属性放到请求(request)中。
<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">${spittle.time}</span>
<span class="spittleLocation">
(<c:out value="${spittle.Longitude}" />, <c:out value="${spittle.latitude}" />)
</span>
</div>
</li>
</c:forEach>
接受请求的输入
可以从页面发送数据到后台,其中,SpringMVC允许多种方式将前端参数传到后台:
- 查询参数(Query Parameter)
- 路径变量(Path Variable)
- 表单参数(Form Parameter)
处理查询参数
在之前查询spittles的例子中,我们只能查询最新的20条记录,不可以查再之前的记录,所以,我们需要一个分页功能,来查询历史记录。
在getSpittleList方法中,查询的是id小于max的spittle,所以如果我们想查第二页,那么,我们可以传上一页,也就是第一页的最后一个记录的id到后台,查找小于这个id的记录。同样,count也可以从页面传来。
这里,我们用到了注解@RequestParam.
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
@RequestParam("max") long max,
@RequestParam("count") int count
) {
return spittleRepository.getSpittleList(max, count);
}
前端请求url如下
/spittles?max=33900&count=50
注意: @RequestMapping中的value值要和请求url中的参数名一样,控制器方法的参数名可以和前面两个不一样
@RequestParam有一个required属性,值是true/false,默认是true,意思是请求中是否要包含这个参数,true意味着请求中一定要有这参数,不然就会报错。
而@RequestParam默认是需要前端传这个注解修饰的参数到后台,不然会报错。这时,为了满足有无参数到2种形式,我们可以使用@RequstParam的属性defaultValue来设置参数到默认值,如果页面没有传参数过来,就是用这个默认值。
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
@RequestParam(value = "max", defaultValue=LONG_MAX_AS_STRING) long max,
@RequestParam(value = "count", defaultValue="20") int count
) {
return spittleRepository.getSpittleList(max, count);
}
private static final String LONG_MAX_AS_STRING = Long.toString(Long.MAX_VALUE);
因为查询参数要是字符串类型的,到控制器方法后,会根据参数到类型进行转换,所以这里的默认值Long的最大值转成字符串类型。
通过路径参数接收输入
现在我们有一个需求,前端传递一个spittle id,后台需要根据这个id来查询spittle,控制器方法我们可以写成下面这样。
@RequestMapping(value="show", method=RequestMethod.GET)
public String spittle(@RequestParam("spittle_id") long spittleId) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
请求的入url将是这样子"/spittles/show/spittle_id=12345",但是,这是查询参数的方式,而路径变量方式的url是"/spittles/12345"。
我们使用注解@PathVariable来将url中的占位符参数映射到控制器的处理器方法参数中。将占位符放到大括号{}中。
使用路径变量的控制器方法如下:
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
其中,@PathVariable的value值要和占位符的参数名一致,控制器方法的参数名可以和前面两者不一样。如果一样,那么@PathVariable的value值可以省略。
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(@PathVariable long spittleId) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
但是,如果控制器方法的参数名需要修改的话,那么占位符的参数名一样修改成一样。
处理表单
当页面只有少量数据需要发送时,我们可以使用查询参数或路径变量来发送数据,但是,如果有很多数据时,使用查询参数和路径变量就很笨拙,这时,我们使用另一种方式,表单参数的方式。
现在有一个需求,在spittr应用中添加注册用户的功能,控制器方法如下:
package spittr.web
@Controller
@RequestMapping("/spitter")
public class SpitterController {
@RequestMapping(value="/regiester", method=RequestMethod.GET) //处理"/spitter/register"的get请求
public String getRegisterationForm() {
return "registerForm";
}
}
registerForm.jsp页面如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page session="false" %>
<html>
<head>
<title>Spittr</title>
</head>
<body>
<form method="post">
firstName:<input type="text" name="firstName" />
secondName:<input type="text" name="secondName" />
username:<input type="text" name="username" />
password:<input type="password" name="password" />
<input type="submit" value="Register" />
</form>
</body>
</html>
这个jsp页面的form表单没有定义action,所以,在提交时,会提交到展现页面时相同的url上,即"/spitter/register",使用http post方式。所以,我们需要定义一个post方法。
编写处理表单的控制器
现在,我们需要编写处理表单的控制器方法,当保存新用户信息后,跳转到用户信息页面。
package spittr.web
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
@RequestMapping(value="/regiester", method=RequestMethod.GET) //处理"/spitter/register"的get请求
public String getRegisterationForm() {
return "registerForm";
}
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(Spitter spitter) {
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.gerUsername;
}
}
在控制器参数中,我们写了pojo类,pojo类对象会把和属性名同名的请求参数作为填充值。
这个控制器方法和之前的相比没多少不同的地方, 唯一不同的是,在返回的字符串中,有一个"redirect:"的前缀。这个前缀的意思是告诉视图解析器,接下来的字符串是以重定向的方式处理。我们还可以添加"forward:"的前缀,表明是用请求转发的方式。
接下来,我们来编写跳转到用户信息页面的控制器处理器方法。
@RequestMapping(value="/{username}", method=RequestMethod.GET)
public String profile(@PathVariable String username, Model model) {
Spitter spitter = spitterRepository.selectByUsername(username);
model.addAttribute(spitter);
return "profile";
}
展示用户信息的页面如下:
<ul>
<li>${spitter.firstname}</li>
<li>${spitter.secondname}</li>
<li>${spitter.username}</li>
<li>${spitter.password}</li>
</ul>
校验表单
现在有一个问题,如果用户提交上来的表单数据,有些域是为空或者长度非常长,这时,可能会出现问题,因此,我们需要对表单数据进行校验。
有一种初级的校验方式,就是如果表单数据不合法时,就跳转到注册页面。虽然我们可以在控制器方法中加入if判断,来校验表单数据,但是,我们不希望在控制器方法中加入过多的非业务逻辑的代码。这时,我们可以使用Java校验API,从Spring3.0开始,就支持Java校验API,只需引入类路径即可。
Java校验API注解如下表:
注 解 | 描 述 |
---|---|
@AssertFalse | 所注解的元素必须是Boolean类型,并且值为false |
@AssertTrue | 所注解的元素必须是Boolean类型,并且值为true |
@DecimalMax | 所注解的元素必须是数字,并且它的值要小于或等于给定的BigDecimalString值 |
@DecimalMin | 所注解的元素必须是数字,并且它的值要大于或等于给定的BigDecimalString值 |
@Digits | 所注解的元素必须是数字,并且它的值必须有指定的位数 |
@Future | 所注解的元素的值必须是一个将来的日期 |
@Max | 所注解的元素必须是数字,并且它的值要小于或等于给定的值 |
@Min | 所注解的元素必须是数字,并且它的值要大于或等于给定的值 |
@NotNull | 所注解元素的值必须不能为null |
@Null | 所注解元素的值必须为null |
@Past | 所注解的元素的值必须是一个已过去的日期 |
@Pattern | 所注解的元素的值必须匹配给定的正则表达式 |
@Size | 所注解的元素的值必须是String、集合或数组,并且它的长度要符合给定的范围 |
除了上表的注解,还有额外的注解,而且,我们也可以定义自己的注解。但是,常用的有2个@NotNull和@Size。注意: 这些注解是写在pojo类的属性上的。
下面,我们使用这两个注解定义Spitter。
public class Spitter {
private Long id;
@NotNull
@Size(max = 16, min = 5)
private String username;
@NotNull
@Size(max = 20, min = 6)
private String password;
@NotNull
@Size(max = 30, min = 2)
private String firstname;
@NotNull
@Size(max = 30, min = 2)
private String secondname;
//get,set方法
}
接着,我们需要重写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();
}
在参数加上注解@Valid,会告诉Spring,要确保这个对象满足校验规则。如果校验出错,我们可以使用Errors对象获取错误信息,但是要注意,Errors参数要紧跟在@Valied注解修饰的参数后面。