自定义转换器&处理JSON&内容协商
1.自定义转换器
1.1基本介绍
- SpringBoot 在响应客户端请求时,将提交的数据封装成对象时,使用了内置的转换器,也就是自动帮我们封装对象。springboot 自带了124个转换器,可以实现大部分的类型间的转换。
- SpringBoot 也支持自定义转换器。但当前台发送请求传递的参数使用内置的转换器不能转换时,这时就需要写一个自定义的数据类型转换器,我们只需要实现 Converter 接口的 convert 方法即可。
1.2应用案例
演示自定义转换器的使用。
(1)save.html- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>save</title>
- </head>
- <body>
- <form action="/saveMonster" method="post">
- 编号:<input name="id" value="10001"/><br/>
- 姓名:<input name="name" value="齐天大圣"/><br/>
- 年龄:<input name="age" value="888"/><br/>
- 婚否:<input name="isMarried" value="false"/><br/>
- 生日:<input name="birth" value="1456/12/12"/><br/>
-
- 坐骑:<input name="car" value="避水金晶兽,666.6"/><br/>
- <input type="submit" value="保存"/>
- </form>
- </body>
- </html>
复制代码 (2)自定义转换器(String-->Car)- package com.li.config;
- import com.li.bean.Car;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.core.convert.converter.Converter;
- import org.springframework.format.FormatterRegistry;
- import org.springframework.util.ObjectUtils;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
- /**
- * @author 李
- * @version 1.0
- * (proxyBeanMethods = false)
- * 1.表示启用Lite模式,保证修饰的配置类中,每个@Bean方法被调用多少次返回的组件都是新创建的,
- * 是多例对象,是非代理方式。
- * 2.proxyBeanMethods 在调用 @Bean 方法时才生效,因此需要先获取BeanConfig 组件,再调用方法
- */
- @Configuration(proxyBeanMethods = false)//设置为设置类
- public class WebConfig {
- //注入bean-WebMvcConfigurer
- @Bean
- public WebMvcConfigurer webMvcConfigurer() {
- return new WebMvcConfigurer() {
- @Override
- public void addFormatters(FormatterRegistry registry) {
- /**
- * 1.在 addFormatters()方法中添加一个自定义的转换器
- * 2.自定义转换器要完成的功能是 String -> Car
- * 3.增加的转换器会注册到 converters 容器中
- * 4.converters 底层结构是 ConcurrentHashMap,默认内置了124个转换器
- */
- registry.addConverter(new Converter<String, Car>() {
- //<sourceType,targetType>
- //匿名内部类
- @Override
- public Car convert(String source) {//source即传入的字符串
- //加入转换的业务代码
- if (!ObjectUtils.isEmpty(source)) {
- Car car = new Car();
- String[] strings = source.split(",");
- car.setName(strings[0]);
- car.setPrice(Double.parseDouble(strings[1]));
- return car;
- }
- return null;
- }
- });
- //还可以增加更多的转换器
- }
- };
- }
- }
复制代码 (3)控制器
Monster和Car是级联对象,Car为Monster对象的属性。
- //处理添加Monster的方法
- @PostMapping("/saveMonster")
- @ResponseBody
- public String saveMonster(Monster monster) {
- System.out.println("monster=" + monster);
- return "success";
- }
复制代码 (4)浏览器提交表单,后台输出如下:- monster=Monster(id=10001, name=齐天大圣, age=888, isMarried=false, birth=Sun Dec 12 00:00:00 CST 1456, car=Car(name=避水金晶兽, price=666.6))
复制代码 可以看到服务器成功获取到表单数据,并将car表单项的value值转换为Car对象的属性值。
1.3注意事项
不同转换器通过key值区分,key=[源类型->目标类型]
如果实现的两个自定义转换器的 key 值相同(即源类型和目标类型相同),则在注入容器时,根据注入的顺序,后一个转换器会覆盖前一个转换器!
2.处理JSON
SpringBoot 支持返回 JSON 格式的数据,在启用 WEB 开发场景时,已经引入了相关的依赖:spring-boot-starter-json。
例子-使用@ResponseBody处理返回 json
- package com.li.controller;
- import com.li.bean.Car;
- import com.li.bean.Monster;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
- import java.util.Date;
- /**
- * @author 李
- * @version 1.0
- */
- @Controller
- public class ResponseController {
- //编写方法,以json格式返回数据
- @GetMapping("/get/monster")
- @ResponseBody
- public Monster getMonster() {
- Monster monster = new Monster();
- monster.setId(199);
- monster.setName("孙悟空");
- monster.setAge(23);
- monster.setIsMarried(false);
- monster.setBirth(new Date());
- Car car = new Car();
- car.setName("奔驰");
- car.setPrice(20000.0);
- monster.setCar(car);
- return monster;
- }
- }
复制代码 浏览器访问该方法,返回如下:
为什么SpringBoot可以将Monster对象以 JSON格式返回呢?
它的底层仍然用到了一个转换器:AbstractJackson2HttpMessageConverter
其中一个重要的方法如下:
返回数据的格式是按照你设置的contentType类型。如果没有指定,默认为json格式。- //参数Object object就是控制器方法返回的对象类型,如Monster
- protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
- //当控制器方法返回的时候,获取返回对象的contentType,一般为application/json
- MediaType contentType = outputMessage.getHeaders().getContentType();
- //获取编码,一般为utf-8
- JsonEncoding encoding = this.getJsonEncoding(contentType);
- Class<?> clazz = object instanceof MappingJacksonValue ? ((MappingJacksonValue)object).getValue().getClass() : object.getClass();
- ObjectMapper objectMapper = this.selectObjectMapper(clazz, contentType);
- Assert.state(objectMapper != null, "No ObjectMapper for " + clazz.getName());
- OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
- try {//生成一个UTF8JsonGenerator对象
- JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, encoding);
- Throwable var10 = null;
- try {//对返回的数据类型进行一系列处理
- this.writePrefix(generator, object);
- Object value = object;
- Class<?> serializationView = null;
- FilterProvider filters = null;
- JavaType javaType = null;
- if (object instanceof MappingJacksonValue) {
- MappingJacksonValue container = (MappingJacksonValue)object;
- value = container.getValue();
- serializationView = container.getSerializationView();
- filters = container.getFilters();
- }
- if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
- javaType = this.getJavaType(type, (Class)null);
- }
- ObjectWriter objectWriter = serializationView != null ? objectMapper.writerWithView(serializationView) : objectMapper.writer();
- if (filters != null) {
- objectWriter = objectWriter.with(filters);
- }
- if (javaType != null && javaType.isContainerType()) {
- objectWriter = objectWriter.forType(javaType);
- }
- SerializationConfig config = objectWriter.getConfig();
- if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
- objectWriter = objectWriter.with(this.ssePrettyPrinter);
- }
- //UTF8JsonGenerator对象处理返回的数据
- objectWriter.writeValue(generator, value);
- this.writeSuffix(generator, object);
- generator.flush();
- } catch (Throwable var26) {
- var10 = var26;
- throw var26;
- } finally {
- if (generator != null) {
- if (var10 != null) {
- try {
- generator.close();
- } catch (Throwable var25) {
- var10.addSuppressed(var25);
- }
- } else {
- generator.close();
- }
- }
- }
- } catch (InvalidDefinitionException var28) {
- throw new HttpMessageConversionException("Type definition error: " + var28.getType(), var28);
- } catch (JsonProcessingException var29) {
- throw new HttpMessageNotWritableException("Could not write JSON: " + var29.getOriginalMessage(), var29);
- }
- }
复制代码 3.内容协商
3.1基本说明
内容协商:服务端和请求端协商决定最终返回什么格式的内容。客户端发送请求的时候可以告知服务器,自己希望对方返回的数据格式列表,而服务器的接口也有能支持响应的格式列表,最终返回的结果会根据这两个类型列表,找到一种两边都能支持的类型返回,如果找不到合适的类型,则报错。
简单来说就是:根据客户端接收能力不同,SpringBoot 返回不同媒体类型的数据
比如:
- 客户端 Http 请求 Accept: application/xml 则返回 xml 数据
- 客户端 Http 请求 Accept: application/json 则返回 json 数据
例子1:使用postman测试
(1)使用postman发送Http请求,在此期间服务器的代码不变,根据请求头不同,返回的数据格式也会不同
返回json格式:
返回xml格式:
SpringBoot 默认支持返回 Json数据,不支持返回xml数据,所以需要导入jackson-dataformat-xml- <dependency>
- <groupId>com.fasterxml.jackson.dataformat</groupId>
- <artifactId>jackson-dataformat-xml</artifactId>
- </dependency>
复制代码例子2:使用浏览器测试
浏览器不能指定Accept属性,我们在浏览器发出请求,发现返回的数据为xml格式:
这是因为浏览器的Accept中指定了多种媒体类型,如下:
- 支持接收html,xhtml+xml,xml格式类型的权重为0.9
- 支持接收image和 */* [所有类型] 格式的权重为0.8
- 所以服务器会优先返回xhtml+xml格式
3.2问题
如上,客户端通过Http请求的Accept来指定能接收的媒体类型。那么服务端的接口是怎么响应这个类型的呢?
内容协商原理
内容协商原理:
- 判断当前响应头中是否已经有确定的媒体类型
- 获取客户端Accept请求头字段
- 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象
- 找到支持操作当前操作对象的converter,把converter支持的媒体类型统计出来
- 进行内容协商得到最佳匹配媒体类型
3.3注意事项
Postman可以通过修改Accept的值来返回不同的数据格式。对于浏览器来说,我们无法修改其Accept的值,这时如果要指定返回json格式,怎么办呢?
解决方案:开启支持基于请求参数的内容协商功能
(1)修改application.yml- spring:
- mvc:
- contentnegotiation:
- favor-parameter: true #开启基于请求参数的内容协商,默认不开启
复制代码 (2)在浏览器请求的时候带上format参数,此时返回的就是指定的格式了
注意:参数format的值是规定好的,在开启请求参数的内容协商功能后,SpringBoot底层ParameterContentNegotiationStrategy会通过format来接收参数,然后返回对应的媒体类型/数据格式,因此format的值也要是SpringBoot能处理的才行。
当然format这个属性名本身也可以通过配置文件修改:
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |