而像HTML、CSS、JS 以及圖片、音頻、視頻等這些資源,我們都稱為靜態資源。 所謂靜態資源,就是指在服務器上存儲的不會改變的數據,通常不會根據用戶的請求而變化。
那與靜態資源對應的還有一類資源,就是動態資源。那所謂動態資源,就是指在服務器端上存儲的,會根據用戶請求和其他數據動態生成的,內容可能會在每次請求時都發生變化。比如:Servlet、JSP等(負責邏輯處理)。而Servlet、JSP這些技術現在早都被企業淘汰了,現在在企業項目開發中,都是直接基于Spring框架來構建動態資源。
而對于我們java程序開發的動態資源來說,我們通常會將這些動態資源部署在Tomcat,這樣的Web服務器中運行。 而瀏覽器與服務器在通信的時候,基本都是基于HTTP協議的。
那上述所描述的這種瀏覽器/服務器的架構模式呢,我們稱之為:BS架構。
- BS架構:Browser/Server,瀏覽器/服務器架構模式。客戶端只需要瀏覽器,應用程序的邏輯和數據都存儲在服務端。
-
- 優點:維護方便
- 缺點:體驗一般
- CS架構:Client/Server,客戶端/服務器架構模式。需要單獨開發維護客戶端。
-
- 優點:體驗不錯
- 缺點:開發維護麻煩
那前面我們已經學習了靜態資源開發技術,包括:HTML、CSS、JS以及JS的高級框架Vue,異步交互技術Axios。 那那接下來呢,我們就要來學習動態資料開發技術,而動態資源開發技術中像早期的Servlet、JSP這些個技術早都被企業淘汰了,現在企業開發主流的就是基于Spring體系中的框架來開發這些動態資源。 所以,我們今天的課程內容內,分為以下四個部分:
- SpringBootWeb入門
- HTTP協議
- SpringBootWeb案例
- 分層解耦
1. SpringBootWeb入門
那接下來呢,我們就要來講解現在企業開發的主流技術 SpringBoot,并基于SpringBoot進行Web程序的開發 。
1.1 概述
在沒有正式的學習SpringBoot之前,我們要先來了解下什么是Spring。
我們可以打開Spring的官網(Spring | Home),去看一下Spring的簡介:Spring makes Java simple。
Spring的官方提供很多開源的項目,我們可以點擊上面的projects,看到spring家族旗下的項目,按照流行程度排序為:
Spring發展到今天已經形成了一種開發生態圈,Spring提供了若干個子項目,每個項目用于完成特定的功能。而我們在項目開發時,一般會偏向于選擇這一套spring家族的技術,來解決對應領域的問題,那我們稱這一套技術為spring全家桶。
而Spring家族旗下這么多的技術,最基礎、最核心的是 SpringFramework。其他的spring家族的技術,都是基于SpringFramework的,SpringFramework中提供很多實用功能,如:依賴注入、事務管理、web開發支持、數據訪問、消息服務等等。
而如果我們在項目中,直接基于SpringFramework進行開發,存在兩個問題:
- 配置繁瑣
- 入門難度大
所以基于此呢,spring官方推薦我們從另外一個項目開始學習,那就是目前最火爆的SpringBoot。 通過springboot就可以快速的幫我們構建應用程序,所以springboot呢,最大的特點有兩個 :
- 簡化配置
- 快速開發
Spring Boot 可以幫助我們非常快速的構建應用程序、簡化開發、提高效率 。
而直接基于SpringBoot進行項目構建和開發,不僅是Spring官方推薦的方式,也是現在企業開發的主流。
1.2 入門程序
1.2.1 需求
需求:基于SpringBoot的方式開發一個web應用,瀏覽器發起請求/hello后,給瀏覽器返回字符串 "Hello xxx ~"。
1.2.2 開發步驟
第1步:創建SpringBoot工程,并勾選Web開發相關依賴
第2步:定義HelloController類,添加方法hello,并添加注解
1). 創建SpringBoot工程(需要聯網)
基于Spring官方骨架,創建SpringBoot工程。
基本信息描述完畢之后,勾選web開發相關依賴。
SpringBoot官方提供的腳手架,里面只能夠選擇SpringBoot的幾個最新的版本,如果要選擇其他相對低一點的版本,可以在springboot項目創建完畢之后,修改項目的pom.xml文件中的版本號。
點擊Create之后,就會聯網創建這個SpringBoot工程,創建好之后,結構如下:
注意:在聯網創建過程中,會下載相關資源(請耐心等待)
static: 存儲一些靜態文件(html\css\js)
templates: 存儲一些模板文件
2). 定義HelloController類,添加方法hello,并添加注解
在com.itheima
這個包下新建一個類:HelloController
HelloController中的內容,具體如下:
package com.itheima;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController //標識當前類是一個請求處理類
public class HelloController {@RequestMapping("/hello") //標識請求路徑public String hello(String name){System.out.println("HelloController ... hello: " + name);return "Hello " + name;}}
3). 運行測試
運行SpringBoot自動生成的引導類 (標識有@SpringBootApplication
注解的類)
打開瀏覽器,輸入 http://localhost:8080/hello?name=itheima
1.2.3 常見問題
大伙兒在下來聯系的時候,聯網基于spring的腳手架創建SpringBoot項目,偶爾可能會因為網內網絡的原因,鏈接不上SpringBoot的腳手架網站,此時會出現如下現象:
此時可以使用阿里云提供的腳手架,網址為:https://start.aliyun.com
然后按照項目創建的向導,一步一步的創建項目即可。
1.3 入門解析
那在上面呢,我們已經完成了SpringBootWeb的入門程序,并且測試通過。 在入門程序中,我們發現,我們只需要一個main方法就可以將web應用啟動起來了,然后就可以打開瀏覽器訪問了。
那接下來我們需要明確兩個問題:
1). 為什么一個main方法就可以將Web應用啟動了?
因為我們在創建springboot項目的時候,選擇了web開發的起步依賴spring-boot-starter-web
。而spring-boot-starter-web
依賴,又依賴了spring-boot-starter-tomcat
,由于maven的依賴傳遞特性,那么在我們創建的springboot項目中也就已經有了tomcat的依賴,這個其實就是springboot中內嵌的tomcat。
而我們運行引導類中的main方法,其實啟動的就是springboot中內嵌的Tomcat服務器。 而我們所開發的項目,也會自動的部署在該tomcat服務器中,并占用8080端口號 。
起步依賴:
- 一種為開發者提供簡化配置和集成的機制,使得構建Spring應用程序更加輕松。起步依賴本質上是一組預定義的依賴項集合,它們一起提供了在特定場景下開發Spring應用所需的所有庫和配置。
-
- spring-boot-starter-web:包含了web應用開發所需要的常見依賴。
- spring-boot-starter-test:包含了單元測試所需要的常見依賴。
- 官方提供的starter:https://docs.spring.io/spring-boot/docs/3.1.3/reference/htmlsingle/#using.build-systems.starters
2. HTTP協議
2.1 HTTP概述
2.1.1 介紹
HTTP:Hyper Text Transfer Protocol(超文本傳輸協議),規定了瀏覽器與服務器之間數據傳輸的規則。
- http是互聯網上應用最為廣泛的一種網絡協議
- http協議要求:瀏覽器在向服務器發送請求數據時,或是服務器在向瀏覽器發送響應數據時,都必須按照固定的格式進行數據傳輸
如果想知道http協議的數據傳輸格式有哪些,可以打開瀏覽器,點擊F12
打開開發者工具,點擊Network(網絡)
來查看
ps: 瀏覽器地址欄中發起的所有請求都是GET方式
瀏覽器向服務器進行請求時,服務器按照固定的格式進行解析:
服務器向瀏覽器進行響應時,瀏覽器按照固定的格式進行解析:
而我們學習HTTP協議,就是來學習請求和響應數據的具體格式內容。
2.1.2 特點
我們剛才初步認識了HTTP協議,那么我們在看看HTTP協議有哪些特點:
- 基于TCP協議: 面向連接,安全
TCP是一種面向連接的(建立連接之前是需要經過三次握手)、可靠的、基于字節流的傳輸層通信協議,在數據傳輸方面更安全
- 基于請求-響應模型: 一次請求對應一次響應(先請求后響應)
請求和響應是一一對應關系,沒有請求,就沒有響應
- HTTP協議是無狀態協議: 對于數據沒有記憶能力。每次請求-響應都是獨立的
無狀態指的是客戶端發送HTTP請求給服務端之后,服務端根據請求響應數據,響應完后,不會記錄任何信息。
- 缺點: 多次請求間不能共享數據
- 優點: 速度快
- 請求之間無法共享數據會引發的問題:
-
- 如:京東購物。加入購物車和去購物車結算是兩次請求
- 由于HTTP協議的無狀態特性,加入購物車請求響應結束后,并未記錄加入購物車是何商品
- 發起去購物車結算的請求后,因為無法獲取哪些商品加入了購物車,會導致此次請求無法正確展示數據
- 具體使用的時候,我們發現京東是可以正常展示數據的,原因是Java早已考慮到這個問題,并提出了使用會話技術(Cookie、Session)來解決這個問題。具體如何來做,我們后面課程中會講到。
剛才提到HTTP協議是規定了請求和響應數據的格式,那具體的格式是什么呢? 接下來,我們就來詳細剖析。
HTTP協議又分為:請求協議和響應協議
2.2 HTTP請求協議
2.2.1 介紹
- 請求協議:瀏覽器將數據以請求格式發送到服務器。包括:請求行、請求頭 、請求體
- GET方式的請求協議:
- 請求行(以上圖中紅色部分) :HTTP請求中的第一行數據。由:
請求方式
、資源路徑
、協議/版本
組成(之間使用空格分隔)
-
- 請求方式:GET
- 資源路徑:/brand/findAll?name=OPPO&status=1
-
-
- 請求路徑:/brand/findAll
- 請求參數:name=OPPO&status=1
-
-
-
-
- 請求參數是以key=value形式出現
- 多個請求參數之間使用
&
連接
-
-
-
-
- 請求路徑和請求參數之間使用
?
連接
- 請求路徑和請求參數之間使用
-
-
- 協議/版本:HTTP/1.1
- 請求頭(以上圖中黃色部分) :第二行開始,上圖黃色部分內容就是請求頭。格式為key: value形式
-
- http是個無狀態的協議,所以在請求頭設置瀏覽器的一些自身信息和想要響應的形式。這樣服務器在收到信息后,就可以知道是誰,想干什么了
- 常見的HTTP請求頭有:
請求頭 | 含義 |
Host | 表示請求的主機名 |
User-Agent | 瀏覽器版本。 例如:Chrome瀏覽器的標識類似Mozilla/5.0 ...Chrome/79 ,IE瀏覽器的標識類似Mozilla/5.0 (Windows NT ...)like Gecko |
Accept | 表示瀏覽器能接收的資源類型,如text/*,image/*或者*/*表示所有; |
Accept-Language | 表示瀏覽器偏好的語言,服務器可以據此返回不同語言的網頁; |
Accept-Encoding | 表示瀏覽器可以支持的壓縮類型,例如gzip, deflate等。 |
Content-Type | 請求主體的數據類型 |
Content-Length | 數據主體的大小(單位:字節) |
舉例說明:服務端可以根據請求頭中的內容來獲取客戶端的相關信息,有了這些信息服務端就可以處理不同的業務需求。
比如:
- 不同瀏覽器解析HTML和CSS標簽的結果會有不一致,所以就會導致相同的代碼在不同的瀏覽器會出現不同的效果
- 服務端根據客戶端請求頭中的數據獲取到客戶端的瀏覽器類型,就可以根據不同的瀏覽器設置不同的代碼來達到一致的效果(這就是我們常說的瀏覽器兼容問題)
- 請求體 :存儲請求參數
-
- GET請求的請求參數在請求行中,故不需要設置請求體
POST方式的請求協議:
- 請求行(以上圖中紅色部分):包含請求方式、資源路徑、協議/版本
-
- 請求方式:POST
- 資源路徑:/brand
- 協議/版本:HTTP/1.1
- 請求頭(以上圖中黃色部分)
- 請求體(以上圖中綠色部分) :存儲請求參數
-
- 請求體和請求頭之間是有一個空行隔開(作用:用于標記請求頭結束)
GET請求和POST請求的區別:
GET方式的請求數據(參數)在url請求到服務器端的
區別方式 | GET請求 | POST請求 |
請求參數 | 請求參數在請求行中。<br/>例:/brand/findAll?name=OPPO&status=1 | 請求參數在請求體中 |
請求參數長度 | 請求參數長度有限制(瀏覽器不同限制也不同) | 請求參數長度沒有限制 |
安全性 | 安全性低。原因:請求參數暴露在瀏覽器地址欄中。 | 安全性相對高 |
2.2.2 獲取請求數據
Web服務器(Tomcat)對HTTP協議的請求數據進行解析,并進行了封裝,封裝到了一個對象當中(HttpServletRequest),并在調用Controller方法的時候傳遞給了該方法。這樣,就使得程序員不必直接對協議進行操作,讓Web開發更加便捷。
代碼演示如下:
@RestController
public class RequestController {/*** 請求路徑 http://localhost:8080/request?name=Tom&age=18* @param request* @return*/@RequestMapping("/request")public String request(HttpServletRequest request){//1.獲取請求參數 name, ageString name = request.getParameter("name");String age = request.getParameter("age");System.out.println("name = " + name + ", age = " + age);//2.獲取請求路徑String uri = request.getRequestURI();String url = request.getRequestURL().toString();System.out.println("uri = " + uri);System.out.println("url = " + url);//3.獲取請求方式String method = request.getMethod();System.out.println("method = " + method);//4.獲取請求頭String header = request.getHeader("User-Agent");System.out.println("header = " + header);return "request success";}}
最終輸出內容如下所示:
url: 完整的訪問路徑
uri:資源訪問路徑
2.3 HTTP響應協議
2.3.1 格式介紹
- 響應協議:服務器將數據以響應格式返回給瀏覽器。包括:響應行 、響應頭 、響應體
- 響應行(以上圖中紅色部分):響應數據的第一行。響應行由
協議及版本
、響應狀態碼
、狀態碼描述
組成
-
- 協議/版本:HTTP/1.1
- 響應狀態碼:200
- 狀態碼描述:OK
- 響應頭(以上圖中黃色部分):響應數據的第二行開始。格式為key:value形式
-
- http是個無狀態的協議,所以可以在請求頭和響應頭中設置一些信息和想要執行的動作,這樣,對方在收到信息后,就可以知道你是誰,你想干什么
- 常見的HTTP響應頭有:
Content-Type:表示該響應內容的類型,例如text/html,image/jpeg ;Content-Length:表示該響應內容的長度(字節數);Content-Encoding:表示該響應壓縮算法,例如gzip ;Cache-Control:指示客戶端應如何緩存,例如max-age=300表示可以最多緩存300秒 ;Set-Cookie: 告訴瀏覽器為當前頁面所在的域設置cookie ;
- 響應體(以上圖中綠色部分): 響應數據的最后一部分。存儲響應的數據
-
- 響應體和響應頭之間有一個空行隔開(作用:用于標記響應頭結束)
2.3.2 響應狀態碼
狀態碼分類 | 說明 |
1xx | 響應中 --- 臨時狀態碼。表示請求已經接受,告訴客戶端應該繼續請求或者如果已經完成則忽略 |
2xx | 成功 --- 表示請求已經被成功接收,處理已完成 |
3xx | 重定向 --- 重定向到其它地方,讓客戶端再發起一個請求以完成整個處理 |
4xx | 客戶端錯誤 --- 處理發生錯誤,責任在客戶端,如:客戶端的請求一個不存在的資源,客戶端未被授權,禁止訪問等 |
5xx | 服務器端錯誤 --- 處理發生錯誤,責任在服務端,如:服務端拋出異常,路由出錯,HTTP版本不支持等 |
關于響應狀態碼,我們先主要認識三個狀態碼,其余的等后期用到了再去掌握:
200 ok
客戶端請求成功404 Not Found
請求資源不存在500 Internal Server Error
服務端發生不可預期的錯誤
2.3.3 設置響應數據
Web服務器對HTTP協議的響應數據進行了封裝(HttpServletResponse),并在調用Controller方法的時候傳遞給了該方法。這樣,就使得程序員不必直接對協議進行操作,讓Web開發更加便捷。
代碼演示:
package com.itheima;import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;@RestController
public class ResponseController {@RequestMapping("/response")public void response(HttpServletResponse response) throws IOException {//1.設置響應狀態碼response.setStatus(401);//2.設置響應頭response.setHeader("name","itcast");//3.設置響應體response.setContentType("text/html;charset=utf-8");response.setCharacterEncoding("utf-8");response.getWriter().write("<h1>hello response</h1>");}@RequestMapping("/response2")public ResponseEntity<String> response2(HttpServletResponse response) throws IOException {return ResponseEntity.status(401).header("name","itcast").body("<h1>hello response</h1>");}}
瀏覽器訪問測試:
響應狀態碼 和 響應頭如果沒有特殊要求的話,通常不手動設定。服務器會根據請求處理的邏輯,自動設置響應狀態碼和響應頭。
3. SpringBootWeb案例
3.1 需求說明
需求:基于SpringBoot開發web程序,完成用戶列表的渲染展示
當在瀏覽器地址欄,訪問前端靜態頁面(http://localhost:8080/usre.html)后,在前端頁面上,會發送ajax請求,請求服務端(http://localhost:8080/list),服務端程序加載 user.txt 文件中的數據,讀取出來后最終給前端頁面響應json格式的數據,前端頁面再將數據渲染展示在表格中。
3.2 代碼實現
1). 準備工作:再創建一個SpringBoot工程,并勾選web依賴、lombok依賴。
2). 準備工作:引入資料中準備好的數據文件user.txt,以及static下的前端靜態頁面
這些文件,在提供的資料中,已經提供了直接導入進來即可。
3). 準備工作:定義封裝用戶信息的實體類。
在 com.itheima
下再定義一個包 pojo
,專門用來存放實體類。 在該包下定義一個實體類User:
package com.itheima.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;/*** 封裝用戶信息*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {private Integer id;private String username;private String password;private String name;private Integer age;private LocalDateTime updateTime;
}
3). 開發服務端程序,接收請求,讀取文本數據并響應
由于在案例中,需要讀取文本中的數據,并且還需要將對象轉為json格式,所以這里呢,我們在項目中再引入一個非常常用的工具包hutool。 然后調用里面的工具類,就可以非常方便快捷的完成業務操作。
pom.xml
中引入依賴
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.27</version>
</dependency>
- 在
com.itheima
包下新建一個子包controller
,在其中創建一個UserController
import cn.hutool.core.io.IoUtil;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONUtil;
import com.itheima.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;@RestController
public class UserController {@RequestMapping("/list")public String list(){//1.加載并讀取文件InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());//2.解析數據,封裝成對象 --> 集合List<User> userList = lines.stream().map(line -> {String[] parts = line.split(",");Integer id = Integer.parseInt(parts[0]);String username = parts[1];String password = parts[2];String name = parts[3];Integer age = Integer.parseInt(parts[4]);LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return new User(id, username, password, name, age, updateTime);}).collect(Collectors.toList());//3.響應數據//return JSONUtil.toJsonStr(userList, JSONConfig.create().setDateFormat("yyyy-MM-dd HH:mm:ss"));return userList;}}
java目錄和resources兩個目錄下的文件在編譯后最后會放在同一個目錄下面,放在class目錄下面,也就是放在類路徑下面
4). 啟動服務測試,訪問:http://localhost:8080/user.html
3.3 @ResponseBody
前面我們學習過HTTL協議的交互方式:請求響應模式(有請求就有響應)。那么Controller程序呢,除了接收請求外,還可以進行響應。
在我們前面所編寫的controller方法中,都已經設置了響應數據。
controller方法中的return的結果,怎么就可以響應給瀏覽器呢?
答案:使用@ResponseBody注解
@ResponseBody注解:
-》作用:將controller返回值直接作為響應體的數據直接響應;返回值是對象/集合->json->響應
- 類型:方法注解、類注解
- 位置:書寫在Controller方法上或類上
- 作用:將方法返回值直接響應給瀏覽器,如果返回值類型是實體對象/集合,將會轉換為JSON格式后在響應給瀏覽器
但是在我們所書寫的Controller中,只在類上添加了@RestController注解、方法添加了@RequestMapping注解,并沒有使用@ResponseBody注解,怎么給瀏覽器響應呢?
這是因為,我們在類上加了@RestController注解,而這個注解是由兩個注解組合起來的,分別是:@Controller 、@ResponseBody。 那也就意味著,我們在類上已經添加了@ResponseBody注解了,而一旦在類上加了@ResponseBody注解,就相當于該類所有的方法中都已經添加了@ResponseBody注解。
提示:前后端分離的項目中,一般直接在請求處理類上加@RestController注解,就無需在方法上加@ResponseBody注解了。
3.4 問題分析
上述案例的功能,我們雖然已經實現,但是呢,我們會發現案例中:解析文本文件中的數據,處理數據的邏輯代碼,給頁面響應的代碼全部都堆積在一起了,全部都寫在controller方法中了。
當前程序的這個業務邏輯還是比較簡單的,如果業務邏輯再稍微復雜一點,我們會看到Controller方法的代碼量就很大了。
- 當我們要修改操作數據部分的代碼,需要改動Controller
- 當我們要完善邏輯處理部分的代碼,需要改動Controller
- 當我們需要修改數據響應的代碼,還是需要改動Controller
這樣呢,就會造成我們整個工程代碼的復用性比較差,而且代碼難以維護。 那如何解決這個問題呢?其實在現在的開發中,有非常成熟的解決思路,那就是分層開發。