第四课 SpringBoot2基础-数据响应与内容协商
tags:
- Spring Boot
- 2021尚硅谷
- 雷丰阳
文章目录

第一节 响应JSON
1.1 jackson.jar+@ResponseBody
pom.xml下starter-web点进去
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
web场景自动引入了json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.4.5</version>
<scope>compile</scope>
</dependency>

public Person getPerson(){
Person person = new Person(); // 断点给它
person.setAge(28);
person.setBirth(new Date());
person.setUserName("zhangsan");
return person;
}
}
1.2 返回值解析器
- 断点看源码。请求:127.0.0.1:8080/test/person。
- CTRL+N全局搜索类DispatcherServlet,CTRL+H搜索继承树
- 断点一:
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - 断点二:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());真正处理目标方法的- 点击handle->点击接口(选择
AbstractHandlerMethodAdapter实现类)->点击handleInternal->选择ModelAndView的实现->方法protected ModelAndView handleInternal
- 点击handle->点击接口(选择
- 断点三:
mav = invokeHandlerMethod(request, response, handlerMethod);直接执行请求方法的。- 追踪过去invokeHandlerMethod
argumentResolvers和returnValueHandlers发现参数解析器和返回值解析器。(也可以断点观察一下)

- 断点四:
invocableMethod.invokeAndHandle(webRequest, mavContainer);真正的请求处理逻辑。invocableMethod看一下invokeAndHandle点进去嘿嘿

- 断点五:
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);执行请求,这里可以追踪请求如何执行 也可以直接下一步可以。- 如果追踪进来:
doInvoke(args);利用反射执行目标方法。得到返回值
- 如果追踪进来:
- 断点六:
this.returnValueHandlers.handleReturnValue->追踪handleReturnValue->selectHandler寻找可以处理返回值的返回值处理器。
// 在这利用所有返回值处理器处理返回值
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
- 断点七:
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);直接step into追踪进去到RequestResponseBodyMethodProcessor - 断点八:
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);就是它把对象转换成了json格式。可以追踪慢慢观察- 内容协商
MediaType(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)->MediaType selectedMediaType = null;下面内容协商的断点。 - 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,

- 内容协商
- 断点九:
for (HttpMessageConverter<?> converter : this.messageConverters)- SpringMVC会挨个遍历所有容器底层的
HttpMessageConverter(跟进去) ,看谁能处理? ctrl + F12, 可以查看类结构 - HttpMessageConverter: 看是否支持将此 Class类型的对象,转为MediaType类型的数据。
- 例子:Person对象转为JSON。或者 JSON转为Person

- SpringMVC会挨个遍历所有容器底层的
- 有十种MessageConverter。最终
MappingJackson2HttpMessageConverter把对象转为JSON(利用底层的jackson的objectMapper转换的)- 0 - 只支持Byte类型的
- 1 - String
- 2 - String
- 3 - Resource
- 4 - ResourceRegion
- 5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
- 6 - MultiValueMap
- 7 - true
- 8 - true
- 9 - 支持注解方式xml处理的

1.3 SpringMVC到底支持返回值类型
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter 响应式
StreamingResponseBody 流式数据
HttpEntity
HttpHeaders
Callable 异步
DeferredResult 异步相关
ListenableFuture 异步相关
CompletionStage 异步相关
WebAsyncTask 异步任务
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
第二节 内容协商
2.1 内容协商使用
- 根据客户端接收能力不同,返回不同媒体类型的数据。
- 比如:浏览器访问返回json,安卓返回xml等。
- 引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
- postman分别测试返回json和xml
- 只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。

- 只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
- 开启浏览器参数方式内容协商功能
- 为了方便内容协商,开启基于请求参数的内容协商功能。
2.2 内容协商原理
1.springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java:205断点:内容协商的核心流程。
2. 第一步:判断当前响应头中是否已经有确定的媒体类型。MediaType
3. 第二步:acceptableTypes = getAcceptableMediaTypes(request);获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)
- 追踪进去contentNegotiationManager.resolveMediaTypes 内容协商管理器 默认使用基于请求头的策略
- 然后追踪strategy.resolveMediaTypes获取head中的Accept请求头字段
- HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型

4. 第三步:List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);获取服务器可以产生的媒体类型。
- 追踪进去:result.addAll(converter.getSupportedMediaTypes(valueClass));把支持的媒体类型加到集合返回。
- 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)。 找到支持操作Person的converter,把converter支持的媒体类型统计出来。 第一次用converter,找到四个
5. 第四步:浏览器可以接收的和服务端可以产生的相匹配的类型。for (MediaType requestedType : acceptableTypes)
6. 第五步:排序后,进行内容协商的最佳匹配for (MediaType mediaType : mediaTypesToUse)最终返回selectedMediaType
7. 第六步:然后再看for (HttpMessageConverter<?> converter : this.messageConverters),converter谁能支持把对象转化成最佳匹配的类型。第二次用converter。只有一个。。 可以缓存优化嘿嘿
8. 第七步:然后调用上面converter进行转换。genericConverter.write(body, targetType, selectedMediaType, outputMessage);追踪进write可以看到person转换为xml类型。
- writeInternal(t, type, outputMessage);追踪进去->objectWriter.writeValue(generator, value);最后返回outputMessage`输出

2.3 参数方式内容协商功能
- 为了方便内容协商,开启基于请求参数的内容协商功能。
mvc:
hiddenmethod:
filter:
enabled: true
# 开启请求参数内容协商模式
contentnegotiation:
favor-parameter: true
- 发起请求:发请求时带上format:
- 希望返回json: http://localhost:8080/test/person?format=json
- 希望返回xml: http://localhost:8080/test/person?format=xml
- 原理:确定客户端接收什么样的内容类型;
- Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
- 最终进行内容协商返回给客户端json即可。

2.4 自定义 MessageConverter
- 实现多协议数据兼容。json、xml、x-guigu
- @ResponseBody 响应数据出去 调用
RequestResponseBodyMethodProcessor处理 - Processor 处理方法返回值。通过
MessageConverter处理 - 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
- 内容协商找到最终的 messageConverter;
- @ResponseBody 响应数据出去 调用
- 假如有需求:
- 浏览器发请求直接返回 xml [application/xml] jacksonXmlConverter
- 如果是ajax请求 返回 json [application/json] jacksonJsonConverter
- 如果硅谷app发请求,返回自定义协议数据 [appliaction/x-guigu] xxxxConverter
- 属性值1;属性值2;
- 解决方案:
- 添加自定义的MessageConverter进系统底层
- 系统底层就会统计出所有MessageConverter能操作哪些类型
- 客户端内容协商 [guigu—>guigu]
4.org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureMessageConverters自动配置类中有configureMessageConverters。系统已启动所有的converters就已经就绪了。
- 系统底层默认添加的converters在
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConvertersmessageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));比如xml的converters导入
- 如何添加自定义的MessageConverter?,
- 无论需要定制SpringMVC的任何功能。只有一个入口,需要给容器中添加一个 WebMvcConfigurer
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
// 给容器中添加自定义的Converters
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
}
- 自定义的Converter
package com.atguigu.boot.converter;
import com.atguigu.boot.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
/**
* 自定义的Converter
*/
public class GuiguMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}
/**
* 服务器要统计所有MessageConverter都能写出哪些内容类型
* application/x-guigu
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议数据的写出
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();
//写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}

2.5 参数字段调用自定义MessageConverter
- format赋值解析成自定义的Converter。
- 希望返回json: http://localhost:8080/test/person?format=gg
- 自定义内容协商策略
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//Map<String, MediaType> mediaTypes 传入支持的媒体类型
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
//指定支持解析哪些参数对应的哪些媒体类型
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
//parameterStrategy.setParameterName("ff");
//基于请求头的内容协商 别忘记加 如果忘记,那么Accept中的就会失效
HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();
// 设置自己的内容协商策略
configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy));
}
}
- 测试:http://localhost:8080/test/person?format=gg
- 当然你也可以自定义基于其他各种东西的: 比如路径变量,以及各种东西的。
- 有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
- 上面别忘记:基于请求头的内容协商 别忘记加 如果忘记,那么Accept中的就会失效
第三节 视图解析和模板引擎
3.1 模板引擎-thymeleaf
- 视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
- thymeleaf简介
- 现代化、服务端Java模板引擎 语法简单贴近jsp供后台使用。
- 缺点:不是高性能的模板引擎。要做高并发还要跳转,最好前后分离,找专业前端。
- 官网:https://www.thymeleaf.org/index.html
- 基本语法可以看官网:
- https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax
- 或者:https://www.yuque.com/atguigu/springboot/vgzmgh#lzseb
- 引入thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- ctrl+N查找ThymeleafAutoConfiguration,查看它的自动配置类。
- 自动配好的策略
- 所有thymeleaf的配置值都在
ThymeleafProperties中 - 配置好了
SpringTemplateEngine模板引擎 - 配好了
ThymeleafViewResolverThymeleaf视图解析器 - 我们只需要直接开发页面
- 所有thymeleaf的配置值都在
// 默认静态页面放置路径
public static final String DEFAULT_PREFIX = "classpath:/templates/";
// 默认后缀名
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
- 页面开发
- $直接取link的变量值
- @把link作为路径去访问如果后端
server.servlet:context-path: /world@也会帮我们自动加上
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<!-- $直接取link的变量值 @把link作为路径去访问如果后端server.servlet:context-path: /world @也会帮我们自动加上 -->
<a href="www.atguigu.com" th:href="${link}">去百度</a> <br/>
<a href="www.atguigu.com" th:href="@{/link}">去百度2</a>
</h2>
</body>
</html>
@Controller
public class ViewTestController {
@GetMapping("/atguigu")
public String atguigu(Model model){
//model中的数据会被放在请求域中 request.setAttribute("a",aa)
model.addAttribute("msg","你好 guigu");
model.addAttribute("link","http://www.baidu.com");
// 直接返回页面
return "success";
}
}
3.2 视图解析器和视图原理
- 首先:thymeleaf快速构建出后台管理系统 (模板知识自己查一下就行)
- 先对
@PostMapping("/login")下的if打断点。进入Ctrl+N,DispatcherServlet的doDispatch加断点。还是之前的流程。=>追踪一个redirct的返回。 - 目标方法处理的过程中,所有数据都会被放在
ModelAndViewContainer里面。包括数据和视图地址,追踪到return getModelAndView(mavContainer, modelFactory, webRequest); - 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在
ModelAndViewContainer - 返回mav之后,通过
processDispatchResult处理派发结果(页面改如何响应)- render(mv, request, response); 进行页面渲染逻辑
- 继续跟进
resolveViewName:根据方法的String返回值得到View对象(定义了页面的渲染逻辑)- 所有的
视图解析器尝试是否能根据当前返回值得到View对象 - 得到了 redirect:/main.html --> Thymeleaf new RedirectView()
ContentNegotiationViewResolver里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。- view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
- RedirectView 如何渲染【重定向到一个页面】
- 获取目标url地址
- response.sendRedirect(encodedURL);

- 所有的
- 返回值以
forward: 开始:new InternalResourceView(forwardUrl);--> 转发request.getRequestDispatcher(path).forward(request, response); - 返回值以 redirect: 开始:
new RedirectView()render就是重定向 - 返回值是普通字符串:
new ThymeleafView()
464

被折叠的 条评论
为什么被折叠?



