[SpringBoot項目回顧總結2-TouTiao]

通用模塊的開發流程

在這裏插入圖片描述

註冊

步驟

在這裏插入圖片描述
在這裏插入圖片描述
LoginCtroller下進行註冊的實現
model包下實現User類。dao下實現UserDAO,service包下UserService

  1. 首先調用userService.register(username, password),返回Map類的對象用於返回錯誤信息,如下。
    在這裏插入圖片描述

  2. 新建一個User對象,存放當前user的名字username,利用牛客網頭像庫隨機生成用戶一個初始頭像,並通過UUID給用戶密碼加salt然後再利用MD5進行進一步加密,使用戶的安全性得到保證。通過userDAO.addUser(user);添加到數據庫MySQL。
    在這裏插入圖片描述

  3. 註冊成功後下發ticket(即token),實現單點登錄(SSO)。返回map對象到Controller供使用。

  4. 用戶註冊成功後在Controller層通過Cookie記錄ticket並返回給客戶端,實現單點登錄。
    在這裏插入圖片描述

  5. 服務端利用攔截器的prehandle實現對ticket的驗證,以依賴注入的方式創建HostHolder類(表示當前用戶是誰),並通過HostHolder(利用ThreadLocal實現,通過線程本地變量存儲每個user,每個線程存儲各自的user,不同線程只能從中 get set remove 自己的變量,而不會影響其他線程的變量,即線程之間互不影響);
    在這裏插入圖片描述在這裏插入圖片描述
    每一條線程只能get當前線程的user。

  6. 並利用攔截器的postHandle實現在渲染前加入用戶信息,使得頁面出現用戶。渲染完成後調用afterCompletion清除該線程。
    關於這三種handle引用炎澤的博客鏈接

這裏是引用

在這裏插入圖片描述
攔截器通過configuration包下的ToutiaoWebConfiguration實現WebMvcConfigurer重寫addInterceptors實現。
在這裏插入圖片描述

總結登錄後進入主頁127.0.0.1:8080的步驟順序

  1. 進入攔截器的prehandle對用戶是否登錄進行驗證
  2. 執行攔截器的posthandle對ModelAndView加入用戶信息
  3. 渲染好首頁後打開
  4. 渲染完畢,清楚ThreadLocal中爲當前user創建的線程。

注意

  1. ticket(token)在註冊(reg)時是直接存入redis的,並且設置了過期時間。因此後續在客戶端對該用戶登錄,服務端對該客戶端的cookie中的ticket進行認證時直接從中間件redis中取出。這個項目原先是將ticket存在mysql中的,後來在回顧代碼總結時發現這樣效率很低,於是直接將ticket存儲在redis中提高效率
  2. 採用token而不採用session是因爲使用session對服務器有壓力,二是因爲不能抵禦CSRF(跨站請求僞造):用戶在訪問銀行網站時,他們很容易受到跨站請求僞造的攻擊,並且能夠被利用其訪問其他的網站。請求中發送token而不再是發送cookie能夠防止CSRF(跨站請求僞造)。即使在客戶端使用cookie存儲token,cookie也僅僅是一個存儲機制而不是用於認證。不將信息存儲在Session中,讓我們少了對session操作。
  3. 用戶每次登錄時都會重新隨機生成一個ticket並更新,然後下發給客戶端。這個Token的值必須是隨機的,不可預測的。由於Token的存在,攻擊者無法再構造一個帶有合法Token的請求實施CSRF攻擊。
    在這裏插入圖片描述
    在這裏插入圖片描述

登錄

登錄邏輯基本與註冊過程一致,唯一區別就是少了註冊過程對新用戶(user)通過dao層寫入數據庫。

退出

調用UserService的logout,刪除redis中緩存的該用戶的ticket,並返回(重定向)到主頁
在這裏插入圖片描述
在這裏插入圖片描述

用戶狀態權限的判斷

比如用戶要訪問/setting頁面的信息,我們明白,這個頁面只有登錄後才能訪問。所以新建一個攔截器LoginRequiredIntercepter,利用該攔截器的prehandle方法來實現該功能。

  1. 首先我們需要在webconfiguration下注冊這第二個攔截器,可以發現LoginRequiredIntercepter只對/setting頁面進行攔截,而上面用戶認證的攔截器是全局層面的攔截器。注意 攔截器代碼的順序即攔截器執行的優先級順序,即此處先執行對用戶登錄的認證,對hostHolder添加當前user,再執行第二個攔截器,如果此時登錄狀態,則hostholder中有user就會順利執行後面的程序,否則直接返回到/?pop=1頁面。
    在這裏插入圖片描述
  2. 重寫prehandle方法實現對未登錄用戶攔截
    在這裏插入圖片描述

階段1總結

在這裏插入圖片描述

  1. 當前傳輸是明文,可採用HTTPS協議,其還可以防止運營商加廣告。
    在這裏插入圖片描述
    比如訪問牛客的一個帖子,訪問的當前頁是1,鏈接中的參數page=1,當點到其他頁時比如第2頁,鏈接中的參數page=1並沒有更新,但是卻已經讀到了數據庫中page=2的內容.
    本項目中ajax直接在前端中裏用了

圖片上傳

在NewsController下進行

  1. 通過Spring的POST請求,利用二進制流上傳MultipartFile,調用newsService的savaImage方法保存上傳過來的圖片
    在這裏插入圖片描述
    newsService的savaImage方法如下
    在這裏插入圖片描述
  2. 利於POSTMAN在"/uploadImage/"頁面鏈接下上傳圖片,通過NewsService層的saveImg方法將圖片存儲在本地庫後將存儲地址返回給Controller層,然後利用JSON打印上傳信息(code,上傳失敗/服務端存儲圖片的鏈接地址fileUrl)
    在這裏插入圖片描述
    注意 HTTP通過header裏面的boundary實現將多個字段/文件進行區分
    在這裏插入圖片描述
    以及對於服務端存儲路徑的權限一定要保證(linux下)

圖片下載

  1. Controller層,利用Spring的GET請求,得到圖片的url即我們在上傳時生成的那個鏈接。設置Content type後直接複製服務端本地的圖片給response的二進制輸出流,是客戶端請求得到該圖片
    在這裏插入圖片描述
    圖片也可上傳到七牛雲服務器,但是現在服務器過期了,所以直接用本地存儲
    靜態獨立/CDN
    用戶請求圖片會根據離你最近的CDN節點分發數據,這樣靜態的文件如圖片訪問速度邊很快。
    在這裏插入圖片描述
    雲存儲的好處
    冗餘備份,統一訪問,CDN緩存同步,多機器併發

資訊詳情頁(news)

在這裏插入圖片描述

  1. 在CommentController下的newsDetail方法下實現資訊詳情頁的展示
    在這裏插入圖片描述

評論中心

  1. 字段設計,通過entity_id和entity_type的組合 實現各種評論的複用(即實現評論中心),如對資訊的評論,以及對資訊的評論的評論。
    在這裏插入圖片描述
  2. 在toutiao數據庫mysql中建表——comment,對entityid與type加聯合索引,在數據量大時提高查詢效率
    在這裏插入圖片描述
  3. 在model包下實現Comment類
  4. 在dao層實現CommentDAO,實現對數據庫的數據獲取,具體實現方法如下
    1)
  5. 在service層下實現相應功能,如下
    在這裏插入圖片描述
  6. 在controller層下,通過CommentController的newsDetail方法實現對新聞本身以及新聞評論的獲取。如下
    在這裏插入圖片描述
  7. 添加評論使用消息隊列實現
  8. 刪除評論通過增加標誌位status,默認爲0,爲1爲被刪除的狀態

消息中心(站內信)

主要包括兩個,一個是消息的列表,還有一個是具體與另一個user的消息詳情。

  1. 字段設計
    在這裏插入圖片描述
    在這裏插入圖片描述
  2. model中建立Message模型
  3. dao層創建MessageDAO,實現數據庫讀取,如下
    1)addMessage()方法實現對每條消息的添加功能
    2)getConversationList()方法實現會話l列表的獲取(當前user與其餘每個user(用戶)的會話的集合列表),並且會話是根據時間倒序的(最新發的消息對應的會話排在最上面),並且每個會話顯示的也都是該會話最新的一條消息。此處sql語句實現爲:先獲取與當前用戶有關的所有消息後按時間降序並作爲子表,後根據conversationId對子表進行group by分組。
    3)getMessageDetail()方法實現獲取單個會話的具體消息列表
    4)getUnReadCount()方法實現獲取某用戶的某個會話的未讀消息數量
    在這裏插入圖片描述
  4. service層實現MessageService,供Controller層調用
    在這裏插入圖片描述
  5. Controller層下新建MessageController:
    1)實現addMessage方法實現對消息的添加。在"/msg/addMessage"下通過Spring的POST請求,同時添加POST請求參數fromId,toId,content,實現對服務端POST message。服務端即在addMessage方法內部通過新建Message的模型model,將POST的參數傳入該模型對象,並進行相應字段的一系列初始化,實現一個消息Message的封裝,然後調用service層添加該消息存儲進入數據庫。實現消息的添加。
    在這裏插入圖片描述
    2)getMessageDetail()方法,在"/msg/detail"下通過Spring的GET請求,同時添加GET請求參數conversationId,實現對服務端GET 所請求的消息詳情。服務端即在getMessageDetail方法內部,通過調用service層即messageService.getMessageDetail()方法,獲取請求的conversationId的所有消息並分頁顯示。通過創建ViewObject的列表,將上一步獲取的所有消息全部加入該列表,並通過消息的fromId獲取並顯示該user的頭像。最後將該列表封裝進Model對象供前端獲取。實現消息詳情的顯示。
    在這裏插入圖片描述
    3)getMessageList()方法,在"/msg/list"下通過Spring的GET請求。首先,通過hostholder獲取當前user。如果user爲空,即沒有登錄,將客戶端重定向到主頁進行登錄;否則即用戶處於已登錄狀態,服務端通過調用service層即messageService.getConversationList()方法,獲取conversationList(會話列表)並分頁顯示,並且實現了對每個會話的未讀消息數量的顯示。
    在這裏插入圖片描述
    注意
    比如訪問當前用戶的會話頁,在進入控制層之前又要訪問攔截器,通過ticket驗證後又在hostholder裏新建當前user的線程。所以這纔是所謂的單點登錄(訪問不同頁面只需要登錄一次即可)!

點贊點踩

基於Redis實現
在這裏插入圖片描述

  1. util包下實現JedisAdapter類,用於實現redis的相關操作。該類實現InitializingBean接口,並重寫afterPropertiesSet()方法實現在JedisAdapter對象初始化後調用該方法把JedisPool初始化。代碼如下
    在這裏插入圖片描述
    在這裏插入圖片描述
  2. 因爲點贊點踩採用Redis實現,所以我們不需要寫DAO層,而是直接在上述創建的JedisAdapter類下,使用redis的相關操作將贊踩的信息存放到緩存裏。首先,我們明確 贊踩的信息是存放在集合裏面的,集合名爲entity名(資訊或者評論),存放的值爲userId ,這樣我們就能實現比如對某一資訊贊踩的記錄。所以我們首先在該類下創建一些redis對於集合的基本操作如下:
    1)給某一集合添加元素,爲service層實現某用戶給某條資訊點贊做基礎
    在這裏插入圖片描述
    2)刪除某集合內的某個元素,爲service層實現取消贊或者取消踩做準備。
    在這裏插入圖片描述
    3)判斷某一元素是否在某集合內,爲service層實現判斷當前用戶贊踩的狀態,從而常亮對應贊或踩的button,即實現更好的可視化效果,提高用戶體驗。
    在這裏插入圖片描述
    4)返回某一集合元素的數量,爲service層獲取贊/踩數做準備。在這裏插入圖片描述
  3. 在service層創建LikeService實現贊踩的服務。
    1)實現判斷某一用戶是否讚了某一條資訊,其中,集合名利用entityId和type組合實現key名字的獲取。
    在這裏插入圖片描述
    2)實現當前用戶hostholder對某一條資訊點贊
    在這裏插入圖片描述
    3)實現當前用戶對某一條資訊點踩
    在這裏插入圖片描述
  4. 在controller層下新建likeController實現贊踩添加路徑入口
    在這裏插入圖片描述
    在這裏插入圖片描述
  5. 在controller層的HomeController下的getNews()方法中,在對每一條資訊封裝到前端時,通過調用likeService.getLikeStatus()方法獲取當前用戶是否贊過該資訊,從而更新前端頁面對應的button是否常亮。
    在這裏插入圖片描述

異步事件處理框架

用處

如對某條資訊點贊後,可能會增加用戶的成就值,積分增加等等一系列操作,這時候就需要用到異步化來處理這一系列操作。即比如牛客的OJ,提交代碼後積分會增加等等會進行一系列服務,用戶並不需要提交後馬上看到積分增加,延遲一兩秒是可以接受的,所以我們通過異步隊列,即編程通過後,影響積分,排行榜等服務。
在這裏插入圖片描述
如上圖,Biz可以理解爲某業務部門,發送某事件後(如編程提交通過)進入事件隊列,然後業務部門就不用管了。有事件消費者來依次處理這些後續服務(排行榜、成就值等服務)。但是用戶要讀取後續修改後的值比如成就值可能會有一點點延遲,因爲這麼多事件的處理需要時間。

好處

  1. 假如後續事件處理(異步框架)的過程掛掉了,隊列如果存儲下來的話,重啓後仍可繼續執行未完成的服務。即使優先隊列掛了,無非就是數據滯後更新而已,不會影響主業務。
  2. 把複雜的業務處理流程切開,把需要及時返回的數據直接返回,對可以滯後更新的數據全部通過異步事件處理框架在後臺依次執行。

實現

  1. 隊列使用Redis實現,通過JedisAdapter類內部的setObject()方法和getObject()方法,實現緩存某個對象與通過key獲取該對象。緩存通過JSON實現,獲取通過JSON解析得到對應的對象。
    在這裏插入圖片描述
  2. 對於一個一個事件,就通過Redis,JSON的方式緩存到隊列,然後解析處理隊列中的事件
  3. 項目路徑下新建async包表示處理異步事件的框架實現,首先新建一個枚舉類EventType,表示發生的事件的類型
    在這裏插入圖片描述
  4. 新建EventModel類表示時間的模型,其中成員變量eventType表示事件類型,actorId表示事件觸發者,entityId和entityType組合表示觸發的對象,entityOwnerId表示這個對象的擁有者,Map<String,String>類型的成員變量exts儲存觸發現場需要保存的數據。
    對其中所有成員變量的set方法均返回當前EventModel對象,以供後續調用。
    在這裏插入圖片描述
  5. async包下新建EventProducer,使用註解@Service將該類作爲服務service,實現事件的生產。該類下fireEvent()方法生產事件,將事件序列化爲json串,添加到事件隊列中,事件隊列使用Redis的雙向列表。具體代碼如下:
    在這裏插入圖片描述
  6. async包下新建實現事件處理的接口EventHandler。其中void doHandler(EventModel model)方法爲處理事件的方法;getSupportEventTypes()方法返回該事件處理函數要處理的事件類型。
    在這裏插入圖片描述
  7. 在async包下新建handler包,該包下可新建多個實現了EventHandler接口的事件處理器Handler。
    1)首先新建LikeHandler並實現EventHandler接口中的方法doHandler()和getSupportEventTypes()。
    ① 因爲是在LikeHandler下,所以getSupportEventTypes()返回的事件類型爲EventType.LIKE。具體代碼如下:
    在這裏插入圖片描述
    ② doHandler()方法實現A用戶對B用戶發表的資訊(new)點贊後,B用戶會收到系統給B發的站內信,以提示B用戶收到點贊。
    在這裏插入圖片描述
  8. 實現EventConsumer,同樣使用註解將該類作爲service,實現事件的消費與處理。
    1)通過實現InitializingBean接口,並重寫afterPropertiesSet()方法,實現在bean初始化時記錄事件的相關信息(需要對所有Handler添加註解@Comment)。
    2)通過實現ApplicationContextAware接口獲取Spring容器ApplicationContext。然後在之前我們已經將所有Handler類都實現了@Coment注入進bean,所以此時可以使用applicationContext.getBeansOfType()方法獲取所有實現了EventHandler的類。
    3)建立一個一個類似路由表的Map<EventType, List< EventHandler >>,將所有事件類型及需要處理它的handler關聯來,放在一個map中。
    4)通過遍歷2)中得到的所有EventHandler類,使用entry.getValue().getSupportEventTypes()方法獲取每個EventHandler支持的事件類型的列表,遍歷當前的這個事件列表,將所有事件類型需要處理的handler添加到路由表map中對應事件類型的List< EventHandler >列表。
    在這裏插入圖片描述
    5)通過多線程處理每個事件需要處理的handler。創建匿名線程,阻塞地從Redis的隊列(雙向列表)右端彈出獲取事件列表,然後遍歷該事件列表,通過JSON解析得到每個事件的對象EventModel,接着對當前遍歷到的事件EventModel再嵌套循環遍歷4)得到的類似路由表的Map<EventType, List< EventHandler >>,得到對應事件需要處理的handler,然後依次執行handler.doHandler()方法處理事件。
    在這裏插入圖片描述
  9. 在controller層的likeController下的like()方法實現點贊事件的生產,實現點贊後對目標資訊的擁有者用戶發送站內信提示。在這裏插入圖片描述
  10. 在handler包下再實現LoginExceptionHandler,實現登錄IP異常的判斷(判斷邏輯複雜,本項目未實現,僅爲模擬異常登錄的情況),登錄異常後發送站內信即實現異步化處理,提高程序執行效率。該登錄事件在controller層下LoginController的login方法中發送給redis中的異步隊列。
    在這裏插入圖片描述
    在這裏插入圖片描述
  11. 在handler包下再實現RegisteHandler,通過MailSender類,實現用戶註冊後給該用戶的郵箱發送註冊成功的通知。該註冊事件在controller層下LoginController的reg方法中發送給redis中的異步隊列。代碼如下:
    在這裏插入圖片描述
    在這裏插入圖片描述
    註冊後效果如下
    在這裏插入圖片描述
  12. 在handler包下再實現CommentHandler,通過CommentService類添加評論,然後通過NewsService更新評論的數量。所以controller層CommentController下的addComment()方法實現的添加評論的功能全部由異步事件框架處理,這樣可以提高效率,代碼如下:
    在這裏插入圖片描述
    在這裏插入圖片描述
    擴展
    例如發送100封郵件,可以把每封郵件通過JSON序列化後,送到隊列中,這樣異步的執行發送郵件,可以提高效率。因爲發郵件也是很卡很慢的。

注意

  1. 使用Redis中的雙向列表作爲異步事件的處理隊列的原因之一是因爲其支持多臺服務器。

郵件發送

在util包下創建MailSender類,用@Service註解表示其爲service層的類。通過實現InitializingBean接口的afterPropertiesSet()方法,實現發送方的郵箱配置的初始化
在這裏插入圖片描述
通過FreeMarker定製化渲染出發送的郵件模板,比如註冊時發送的郵件簡單的模板例子如下:
在這裏插入圖片描述
設置發送郵件的標題,目標,內容等
在這裏插入圖片描述

單元測試

@Test表示跑一個測試用例
@Before初始化數據
@After清理數據
@BeforeClass在全局開始執行
@AfterClass在最後執行
在這裏插入圖片描述
對應註解的執行順序
@BeforeClass——>(@Before——>@Test——>@After)(第一個測試用例的執行)——>(@Before——>@Test——>@After)(第二個測試用例的執行)——>…——>(@Before——>@Test——>@After)(最後一個測試用例的執行)——>@AfterClass
測試用例通常使用 Assert.assertEquals() 等方法來測試
若要測試某用例本來就應該拋出異常,應在註解加額外配置,如利用IllegalArgumentException.class獲取IllegalArgumentException的Class對象。具體如下,這樣運行測試程序就不會報異常
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章