处理器映射
web工程使用了SpringMVC,那么他在启动阶段就会将注解@RequestMapping所配置的内容保存到处理器映射(HanlderMapping)机制中去,然后等待请求的到来,通过拦截器请求的信息与handlerMapping进行匹配,找到对应的处理器(他包含处理器逻辑),并将处理器及其拦截器保存到HandlerExecutionChain对象中,返回给DispatcherServlet,这样DispatcherServlet就可以运行他们了。从以上论述中可以看出,HandlerMapping的主要任务是将请求定位到具体的处理器上。
RequestMapping源代码:
package org.springframework.web.bind.annotation;
/**import**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
//配置请求映射名称
String name() default "";
//通过路径映射
@AliasFor("path")
String[] value() default {};
//通过路径映射会回path配置项
@AliasFor("value")
String[] path() default {};
//限定只响应HTTP请求类型,如GET、POST、HEAD、OPTIONS、PUT、TRACE等
//在默认情况下可以响应所有请求类型
RequestMethod[] method() default {};
//当存在对应的HTTP请求参数时才响应
String[] params() default {};
//限定请求头存在对应的参数时才响应
String[] headers() default {};
//限定HTTP请求提交类型,如“application/json”,“text/html”
String[] consumes() default {};
//限定返回的内容类型,仅当HTTP请求头中的accept类型中包含该指定类型时才返回
String[] produces() default {};
}
路径是必须的配置项,这里的method配置项可以限定http的请求类型,这是最常用的配置项,可以区分http的get或post等不同的请求。为了简化method配置项的配置新增了几个注解,如@GetMapping,@PostMapping,@PatchMapping,@PutMapping和@DeleteMapping。从名称可以看出@GetMapping对应的是HTTP的GET方法,@PostMapping对应的是HTTP的POST方法。
获取控制器参数
处理器是对控制器的包装,在处理器运行的过程中会调度控制器的方法,只是他在进入控制器方法之前会对http的参数和上下文进行解析,将他们转换为控制器所需的参数。这一步是处理器首先需要做的事情,只是在大部分情况下不需要自己去开发这一步,因为springMVC已经提供了大量的转换规则,通过这些规则就能非常简易的获取大部分的参数。
- 在无注解下获取参数
在没有注解的情况下,SpringMVC也可以获取参数,且参数允许为空,唯一的要求是参数名称和HTTP请求的参数名称保持一致,如下代码所示:
package com.springboot.chapter10.controller;
/**import**/
@Controller
@RequestMapping("/my")
public class MyController {
/**
* 在无注解的情况下获取参数,要求参数名称和http请求参数名称一致
* @param intVal
* @param longVal
* @param str
* @return 响应json参数
*/
//http get请求
@GetMapping("/no/annotation")
@ResponseBody
public Map<String,Object> noAnnotation(Integer intVal,Long longVal,String str){
Map<String,Object> map = new HashMap<String, Object>();
map.put("intVal", intVal);
map.put("longVal", longVal);
map.put("str", str);
return map;
}
}
启动springboot应用后,在浏览器中请求URL:
http://localhost:8806/my/no/annotation?intVal=10&longVal=200
从代码中可以看出控制器方法参数中还有一个字符串参数str,但因为参数在默认的规则下可以为空,所以这个请求并不会报错,因为方法标注了@ResponseBody,所以在控制器返回的结果就会转化为JSON数据集。
- 使用@RequestParams获取参数
在无需任何注解的情况下,就要求HTTP参数和控制器方法参数名称保持一致。然而在前后端分离的趋势下,前端的命名规则可能与后端的规则不同,这时需要把前端的参数与后端的对应起来。SpringMVC提供了注解@RequestParam来确定前后端参数名称的映射关系,如下代码:
/**
* 通过注解@RequestParam获取参数,用于前后端参数不一致
* @RequestParam(value = "str_val",required = false)String str
* 加一个required = false允许参数为空
* @param intVal
* @param longVal
* @param str
* @return
*/
@GetMapping("/param")
@ResponseBody
public Map<String, Object> requestParam(
@RequestParam("int_val")Integer intVal,
@RequestParam("long_val")Long longVal,
@RequestParam(value = "str_val",required = false)String str){
Map<String, Object> map = new HashMap<String, Object>();
map.put("intVal", intVal);
map.put("longVal", longVal);
map.put("str", str);
return map;
}
从代码中可以看出,在方法参数处使用了注解@RequestParam,其目的是指定HTTP参数和方法参数的映射关系,这样处理器就会按照其配置的映射关系来得到参数,然后调用控制器的方法。启动SpringBoot应用后,在浏览器地址栏输入http://localhost:8080/my/param?int_val=1&long_val=2&str_val=str,就能够看到请求的结果了。但如果把3个HTTP参数中的任意一个删去,就会得到异常报错的信息,因为在默认的情况下@RequestParam标注的参数是不能为空的,为了让他能够为空,可以配置其属性required为false,例如:
@RequestParam(value = "str_val",required = false)String str
这样,对应的参数就可以为空了。
- 传递数组
在springMVC中,除了可以像上面那样传递一些简单的值外,还可以传递数组。springMVC内部已经能够支持用逗号分隔的数组参数。
/**
* 传递数组参数
* @param intArr
* @param longArr
* @param strArr
* @return
*/
@RequestMapping("/array")
@ResponseBody
public Map<String, Object> requestArray(
@RequestParam("int_arr")Integer[] intArr,
@RequestParam("long_arr")Long[] longArr,
@RequestParam("str_arr")String[] strArr){
Map<String, Object> map = new HashMap<String, Object>();
map.put("intArr", intArr);
map.put("longArr", longArr);
map.put("strArr", strArr);
return map;
}
方法里定义了采用数组,那么前端就需要依照一定的规则传递给这个方法,例如,输入http://localhost:8080/my/array?int_arr=1,2,3&long_arr=4,5,6&str_arr=str1,str2,str3,可以看到需要传递数组参数时,每个参数的数组元素只需要通过逗号分隔即可。
- 传递JSON
在前后端分离的情况下,使用json已经十分普遍了。对于前端的页面或者手机应用,可以通过请求后端的json数据集,这样它们就能方便地将数据渲染到视图中。有时前端也需要提交复杂的数据到后端,为了更好的组织和提高代码的可读性,可以将数据转换为json数据集,通过HTTP请求体提交给后端,对此springMVC也提供了良好的支持。
新增用户表单:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>新增用户</title>
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$("#submit").click(function(){
var user_name = $("#user_name").val();
var note = $("#note").val();
if($.trim(user_name)==''){
alert("用户名不能为空!");
return;
}
var params = {
user_name : user_name,
note : note
};
$.post({
url : "./insert",
//此处告知传递参数类型为json,不能缺少
contentType : "application/json",
//将json转化为字符串
data : JSON.stringify(params),
//成功后的方法,方法回调
success : function(result){
if(result == null|| result.id == null){
alert("插入失败!");
return;
}
alert("插入成功!");
}
});
});
});
</script>
</head>
<body>
<div style="margin: 20px 0;" align="center">
<form id="insertForm">
<table>
<tr>
<td>用户名称:</td>
<td><input id="user_name" name="user_name"></td>
</tr>
<tr>
<td>备注:</td>
<td><input id="note" name="note"></td>
</tr>
<tr>
<td></td>
<td align="center"><input id="submit" type="button" value="提交" ></td>
</tr>
</table>
</form>
</div>
</body>
</html>
使用jQuery进行ajax提交。这里先组织了一个json数据集,而且把提交类型也设置为了json类型,然后才提交到控制器。这样控制器就可以得到一个json数据集的请求体了。
package com.springboot.chapter10.controller;
@RequestMapping("/user")
@Controller
public class UserController {
@Autowired
private UserService userService;
/**
* 打开请求页面
* @return
*/
@RequestMapping("/add")
public String add() {
return "/user/add";
}
/**
* 新增用户,@RequestBody注解得到页面传过来的json参数
* @param user
* @return
*/
@PostMapping("/insert")
@ResponseBody
public User insert(@RequestBody User user) {
userService.insert(user);
return user;
}
}
从图中可以看到请求的add方法就能够打开jsp页面,入录表单后,启动浏览器的监控功能,然后按下提交按钮,在监控区域可以看到后端返回了新增用户信息,并以json数据集给予展示。
- 通过URL传递参数
在一些网站中,提出了REST风格,这时参数往往通过URL进行传递。例如获取编号为1的用户,URL就要写成/user/1,这里1代表的是用户编号(id)。SpringMVC对此也提供了良好的支持,可以通过处理器映射和注解@PathVariable的组合获取URL参数。首先通过处理器可以定位参数的位置和名称,而@PathVariable则可以通过名称来获取参数。
/**
* 通过URL传递参数 查询单个用户,Rest风格,处理器先定位参数,@PathVariable注解来获取参数
* @param id
* @return
*/
// {...}代表占位符,还可以配置参数名称
@GetMapping("/{id}")
@ResponseBody
public User getUser(@PathVariable("id") Integer id) {
User user = userService.getUser(id);
return user;
}
代码中首先通过@GetMapping指定一个URL,然后用{…}来标明参数的位置和名称。这里指定名称为id,这样springMVC就会根据请求去匹配这个方法。@PathVariable配置的字符串为id,它对应URL的参数声明,这样spring就知道如何从URL中获取参数。
- 获取格式化参数
在一些应用中,往往需要格式化数据,其中最为典型的当属时间和货币。例如,在一些系统中日期格式约定为yyyy-MM-dd,金额约定为货币符号和用逗号分隔。同样地,springmvc中也对此提供了良好的支持。对日期和数字类型的转换注解进行处理,分别是@DateTimeFormat和@NumberFormat。其中@DateTimeFormat是针对日期进行格式化的,@NumberFormat则是针对数字进行格式化的。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>格式转换</title>
</head>
<body>
<form action="./commit" method="post">
<table>
<tr>
<td>日期(yyyy-MM-dd)</td>
<td><input type="text" name="date" value="2017-08-08" /></td>
</tr>
<tr>
<td>金额(#,###.##)</td>
<td><input type="text" name="number" value="1,234,567.89" /></td>
</tr>
<tr>
<td colspan="2" align="right"><input type="submit" value="提交" />
</td>
</tr>
</table>
</form>
</body>
</html>
/***
* 打开formatter页面
* @return
*/
@GetMapping("/format/form")
public String showFormat(){
return "/format/formatter";
}
/**
* 日期和数字格式化转换
* @param date
* @param number
* @return
*/
@PostMapping("/format/commit")
@ResponseBody
public Map<String, Object> format(@DateTimeFormat(iso = ISO.DATE)Date date,
@NumberFormat(pattern = "#,###.##")Double number){
Map<String, Object> map = new HashMap<String, Object>();
map.put("date", date);
map.put("number", number);
return map;
}
在springboot中,日期参数的的格式化也可以不使用@DateFormat,而只是在配置文件application.properties中加入如下配置项即可:
spring.mvc.date-formate=yyyy-MM-dd
自定义参数转换规则
当与第三方公司进行合作,参数从第三方公司以密文的形式传递,或者其所定义的参数规则是现有springMVC所不能的支持的,这是则需要通过自定义参数转换规则来满足这些特殊的要求。
在springMVC中只需要简单的注解,甚至是不用任何注解就能够得到参数。那是因为springMVC提供的处理器会先以一套规则来实现参数的转换。实际上处理器的转换规则还包含控制器返回后的处理。
HTTP的请求包含请求头(header)、请求体(body)、URL和参数等内容,服务器还包含其上下文环境和客户端交互会话(Session)机制,而这里的消息转换是指请求体的转换。下面是springMVC是如何从HTTP请求中获取参数的。
- 处理器获取参数逻辑
当请求来到时,在处理器执行的过程中,首先会从HTTP请求和上下文环境来得到参数。如果是简易的额参数他会以简易的转换器进行转换,而这些简单的转换器时springMVC自身已经提供了的。但是如果是转换HTTP请求体(body),他就会调用HTTPmassageConverter接口的方法对请求体的信息进行转换,首先他会判断能否对请求体进行转换,如果可以将会转换成java类型。
package org.springframework.http.converter;
/**import**/
public interface HttpMessageConverter<T> {
//是否可读,其中clazz为java类型,mediaType为HTTP请求类型
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
//判断clazz类型是否能够转换为mediaType媒体类型
//其中calzz为java类型,mediaType为HTTP响应类型
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
//可支持的媒体类型列表
List<MediaType> getSupportedMediaTypes();
//当canRead验证通过后,读入HTTP请求信息
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
//当canWrite方法验证通过后,写入响应
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
在UserController类中,控制器的方法参数标注了@RequestBody,所以处理器会采用请求体(body)的内容进行参数转换,而前端的请求体为json类型,所以首先他会调用canRead方法来确定请求体是否可读。如果判定可读后,接着就是使用read方法,将前端提交的用户json类型的请求体转换为控制器的用户(User)类参数,这样控制器就得到参数了。
HttpMessageConverter接口只是将HTTP的请求体转换为对应的java对象,而对于HTTP参数和其他内容,以性别参数来说,前端可能传递给控制器的是一个整数,而控制器参数却是一个枚举,这就需要提供自定义的参数转换规则了。
自定义参数转换规则,首先需要了解处理器转换参数的过程。在springMVC中,是通过webDataBinder机制来获取参数的,他的主要作用是解析HTTP请求的上下文,然后在控制器调用之前转换参数并且提供验证的功能,为调用控制器的方法做准备。处理器会从HTTP请求中读取数据,然后通过三种接口来进行各类参数转换,这三种接口是Converter、Formatter和GenericConverter。在springmvc的机制中,这三种记得实现类都采用了注册机的机制,默认的情况下springMVC已经在注册机内注册了许多的转换器,这样就可以实现大部分数据类型的转换,所以在大部分情况下无需开发者再提供转换器。同样地,在自定义转换规则时,只需要在注册机上注册自己的转换器就可以了。
实际上,webDataBinder机制还有一个重要的功能,就是验证转换结果。
控制器的参数是处理器通过Converter、Formatter和GenericConverter这三个接口转换出来的。首先,Converter是一个普通的转换器,例如,有一个Integer类型的控制器参数,而从HTTP对应的为字符串,对应的Converter就会将字符串转换为Integer类型;其次,Formatter是一个格式化转换器,类似日期字符串就是通过他按照约定的格式转换为日期的;最后,GenericConverter转换器则将HTTP参数转换为数组。
对于数据类型转换,springMVC提供了一个服务机制去管理,就是ConversionService接口。在默认情况下,会使用这个接口的子类DefaultFormattingConversionService对象来管理这些转换类。
- 一对一转换器(Converter)
Converter是一对一的转化器,也就是从一种类型转换为另一种类型。
Converter接口源码:
package org.springframework.core.convert.converter;
import org.springframework.lang.Nullable;
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
//转换方法,S代表原类型,T代表目标类型
T convert(S source);
}
测试字符串用户转换器:
package com.springboot.chapter10.converter;
/**import**/
/**
* 自定义字符串用户转换器
* @author Administrator
*/
@Component
public class StringToUserConverter implements Converter<String, User> {
@Override
public User convert(String source) {
// TODO Auto-generated method
User user = new User();
String [] str = source.split("-");
user.setId(Integer.parseInt(str[0]));
user.setUser_name(str[1]);
user.setNote(str[2]);
return user;
}
}
- GenericConverter集合和数组转换
GenericConverter是数组转换器,因为springMVC自身就提供了一些数组转换器,需要自定义的并不多。假设需要同时新增多个用户,这样便需要传递一个用户列表(List< User>)给控制器。此时springmvc会使用StringToCollectionConverter转换他,这个类就实现了GenericConverter接口,并且是SpringMVC内部已经注册的数组转换器。他首先会把字符串用逗号分隔为一个个的子字符串,然后根据原类型泛型为String、目标类型为User类,找到对应的Converter进行转换,将子字符串转换为User对象。
/**
* 使用集合传递多个用户,传递过来的请求参数先用StringToCollectionConverter处理,
* 再用自定义的StringToUserConverter转换成user
* @param list
* @return
*/
@GetMapping("/list")
@ResponseBody
public List<User> list(List<User> userList) {
return userList;
}
这里的参数使用了一个个逗号分隔,StringToCollectionConverter在处理时也就通过逗号分隔,然后通过之前自定义的转换器StringToUserConverter将其变为用户类对象,再组成一个列表(List)转递给控制器。
数据验证
在处理器逻辑中谈到了参数的转换,转换参数出来之后,紧接着就是验证参数的合法性,因此SpringMVC也就提供了验证参数的机制。一方面,他可以支持JSR-303注解验证,在默认的情况下,springboot会引入关于Hibernate Validator机制来支持JSR-303验证规范;另外一方面,因为业务比较复杂,所以需要自定义验证规则。
- JSR-303验证
JSR-303验证主要是通过注解的方式进行的。这里先定义一个需要验证的pojo,此时需要在其属性中加入相关的注解。
package com.springboot.chapter10.pojo;
/**import**/
/**
* 验证JSR-303
* @author Administrator
*/
public class ValidatorPojo {
//非空判断
@NotNull(message = "id不能为空")
private Integer id;
@Future(message = "需要一个将来日期")//只能是将来的日期
//@Past //只能是过去的日期
@DateTimeFormat(pattern = "yyyy-MM-dd")//日期格式转换
@NotNull //非空判断
private Date date;
@NotNull
@DecimalMin(value = "0.1") //最小值为0.1元
@DecimalMax(value = "10000.00") //最大值为10000元
private Double doubleValue;
@Max(value = 88,message = "最大值为88") //最大值为88
@Min(value = 1,message = "最小值为1") //最小值为1
@NotNull
private Integer integer;
@Range(min = 1,max = 888,message = "范围是1到888") //限定范围
private Long range;
@Email(message = "邮箱格式错误") //邮箱验证
private String mail;
@Size(min = 20,max = 30,message = "字符串长度要求在20到30之间")
private String size;
//get和set方法
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试JSR-303</title>
<!-- 加载Query文件-->
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$(document).ready(function(){
//请求验证的pojo
var pojo = {
id:null,
date:'2019-09-29',
doubleValue:999999.09,
integer:100,
range:1000,
email:'email',
size:'adv1212',
regexp:'a,b,c,d'
}
$.post({
url:"./validate",
contentType:"application/json",
data:JSON.stringify(pojo),
success:function(result){
}
});
});
</script>
</head>
<body></body>
</html>
打开页面和后台验证方法
@GetMapping("/valid/page")
public String validPage() {
return "/validator/pojo";
}
/**
* 解析验证参数错误
* @param vp 需要验证的pojo,使用注解@Valid表示验证
* @param errors 错误信息,他由springMVC通过验证pojo后自动填充
* @return 错误信息map
*/
@RequestMapping("/valid/validate")
@ResponseBody
public Map<String, Object> validate(@Valid @RequestBody ValidatorPojo vp,Errors errors){
Map<String, Object> errMap = new HashMap<String, Object>();
//获取错误列表
List<ObjectError> oes = errors.getAllErrors();
for (ObjectError oe : oes) {
String key = null;
String msg = null;
//字段错误
if(oe instanceof FieldError) {
FieldError fe = (FieldError) oe;
key = fe.getField(); //获取错误验证字段名
}else {
//非字段错误
key = oe.getObjectName(); //获取验证对象名
}
msg = oe.getDefaultMessage();
errMap.put(key, msg);
}
return errMap;
}
@RequestBody代表着接收一个json参数,这样spring就会获取页面通过ajax提交的json请求体,然后@Valid注解则表示启动验证机制,这样spring就会启用JSR-303验证机制进行验证。他会自动将最后的验证结果放入Errors对象中,这样就可以获取相关验证过后的信息。
- 参数验证机制
为了更加灵活的提供验证机制,Spring只提供自己的验证机制。在参数转换时,可以看到在springMVC中,存在WebDataBinder机制进行管理,在默认情况下Spring会自动地根据上下文通过注册了的转换器转换出控制器所需的参数。在WebDataBinder中除了可以注册转换器外,还允许注册验证器(Validator)。
在spring控制器中,它还允许使用注解@InitBinder,这个注解的作用是允许在进入控制器方法前修改WebDataBinder机制。
springMVC的验证机制接口Validator:
package org.springframework.validation;
public interface Validator {
/**
*判定当前验证器是否支持该Class类型的验证
*@param clazz pojo类型
*@return 当前验证器是否支持该pojo类型
*/
boolean supports(Class<?> clazz);
/*
*如果supports返回true,则这个方法执行验证逻辑
*@param target被验证pojo对象
*@param errors错误对象
*/
void validate(Object target, Errors errors);
}
实例:
自定义用户验证器
package com.springboot.chapter10.validator;
/**import**/
/**
* 自定义用户验证器
* @author Administrator
*/
public class UserValidator implements Validator {
//该类只支持User类验证
@Override
public boolean supports(Class<?> clazz) {
// TODO Auto-generated method stub
return clazz.equals(User.class);
}
//验证逻辑
@Override
public void validate(Object target, Errors errors) {
// TODO Auto-generated method stub
//对象为空
if(target == null) {
errors.rejectValue("", null, "用户不能为空");
}
//强制转换
User user = (User) target;
//用户名非空串
if(StringUtils.isEmpty(user.getUser_name())) {
//增加错误,可以进入控制器方法
errors.rejectValue("user_name", null, "用户名不能为空");
}
}
}
有了这个验证器,spring还不会启动它,因为还没有绑定给webDataBinder机制。在springMVC提供了一个注解@InitBinder,它的作用是在执行控制器方法之前,处理器会先执行被@InitBinder标注的方法。这时可以将webDataBinder对象作为参数传递到方法中,通过这层关系得到webDataBinder对象,这个对象有一个setValidator方法,他可以绑定自定义的验证器,他可以绑定自定义的验证器,这样就可以在获取参数之后,通过自定义的验证器去验证参数。
绑定验证器:
/**
* 调用控制器前先执行这个方法
*
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
// 绑定验证器
binder.setValidator(new UserValidator());
// 定义日期参数格式,参数不在需要注解@DataTimeFormat,boolean参数表示是否允许为空
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
/**
*
* @param user 用户对象用StringToUserConverter转换
* @param errors 验证器返回的错误
* @param date 因为WebDataBinder已经绑定了格式,所以不再需要注解
* @return
*/
@GetMapping("/validator")
@ResponseBody
public Map<String, Object> validator(@Valid User user, Errors errors, Date date) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("user", user);
map.put("date", date);
if (errors.hasErrors()) {
// 获取全部错误
List<ObjectError> oes = errors.getAllErrors();
for (ObjectError oe : oes) {
// 判断是否字段错误
if (oes instanceof FieldError) {
// 字段错误
FieldError fe = (FieldError) oe;
map.put(fe.getField(), fe.getDefaultMessage());
} else {
// 对象错误
map.put(oe.getObjectName(), oe.getDefaultMessage());
}
}
}
return map;
}
数据模型
在springMVC的应用中,控制器是业务逻辑核心内容,而控制器的核心内容之一就是对数据的处理。通过对springMVC全流程的学习,可以看到允许控制器自定义模型和视图(ModelAndView),其中模型是存放数据的地方,视图则是展示给用户。
数据模型的作用是绑定数据,为后面的视图渲染做准备
SpringMVC数据模型设计图
从上图中可以看出,在类ModelAndView中存在一个ModelMap类型的属性,ModelMap继承了LinkHashMap类,所以他具备map接口的一切特性,除此之外它还可以增加数据属性。在springMVC应用中,如果在控制器方法的参数中使用ModelAndView、Model或者ModelMap作为参数类型,SpringMVC会自动创建数据模型对象,如下代码:
package com.springboot.chapter10.controller;
/**imports**/
@Controller
@RequestMapping("/data")
public class DataModelController {
@Autowired
private UserService userService;
// 测试Model接口
@GetMapping("/model")
public String useModel(Integer id, Model model) {
User user = userService.getUser(id);
model.addAttribute("user", user);
return "/data/user";
}
// 测试modelmap类
@GetMapping("/modelMap")
public ModelAndView useModelMap(Integer id, ModelMap modelMap) {
User user = userService.getUser(id);
ModelAndView mav = new ModelAndView();
// 设置视图名称
mav.setViewName("/data/user");
// 设置数据模型,此处modelmap没有与modelandview绑定,这步系统会自动处理
modelMap.addAttribute("user", user);
return mav;
}
// 测试modelAndview
@GetMapping("/mav")
public ModelAndView useModelAndView(Integer id, ModelAndView mv) {
User user = userService.getUser(id);
mv.setViewName("/data/user");
mv.addObject("user", user);
return mv;
}
}
用户视图如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户信息</title>
</head>
<body>
<table>
<tr>
<td>编号</td>
<td>${user.id}</td>
</tr>
<tr>
<td>用户名</td>
<td>${user.user_name}</td>
</tr>
<tr>
<td>备注</td>
<td>${user.note}</td>
</tr>
</table>
</body>
</html>
视图和视图解析器
视图是渲染数据模型展示给用户的组件,在springMVC中又分为逻辑视图和非逻辑视图。逻辑视图是需要视图解析器(ViewResolver)进行进一步定位。对于非逻辑视图,则并不需要进一步地定位视图的位置,他只需要直接将数据模型渲染出来即可。
- 视图实例——导出PDF文件
AbstractPdfView属于非逻辑视图,所以它并不需要任何的视图解析器(ViewResolver)去定位。
在Maven的配置文件中加入相关的依赖:
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>core-renderer</artifactId>
<version>R8</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.12</version>
</dependency>
定义PDF导出接口:
package com.springboot.chapter10.view;
/**imports**/
public interface PdfExportService {
public void make(Map<String, Object> model, Document document, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response);
}
PDF导出视图类
package com.springboot.chapter10.view;
/**imports**/
public class PdfView extends AbstractPdfView {
//导出服务接口
private PdfExportService pdfExportService;
//创建对象是载入导出服务接口
public PdfView(PdfExportService pdfExportService) {
super();
this.pdfExportService = pdfExportService;
}
//调用接口实现
@Override
protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
pdfExportService.make(model, document, writer, request, response);
}
}
在用户控制器中导出PDF数据
// 导出接口
@GetMapping("/export/pdf")
public ModelAndView exportPdf() {
// 查询用户信息列表
List<User> userList = userService.getAllUser();
// 定义PDF视图
View view = new PdfView(pdfExportService());
ModelAndView model = new ModelAndView();
// 设置视图
model.setView(view);
// 加入数据模型
model.addObject("userList", userList);
return model;
}
// 导出PDF定义
@SuppressWarnings("unchecked")
private PdfExportService pdfExportService() {
// TODO Auto-generated method stub
// 使用Lambda表达式定义自定义导出
return (model, document, writer, request, response) -> {
try {
// A4纸张
document.setPageSize(PageSize.A4);
// 标题
document.addTitle("用户信息");
// 换行
document.add(new Chunk("\n"));
// 表格,3列
PdfPTable table = new PdfPTable(3);
// 单元格
PdfPCell cell = null;
// 字体,定义为蓝色加粗
Font f8 = new Font();
f8.setColor(Color.BLUE);
f8.setStyle(Font.BOLD);
// 标题
cell = new PdfPCell(new Paragraph("id", f8));
// 居中对齐
cell.setHorizontalAlignment(1);
// 将单元格加入表格
table.addCell(cell);
cell = new PdfPCell(new Paragraph("user_name", f8));
// 居中对齐
cell.setHorizontalAlignment(1);
table.addCell(cell);
cell = new PdfPCell(new Paragraph("note", f8));
cell.setHorizontalAlignment(1);
table.addCell(cell);
// 获取数据模型中的用户列表
List<User> userList = (List<User>) model.get("userList");
for (User user : userList) {
document.add(new Chunk("\n"));
cell = new PdfPCell(new Paragraph(user.getId() + ""));
table.addCell(cell);
cell = new PdfPCell(new Paragraph(user.getUser_name()));
table.addCell(cell);
String note = user.getNote() == null ? "" : user.getNote();
cell = new PdfPCell(new Paragraph(note));
table.addCell(cell);
}
// 在文档中加入表格
document.add(table);
} catch (DocumentException e) {
e.printStackTrace();
}
};
}
文件上传
配置
#上传文件配置
#指定默认上传的文件夹
spring.servlet.multipart.location=E:/springboot
#限制单个文件最大大小,这里设置为5M
spring.servlet.multipart.max-file-size=5242880
#限制所有文件最大大小,这里为20MB
spring.servlet.multipart.max-request-size=20MB
文件上传jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form method="post" action="./multipart" enctype="multipart/form-data">
<input type="file" name="file" value="请选择上传的文件" /> <input
type="submit" value="提交" />
</form>
</body>
</html>
文件上传控制器
package com.springboot.chapter10.controller;
/**imports**/
@Controller
@RequestMapping("/file")
public class FileController {
//打开上传页面
@GetMapping("/upload/page")
public String uploadPage() {
return "/file/upload";
}
//使用HttpServletRequest作为参数
@PostMapping("/upload/request")
@ResponseBody
public Map<String, Object> uploadRequest(HttpServletRequest request){
MultipartHttpServletRequest mreq = null;
if(request instanceof MultipartHttpServletRequest) {
//强制转换为MultipartHttpServletRequest接口对象
mreq = (MultipartHttpServletRequest) request;
}else {
return dealResaultMap(false,"上传文件失败!");
}
//获取MultipartFile文件信息
MultipartFile mf = mreq.getFile("file");
//获取源文件信息
String filename = mf.getOriginalFilename();
File file = new File(filename);
try {
//保存文件
mf.transferTo(file);
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
dealResaultMap(false,"上传文件失败!");
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
dealResaultMap(false,"上传文件失败!");
e.printStackTrace();
}
return dealResaultMap(true,"上传文件成功!");
}
//使用SpringMVC的MultipartFile类作为参数
@PostMapping("/upload/multipart")
@ResponseBody
public Map<String, Object> uploadMultipartFile(MultipartFile file){
String fileName = file.getOriginalFilename();
File dest = new File(fileName);
try {
file.transferTo(dest);
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
dealResaultMap(false,"上传文件失败!");
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
dealResaultMap(false,"上传文件失败!");
e.printStackTrace();
}
return dealResaultMap(true,"上传文件成功!");
}
//使用Part类作为参数(推荐)
@RequestMapping("/upload/part")
@ResponseBody
public Map<String, Object> uploadPart(Part file){
//获取提交文件名称
String fileName = file.getSubmittedFileName();
try {
//写入文件
file.write(fileName);
} catch (IOException e) {
// TODO Auto-generated catch block
dealResaultMap(false,"上传文件失败!");
e.printStackTrace();
}
return dealResaultMap(true,"上传文件成功!");
}
//处理上传文件结果
private Map<String, Object> dealResaultMap(boolean flag,String msg){
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", flag);
map.put("message", msg);
return map;
}
}
拦截器
所有的拦截器都需要实现HanlderInterceptor接口
- 自定义拦截器:
package com.springboot.chapter10.interceptor;
/**imports**/
/**
*自定义简单拦截器
* @author Administrator
*
*/
public class Interceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO Auto-generated method stub
System.out.println("处理器前方法");
//返回true不会拦截后续的处理
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
System.out.println("处理器后方法");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
System.out.println("处理器完成方法");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
注册拦截器
package com.springboot.chapter10.main;
/**imports**/
/**
* 实现WebMvcConfigurer接口,覆盖其addInterceptors方法进行注册拦截器
* @author Administrator
*
*/
@SpringBootApplication(scanBasePackages = "com.springboot.chapter10")
@MapperScan(basePackages = "com.springboot.chapter10",annotationClass = Mapper.class)
@Configuration
public class Chapter10Application implements WebMvcConfigurer{
public static void main(String[] args) {
SpringApplication.run(Chapter10Application.class, args);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TODO Auto-generated method stub
//注册拦截器到springMVC机制,然后他会返回一个拦截器注册
InterceptorRegistration ir = registry.addInterceptor(new Interceptor1());
//指定拦截器匹配模式,限制拦截器拦截请求
ir.addPathPatterns("/interceptor/*");
}
}
拦截控制器
package com.springboot.chapter10.controller;
/**imports**/
@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
@GetMapping("/start")
public String start () {
System.out.println("执行处理器逻辑");
return "/welcome";
}
}
jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>深入Spring MVC</title>
</head>
<body>
<h1><%
System.out.println("视图渲染");
out.print("欢迎学习Spring Boot MVC章节\n");
%></h1>
</body>
</html>
- 多个拦截器的顺序
定义多个拦截器,其余几个省略
package com.springboot.chapter10.interceptor;
/**imports**/
/**
*自定义多个拦截器
* @author Administrator
*
*/
public class MulitiInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO Auto-generated method stub
System.out.println("【"+this.getClass().getSimpleName()+"】"+"处理器前方法");
//返回true不会拦截后续的处理
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
System.out.println("【"+this.getClass().getSimpleName()+"】"+"处理器后方法");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
System.out.println("【"+this.getClass().getSimpleName()+"】"+"处理器完成方法");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
注册多个拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TODO Auto-generated method stub
//注册拦截器到springMVC机制,然后他会返回一个拦截器注册
// InterceptorRegistration ir = registry.addInterceptor(new Interceptor1());
//指定拦截器匹配模式,限制拦截器拦截请求
// ir.addPathPatterns("/interceptor/*");
InterceptorRegistration ir1 = registry.addInterceptor(new MulitiInterceptor1());
ir1.addPathPatterns("/interceptor/*");
InterceptorRegistration ir2 = registry.addInterceptor(new MulitiInterceptor2());
ir2.addPathPatterns("/interceptor/*");
InterceptorRegistration ir3 = registry.addInterceptor(new MulitiInterceptor3());
ir3.addPathPatterns("/interceptor/*");
}
国际化
配置国际化消息
#国际化配置
#设置国际化消息是否总是采用格式化,默认为false
spring.messages.always-use-message-format=false
#设置国际化属性名称,如果多个可以使用逗号分隔
spring.messages.basename=international
#设置国际化消息缓存超时秒数,默认为永不过期,如果0表示每次都加载
spring.messages.cache-duration=3600
#国际化消息编码
spring.messages.encoding=UTF-8
#如果没有找到特定区域设置的文件,则设置是否返回到系统区域设置
spring.messages.fallback-to-system-locale=true
#是否使用消息编码作为默认的响应消息,而非抛出NoSuchMessageException异常,只建议在开发阶段使用
spring.messages.use-code-as-default-message=false
国际化属性文件:
international_en_US.properties
msg=Spring MVC internationalization
international_zh_CN.properties
msg=Spring MVC\u56FD\u9645\u5316
international.properties
msg=Spring MVC\u56FD\u9645\u5316
添加国际化解析器和拦截器
在springboot的启动类中增加如下代码:
//国际化拦截器
private LocaleChangeInterceptor lci;
//国际化解析器。注意,这个bean name要为localeResolver
@Bean(name = "localeResolver")
public LocaleResolver initLocaleResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
//默认国际化区域
slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return slr;
}
//创建国际化拦截器
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
if(lci != null) {
return lci;
}
lci = new LocaleChangeInterceptor();
//设置参数名
lci.setParamName("language");
return lci;
}
国际化控制器
package com.springboot.chapter10.controller;
/**imports**/
@Controller
@RequestMapping("/i18n")
public class I18nController {
//注入国际化消息接口对象
@Autowired
private MessageSource messageSource;
//后台获取国际化信息和打开国际化视图
@GetMapping("/page")
public String page(HttpServletRequest request) {
//后台获取国际化区域
Locale locale = LocaleContextHolder.getLocale();
//获取国际化消息
String msg = messageSource.getMessage("msg", null, locale);
System.out.println("msg="+msg);
//返回视图
return "/i18n/internationalization";
}
}
视图国际化
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="mvc" uri="http://www.springframework.org/tags/form"%>
<%@taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC国际化</title>
</head>
<body>
<!-- 通过HTTP请求参数变化国际化 -->
<a href="./page?language=zh_CN">简体中文</a>
<a href="./page?language=en_US">美国英文</a>
<h2>
<!-- 找到属性文件变量名为welcome的配置 -->
<spring:message code="msg" />
</h2>
<!-- 当前国际化区域 -->
Locale: ${pageContext.response.locale }
</body>
</html>
SpringMVC拾遗
-
@ResponseBody注解转换为JSON流程图
-
重定向
//重定向
//显示用户
@GetMapping("/show")
public String showUser(Integer id,Model model) {
User user = userService.getUser(id);
model.addAttribute("user", user);
return "/data/user";
}
//使用字符串指定跳转
@GetMapping("/redirect1")
public String redirect1(String user_name,String note) {
User user = new User();
user.setNote(note);
user.setUser_name(user_name);
userService.insert(user);
System.out.println("user.getId()="+user.getId());
return "redirect:/user/show?id="+user.getId();
}
//使用模型和视图指定跳转
@GetMapping("/redirect2")
public ModelAndView redirect2(String user_name,String note) {
User user = new User();
user.setUser_name(user_name);
user.setNote(note);
userService.insert(user);
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:/user/show?id="+user.getId());
return mv;
}
重定向传递java对象
//重定向传递java对象
//显示用户
@GetMapping("/showUser")
public String showUser(User user,Model model) {
System.out.println(user.getId());
model.addAttribute("user", user);
return "/data/user";
}
@RequestMapping("/redirect1")
public String redirect1(String user_name,String note,RedirectAttributes ra) {
User user = new User();
user.setNote(note);
user.setUser_name(user_name);
userService.insert(user);
ra.addFlashAttribute("user", user);
return "redirect:/user/showUser";
}
@RequestMapping("/redirect2")
public ModelAndView redirect2(String user_name,String note,RedirectAttributes ra) {
User user = new User();
user.setNote(note);
user.setUser_name(user_name);
userService.insert(user);
ra.addFlashAttribute("user", user);
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:/user/showUser");
return mv;
}
- 操作会话对象
测试操作:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Session</title>
</head>
<body>
<%
session.setAttribute("id", 1); //session记录数据
response.sendRedirect("./test"); //转发URL
%>
</body>
</html>
使用注解@SessionAttributes和@SessionAttribute
package com.springboot.chapter10.controller;
/**imports**/
//@SessionAttributes指定数据模型名称或者属性类型,保存到session中
@SessionAttributes(names = {"user"},types = Integer.class)
@Controller
@RequestMapping("/session")
public class SessionController {
@Autowired
private UserService userService;
@RequestMapping("/open")
public String openSession() {
return "/session/session1";
}
@GetMapping("/test")
//@SessionAttribute从HttpSession中取出数据,填充控制器方法参数
public String test(@SessionAttribute("id") Integer id,Model model) {
User user = userService.getUser(id);
//根据类型填充到session中
model.addAttribute("id_new", id);
//根据名称填充到session中
model.addAttribute("user", user);
return "/session/test";
}
}
测试@SessionAttributes视图
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="com.springboot.chapter10.pojo.User" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>展示</title>
</head>
<body>
<%
//从session中获取数据
Integer id = (Integer)session.getAttribute("id_new");
User user = (User)session.getAttribute("user");
//展示数据
out.print("<br>user_name="+user.getUser_name());
out.print("<br>user_id="+id);
%>
</body>
</html>
- 给控制器添加通知
定义控制器通知:
package com.springboot.chapter10.controller.advice;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
//定义一个控制器通知类
@ControllerAdvice(
//指定拦截的包
basePackages = "com.springboot.chapter10.controller.advice.test",
//限定被标注为@Controller的类才被拦截
annotations = Controller.class)
public class MyControllerAdvice {
//绑定格式化、参数转换规则和增加验证器等
@InitBinder
public void initDataBinder(WebDataBinder binder) {
//自定义日期编辑器,限定格式为yyyy-MM-dd,且参数不能为空
CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false);
//注册自定义日期编辑器
binder.registerCustomEditor(Date.class, dateEditor);
}
//在执行控制器之前先执行,可以初始化数据模型
@ModelAttribute
public void projectModel(Model model) {
model.addAttribute("project_name", "chapter10");
}
//异常处理,使得被拦截的控制器方法发生异常时,都能使用相同的视图响应
@ExceptionHandler(value = Exception.class)
public String exception(Model model,Exception ex) {
//给数据模型增加异常信息
model.addAttribute("exception_message", ex.getMessage());
//返回异常视图
return "/exception";
}
}
测试控制器通知
package com.springboot.chapter10.controller.advice.test;
/**imports**/
@Controller
@RequestMapping("/advice")
public class AdviceController {
@GetMapping("/test")
public String test(Date date,ModelMap modelMap) {
//从数据模型中获取数据
System.out.println(modelMap.get("project_name"));
//打印日期参数
// System.out.println(DateUtils.format(date,"yyyy-MM-dd"));
throw new RuntimeException("异常了,跳转到控制器通知的异常信息里");
}
}
展示异常页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>${exception_message}</h3>
</body>
</html>
- 获取请求头参数
注解@RequestHeader
带请求头的HTTP参数
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>获取请求头参数</title>
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$.post({
url:"./user",
//设置请求头参数
headers:{id:'1'},
success:function(user){
if(user==null||user.id==null){
alert("获取失败!");
return;
}
//弹出请求返回的用户信息
alert("id="+user.id+",user_name="+user.user_name+",note="+user.note);
}
});
</script>
</head>
<body>
</body>
</html>
使用@RequestHeader接收请求头参数
//获取请求头参数
@GetMapping("/header/page")
public String headerPage() {
System.out.println("headerPage");
return "/header";
}
@PostMapping("/header/user")
@ResponseBody
//用@RequestHeader获取请求头参数
public User headerUser(@RequestHeader("id") Integer id) {
System.out.println("id="+id);
User user = userService.getUser(id);
System.out.println(user);
return user;
}