目录

一、Bean封装器:BeanWrapper

1.在BeanWrapper中使用PropertyEditor

2.在BeanWrapper中使用ConversionService

二、数据绑定器:DataBinder与WebDataBinder

1.数据绑定器(DataBinder)

2.Web端数据绑定器(WebDataBinder)

3.绑定器初始化注解(@InitBinder)

三、HTTP消息转换器(HttpMessageConverter)

1.HttpEntity、RequestEntity和ResponseEntity

2.@RequestBody和@ResponseBody

四、SpringMVC使用JSON

1.JSON依赖包导入和消息转换器

2.JSON内容类型的数据返回

2.1 方式一:HttpServletResponse手动指定

2.2 方式二:使用@ResponseBody注解

3.JSON类型的请求参数匹配(@RequestBody)


Bean Wrapper(Bean封装器)DataBinder(数据绑定器)是Spring中的两种数据绑定的实现。两者都实现了PropertyEditorRegister和TypeConverter接口,可以实现数据的类型转换。DataBinder还可以用来效验数据有效性,在SpringMVC容器中,使用WebDataBinder将前端请求参数转换为后端类型的对象,如果要处理JSON等类型的请求和响应,则需要结合HTTP消息转换器及@RequestBody和@ResponseBody注解。

一、Bean封装器:BeanWrapper

BeanWrapper是Spring提供的对Bean包装的接口,BeanWrapperImpl是对该接口的实现。Bean通过BeanWrapperImpl封装后,可以通过调用getPropertyValue()和setPropertyValue()方法,以统一的方式对属性进行操作。以User类(存在一个String类型的userName属性)为例,使用BeanWrapperImpl对User类对象封装后,操作userName属性的方式如下:

public static void main(String[] args) {
    User user = new User();
    BeanWrapperImpl userWrapper = new BeanWrapperImpl(user);
    userWrapper.setPropertyValue("userName", "User 1");
    String userName = (String) userWrapper.getPropertyValue("userName");
}

使用BeanWrapperImpl对Bean进行封装的好处是可以让不同类型Bean的不同属性使用统一的方式获取和设置值,除此之外,Bean还能结合属性编辑器(PropertyEditor)和类型转换器(ConversionService)对Bean中的属性进行类型转换。

1.在BeanWrapper中使用PropertyEditor

Spring核心容器内部默认就是使用BeanWrapper结合属性编辑器初始化Bean。除了默认的属性编辑器,自定义的属性编辑器通过registerCustomEditor()方法注册到BeanWrapper对象中,该方法的第一个参数是需要转换的目标类型,第二个参数是编辑器的对象

以User类(存在一个Date类型的userBirthday属性)为例,BeanWrapper默认支持yyyy/mm/dd日期格式的转换,如下结合CustomDateEditor实现对yyyy-mm-dd日期字符串转换的示例:

public static void main(String[] args) {
    User user = new User();
    BeanWrapperImpl userWrapper = new BeanWrapperImpl(user);
    userWrapper.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-mm-dd"), false));
    userWrapper.setPropertyValue("userName", "User 1");
    userWrapper.setPropertyValue("userBirthday", "2021-04-30"); // 设置字符串类型的日期,会被属性编辑器转换为Date类型然后赋值
    System.out.println(userWrapper.getPropertyValue("userBirthday") instanceof Date); // true
}

2.在BeanWrapper中使用ConversionService

BeanWrapper通过setConversionService()设置转换器服务,如果需要,可以在转换器服务中添加自定义的转换器。Spring还支持在日期和数字类型的属性中使用注解进行类型转换的细粒度控制,对应注解@DateTimeFormat@NumberFormat。比如设置上面的userBirthday转换格式为yyyy+mm+dd,则只需要在属性中添加如下注解:

public class User {
    @DateTimeFormat(pattern = "yyyy+mm+dd")
    private Date userBirthday;
    //……
}
public static void main(String[] args) {
    User user = new User();
    BeanWrapperImpl userWrapper = new BeanWrapperImpl(user);
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
    userWrapper.setConversionService(conversionService); // 设置转换器服务
    userWrapper.setPropertyValue("userBirthday", "2021+04+30");
    System.out.println(userWrapper.getPropertyValue("userBirthday") instanceof Date); // true
}

二、数据绑定器:DataBinder与WebDataBinder

与BeanWrapper类似,DataBinder可以对Bean进行封装绑定。DataBinder实现了PropertyEditorRegister,可以注册自定义的属性编辑器:DataBinder包含一个ConversionService属性,在绑定器初始化时容器会注入该转换器服务的依赖,也可以添加自定义的转换器和格式化转换器。除此之外,DataBinder还提供了数据有效性验证等功能,DataBinder有一个子类WebDataBinder,用于SpringMVC中的数据绑定。

1.数据绑定器(DataBinder)

以User类(存在一个Date类型的userBirthday属性)为例如下:

public static void main(String[] args) {
    User user = new User();
    // 绑定对象: 第一个参数是(需要绑定的对象),第二个参数是(绑定的对象的名字)
    DataBinder dataBinder = new DataBinder(user, user.getClass().getName());
    // 添加格式转换器
    dataBinder.addCustomFormatter(new DateFormatter("yyyy-mm-dd"));
    // 属性值对象初始化
    MutablePropertyValues propertyValues = new MutablePropertyValues();
    propertyValues.add("userName", "Zhang San"); // 添加userName属性值
    propertyValues.add("userBirthday", "2021-04-30");// 添加userBirthday属性值
    dataBinder.bind(propertyValues); // 绑定属性值
    // 得到绑定后的结果
    BindingResult bindingResult = dataBinder.getBindingResult();
    // 从绑定结果中得到绑定对象
    user = (User) bindingResult.getTarget();
}

2.Web端数据绑定器(WebDataBinder)

在SpringMVC项目中,DispatcherServlet会自动将前端的请求参数绑定为处理方法的参数类型的对象后进行调用。DispatcherServlet内部使用的就是WebDataBinder进行数据绑定,WebDataBinder继承自DataBinder,除了具备DataBinder的数据转换和验证等功能外,还提供了属性前缀文件类型(MultipartFile)的数据绑定。

在应用开发中,DataBinder和WebDataBinder基本不会直接使用,而是由容器本身进行维护和调用。如果要添加自定义编辑器和转换器,或指定匹配的属性前缀等规则,则可以通过@InitBinder注解或配置方式达成。

3.绑定器初始化注解(@InitBinder)

@InitBinder用于在控制器方法中对WebBinder对象的初始化进行设置,包括注册自定义属性编辑器,添加格式化转换器或设置属性的匹配前缀。在基于注解的开发中,添加自定义的编辑器只需要在某个控制器方法上使用@InitBinder注解,该方法的形参类型是WebDataBinder,在注解方法中获取WebDataBinder的对象并进行格式化的设定。使用在某个控制器上的@InitBinder注解,只会对该控制器内的方法有效,而且@InitBinder注解的方法是执行多次的,一次请求来就执行一次。当某个Controller上的第一次请求由SpringMVC中央控制器匹配到该Controller之后,首先会查找该Controller类中所有标注了@InitBinder的方法,并且存入RequestMappingHandlerAdapter的initBinderCache,下次一请求执行对应业务方法之前时,则按照该Controller的类型得到该类所有@InitBinder注解的方法。

public @interface InitBinder {
    String[] value() default {};
}

该注解有一个String[]类型的value属性,作用是:限定对哪些@RequestMapping方法起作用,具体筛选条件就是通过@RequestMapping方法入参来筛选(也就是前端传递过来的请求参数的key),默认不写就代表对所有@RequestMapping的方法起作用;

@Controller
public class InitBinderController {
    // @InitBinder方法不能有返回值,必须为void
    @InitBinder("param")
    protected void initBinder(WebDataBinder binder) {
        System.out.println("initBinder方法内输出:" + binder.getObjectName());
    }

    @RequestMapping("/initbinder")
    @ResponseBody
    public void testInitBinder(String param) {
        System.out.println("param: " + param);
    }
}

如上例中浏览器访问http://localhost:9090/SpringMVC/initbinder?param=123  时改变@InitBinder注解的value属性值会出现如下两种输出:

  • @InitBinder("param")时:initBinder方法内输出:param            (换行输出)param:123
  • @InitBinder("user")时:param:123

当浏览器访问:initbinder?param=123&name=yang时两个方法都有输出。

@InitBinder标注的方法, 方法入参和@RequestMapping方法入参可选范围一样(比如HttpServletRequest、ModelMap这些), 通常一个入参WebDataBinder就够我们使用了。我们可以利用WebDataBinder参数进行格式化设定,如下:

@InitBinder
protected void initBinder(WebDataBinder binder) {
    // 添加自定义属性编辑器
    binder.registerCustomEditor(Date.class, new MyDatePropertyEditor());
    // 添加自定义格式化转换器
    binder.addCustomFormatter(new MyDateFormatter());
}

我们还可以使用setFieldDefaultPrefix()方法对前端参数的前缀进行匹配。加入当前有如下两个model,它两有重名的属性name:

public class Model1 {
    private String name;
    private String id;
    //……
}

public class Model2 {
    private String name;
    private int age;
    //……
}

假如此时有如下请求处理方法:

@RequestMapping("/initbinder")
@ResponseBody
public void testInitBinder1(Model1 model1, Model2 model2) {
    System.out.println("model1 : " + model1);
    System.out.println("model2 : " + model2);
}

那么浏览器地址栏访问:initbinder?id=123&name=yang&age=22,则输出如下:

可以看到封装的model1和model2的对象中的name的值都是相同的,此时可以利用设置前缀的方式解决,如下:

@InitBinder("model1")
protected void initBinderModel1(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("model1.");
}

@InitBinder("model2")
protected void initBinderModel2(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("model2.");
}

此时传递这两个重名属性的时候只需加上前缀即可,浏览器访问:initbinder?id=123&model1.name=yang&age=22&model2.name=hao,输出如下:

使用setDisallowedFields方法可以设置禁止接收的参数。如下只在initBinderModel2方法中添加一行设置即可,其他不变:

@InitBinder("model2")
protected void initBinderModel2(WebDataBinder binder) {
    binder.setDisallowedFields("age");
    binder.setFieldDefaultPrefix("model2.");
}

此时再访问同样url则会有如下输出(可以看到model2中的age并不是22,而是int默认值0):

使用在控制器上的@InitBinder注解,只会对该控制器内的方法有效。如果需要全局的控制器或者匹配的控制器适用的话,可以将@InitBinder使用在@ControllerAdvice@RestControllerAdvice注解类的方法中,该方式使用AOP来匹配控制器。

除了使用@InitBinder注解对绑定器进行设置外,还可以使用XML进行配置,通过配置请求映射处理器适配器(RequestMappingHandlerAdapter)BeanwebBindingInitializer属性指定自定义的WebBindingInitializer,在自定义的WebBindingInitializer中就可以任意设置了,配置示例如下:

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <!-- 配置自定义的数据绑定初始化器 -->
    <property name="webBindingInitializer">
        <bean class="com.mec.springmvc.databinder.MyWebBindingInitializer"></bean>
    </property>
</bean>	

三、HTTP消息转换器(HttpMessageConverter)

典型的Web开发的参数传递,一般通过GET的URL或POST方法的Form表单数据以键值对方式进行传递,SpringMVC框架后端使用WebDataBinder对参数进行转换和绑定。但如果传递的是JSON字符串等其他格式,WebDataBinder就无法处理了。

请求响应的返回,典型的是返回ModelAndView对象,但随着前后端分离框架的流行,Controller中的请求方法只需要直接返回一个字符串、JSON或者XML等其他格式的数据即可,WebBinder也无法处理。为此SpringMVC中提供了更高层级的HttpMessageConverter以及HttpEntity等相关的HTTP类用来请求和响应进行转换。HTTP消息转换器的位置如图所示:

1.HttpEntity、RequestEntity和ResponseEntity

HttpEntity<T>是SpringWeb提供的HTTP的请求和响应对象的泛型实体类,T可以指定具体的后端实体类。HttpEntity的类名在其他很多的HTTP相关的依赖包和框架中都出现过(比如HttpClient)。在SpringMVC中,HttpEntity主要包含请求头(HttpHeaders)和请求实体(T)。该类有两个主要子类,即RequestEntityResponseEntity,分别对应请求实体类和响应实体类。ResponseEntity包含了状态码的属性。

public class ResponseEntity<T> extends HttpEntity<T> {
    //T body:响应体的对象,会被转换成不同类型的HTTP消息(比如String、JSON)
    //headers:可以用来设置键值对的响应头参数。
    //status:返回的状态信息,HttpStatus.OK对应的就是200的状态码
    public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, HttpStatus status) {
        super(body, headers);
        Assert.notNull(status, "HttpStatus must not be null");
        this.status = status;
    }
}

如果在请求映射方法中的参数类型是RequestEntity或者方法的返回是ResponseEntity,则SpringMVC会调用HttpEntityMethodProcessor进行处理。

public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
    //……
}

AbstractMessageConverterMethodProcessor使用容器中存在的HttpMessageConverter进行转换,SpringMVC支持String、JSON、XML等常用类型的HTTP消息转换器。使用示例如下:

@Controller
public class HttpEntityController {    
    @RequestMapping("/httpEntity")
    public ResponseEntity<String> requestEntity(RequestEntity<String> requestEntity) { // 获取请求参数
        String str = requestEntity.getBody(); // 获取前端请求体内容
        str = "This is New String";
        HttpHeaders responseHeaders = new HttpHeaders(); // HTTP头部
        // 响应对象由:响应体、响应头、状态码组成
        ResponseEntity<String> responseEntity = new ResponseEntity<String>(str, responseHeaders, HttpStatus.OK);
        return responseEntity;
    }
}

对象类型的处理和String类型类似,但需要导入一些额外的依赖包,处理JSON类型的请求类型或者响应时,需要导入JSON依赖包,在Maven中配置如下:

<!-- jackson核心依赖包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.7</version>
</dependency>
<!-- jackson数据绑定依赖包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.7</version>
</dependency>
<!-- jackson注解支持依赖包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.7</version>
</dependency>
// 如果前端传回来一个User对象的JSON字符串
@RequestMapping
public ResponseEntity<User> requestEntityUser(RequestEntity<User> requestEntity) {
    User user = requestEntity.getBody(); // JSON字符串会被自动转换成User对象
    user.setUserName("Zhang San");
    // 使用这种简化方式也可以返回响应对象。参数是响应体。返回前会将User对象转换为JSON格式字符串
    return ResponseEntity.ok(user);
}

2.@RequestBody和@ResponseBody

在SpringMVC和Spring Boot中更多使用的是@RequestBody@ResponseBody注解来替代前面在方法和参数中对类型的指定。框架内部使用RequestResponseBodyMethodProcessor对这两个注解进行处理,调用合适的HttpMessageConverter进行请求和响应的转换,接受和返回非HTML页面格式的数据(JSON、XML等)。

@RequestBody使用在请求方法中,用于解析和转换请求体中的参数;@ResponseBody将后端对象以不同的内容类型进行返回。@ResponseBody还可以结合@ResponseStatus来注解返回的状态。

@ResponseBody
@RequestMapping("/requestResponseBody")
public User requestResponseBody(@RequestBody User user) {
    user.setUserName("Zhang San");
    return user;    //会返回user对象JSON格式的字符串
}

四、SpringMVC使用JSON

静态HTML页面请求返回text/html内容类型的响应;动态JSP页面结合获取的后端数据返回text/html类型的响应。这种方式对于数据量大、数据查询需要耗费较长时间的请求,页面载入慢。由于这个问题,所以出现了Ajax异步获取局部页面的HTML代码段,使用JS对HTML DOM进行局部更新。当然,也可以只返回后端数据,在前端使用JS语言产生HTML元素。在前端框架逐步流行和成熟的基础上,返回application/json内容类型的JSON数据已成为主流的开发方式。

SpringMVC支持不同内容类型的请求处理,特别是传统的text/html和流行的application/json。使用HTTP消息转换器(HttpMessageConverter)对前端请求内容和响应进行转换。spring-web模块中提供了JSON类型的HTTP消息转换器子类,底层使用jackson或Gson库进行JSON类型和Java对象类型的转换,对应的转换器类分别是MappingJackson2HttpMessageConverterGsonHttpMessageConverter。使用不同的JSON消息转换器需要导入对应的JSON依赖包。

1.JSON依赖包导入和消息转换器

SpringBoot默认使用JSON作为响应格式,自带Jackson库进行转换。SpringMVC没有自带JSON数据转换包,但提供了Jackson和Gson的HTTP消息转换器,需要导入Jackson或Gson的依赖包。Jackson和Gson的消息转换器一般不需要配置,如果需要额外的设定,则可以进行配置,配置方式通过对<mvc:annotation-driven><mvc:message-converters>子标签进行配置。如下:

<!-- MVC消息注解驱动 -->
<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.GsonHttpMessageConverter"></bean>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"></bean>
    </mvc:message-converters>
</mvc:annotation-driven>

也可以使用FastJson作为SpringMVC的HTTP消息转换器。因为SpringMVC默认没有提供对应的转换器,但FastJson提供了Spring使用转换器类,所以除了导入依赖包之外,还需要配置相应的转换器Bean。示例如下:

<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- FastJson消息转换器Bean -->
    	<bean id="fastJsonHttpMessageConverter" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
    	    <property name="supportedMediaTypes"><!-- 支持的内容类型 -->
                <list>
    		    <value>text/html;charset=UTF-8</value>
    		    <value>application/json;charset=UTF-8</value>
    		</list>
    	    </property>
    	</bean>
    </mvc:message-converters>
</mvc:annotation-driven>

2.JSON内容类型的数据返回

导入JSON依赖包,并将Java对象转换为JSON字符串后,就可以使用resopnse对象返回application/json内容类型的JSON字符串。结合消息转换器,可以在控制器类和映射方法中使用@ResopnseBody注解,由容器自动处理Java对象的转换并返回(这种编码方式更为简洁)。

2.1 方式一:HttpServletResponse手动指定

通过HttpServletResponse指定返回的内容类型为application/json的字符串。

@RequestMapping("/json/getUser")
public void getJsonUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json");
    User user = new User(12, "Zhang San");
    // 通过Jackson的ObjectMapper将Java对象转换为JSON串
    ObjectMapper objectMapper = new ObjectMapper();
    String userJsonStr = objectMapper.writeValueAsString(user);
    PrintWriter out = response.getWriter();
    out.write(userJsonStr);
    out.flush();
    // 或者通过OutputStream对象输出字符串
//  ServletOutputStream outStream = response.getOutputStream();
//  outStream.print(userJsonStr);
//  outStream.flush();
}

2.2 方式二:使用@ResponseBody注解

@ResponseBody可以使用在映射注解方法上,容器内部通过HttpMessageConverter将方法返回的Java对象转换为JSON字符串后写入Response对象的body数据区。底层使用的就是response的输出流对象。示例如下:

@RequestMapping("/json/getUser")
@ResponseBody
public User getJsonUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
    return new User(12, "Zhang San");
}

3.JSON类型的请求参数匹配(@RequestBody)

在SpringMVC的请求映射方法中,使用@RequestParam注解或不使用注解,SpringMVC容器会自动进行参数的匹配并组装成对应的方法参数定义的类对象。默认请求请求参数的内容类型是application/x-www-form-urlencode,参数以键值对的方式传递(如username=”admin“&password=123)。如果需要传递JSON类型的请求参数,则前端需要设定ContentType属性值为application/json,后端方法参数使用@RequestBody注解进行匹配。 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐