文章目录
SpringMVC表单处理
1. 向前端传递模型数据
在上一章的入门案例中,我们学习了SpringMVC的基本处理流程,本章我们将继续学习SpringMVC的数据处理,尤其是表单数据的处理。
我们把上一章的向前台返回数据完善一下,变成一个从数据库查询城市列表,然后返回前台展示的案例。
首先在pom中增加必要的数据访问类库,最后的pom文件应该是这个样子:
<!-- Spring 组件 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.19.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.19.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.19.RELEASE</version>
</dependency>
<!-- 数据访问 组件 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 其它 组件 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.7</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jcl</artifactId>
<version>2.11.1</version>
</dependency>
我们将pom文件分为3部分,依次是spring的模块,数据访问组件以及其它组件,其中,为了方便在jsp中展示数据,我们添加了jstl的标签库。
其次,我们需要把相关数据访问有关的bean移植到RooConfig中:
@Configuration
@ComponentScan(basePackages = { "com.turing" }, excludeFilters = {
@Filter(type = FilterType.ANNOTATION, value = { EnableWebMvc.class, Controller.class }) })
@MapperScan("com.turing.mapper")
@ImportResource("classpath:spring-transaction.xml")
public class RootConfig {
@Bean
public DataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
// jdbc四要素
ds.setUsername("root");
ds.setPassword("admin");
ds.setUrl("jdbc:mysql://localhost:3306/world");
ds.setDriverClassName("com.mysql.jdbc.Driver");
// dbcp数据源属性
ds.setInitialSize(5);
ds.setMinIdle(2);
ds.setMaxIdle(16);
ds.setMaxTotal(8);
ds.setMaxWaitMillis(60000);
ds.setValidationQuery("select 1");
return ds;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 注入数据源
factoryBean.setDataSource(dataSource);
// 设置mybatis自身属性
org.apache.ibatis.session.Configuration cfg = new org.apache.ibatis.session.Configuration();
cfg.setLogImpl(Log4j2Impl.class);
cfg.setMapUnderscoreToCamelCase(true);
cfg.setLazyLoadingEnabled(true);
cfg.setAggressiveLazyLoading(false);
cfg.setLazyLoadTriggerMethods(new HashSet<>(Arrays.asList("")));
// 设置entity类型别名
cfg.getTypeAliasRegistry().registerAliases("com.turing.entity");
factoryBean.setConfiguration(cfg);
SqlSessionFactory sqlSessionFactory = factoryBean.getObject();
return sqlSessionFactory;
}
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
return transactionManager;
}
}
控制器代码:
@Controller
public class CityController {
@Autowired
private CityService cityService;
@RequestMapping(value = "/findCities", method = RequestMethod.GET)
public String findCities(Model model) {
List<City> list = cityService.findAll();
model.addAttribute("cityList", list);
// model.addAttribute(list);// 默认的key会根据类型推断为cityList
return "cityList";
}
}
在上述方法中,为了向前台传递数据,我们在方法中提供了一个Model
参数,SpringMVC提供的这个Model
实际上就是一个基于key-value
的Map,它会帮助我们传递数据给视图层,数据默认是存放在请求作用域的。
cityList.jsp
<ul>
<c:forEach var="city" items="${cityList }">
<li>${city.id }-----${city.name }</li>
</c:forEach>
</ul>
2.接收请求输入
SpringMVC允许以多种方式将客户端的数据传送到控制器的处理方法中,我们将讲解主要用的到3种。
2.1 查询参数
以查询参数的方式提交数据应该是最传统的一种方式了,提交的url如下:
<span><a href="findCity?id=4">根据id查询City信息(查询参数)</a></span>
控制器代码:
@RequestMapping(value = "/findCity", method = RequestMethod.GET)
public String findCity(@RequestParam("id") int id, Model model) {
City city = cityService.findById(id);
model.addAttribute("city", city);
return "city";
}
为了接收这个参数,我们只需要在控制器的处理方法中增加一个或多个对应的参数就可以了。@RequestParam("id")
将负责把url问号后面的键值对形式的参数设置到控制器方法的参数上。当前台提交的参数名和后台处理方法中接收的参数名相一致时,整个@RequestParam("id")
都可以省略掉。
2.2 路径变量
目前还有一种较为流行的Restful风格,上述提交请求会变成如下形式:
<span><a href="findCity/4">根据id查询City信息(路径变量)</a></span>
提交的数据是作为路径的一部分,而不是以键值对的方式附加在问号后面的。
控制器代码:
@RequestMapping(value = "/findCity/{id}", method = RequestMethod.GET)
public String findCityByPath(@PathVariable("id") int id, Model model) {
City city = cityService.findById(id);
model.addAttribute("city", city);
return "city";
}
在@RequestMapping
中,{id}
表示提交的数据所对应的占位符,在处理方法中,@PathVariable("id")
将负责把路径中变量名为的“id”的值赋给该注解附着的变量int id
。如果占位符和参数名刚好相等的话,@PathVariable("id")
可以简写成@PathVariable
。
2.3 表单参数
当前台提交的数据非常多的时候,无论使用查询参数或是路径变量,就显得非常笨重了,这时我们可以借助表单。为了更方便的展示SpringMVC的特性,我们在编制新增City的表单时,将使用SpringMVC提供的JSP标签库。该标签库类似struts2的标签库一样,除开可以将后端的数据预先显示在页面上,还可以在发生错误的时候,把校验得出的错误信息展示在前台。
cityForm.jsp:
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf"%>
<sf:form action="city" commandName="city" method="post">
城市名:<sf:input path="name" /><br>
国家代码:<sf:input path="countrycode" /><br>
区域:<sf:input path="district" /><br>
人口:<sf:input path="population" /><br>
<input type="submit" value="提交">
</sf:form>
我们需要编写一个专门的方法跳转到这个新增city的页面:
@RequestMapping(value = "/city", method = RequestMethod.GET)
public String showCityForm(Model model) {
model.addAttribute("city", new City());
return "cityForm";
}
SpringMVC标签库最大的特点,就是可以把表单绑定到模型上,但是,这同时也是一个额外的要求。如上述代码中,在后台处理跳转到cityForm页面的逻辑中,往model中我们就必须要设置一个key,key的变量名,必须与表单中commandName
指定的变量名相同,否则,会导致表单绑定到模型失败,JSP会出现异常。
<sf:input path="name" />
中的path属性,就是从commandName
指定的模型对象中,来按照对应的属性来提交或者获取值。这个标签,最终会被转变成类型为text的标准html的input
标签,SpringMVC提供了绝大部分表单所需的标签组件。(submit
除外)
处理新增的逻辑:
@RequestMapping(value = "/city", method = RequestMethod.POST)
public String addCity(City city, Errors errors) {
cityService.save(city);
return "redirect:/findCity/" + city.getId();
}
这个方法虽然与跳转至cityForm的路径一样都是“/city”,但是提交的方式为POST,所以SpringMVC可以区分。
为了防止用户在新增city的时候出现刷新导致的重复提交,我们可以在访问路径前面添加“redirect:”实现重定向。重定向以后,即使用户不停的刷新页面,由于浏览器的地址栏已经变成了根据id查询city信息的地址,所以这时候的不断提交,就不会导致数据产生意外变化了。
2.4 中文编码
由于在处理表单数据的时候,我们经常会提交中文数据,所以还必须在SpringMVC中设置好中文字符的编码格式,SpringMVC已经把这个中文处理的过滤器为我们准备好了,我们只需直接拿来注册即可:
public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("utf-8");
encodingFilter.setForceEncoding(true);
return new Filter[] { encodingFilter };
}
}
2.5 JSR-303校验
表单的基本流程编写完以后,现在我们来考虑这样一个非常常规的问题:如何校验表单提交的字段?
- 如果把校验逻辑编写在前台页面,当然,这是非常有必要的,但是如果用户直接在地址栏或者通过其他工具通过查询参数来提交,这样就绕过JS的校验逻辑了,如何处理?
- 如果把校验逻辑编写在后台,势必会让校验逻辑和controller中的业务逻辑混淆在一起,往后不容易维护这样的代码。
从Spring3.0开始,在SpringMVC中提供了对Java校验API(Java Validation API,又称JSR303)的支持,要使用这套Java校验API的话,只需要在项目中包含这个Java API的实现就可以了,比如Hibernate Validator。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.Final</version>
</dependency>
首先我们需要在被校验的实体类上添加校验约束(constraint
,在附录中,有详细的constraint说明,供以后查阅):
public class City {
private Integer id;
@NotNull
@Size(min = 3, max = 8, message = "{name.size}")
private String name;
@NotNull(message = "{countrycode}")
private String countrycode;
@NotNull(message = "{district}")
private String district;
@Min(value = 0, message = "{population}")
private Integer population;
//getter & setter
}
其中message属性用来指定错误信息,{name.size}
会指向一个专门用来编写错误信息的配置文件中对应的key,这样可以让每个错误信息与对应的属性对应起来,ValidationMessages.properties:
name.size=城市名称必须在{min}~{max}之间
countrycode=国家代码不能为空
district=区域代码不能为空
population=人口不能小于{value}
{min}
,{max}
,{value}
是占位符,用来获取实体类中注解中对应的属性的。
最后我们处理表单提交的controller代码会要稍作调整:
@RequestMapping(value = "/city", method = RequestMethod.POST)
public String addCity(@Valid City city, Errors errors) {
if (errors.hasErrors()) {
return "cityForm";
}
cityService.save(city);
return "redirect:/findCity/" + city.getId();
}
@Valid
用来启用对这个提交的模型的校验。SpringMVC会把所有校验产生的错误都封装到一个Errors的对象中,传递给我们的方法,我们可以通过errors.hasErrors()
来判断是否在提交的时候产生了错误,如果有的话,我们就重新跳转到新增页面,并显示错误信息:
<sf:form action="city" commandName="city" method="post">
城市名:<sf:input path="name" />
<sf:errors path="name" />
<br>
国家代码:<sf:input path="countrycode" />
<sf:errors path="countrycode" />
<br>
区域:<sf:input path="district" />
<sf:errors path="district" />
<br>
人口:<sf:input path="population" />
<sf:errors path="population" />
<br>
<input type="submit" value="提交">
</sf:form>
<sf:errors path="name" />
可以用来获取到对应属性产生的错误信息,如果没有的话,不会显示任何内容。
3.Session Attribute
在上面的案例中,为了防止重复提交,我们使用了重定向,但重新根据刚才新增city的id来查询一次数据库,又是多余的,如何在成功页面显示刚才新增的数据呢?我们可以使用@SessionAttributes
来将刚才的数据存放在session中:
@RequestMapping(value = "/city", method = RequestMethod.POST)
public String addCity(@Valid City city, Errors errors, Model model) {
if (errors.hasErrors()) {
return "cityForm";
}
cityService.save(city);
model.addAttribute("city", city);
return "redirect:success.jsp";
}
首先将新增的city存放在模型中,然后使用@SessionAttributes
来标记模型中哪些属性应存放在session中:
@Controller
@SessionAttributes("city")
public class CityController {
//...
}
4.Flash Attribute
把要跨页面访问的数据存放在session中,是一种较为方便的做法,但是如果session中的数据不及时清理的话,还是会给服务器造成不必要的负担,SpringMVC还提供了另一个API(flash attribute)来帮助我们解决这个问题。
@RequestMapping(value = "/city", method = RequestMethod.POST)
public String addCity(@Valid City city, Errors errors, RedirectAttributes model) {
if (errors.hasErrors()) {
return "cityForm";
}
cityService.save(city);
model.addFlashAttribute("city", city);
return "redirect:success.jsp";
}
RedirectAttributes在执行重定向之前,所有的flash属性都会复制到会话中。在重定向之后,存在会话中的flash属性会被取出,并从会话转移到模型之中。flash属性会一直携带这些数据知道下一次请求,然后才会消失。
5.附录
5.1JSR-303提供的约束
Constraint | 详细信息 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
5.2Hibernate-Validator附加的约束
Constraint | 详细信息 |
---|---|
@Email | 被注释的元素必须是电子邮箱地址 |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
| 被注释的元素必须是一个将来的日期 |
| @Pattern(value)
| 被注释的元素必须符合指定的正则表达式 |
5.2Hibernate-Validator附加的约束
Constraint | 详细信息 |
---|---|
@Email | 被注释的元素必须是电子邮箱地址 |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |