赞
踩
我们在开发的时候,经常会从html,jsp中将请求参数通过request对象传递到后台,可是经常会遇到这么一种情况,那就是传过来的数据到后台后,还要再组装成一种对象的格式
。
Spring
可以自动将request中的请求参数数据绑定到对象的每个property上
,但是只会绑定一些简单数据类型(比如Strings,int,float)到对应的对象中
。可是如果面对复杂的对象
,那就要借助PropertyEditor接口
来帮助我们完成复杂对象的绑定。
PropertyEditor
这个接口提供了两个方法,一个方法是将String类型的值转成property对应的数据类型
,另一个方法是将property转成String
。
CustomDateEditor
继承关系
@InitBinder
作用于@Controller中的方法
,表示为当前控制器注册一个属性编辑器
,对WebDataBinder
进行初始化
,且只对当前的Controller有效
。
这种自定义绑定的范围不仅限于对 request parameters 类型的参数(get 请求的类型)
, 也可以对 URI 变量或者 Post Form 数据
进行处理
使用 @InitBinder
注解的方法支持的参数类型
和使用 @RequestMapping
注解的方法相同, 除了 validation 校验结果对象
注意: 方法中需要有个 WebDataBinder 参数
, 返回值类型应该为 void
@Controller
public class MyController{
@InitBinder
public void customizeBinding (WebDataBinder binder, ......) {
}
....
}
WebDataBinder extends DataBinder
WebDataBinder
可用于注册
我们自定义的参数格式化方法
, 参数校验方法
, 参数修改方法
WebDataBinder.addCustomFormatter(..);
WebDataBinder.addValidators(..);
WebDataBinder.registerCustomEditor(..);
为了规定InitBinder
方法处理哪些对象
, 我们可以使用 @InitBinder 的 value 属性
, value 属性的值可以是一个或者多个参数
, 这些参数可来自于 Get 请求的参数或者 Post 的表单内容
,可以定义多个不同名称的 @InitBinder
方法.
@InitBinder("user") //拦截 请求参数 user
public void customizeBinding (WebDataBinder binder) {...}
但是执行时
,会先进入 拦截所有参数
,@InitBinder 没有设定 value 的 initBinder 方法中
注意
@InitBinder("value") 这里的 value 是以类名为准
比如 入参 UserInfo user ,value 为 userInfo 时,才能正确拦截进入 initBinder()
@InitBinder("userInfo") //@InitBinder("user") 是进入不了该方法的
public void initBinder(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(Long.class, new UserRegisterPropertyEditor());
}
@PostMapping("/register")
public String handlePostRequest(@RequestBody @Valid UserInfo user, BindingResult bindingResult) {
...
//注意这里的参数 UserInfo user
}
@InitBinder
注解被解析的时机,是其所标注的方法,在该方法被请求执行之前
。同时@InitBinder标注的方法是可以多次执行的
,也就是说来一次请求就执行一次@InitBinder解析
get 请求,每个参数 执行一次@InitBinder解析
post 请求,每个请求体的每个字段
但是执行时
,会先进入 拦截所有参数
,@InitBinder 没有设定 value 的 initBinder 方法中
当某个Controller上的第一次请求
,由SpringMVC前端控制器匹配到该Controller之后
,根据Controller的 class 类型
来查找所有标注了@InitBinder注解的方法
,并且存入RequestMappingHandlerAdapter里的 initBinderCache 缓存
中。
等下一次请求执行对应业务方法之前,会先走initBinderCache缓存,而不用再去解析@InitBinder
。
创建Controller测试接口
@Slf4j
@RestController
public class BindController {
@GetMapping(value = "/bind")
public Map<String, Object> getFormatData(Date date) throws ParseException {
log.warn("date={}", date);
Map<String, Object> map = new HashMap<>();
map.put("name", "一一哥");
map.put("age", 30);
map.put("date", date);
return map;
}
}
启动程序进行测试
此时我们在postman中输入地址:
http://localhost:8080/bind?...
经过测试,发现此时产生400
状态码,具体原因是无法将前端传递过来的String类型的时间字符串转换为Date类型
添加@InitBinder代码,重新测试
/**
* @InitBinder标注的方法,只针对当前Controller有效!
* 如果没有该方法,则会产生400状态码!
* MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Date!
*/
@InitBinder
public void InitBinder(WebDataBinder binder) {
//前端传入的时间格式必须是"yyyy-MM-dd"效果!
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
CustomDateEditor dateEditor = new CustomDateEditor(df, true);
binder.registerCustomEditor(Date.class, dateEditor);
}
然后我们在postman中重新输入地址:
http://localhost:8080/bind?...
可以发现前端传递的时间字符串被成功的传递到后端,并且转换成了Date类型!
系统注入的只能是基本类型
,如int,char,String
public class User {
private String account;
private String phone;
private Integer age;
private String city;
private Date birthday;
private Date createTime;
}
public class UserController extends SimpleFormController {
protected void initBinder(HttpServletRequest req, ServletRequestDataBinder binder) throws Exception {
binder.registerCustomEditor(Integer.class, "age", new IntegerEditor());
binder.registerCustomEditor(Date.class, "birthday", new DateEditor());
}
protected Map referenceData(HttpServletRequest req) throws Exception {
Map map = new HashMap();
List cityList = new ArrayList();
City city1 = new City();
city1.setCityName("BeiJing");
city1.setCityNo("010");
cityList.add(city1);
City city2 = new City();
city2.setCityName("ShangHai");
city2.setCityNo("020");
cityList.add(city2);
map.put("cityList", cityList);
return map;
}
protected void onBind(HttpServletRequest req, Object obj) throws Exception {
User user = (User) obj;
//format the infor
user.setAccount(user.getAccount().trim());
user.setPhone(user.getPhone().trim());
}
protected void onBindAndValidate(HttpServletRequest req, Object obj, BindException err) throws Exception {
User user = (User) obj;
user.setCreateTime(new Date());
}
protected ModelAndView onSubmit(Object obj) throws Exception {
User user = (User) obj;
return new ModelAndView(this.getSuccessView(), "user", user);
}
}
可以看出使用了 binder.registerCustomEditor
方法,它是用来注册
的。所谓注册即告诉Spring
,注册的属性由我来注入,不用你管了。可以看出我们注册了“age”和“birthday”属性
。那么这两个属性就由我们自己注入了。那么怎么注入呢,就是使用 IntegerEditor() 和DateEditor()
方法。希望仔细思考一下registerCustomEditor方法的参数
含义。
public class IntegerEditor extends PropertyEditorSupport {
public String getAsText() {
Integer value = (Integer) getValue();
if(null == value){
value = new Integer(0);
}
return value.toString();
}
@OverWrite
public void setAsText(String text) throws IllegalArgumentException {
Integer value = null;
if(null != text && !text.equals("")){
value = Integer.valueOf(text);
}
setValue(value);
}
}
public class DateEditor extends PropertyEditorSupport {
public String getAsText() {
Date value = (Date) getValue();
if(null == value){
value = new Date();
}
SimpleDateFormat df =new SimpleDateFormat("yyyy-MM-dd");
return df.format(value);
}
public void setAsText(String text) throws IllegalArgumentException {
Date value = null;
if(null != text && !text.equals("")){
SimpleDateFormat df =new SimpleDateFormat("yyyy-MM-dd");
try{
value = df.parse(text);
}catch(Exception e){
e.printStackTrace();
}
}
setValue(value);
}
}
创建VO对象
public class User {
// 后续操作中我们对id加密传输给前端, 并对前端回传的数据进行解密
private Long id;
@Size(min = 5, max = 20)
private String name;
@Size(min = 6, max = 15)
@Pattern(regexp = "\\S+", message = "Spaces are not allowed")
private String password;
@NotEmpty
@Email
private String emailAddress;
@NotNull
private Date dateOfBirth;
//getters and setters
}
创建 Controller 和 @InitBinder 注解的方法
@Controller
@RequestMapping("/register")
public class UserRegistrationController {
@Autowired
private UserService userService;
@InitBinder("user")
public void customizeBinding (WebDataBinder binder) {
// Long.class 字段被转换之后的目标类型
// id 表示需要被转换的字段名称
binder.registerCustomEditor(Long.class, "id", new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws java.lang.IllegalArgumentException {
// text 为 request 的原始参数值
setValue(decode(text));
}
});
}
/**
* 功能描述: 模拟对User的id字段解密方法
*
* @param text 加密内容
* @return 解密后的值
*/
private long decode(String text) {
return 1L;
}
/**
* 功能描述: 使用Post方法注册用户
*
* @param user 用户信息, 注意这里user.id的值已不再是request表单中原始值了, 而是被解密之后的内容
* @return 注册结果
*/
@RequestMapping(method = RequestMethod.POST)
public String handlePostRequest (@RequestBody @Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "user-registration";
}
userService.saveUser(user);
return "registration-done";
}
}
自定义属性编辑器
继承java.beans.PropertyEditorSupport类
重写其setAdText(String text)方法完成
调用setValue(Object Value)方法完成转换后的值的设置
public class StringToListPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
....
setValue(resultArr);
}
}
initBinder方法框架
注解 @InitBinder
参数 WebDataBinder binder
最后注册 binder.registerCustomEditor(...,...);
//接口发送时,接口请求体中的所有参数逐个进入此方法,进行处理,然后返回处理的参数类型
@InitBinder
public void initBinderXXX(WebDataBinder binder) {
.....
//当url请求参数、请求体中的参数类型是或者包含 A,则进入自定义编辑器 AToPropertyEditor 中执行 setAsText() ,处理完后返回 A.class
binder.registerCustomEditor(A.class,new AToPropertyEditor());
//当url请求参数、请求体中的参数类型是或者包含 B,则进入自定义编辑器 BToPropertyEditor 中执行 setAsText(),处理完后返回 B.class
binder.registerCustomEditor(B.class,new BToPropertyEditor());
}
属性编辑器的作用
就是 把请求中的参数、内容,先进行转换
,然后作为controller接口的参数
客户端 url 传参 --> @InitBinder 属性编辑器 --> controller接口
而不是
先把参数作为controller接口的参数,再进行编辑,然后再回到controller接口中操作
重点
WebDateBinder
是用来绑定请求参数到指定的属性编辑器
.由于前台传到controller 里的值
是String类型
的,当往Model中Set这个值
的时候,如果set的这个属性是个对象
,Spring就会去找到对应的editor进行转换
,然后再SET进去
。
@ResponseBody
@RequestMapping(value = "/test")
public String test(@RequestParam String name,@RequestParam Date date) throws Exception {
System.out.println(name);
System.out.println(date);
return name;
}
@InitBinder
public void initBinder(WebDataBinder binder){
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
@InitBinder
方法会帮助我们把String类型的参数
先trim再绑定
,而对于Date类型的参数
会先格式化再绑定。
例如当请求是/test?name=%20zero%20&date=2018-05-22
时,会把zero绑定到name
,再把时间串格式化为Date类型,再绑定到date
。
这里的@InitBinder
方法只对当前Controller生效
,要想全局生效
,可以使用@ControllerAdvice
。通过@ControllerAdvice
可以将对于控制器的全局配置放置在同一个位置
,注解了@ControllerAdvice的类的方法
可以使用@ExceptionHandler,@InitBinder,@ModelAttribute注解到方法上
,这对所有注解了@RequestMapping的控制器内的方法
有效
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
}
@ControllerAdvice
中除了配置@InitBinder
, 还可以有@ExceptionHandler
用于全局处理控制器里面的异常
;@ModelAttribute
作用是绑定键值对到Model里
,让全局的@RequestMapping
都能获得在此处设置的键值对
。
补充:如果 @ExceptionHandler
注解中未声明要处理的异常类型
,则默认为方法参数列表中的异常类型
。示例:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
String handleException(Exception e){
return "Exception Deal! " + e.getMessage();
}
}
除了使用@ControllerAdvice
来配置全局的WebDataBinder
,还可以使用RequestMappingHandlerAdapter
注入 容器中
@Bean
public RequestMappingHandlerAdapter webBindingInitializer() {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setWebBindingInitializer(new WebBindingInitializer(){
@Override
public void initBinder(WebDataBinder binder, WebRequest request) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
});
return adapter;
}
我们在接收参数的时候,对于基础的数据类型,比如接收string,int等类型,springmvc是可以直接处理的
但是对于其他复杂的对象类型,有时候是无法处理的
,这时候就需要属性编辑器来进行处理(源数据为string)
过程一般就是String->属性编辑器->目标类型
spring为我们提供了一些默认的属性编辑器
,如org.springframework.beans.propertyeditors.CustomDateEditor
就是其中一个,我们也可以通过继承java.beans.PropertyEditorSuppotor来根据具体的业务来定义自己的属性编辑器
。
定义controller
并使用@InitBinder注册属性编辑器
这里注册的属性编辑器为org.springframework.beans.propertybeans.CustomDateEditor
,作用是根据提供的java.text.SimpleDateFormat将输入的字符串数据转换为java.util.Date类型的数据,核心源码如下:
public class CustomDateEditor extends PropertyEditorSupport {
public void setAsText(@Nullable String text) throws IllegalArgumentException {
...
// 使用用户提供的java.text.SimpeDateFormat来将目标字符串格式化为java.util.Date类型,并通过SetValue方法设置最终值
setValue(this.dateFormat.parse(text));
}
}
1 定义类
@Controller
@RequestMapping("/myInitBinder0954")
public class MyInitBinderController {
/*
注册将字符串转换为Date的属性编辑器,该编辑器仅仅对当前controller有效
*/
@InitBinder
public void initBinderXXX(WebDataBinder binder) {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
CustomDateEditor dateEditor = new CustomDateEditor(df, true);
binder.registerCustomEditor(Date.class, dateEditor);
}
// http://localhost:8080/myInitBinder0954/test?date=2020-09-03%2010:17:17会使用在
// dongshi.controller.initbinder.MyInitBinderController.initBinderXXX注册的属性编辑器转换为,
// Date类型的
@RequestMapping(value = "/test", method = RequestMethod.GET)
@ResponseBody
public String testFormatData(Date date) {
Map<String, Object> map = new HashMap<>();
map.put("date", date);
return map.toString();
}
}
2 访问测试,string参数--->initBinder 属性编辑器---->接口
看到返回了Date的toString的结果,就是说明成功了。
假设我们的需求是这样的,调用方传过来的值是一个_竖线分割的字符串
,但是处理的过程使用的是通过_号分割得到的一个String[]
,这里就可以定义一个将竖线分割的多个字符串转换为String[]的自定义属性编辑器
来实现。
1 自定义属性编辑器
通过继承java.beans.PropertyEditorSupport
类并重写其setAdText(String text)方法完成
,最后调用setValue(Object Value)方法完成转换后的值的设置
。
public class StringToListPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
String[] resultArr = null;
if (!StringUtils.isEmpty(text)) {
resultArr = text.split("_");
}
setValue(resultArr);
}
}
2 使用
@RequestMapping("/myStringToList")
@Controller
public class StringToListController {
@InitBinder
public void myStringToListBinder(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(String[].class, new StringToListPropertyEditor());
}
@RequestMapping(value = "/test", method = RequestMethod.GET)
@ResponseBody
public String myStringToListTest(String[] strToListArr, HttpServletResponse response) {
response.setCharacterEncoding("UTF-8");
String result = "_分割字符串转String[]不成功!";
if (strToListArr != null && strToListArr.length > 0) {
result = Arrays.asList(strToListArr).toString();
}
return result;
}
}
3 访问测试
111_222_333 ------>【 initBinder ----> String[] resultArr = {“111”,“222“,”333”} 】----> /test 接口
比如这样的场景,在People,Address两个类中都有name字段
,但是我们需要在一个表单中录入People和Address的信息
,然后在接口中直接通过People,Address两个对象来接收页面的表单数据
,但是两个name是无法区分的
一般的做法就是指定一个前缀
通过@InitBinder通过调用org.springframework.web.bind.WebDataBinder的setFieldDefaultPrefix(@Nullable String fieldDefaultPrefix)方法
然后在接口中使用注解public @interface ModelAttribute设置要接收的参数的前缀
,就可以区分并接收对应的参数了。
1 定义用到的实体
public class People {
private String name;
private String age;
// getter setter toString
}
public class Address {
private String name;
private String city;
// getter setter toString
}
2 定义测试使用的表单
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/myInitBinder0954/test0942" method="post" enctype="multipart/form-data">
这里name <input type="text" name="people.name" placeholder="人名"><br><br>
<input type="text" name="people.age" placeholder="人年龄"><br><br>
这里name <input type="text" name="address.name" placeholder="地址名称"><br><br>
<input type="text" name="address.city" placeholder="地址所在城市"><br><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
3 定义接口
@RequestMapping("/myInitBinder0954")
@Controller
public class MyInitBinderController {
@InitBinder(value = "people") 这里
public void initBinderSetDefaultPreifixPeople(WebDataBinder dataBinder) {
dataBinder.setFieldDefaultPrefix("people."); 设置defalut前缀
}
@InitBinder(value = "address") 这里
public void initBinderSetDefaultPreifixAddress(WebDataBinder dataBinder) {
dataBinder.setFieldDefaultPrefix("address."); 设置defalut前缀
}
@RequestMapping(value = "/test0942", method = RequestMethod.POST)
@ResponseBody
public String test0942(@ModelAttribute("people") People people, @ModelAttribute("address") Address address) {
StringBuffer sb = new StringBuffer();
sb.append(people.toString());
sb.append("---");
sb.append(address.toString());
return sb.toString();
}
}
4 访问测试
1 定义测试实体
public class User {
private String userName;
// getter setter toString
}
2 自定义校验器
直接实现org.springframework.validation.Validator
,该接口只有两个方法,一个是校验是否支持校验的support
(Class<?> clazz)方法,一个是进行具体校验的validate
(Object target, Errors errors)方法,源码如下:
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
定义一个校验器:
该校验器校验用户录入的userName长度是否大于8
,并给出响应的错误信息,错误信息直接设置到errors中
,最终会设置到org.springframework.validation.BindingReuslt,在接口中直接定义该对象则会自动注入对象值,从而可以获取到对应的错误信息。
@Component
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// 只支持User类型对象的校验
return User.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
User user = (User) target;
String userName = user.getUserName();
if (StringUtils.isEmpty(userName) || userName.length() < 8) {
errors.rejectValue("userName", "valid.userNameLen",
new Object[] { "minLength", 8 }, "用户名不能少于{1}位");
}
}
}
3 定义控制器
@Controller
@RequestMapping("/valid")
public class ValidatorController {
@Autowired
private UserValidator userValidator;
@InitBinder
private void initBinder(WebDataBinder binder) {
binder.addValidators(userValidator); 这里
}
@RequestMapping(value = { "/index", "" }, method = { RequestMethod.GET })
public String index(ModelMap m) throws Exception {
m.addAttribute("user", new User());
return "initbinder/user.jsp";
}
@RequestMapping(value = { "/signup" }, method = { RequestMethod.POST })
这里 public String signup(@Validated User user, BindingResult br, RedirectAttributes ra) throws Exception {
// 携带用户录入的信息方便回显
ra.addFlashAttribute("user", user);
return "initbinder/user.jsp";
}
}
4 定义jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>validate user</title>
</head>
<body>
<form:form modelAttribute="user" action="/valid/signup" method="post">
<!-- 显示所有的错误信息 -->
<form:errors path="*"></form:errors><br><br>
用户名:<form:input path="userName"/><form:errors path="userName"/>
</form:form>
</body>
</html>
5 测试
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。