springMVC之@InitBinder 和 Validator

关于 @InitBinder

请求参数绑定流程

我们在开发的时候,经常会从html,jsp中将请求参数通过request对象传递到后台,可是经常会遇到这么一种情况,那就是传过来的数据到后台后,还要再组装成一种对象的格式
在这里插入图片描述

Spring中请求参数绑定

Spring可以自动将request中的请求参数数据绑定到对象的每个property上,但是只会绑定一些简单数据类型(比如Strings,int,float)到对应的对象中。可是如果面对复杂的对象,那就要借助PropertyEditor接口来帮助我们完成复杂对象的绑定。

PropertyEditor这个接口提供了两个方法,一个方法是将String类型的值转成property对应的数据类型,另一个方法是将property转成String
在这里插入图片描述
CustomDateEditor继承关系
在这里插入图片描述

@InitBinder注解简介

@InitBinder作用于@Controller中的方法,表示为当前控制器注册一个属性编辑器,对WebDataBinder进行初始化 ,且只对当前的Controller有效

这种自定义绑定的范围不仅限于对 request parameters 类型的参数(get 请求的类型), 也可以对 URI 变量或者 Post Form 数据进行处理

@InitBinder 方法的特点

使用 @InitBinder 注解的方法支持的参数类型和使用 @RequestMapping 注解的方法相同, 除了 validation 校验结果对象

注意: 方法中需要有个 WebDataBinder 参数, 返回值类型应该为 void

@Controller
public class MyController{
	@InitBinder
	public void customizeBinding (WebDataBinder binder, ......) {
	}
  ....
}

WebDataBinder 可以做什么

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标注的方法是可以多次执行的,也就是说来一次请求就执行一次@InitBinder解析

get 请求,每个参数 执行一次@InitBinder解析
post 请求,每个请求体的每个字段

但是执行时,会先进入 拦截所有参数@InitBinder 没有设定 value 的 initBinder 方法中

@InitBinder执行原理

某个Controller上的第一次请求,由SpringMVC前端控制器匹配到该Controller之后,根据Controller的 class 类型查找所有标注了@InitBinder注解的方法,并且存入RequestMappingHandlerAdapter里的 initBinderCache 缓存中。

下一次请求执行对应业务方法之前,会先走initBinderCache缓存,而不用再去解析@InitBinder

CustomDateEditor 测试

创建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类型!

Integer,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);
	}
}

HTTP 请求中参数解密

创建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

全局生效

@ControllerAdvice

这里的@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();
    }
}

RequestMappingHandlerAdapter 注入

除了使用@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 接口
在这里插入图片描述

处理带有前缀的form字段

比如这样的场景,在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 测试
在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值