很抽象,我自己也不好理解,僅作為一個前端轉后端的個人理解
一、先解釋一個案例,以這個案例來分析“三層架構”
這里我先解釋一下黑馬程序員里的這個案例,兄弟們看視頻的可以跳過這節課:Day05-08. 請求響應-響應-案例_嗶哩嗶哩_bilibili?看我這里的解說就可以,因為他這里把前端的文件塞到后端這里弄,還要解析XML文件來獲取數據,完全沒必要,沒有任何地方會這樣做了,直接看我的解釋過一遍就行
首先他把一個寫了我們要的數據的XML引入到spring boot項目中,我們可以理解類似為前端有時會在一個js文件里寫一堆數據用于測試、模擬后端數據的(但實際后端應該是連接數據庫,數據是來源于數據庫的)
然后他的這個鬼XML文件里的數據里還不直接寫明白“性別”是(男/女),而是寫成(1/0),“職業”也是,非要寫成(1/2/3),然后還得到獲取數據的地方把(1/0)、(1/2/3)解析成(男/女)、(講師/班主任/....),這不是脫褲子放屁,多此一舉嗎
于是,他又去請求頁面,再解析獲取到XML數據后,再去進行以上這些邏輯處理......我們暫且理解為vue里的<script></script>這部分的邏輯處理吧
最后再把這些數據發送請求
(我只是為了方便各位理解!!不是一個東西啊!!)
那么結合前端我們可以理解為有點類似“表單數據傳送”,我們先獲取表單的所有數據,然后進行邏輯處理:拆分出數據然后封裝進一個對象,最后發送請求給后端
但是看看這一坨代碼這么亂......
二、三層架構是啥
那么分析一下一坨代碼,可以看出其實可以分成三大塊:
【數據訪問】:解析數據源,拿到數據
【邏輯處理】:把數據抽出來進行一些邏輯處理
【接收請求、響應數據】:把數據封裝好給回前端
那么我們應該把這三塊分開來寫,這樣的話,那一部分出問題可以直接找到這一部分查看問題
因此人們就規定應該分成這三塊:
這三塊要做的事:
-
Controller:控制層。接收前端發送的請求,對請求進行處理,并響應數據。
-
Service:業務邏輯層。處理具體的業務邏輯。
-
Dao:數據訪問層(Data Access Object),也稱為持久層。負責數據訪問操作,包括數據的增、刪、改、查。
那么這個三層架構的執行流程是:
1、前端發起的請求,由Controller層接收(Controller響應數據給前端)
2、Controller層調用Service層來進行邏輯處理(Service層處理完后,把處理結果返回給Controller層)
3、Serivce層調用Dao層(邏輯處理過程中需要用到的一些數據要從Dao層獲取)
4、Dao層操作文件中的數據(Dao拿到的數據會返回給Service層)
好,那么與之相反的,我們開發人員寫代碼的流程是:(切記!不要去管這里代碼寫了什么,過一遍流程就行!!!這里的代碼不值得去花時間學習)
1、在【請求處理類】同級目錄下創建一個【dao】目錄
2、然后在【dao】目錄下創建一個代表該獲取的數據的接口(java里的接口,不是請求數據的那個接口)
然后創建這個接口的實現類(連同包含實現類的文件夾目錄一塊),implements實現這個類,然后把獲取數據、解析數據的內容放到這里實現
記得把數據return出去
3、然后同樣的流程【創建service目錄】——>【創建代表數據的接口】——>【然后創建(連文件夾包一起)實現這個接口的實現類】——>【記得先創建并調用Dao的實現類對象,因為現在數據在Dao那】——>【然后service的實現類的內容就是處理數據的邏輯】
4、最后,創建【controller】目錄,把一個【請求處理類】放到【controller目錄】——>【然后現在完整、處理好的數據源在service實現類】——>【在方法外面創建service的實現類對象】——>【然后方法里調用service實現類對象,拿到數據】——>【然后通過請求return出去,響應給前端】
總結就是:
開發視角:數據源從外部到Dao——>然后我們要用service獲取Dao的數據,然后處理——>然后Controller最后獲取service處理好的數據,響應回前端
前端視角:發送請求給到Controller——>Controller找service要——>service找Dao要
三、分層解耦
1、理解耦合是什么?
耦合問題
首先需要了解軟件開發涉及到的兩個概念:內聚和耦合。
內聚:通俗講就是各功能功能模塊跟自身功能聯系
比如【Controller】的功能是接受請求、實現請求接口、響應回前端,【service】的功能是處理數據、做邏輯處理,【Dao】功能就是解析數據
那么這三塊分開,各自里面的內容是各自的功能實現,這就是【內聚高】,互不影響
那么如果不分開,這三個內容的代碼全堆【Controller】那里,那么【Controller】本來功能只用實現請求、響應前端的,現在當黑奴,一人干幾份活,這就是【內聚低】
耦合:衡量軟件中各個層/模塊之間的依賴、關聯的程度。
比如【Controller】接收到前端請求后,找數據需要通過調用【service】來獲取數據,而【service】找數據也得靠調用【Dao】來獲取數據,這種聯系就叫【耦合】
當他們是三個部門,當少了一方部門、或者其中一個部門要改名字啥的,會牽連到三個部門,這就是【耦合高】
但是軟件工程開發要求【內聚低耦合高】
那么就要分層解耦
2、分層解耦是什么?
那么現在我們要解耦,該怎么辦?
我們思考一下【Controller】調用的是什么?是【service的實現類】。
那么【Controller】調用的時候其實不管你是叫【service_A】還是【service_B】,只要是implement實現了【service】這個接口的實現類,【Controller】都需要
那么就可以有這么一個外部【容器】,他裝有實現了【service】接口的【實現類】,當【Controller】需要的時候就去【容器】找有沒有,有的話就拿去用
(我們理解為“華為公司”需要招聘員工,不管你是清華學子、還是北大學子,只要你是實現、集合了“985/211重本院校”的學生,他都要。那么我們就可以設一個“人才市場”,里面有各個[實現、集合了“985/211重本院校”的學生],華為公司需要誰,就去人才市場要)
這里還有幾個概念要知道:
控制反轉: Inversion Of Control,簡稱IOC。對象的創建控制權由程序自身轉移到外部(容器),這種思想稱為控制反轉。(通俗理解給自己簽訂賣身契,賣到人口市場,隨時被人交易)
依賴注入: Dependency Injection,簡稱DI。容器為應用程序提供運行時,所依賴的資源,稱之為依賴注入。(通俗理解為買家跟人口市場綁定了,有符合要求的就自動買了)
IOC容器中創建、管理的對象,稱之為:bean對象 (理解為人口市場的奴隸hh)
四、IOC控制反轉 + DI注入依賴
1、首先先把【Controller】【service】【Dao】之間的聯系刪掉
(理解為華為公司取消了跟某些高校的強制合作,不要固定那幾個高校推薦內推的學生,選擇讓人才市場給自己分配員工)
2、然后開始【控制反轉】
給【service】和【Dao】加上容器注解【@Component】
加上它,就意味著要將當前類“交給”IOC容器管理,成為IOC容器里的一個bean對象
(可以理解為高材生把自己的簡歷投放到人才市場了,由人才市場保管自己的簡歷)
3、然后開始【依賴注入】
要將【Controller】綁定上【依賴注入】,加上注解【@Autowired】
加上【@Autowired】就意味著運行時,IOC容器會提供該類型的bean對象,并賦值給該變量
(理解為華為公司把自己的HR丟人才市場,任由HR發揮,只要符合要求就直接招進公司)
4、那如果要更換bean對象怎么辦?
加入我有一個【service】實現類【A】,我現在不要用它了,我要改成【B】怎么辦?
把【A】的【@Component】注釋掉,在【B】上面加上【@Component】就行了......就這么簡單
五、Bean另外的衍生注解
注解 | 說明 | 位置 |
@Component | 聲明bean的基礎注解 | 不屬于以下三類時,用此注解 |
@Controller? | @Component?的衍生注解 | 標注在控制器類上 |
@Service | @Component的衍生注解 | 標注在業務類上 |
@Repository | @Component的衍生注解 | 標注在數據訪問類上 (由于與mybatis整合,用的少) |
是啥個意思呢?
意思是【控制反轉】有的時候不一定要用【@Component】,其實【Controller】、【Service】、【Dao】的【控制反轉】可以分別用【@Controller】、【@Service】、【@Repository】來注解
然后因為其實【@Service】、【@Repository】的源代碼里其實是包含了【@Component】的,所以用【@Service】、【@Repository】就等于用【@Component】
但是spring boot的web開發里,【Controller】控制器不能用【Component】,不過它除了注解【@Controller】,我們之前講過【@RestController】也包含了【@Controller】,所以用【@ReastController】就可以了
六、主包(主目錄)外面的包的Bean對象掃描不到
問題:使用前面學習的四個注解聲明的bean,一定會生效嗎?
答案:不一定。(原因:bean想要生效,還需要被組件掃描,比如你主包外面的bean對象就不會生效)
那就要用到組件掃描:【@ComponentScan】
但是【@ComponentScan】并不是亂用的,首先回到我們的【啟動類】
我們知道【@SpringBootApplication】是【啟動類】注解,但其實他還是包含了【@ComponentScan】的組件掃描注解,它可以掃描【當前包以及其自包里的所有的Bean】
但是要是Bean在主包的外面怎么辦?
那這是才只能手動寫【@ComponentScan】來導入要掃描的“包(目錄)”,并且還得帶上原本當前這個“包(目錄)”,因為會覆蓋下面的【@SpringBootApplication】
具體語法是:
@ComponentScan( { "外部含Bean的那個包" , "當前包" } )
但其實不建議這樣,一般情況推薦全部Bean都放到啟動類的包下,方便全局掃描
七、DI注入依賴選擇一個Bean來注入
我們前面說過,如果想要注入依賴的時候,容器里有兩個符合條件的實現類,只選擇其中一個的話,就把另一個實現類的【@Component】注釋掉
但是如果我容器里有成千上萬個符合條件的【實現類Bean】怎么辦?難道我要全部一個一個去注釋嗎?
比如下面這個例子,我有兩個都寫了【@Sevice】(怕忘了,再提醒一下這個也等于@Component)的【service實現類Bean】
然后此時再運行時就會報錯,因為超過兩個同類型的實現類Bean
這是因為注入依賴里的注解【@Autowired】,它是根據類型來自動獲取容器里所有的符合的實現類Bean的,那么容器里有兩個就拿兩個,有十個就拿十個,而一旦超過了一個Bean就會報錯
那么解決辦法有三個
1、(不太推薦)@Primary:在要希望生效的那個Bean上添加這個注解
2、(可以)@Qualifier:在要注入依賴的地方、@Autowired前面指定你要哪一個Bean
3、(非常推薦)@Resouce:直接在注入依賴的地方,去掉@Autowired,替換成這個并指定要哪個Bean
1、@Primary是在要希望生效的那個Bean上添加這個注解,不推薦是因為跟注釋【@Component】其實是半斤八兩的啊.......你還得去一個一個找到這個Bean,然后再Bean的代碼里改
2、@Qualifier就是在要注入依賴的地方、@Autowired前面指定你要哪一個Bean,這樣確實很方便,直接指明要誰,美中不足就是,我感覺跟@Autowired有點重復,兩在一塊總感覺有點多余
另外解釋為什么【實現類EmpServiceA】在@Qualifier這要寫成【empServiceA】,因為我前面懶,省略了講:Bean對象的名字其實是【實現類】名字的首字母小寫......而不是它原本的實現類的那個名字
3、@Resouce為甚推薦?因為它是直接在注入依賴的地方,去掉@Autowired,替換成@Resouce,并寫上指定要哪個Bean的名字就行
為什么它可以直接不要【@Autowired】,因為【@Resouce】是根據名字來注入Bean的!!
另外【@Primary】跟【@Qualifier】是spring boot框架提供的,而【@Resouce】是Java的JDK提供的