內容協商
實現:一套系統適配多端數據返回
多端內容適配:
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刷新
?