内容协商
实现:一套系统适配多端数据返回
多端内容适配:
1. 默认规则
SpringBoot 多端内容适配。
基于请求头内容协商:(默认开启)
客户端向服务端发送请求,携带HTTP标准的Accept请求头。
Accept: application/json(要求返回json形式的数据)、text/xml、text/yaml
服务端根据客户端请求头期望的数据类型进行动态返回
基于请求参数内容协商:(需要开启)
客户端发送参数时,携带一个format参数,其值就是我们要求的参数的形式
发送请求 GET /projects/spring-boot?format=json
匹配到 @GetMapping("/projects/spring-boot")
根据参数协商,优先返回 json 类型数据【需要开启参数匹配设置】
发送请求 GET /projects/spring-boot?format=xml,优先返回 xml 类型数据
代码举例:
Application.properties配置文件:
#使用参数进行内容协商
spring.mvc.contentnegotiation.favor-parameter=true
#自定义参数名,默认为format
spring.mvc.contentnegotiation.parameter-name=format
实体类
@Data
@JacksonXmlRootElement//可以写出xml文档
public class Person {
private Integer id;
private String username;
private String email;
private int age;
}
controller类
@RestController
@Slf4j
public class HelloController {
/**
* @RequestBody默认对象以json形式返回
* 想要返回xml形式的数据步骤:
* ①引入支持写出xml内容依赖:jackson-dataformat-xml
* ②在实体类加上注解 @JacksonXmlRootElement//可以写出xml文档
* ③可以 基于请求头内容协商或 基于请求参数内容协商 两种方式:要求返回xml/json形式的数据
* @return
*/
@GetMapping("/person")
public Person person(){
Person person = new Person();
person.setId(1);
person.setUsername("张三");
person.setEmail("aaa@qq.com");
person.setAge(19);
return person;
}
}
测试;
HttpMessageConverter原理(内容协商的底层原理)
希望返回yaml形式的数据
● HttpMessageConverter 怎么工作?合适工作?
● 通过定制 HttpMessageConverter 来实现多端内容协商
● 编写WebMvcConfigurer提供的configureMessageConverters底层,修改底层的MessageConverter
1. @ResponseBody由HttpMessageConverter处理
标注了@ResponseBody的返回值 将会由支持它的 HttpMessageConverter写给浏览器
如果controller方法的返回值标注了 @ResponseBody 注解
过程
①请求进来先来到DispatcherServlet的doDispatch()进行处理
② 找到一个 HandlerAdapter 适配器。利用适配器执行目标方法
③到RequestMappingHandlerAdapter来执行,调用invokeHandlerMethod()来执行目标方法
④目标方法执行之前,准备好两个东西
HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理
⑤RequestMappingHandlerAdapter 里面的invokeAndHandle()(在invokeHandlerMethod()中调用此方法)真正执行目标方法
目标方法执行完成,会返回返回值对象
找到一个合适的返回值处理器 HandlerMethodReturnValueHandler(然后寻找合适的MessageConverter)
最终找到 RequestResponseBodyMethodProcessor能处理 标注了 @ResponseBody注解的方法
RequestResponseBodyMethodProcessor 调用writeWithMessageConverters ,利用MessageConverter把返回值写出去
上面解释:@ResponseBody由HttpMessageConverter处理
HttpMessageConverter 会先进行内容协商
遍历所有的MessageConverter看谁支持这种内容类型的数据
总结;@ResponseBody标注的方法最终会由MessaConverter进行内容协商写出去,系统中有什么MessageConverter(导那个依赖就有那个的MessageConverter)能处理,就用那个。
WebMvcAutoConfiguration提供几种默认HttpMessageConverters
● EnableWebMvcConfiguration通过 addDefaultHttpMessageConverters添加了默认的MessageConverter;如下:
○ ByteArrayHttpMessageConverter: 支持字节数据读写(将对象写成字节数组)
○ StringHttpMessageConverter: 支持字符串读写(写成字符串)
○ ResourceHttpMessageConverter:支持资源读写
○ ResourceRegionHttpMessageConverter: 支持分区资源写出
○ AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
○ MappingJackson2HttpMessageConverter: 支持请求响应体Json读写
系统提供默认的MessageConverter 功能有限,仅用于json或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter
WebMvcAutoConfiguration
提供了很多默认设置。
判断系统中是否有响应的类,如果有就加入响应的HttpMessageConverter
增加yaml格式内容协商
/**
* 想要返回yaml格式的数据
* 步骤:
* ①导入相应的包
* 支持返回yaml格式的数据
* <dependency>
* <groupId>com.fasterxml.jackson.dataformat</groupId>
* <artifactId>jackson-dataformat-yaml</artifactId>
* </dependency>
* ② 在handler方法中通过mapper干涉(在底层,将返回值对象转成json形式,xml形式,yaml形式,... 都是由mapper来干涉的)
* 创建mapper对象,并传入yaml工厂的对象new YAMLFactory()
* new ObjectMapper(new YAMLFactory());
* 通过mapper调用writeValueAsString(person) ,传入要返回的对象,
* 此字符串就是yaml格式的
* ③内容协商(告知系统,存在一种yaml格式)
* 在application.properties中编写;
* #增加一种新的类型(返回对象的格式)
* #新格式的key--》yaml 新格式的value--》application/yaml(客户端发送请求指明返回对象格式的请求头Accept的值)
* spring.mvc.contentnegotiation.media-types.yaml=application/yaml
* ④在配置类中重写configureMessageConverters方法新增一个MessageConverter组件
增加HttpMessageConverter组件,专门负责把对象写出为yaml格式
⑤创建一个自定义的MessageConverter类
*
*/
controller类
@RestController
@Slf4j
public class HelloController {
@GetMapping("/person2")
public String person2() throws JsonProcessingException {
Person person = new Person();
person.setId(1);
person.setUsername("张三");
person.setEmail("aaa@qq.com");
person.setAge(19);
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());//传入yaml工厂的对象new YAMLFactory()
//在底层,将返回值对象转成json形式,xml形式,yaml形式,... 都是由mapper来干涉的
String s = mapper.writeValueAsString(person);
System.out.println(s);//------------------------②在handler方法中通过mapper干涉
return s;
}
}
Application.properties文件-----------------③内容协商(告知系统,存在一种yaml格式)
#增加一种新的类型(返回对象的格式)
#新格式的key--》yaml 新格式的value--》application/yaml(客户端发送请求指明返回对象格式的请求头Accept的值)
spring.mvc.contentnegotiation.media-types.yaml=application/yaml
配置类(手自一体模式)-----------------------④在配置类中重写configureMessageConverters方法新增一个MessageConverter组件
@Configuration//这是一个配置类
public class MyConfig implements WebMvcConfigurer {
//配置能把对象转为yaml的messageConverter
/**
*通过converters.add()方法传入自定义的MessageConverter
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyYamlHttpMessageConverter());
}
}
⑤自定义的MessageConverter
/**
* 创建一个yaml的MessageConverter
* 步骤:
* ①继承AbstractHttpMessageConverter<泛型> 该泛型代表,可以将什么样的类型转化为我们自定义的格式
* ②创建一个空参构造器在其中:
* 1创建mapper对象,并传入yaml工厂的对象new YAMLFactory(),通过mapper干涉对象的格式转换
* 2告诉SpringBoot这个MessageConverter支持那种媒体类型
* 创建一个媒体类(MediaType)对象,传入type("application/text")与subtype(yaml),字符编码----------与配置类中定义的媒体类型名一致
* 在构造器的首行传入调用super()传入MediaType对象
* ③实现三个方法
*/
public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper objectMapper= null;//把对象转成yaml
public MyYamlHttpMessageConverter(){
//告诉SpringBoot这个MessageConverter支持那种媒体类型
super(new MediaType("application", "yaml",Charset.forName("UTF-8")));
YAMLFactory factory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
this.objectMapper=new ObjectMapper(factory);
}
@Override
protected boolean supports(Class<?> clazz) {
//只要是对象类型,不是基本类型都支持转换
return true;
}
/**
*readInternal方法配合注解;@RequestBody
* 把对象怎么读进来
*/
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}//读数据
/**
*writeInternal方法配合注解@ResponseBody
* 把对象怎么写出去
* methodReturnValue--------------方法的返回值
* outputMessage---------------对应响应的输出流
* 步骤:
* ①通过outputMessage调用getBody()得到一个输出流------------body
* ②通过mapper对象调用writeValue(body,methodReturnValue)方法,传入输出流body与methodReturnValue返回值对象
* ③关闭输出流
*/
@Override
protected void writeInternal(Object methodReturnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
OutputStream body = outputMessage.getBody();//得到输出流
try {
this.objectMapper.writeValue(body,methodReturnValue);
} finally {
body.close();
}
// try(OutputStream os = outputMessage.getBody()){
// this.objectMapper.writeValue(os,methodReturnValue);
// }//------------------------try-with写法自动关流
}
}
总结:
如何增加其他
● 配置媒体类型支持:
○ spring.mvc.contentnegotiation.media-types.yaml=text/yaml
● 编写对应的HttpMessageConverter,要告诉Boot这个支持的媒体类型
● 把MessageConverter组件加入到底层
○ 容器中放一个`WebMvcConfigurer` 组件,并配置底层的MessageConverter
web开发-Thymeleaf
模板引擎
● 由于 SpringBoot 使用了嵌入式 Servlet 容器。所以 JSP 默认是不能使用的。
● 如果需要服务端页面渲染,优先考虑使用 模板引擎。
模板引擎页面默认放在 src/main/resources/templates
SpringBoot 包含以下模板引擎的自动配置
● FreeMarker
● Groovy
● Thymeleaf
● Mustache
Thymelea整合
自动配置原理
1. 开启了 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration 自动配置
2. 属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容
3. 所有的模板页面默认在 classpath:/templates文件夹下
4. 默认效果
a. 所有的模板页面在 classpath:/templates/下面找--------前缀
b. 找后缀名为.html的页面
可以修改
spring.thymeleaf.prefix=前缀
spring.thymeleaf.suffix=后缀
#缓存的开启与关闭(默认时true开启的)
#推荐开发期间关闭,上线以后开启
spring.thymeleaf.cache=false
步骤:
①导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
②handler方法中利用模板引擎跳转到指定页面
* 1,由共享数据,将需要给页面共享的数据放到model中
* model调用addAttribute("msg",name)传入接收参数的变量名与参数
* 页面:<span th:text="${msg}"></span> ${}利用模板字符串
* 2,直接返回页面的地址(去掉前缀与后缀),实现跳转
* @return
* * 模板的逻辑视图名
* * 物理视图=前缀+逻辑视图名+后缀
* * 真实地址=classpath:/templates/welcome.html
测试代码
controller类
@Controller//适配服务端渲染 前后端不分离模式
public class WelcomeController {
/**
* 利用模板引擎跳转到指定页面
* ①由共享数据,将需要给页面共享的数据放到model中
* model调用addAttribute("msg",name)传入接收参数的变量名与参数
* 页面:<span th:text="${msg}"></span> ${}利用模板字符串
* ②直接返回页面的地址(去掉前缀与后缀),实现跳转
* @return
* * 模板的逻辑视图名
* * 物理视图=前缀+逻辑视图名+后缀
* * 真实地址=classpath:/templates/welcome.html
*/
@GetMapping("/well")
public String hello(@RequestParam("name") String name, Model model){
model.addAttribute("msg",name);
return "welcome";
}
}
核心用法
th:xxx:动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)
● th:text:标签体内文本值渲染
○ th:utext:不会转义,显示为html原本的样子。
● th:属性:标签指定属性渲染
● th:attr:标签任意属性渲染
● th:ifth:each...:其他th指令
代码举例:html文件
<body>
<h1>你好: <span th:text="${msg}"></span></h1>
<hr>
th:任意html属性;实现动态替换任意属性的值
th:text 替换标签体的内容 (哈哈被替换为msg的内容)---------------不会识别html的标签,会将html标签转义
th:utext 替换标签体的内容----------------会识别html的标签
<h1 th:text="${msg}">哈哈</h1>
<h1 th:utext="${msg}">呵呵</h1>
th:src 动态的路径
<img th:src="${imgUrl}" style="width:300px"/>
th:attr 任意属性指定(所有的属性都可以从attr中动态取出)
<img th:attr="src=${imgUrl},style=${style}"/>
th:其他指令
<img th:src="${imgUrl}" th:if="${show}"/>
</body>
表达式:用来动态取值
● ${}:变量取值;使用model共享给页面的值都直接用${}
● @{}:url路径;(自动处理应用上下文路径,支持绝对相对路径,动态拼接参数)
举例:
@{} 专门用来去各种路径
<img th:src="@{${imgUrl}}" th:if="${show}"/>
● #{}:国际化消息
● ~{}:片段引用
● *{}:变量选择:需要配合th:object绑定对象
系统工具&内置对象
● param:请求参数对象
● session:session对象
● application:application对象
● #execInfo:模板执行信息
● #messages:国际化消息
● #uris:uri/url工具
● #conversions:类型转换工具
● #dates:日期工具,是java.util.Date对象的工具类
● #calendars:类似#dates,只不过是java.util.Calendar对象的工具类
● #temporals: JDK8+ java.time API 工具类
● #numbers:数字操作工具
● #strings:字符串操作
● #objects:对象操作
● #bools:bool操作
● #arrays:array工具
● #lists:list工具
● #sets:set工具
● #maps:map工具
● #aggregates:集合聚合工具(sum、avg)
● #ids:id生成工具
代码举例:
转大写:<h1 th:text="${#strings.toUpperCase(msg)}">哈哈</h1>
语法示例
表达式:
● 变量取值:${...}
● url 取值:@{...}
● 国际化消息:#{...}
● 变量选择:*{...}
● 片段引用: ~{...}
常见:
● 文本: 'one text','another one!',...
● 数字: 0,34,3.0,12.3,...
● 布尔:true、false
● null: null
● 变量名: one,sometext,main...
文本操作:
● 拼串: +
● 文本替换:| The name is ${name} |
举例:
拼接操作 | |代表要进行拼接操作
<h1 th:text="${'前缀'+msg+'后缀'}">哈哈</h1>
<h1 th:text="|前缀:${msg} 后缀|">哈哈</h1>
布尔操作:
● 二进制运算: and,or
● 取反:!,not
比较运算:
● 比较:>,<,<=,>=(gt,lt,ge,le)
● 等值运算:==,!=(eq,ne)
条件运算:
● if-then: (if)?(then)
● if-then-else: (if)?(then):(else)
● default: (value)?:(defaultValue)
Thymeleaf遍历
遍历语法: th:each="元素名,迭代状态 : ${集合}"
迭代状态 有以下属性:
● index:当前遍历元素的索引,从0开始
● count:当前遍历元素的索引,从1开始
● size:需要遍历元素的总数量
● current:当前正在遍历的元素对象
● even/odd:是否偶数/奇数行
● first:是否第一个元素
● last:是否最后一个元素
Thymeleaf判断
th:if
如果if中的语句为true,渲染该元素,否完全移除(不会生成空标签)
th:switch
行内写法:[[...]] or [(...)]
代码举例:
controller类
@Controller//适配服务端渲染 前后端不分离模式
public class WelcomeController {
@GetMapping("/list")
public String list(Model model){
List<Person> list = Arrays.asList(
new Person(1, "张三1", "zsl@qq.com", 15),
new Person(1, "张三2", "zsl@qq.com", 15),
new Person(1, "张三3", "zsl@qq.com", 15),
new Person(1, "张三4", "zsl@qq.com", 15),
new Person(1, "张三5", "zsl@qq.com", 15)
);
model.addAttribute("persons",list);//与页面共享数据
return "list";
}
}
List.html文件
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table>
<thead>
<tr>
<th>id</th>
<th>username</th>
<th>email</th>
<th>age</th>
<th>状态索引</th>
</tr>
</thead>
<tbody>
<tr th:each="person,stats : ${persons}">
<!-- 行内写法:[[...]] or [(...)]-->
<th>[[${person.id}]]</th>
<th th:text="${person.username}"></th>
<!-- 判断语法-->
<!--th:if-->
<th th:if="${#strings.isEmpty(person.email)}" th:text="联系不上"></th>
<th th:if="not ${#strings.isEmpty(person.email)}" th:text="${person.email}"></th>
<!-- 三目运算符 <th th:text="|${person.age}-${person.age>=18? '成年':'未成年'}|"></th>-->
<!-- th:switch-->
<th th:switch="${person.age}">
<button th:case="15">未成年</button>
<button th:case="16">未成年</button>
<button th:case="17">未成年</button>
<button th:case="18">成年</button>
<button th:case="19">成年</button>
</th>
<th th:text="${stats.index}"></th>
</tr>
</tbody>
</table>
</body>
</html>
属性优先级
Order | Feature | Attributes |
1 | 片段包含 | th:insert th:replace |
2 | 遍历 | th:each |
3 | 判断 | th:if th:unless th:switch th:case |
4 | 定义本地变量 | th:object th:with |
5 | 通用方式属性修改 | th:attr th:attrprepend th:attrappend |
6 | 指定属性修改 | th:value th:href th:src ... |
7 | 文本值 | th:text th:utext |
8 | 片段指定 | th:fragment |
9 | 片段移除 | th:remove |
片段包含>遍历>判断
变量选择
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
等同于
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div
模板布局
(把公共的部分抽取出来,在别的页面可以引用)
● 定义模板: th:fragment="片段名"
● 引用模板:~{templatename::selector}即 ~{模板名:: 片段名}
● 插入模板:th:insert、th:replace="~{模板名:: 片段名}"
代码举例
写在footer.html文件中的公共部分
<footer th:fragment="copy">© 2011 The Good Thymes Virtual Grocery</footer>
其他文件引用公共部分
引用格式: ~{模板名:: 片段名}
<body>
<div th:insert="~{footer :: copy}"></div>
<div th:replace="~{footer :: copy}"></div>
</body>
<body>
结果:
<body>
<div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
</div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
</body>
</body>
Devtools使用
(修改.xml文件,浏览器显示的内容立即同步更新)
当修改Thymeleaf模板文件(.html)时:
自动更新加载
即时生效,刷新浏览器即可看到更改
java代码的修改,如果devtools热启动了,可能会引起一些bug,难以排查
使用:
①导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
②修改完.xml文件后要ctrl+f9刷新