黑馬點評--基于Redis實現共享session登錄

集群的session共享問題分析

session共享問題:多臺Tomcat無法共享session存儲空間,當請求切換到不同Tomcat服務時,原來存儲在一臺Tomcat服務中的數據,在其他Tomcat中是看不到的,這就導致了導致數據丟失的問題。

雖然系統為單體式的架構,但是為了將來應對并發要做水平擴展,部署多個形成負載均衡的集群。

當請求進入nignx會在多臺Tomcat之間做一個輪詢,每一個Tomcat都有自己的session空間,假設用戶請求第一次被負載均衡到了Tomcat1,去發送驗證碼或者登錄時所獲取到的用戶信息僅僅是保存到這一臺Tomcat里了,當用戶第二次來執行某些業務被負載均衡到了第二臺Tomcat服務上,當該服務要去獲取驗證碼或者用戶信息時,而他自身的session內存空間空無一物,這時服務就會中斷。

這就是session共享問題。

解決方案:

在初期,為了解決session共享問題,官方提供了session拷貝功能,多臺Tomcat之間只要做好一些配置,它們之間就可以互相實現一個數據拷貝,但數據拷貝也有不小的問題,首先就是多臺Tomcat保存相同的數據,浪費內存空間,其次拷貝數據是需要時間的,這就造成了時間延遲,在這個延遲之間如果有服務來訪問,依舊會造成數據不一致的問題。問題太多,該方案就被pass了。

因此就必須尋找到可以替代session的方案,且必須滿足:

  • 數據共享

  • 內存存儲(因為session是基于內存的,讀寫效率較高,像登錄校驗這種業務訪問頻率較高,需要滿足高并發的需求)

  • 鍵值對結構(session存儲較為簡單)

這就是Redis,首先redis是在Tomcat以外的一個存儲方案,,任意一臺Tomcat都能訪問到Redis,所以就可以實現數據共享了,就不會出現數據丟失的情況 其次Redis就是存儲在內存中的,而且性能非常強,并且redis就是鍵值對類型的數據庫,因此我們是可以用redis來代替session。

基于Redis實現共享session登錄
業務流程

如果要使用Redis來代替session,那么前面的短信登錄業務也有相應的變化。

比如在發送驗證碼的業務流程中,需要將驗證碼存入redis,并且還需要考慮value是什么類型的數據結構,存入驗證碼時就可以直接用String類型即可。

而key類型就不能和原先一致,因為session在發送請求的時候都有一個獨立的session,在Tomcat服務中維護了很多session,那么不同瀏覽器攜帶的手機號請求服務時都有著自己獨立的session,這些服務都是使用code作為key,但是互相之間互不干擾。

在使用session時,不需要考慮取數據的問題,因為Tomcat會自動的幫助我們去維護session:瀏覽器發去請求時,Tomcat就為瀏覽器新建一個session,如果session存在,直接使用即可,在創建session時,就會自動創建sessionID寫入到對應瀏覽器的cookie中,以后瀏覽器的每次請求就會帶著cookie,帶著sessionID,這樣就能精確找到對應的session,也不用去考慮取的問題。

但redis是一個共享的內存空間,不管是哪個服務發請求,都是往redis的內存空間存儲的,如果每個服務使用的key都是code,就會相互覆蓋,就會造成數據丟失。而必須要確保每次不同服務訪問的key都不同。

那既然如此,那就直接將手機號碼作為每個服務的key,這樣就能保證每個服務都有自己不同的key,這樣的操作也有助于將來我們去獲取驗證碼進行驗證。

image-20250524144322613

現在要使用redis,沒有維護,現在以手機為key存入進去,那瀏覽器做登錄時還需要帶著這個key的值來取,才可以驗證。

而在等短信驗證碼登錄注冊時,需要將手機號碼與驗證碼存入,正好可以根據手機號碼去拿到value。

再去根據手機號查詢用戶,如果用戶存在,則將用戶存入redis中。

此時需要考慮兩個問題,一是value的數據類型選擇問題,二是考慮key的命名,

在短信登錄業務存入的是Java對象,那么redis的value雖然可以使用string類型,用json字符串保存,比較直觀,但是無法針對單個字段作出修改,只能修改整個字段。

這時可以使用hash類型,hash結構可以將對象中的每個字段獨立存儲,可以針對單個字段做修改,并且內存占用更少(Hash結構只需要保存數據本身即可,但是String類型還需要保存json字符串的格式)。

如果從優化角度來看,比較推薦Hash結構。

而key的要求也有兩點:一是唯一性,二是較為便攜。

這里推薦使用隨機的token(隨機字符串,可以使用UUID來生成)作為key存儲用戶數據。

而在登錄校驗這一業務中,以前使用session時的登錄憑證就是sessionID,被存在瀏覽器的cookie中被一直攜帶,且一直被Tomcat維護。

而現在使用的redis來代替session,則我們使用的隨機token則是登錄憑證,也就意味著以后瀏覽器來訪問我們需要攜帶token將其作為憑證。而Tomcat不會將其自動的寫入瀏覽器中,我們需要手動的將其返回前端,那么此處流程就產生了變化。

image-20250524142713311

那當服務器拿到token之后,我們就可以基于token來從redis獲取用戶信息,剩下校驗登錄狀態流程就不變

image-20250524142915111

而登錄憑證是通過前端的邏輯代碼進行接收并保留的,在前端使用axios的攔截器,利用攔截器將用戶token放在“authorization”頭,這樣每一條用戶請求就會攜帶token。如果我們使用手機號碼作為key去保存,將來返回到前端直接保存在瀏覽器會有泄漏的風險。這就是我們key不能再次使用手機號碼的原因。

代碼修改

發送驗證碼

?@Overridepublic Result sendCode(String phone, HttpSession session) {//1.校驗手機號if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤");}//3.如果符合,生成驗證碼String code = RandomUtil.randomNumbers(6);//4.保存驗證碼到redis 還需要給驗證碼設置有效時間 set key value ex 120//一般都會定義一個工具類來保存常量,避免重復及手誤stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//5.發送驗證碼(模擬發送驗證碼,該業務并未實現)log.debug("發送短信驗證碼成功,驗證碼:{}",code);// 6.返回結果return Result.ok();}

短信驗證碼登錄注冊

?@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校驗手機號String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,返回錯誤信息return Result.fail("手機號格式錯誤");}//2.從Redis中獲取驗證碼并校驗String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();// 一般校驗時,從反向校驗,這種校驗不需要if嵌套,否則會嵌套if,避免if嵌套過深if (cacheCode == null || !cacheCode.equals(code)){//3.不一致,返回錯誤信息return Result.fail("驗證碼錯誤");}//4.一致,根據手機號查詢用戶 select ? * from user where phone = ?User user = query().eq("phone", phone).one();//5.判斷用戶是否存在if (user == null){//6.不存在,創建新用戶并保存// 方法定義在函數中 創建用戶//在創建完用戶到數據庫后還需要保存在session中,所以直接賦值給useruser = createUserWithPhone(phone);}//7.保存用戶信息到redis,//7.1隨機生成token,作為登錄令牌String token = UUID.randomUUID().toString(true);//7.2將user對象轉換為hash存儲UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);//7.3存儲到redis中 利用工具類將userDTO轉為mapstringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,BeanUtil.beanToMap(userDTO));// 7.4設置token有效期stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);//7.4返回token給客戶端return Result.ok(token);}

問題提出:

在之前使用session時,session的有效期是30分鐘,但session的有效期是指只要一直在訪問session,那么session的有效期就一直是30分鐘,只有超過30分鐘不訪問session,session才會失效。

但是redis的有效時間就是從新建到移除的時間,不在乎是否訪問,這樣就有弊端。

應該像session一樣,只要用戶在訪問,就應該更新有效時間(即Redis中的token有效期),但是Redis無法得知用戶有沒有訪問服務端,也無法得知用戶何時訪問服務端。

而在登錄攔截校驗中,我們所有的請求訪問時都要經過攔截器的攔截與校驗,只要經過了這個校驗,就能證明該瀏覽器是一個正在活躍著的用戶,這是我們就可以更新redis的有效期。

這樣就可以做到和session一樣的效果,只要有瀏覽器訪問服務端,那么Redis就會去更新token的有效期。所以在接下來修改登錄攔截校驗代碼時還需要添加更新token有效期的邏輯。

代碼如下:

?public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// 前置攔截器@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的tokenString token = request.getHeader("authorization");// 判斷是否存在if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}//  2.以token為key獲取redis的用戶Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);//  3.判斷用戶是否存在if (userMap.isEmpty()) {//  4.不存在 攔截器攔截 返回401狀態碼 未授權response.setStatus(401);return false;}//5.將查詢到的Hash數據轉換為UserDTO對象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//  6.存在,保存用戶信息到ThreadLocal// 在工具類中定義了一個UserHolder 是一個線程安全的ThreadLocal變量,用于保存當前線程的用戶信息。// 其中有三個方法:saveUser( 保存),getUser(拿到),removeUser(移除)。UserHolder.saveUser(userDTO);//7.刷新token有效期stringRedisTemplate.expire(LOGIN_USER_KEY+token, LOGIN_USER_TTL, TimeUnit.MINUTES);//8.放行return true;}?//  攔截器 后處理@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//  移除用戶 避免用戶泄漏UserHolder.removeUser();}}

修改成功,

測試結果:

報錯,類強制轉換異常ClassCastException,redis serializer報錯 long類型不能轉換成String類型,在UserServiceImpl中向redis存入用戶信息時報錯,userMap來自userDTO,userDTO中的id為long類型,無法存儲到redis中去,因為我們使用的RedisTemplate為StringRedisTemplate,他要求key與value都必須是string類型,但userMap中的id為long類型,因此報錯。

解決方案:

  • 自己在重寫toMap函數,在將userDTO轉換成userMap時將值的類型轉換成string字符串

  • 提供的工具類有自定義的功能,如下所示

?Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));

再次測試:

image-20250524200915128

解決狀態登錄刷新的問題

問題引入:

目前的短信登錄攔截器無法做到只要用戶一直登陸就不會過期。

因為攔截器攔截的不是一切路徑,而是那些需要登錄校驗的路徑,比如user/me,或者將來用戶的下單,支付這樣一些對用戶信息有需求的路徑,或者說被攔截器攔截的路徑,但攔截器并不是攔截一切。

如果用戶一直訪問的是不需要登錄的頁面,比如首頁或者商戶詳情頁,這些不需要登錄校驗就可以看,這些就不會去刷新有效期,過了指定的有效期后,即使用戶還在訪問,但token就會被移除,問題因此出現。

解決方案:

在原有攔截器的基礎上再加上一個攔截器,這樣用戶請求就要先經過第一個攔截器,在經過第二個,第一個攔截器攔截全部路徑,所有請求都會被攔截,就可以在這個攔截器中做刷新token有效期的業務(獲取token,查詢Redis用戶,保存到ThreadLocal中,刷新token有效期,放行),第一個攔截器不做攔截,這樣就可以確保一切請求都可以觸發刷新的動作,第二個攔截器只需要做攔截業務(查詢ThreadLocal的用戶,不存在則攔截,存在,則繼續)即可

代碼展示:

LoginInterceptor.java

?public class LoginInterceptor implements HandlerInterceptor {// 前置攔截器@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.判斷是否需要攔截( ThreadLocal中是否有用戶)if (UserHolder.getUser() == null) {//  2.沒有,攔截,返回401response.setStatus(401);return false;} else {// 有用戶,放行return true;}}

RefreshTokenInterceptor.java

?public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;?public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}?// 前置攔截器@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的tokenString token = request.getHeader("authorization");// 判斷是否存在if (StrUtil.isBlank(token)) {return true;}//  2.以token為key獲取redis的用戶Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);//  3.判斷用戶是否存在if (userMap.isEmpty()) {//  4.不存在 攔截器攔截 返回401狀態碼 未授權return true;}//5.將查詢到的Hash數據轉換為UserDTO對象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//  6.存在,保存用戶信息到ThreadLocal// 在工具類中定義了一個UserHolder 是一個線程安全的ThreadLocal變量,用于保存當前線程的用戶信息。// 其中有三個方法:saveUser( 保存),getUser(拿到),removeUser(移除)。UserHolder.saveUser(userDTO);//7.刷新token有效期stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.SECONDS);//8.放行return true;}?//  攔截器 后處理@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//  移除用戶 避免用戶泄漏UserHolder.removeUser();}}

裝配攔截器:

?
@Configurationpublic class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//  登錄攔截器registry.addInterceptor(new LoginInterceptor())//  除了這些路徑,其他路徑都進行攔截.excludePathPatterns("/user/code","/user/login","/blog/hot","/voucher/**","/shop/**","/shop-type/**","/upload/**","/blog/query/hot","/druid/**").order(1);// token刷新攔截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}}

如何控制攔截器的前后順序:在源碼中order的值越大,優先級越低,值越小,優先級越高。

測試成功,無論訪問哪個頁面,都會刷新token的有效期。

至此,基于Redis實現共享session登錄的業務完成。

總結:基于Redis改造短信登錄,改造的點如下:

  • 發送短信驗證碼時將驗證碼存入redis中,key使用的是手機號碼,value的類型為String

  • 短信登錄時保存用戶到Redis,key要保證唯一以及便攜,因此將key設為了UUID,放在了前端的請求頭中,返回給了用戶,保存到瀏覽器中,這樣一來瀏覽器可以攜帶token來訪問服務端,從而實現登陸的效果。

注意事項:

  • 在使用redis存儲數據的時候,key的規范非常重要。還有數據類型的選擇,code選擇String類型,而用戶選擇了Hash類型。

  • 我們在存儲數據的過程中,要記得設置存儲有效期。

  • 要選擇合適的存儲粒度,我們并沒有存儲完整的用戶信息,而是將一些敏感信息給去掉了,只保存一些不太敏感、頁面需要的數據,這樣還可以節省內存空間

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/82396.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/82396.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/82396.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SkyWalking啟動失敗:OpenSearch分片數量達到上限的完美解決方案

?? 問題現象 SkyWalking OAP服務啟動時報錯: org.apache.skywalking.oap.server.library.module.ModuleStartException: java.lang.RuntimeException: {"error":{"root_cause":[{"type":"validation_exception", "reason&q…

向量數據庫選型實戰指南:Milvus架構深度解析與技術對比

導讀&#xff1a;隨著大語言模型和AI應用的快速普及&#xff0c;傳統數據庫在處理高維向量數據時面臨的性能瓶頸日益凸顯。當文檔經過嵌入模型處理生成768到1536維的向量后&#xff0c;傳統B-Tree索引的檢索效率會出現顯著下降&#xff0c;而現代應用對毫秒級響應的嚴苛要求使得…

MySQL#秘籍#一條SQL語句執行時間以及資源分析

背景 一條 SQL 語句的執行完&#xff0c;每個模塊耗時&#xff0c;不同資源(CPU/IO/IPC/SWAP)消耗情況我該如何知道呢&#xff1f;別慌俺有 - MySQL profiling 1. SQL語句執行前 - 開啟profiling -- profiling (0-關閉 1-開啟) -- 或者&#xff1a;show variables like prof…

【數據結構】實現方式、應用場景與優缺點的系統總結

以下是編程中常見的數據結構及其實現方式、應用場景與優缺點的系統總結&#xff1a; 一、線性數據結構 1. 數組 (Array) 定義&#xff1a;連續內存空間存儲相同類型元素。實現方式&#xff1a;int[] arr new int[10]; // Javaarr [0] * 10 # Python操作&#xff1a; 訪問&…

PyTorch中cdist和sum函數使用示例詳解

以下是PyTorch中cdist與sum函數的聯合使用詳解: 1. cdist函數解析 功能:計算兩個張量間的成對距離矩陣 輸入格式: X1:形狀為(B, P, M)的張量X2:形狀為(B, R, M)的張量p:距離類型(默認2表示歐式距離)輸出:形狀為(B, P, R)的距離矩陣,其中元素 d i j d_{ij} dij?表示…

Ansible配置文件常用選項詳解

Ansible 的配置文件采用 INI 格式&#xff0c;分為多個模塊&#xff0c;每個模塊包含特定功能的配置參數。 以下是ansible.cfg配置文件中對各部分的詳細解析&#xff1a; [defaults]&#xff08;全局默認配置&#xff09; inventory 指定主機清單文件路徑&#xff0c;默認值為 …

了解FTP搜索引擎

根據資料&#xff0c; FTP搜索引擎是專門搜集匿名FTP服務器提供的目錄列表&#xff0c;并向用戶提供文件信息的網站&#xff1b; FTP搜索引擎專門針對FTP服務器上的文件進行搜索&#xff1b; 就是它的搜索結果是一些FTP資源&#xff1b; 知名的FTP搜索引擎如下&#xff0c; …

【大模型面試每日一題】Day 28:AdamW 相比 Adam 的核心改進是什么?

【大模型面試每日一題】Day 28&#xff1a;AdamW 相比 Adam 的核心改進是什么&#xff1f; &#x1f4cc; 題目重現 &#x1f31f;&#x1f31f; 面試官&#xff1a;AdamW 相比 Adam 的核心改進是什么&#xff1f; #mermaid-svg-BJoVHwvOm7TY1VkZ {font-family:"trebuch…

C++系統IO

C系統IO 頭文件的使用 1.使用系統IO必須包含相應的頭文件&#xff0c;通常使用#include預處理指令。 2.頭文件中包含了若干變量的聲明&#xff0c;用于實現系統IO。 3.頭文件的引用方式有雙引號和尖括號兩種&#xff0c;區別在于查找路徑的不同。 4.C標準庫提供的頭文件通常沒…

多模態理解大模型高性能優化丨前沿多模態模型開發與應用實戰第七期

一、引言 在前序課程中&#xff0c;我們系統剖析了多模態理解大模型&#xff08;Qwen2.5-VL、DeepSeek-VL2&#xff09;的架構設計。鑒于此類模型訓練需消耗千卡級算力與TB級數據&#xff0c;實際應用中絕大多數的用戶場景均圍繞推理部署展開&#xff0c;模型推理的效率影響著…

各個網絡協議的依賴關系

網絡協議的依賴關系 學習網絡協議之間的依賴關系具有多方面重要作用&#xff0c;具體如下&#xff1a; 幫助理解網絡工作原理 - 整體流程明晰&#xff1a;網絡協議分層且相互依賴&#xff0c;如TCP/IP協議族&#xff0c;應用層協議依賴傳輸層的TCP或UDP協議來傳輸數據&#…

11.8 LangGraph生產級AI Agent開發:從節點定義到高并發架構的終極指南

使用 LangGraph 構建生產級 AI Agent:LangGraph 節點與邊的實現 關鍵詞:LangGraph 節點定義, 條件邊實現, 狀態管理, 多會話控制, 生產級 Agent 架構 1. LangGraph 核心設計解析 LangGraph 通過圖結構抽象復雜 AI 工作流,其核心要素構成如下表所示: 組件作用描述代碼對應…

相機--基礎

在機器人開發領域&#xff0c;相機種類很多&#xff0c;作為一個機器人領域的開發人員&#xff0c;我們需要清楚幾個問題&#xff1a; 1&#xff0c;相機的種類有哪些&#xff1f; 2&#xff0c;各種相機的功能&#xff0c;使用場景&#xff1f; 3&#xff0c;需要使用的相機…

【備忘】 windows 11安裝 AdGuardHome,實現開機自啟,使用 DoH

windows 11安裝 AdGuardHome&#xff0c;實現開機自啟&#xff0c;使用 DoH 下載 AdGuardHome解壓 AdGuardHome啟動 AdGuard Home設置 AdGuardHome設置開機自啟安裝 NSSM設置開機自啟重啟電腦后我們可以訪問 **http://127.0.0.1/** 設置使用 AdGuardHome DNS 效果圖 下載 AdGua…

安裝部署配置jenkins

隨著現代軟件開發流程的不斷演進,持續集成(CI)和持續交付(CD)已經成為了開發團隊必不可少的工具。而Jenkins作為最為廣泛應用的CI/CD工具,能夠自動化執行構建、測試、部署等任務。Maven作為Java生態中廣泛使用的構建工具,它能夠幫助開發人員自動化管理項目的構建、依賴和…

How to balance work and personal life?

How to balance work and personal life? 1. Background2. How to balance work and personal life?References 1. Background Let me introduce /??ntr??dju?s/ the background /?bkɡra?nd/ first. Today we will talk about this topic: How to balance work and …

存儲引擎系列--LSM的Compaction研究方法論

本文主要包含以下內容: 1、Compaction 設計空間的四個原語:觸發器、數據布局、壓縮粒度、數據移動策略。任何已有的compaction策略和新的策略都可以由這個四個原語組建構成。 2、詳細介紹這四個原語的定義,策略方法 3、現有的基于LSM的知名系統的compaction策略按照四個原語…

關系數據庫基礎入門

關系數據庫概述 相關名詞 1、關系&#xff1a;在關系數據庫中&#xff0c;實體以及實體間的聯系都是用關系來表示的。類似于程序設計語言中變量的概念。 2、關系模式&#xff1a;是對關系的描述。類似于程序設計語言中類型定義的概念。 3、關系模型&#xff1a;是由若干個關系…

圖解BERT

圖解 Bert 大家可以訪問 圖解Bert 獲取更加優質的閱讀體驗。 圖解BERT一文還在持續更新中。 環境搭建 按序執行以下命令完成環境搭建: git clone https://github.com/DA-southampton/Read_Bert_Code.git cd Read_Bert_Code conda create -n Read_Bert_Code python3.9.22 co…

【HarmonyOS 5】鴻蒙中的UIAbility詳解(一)

【HarmonyOS 5】鴻蒙中的UIAbility詳解&#xff08;一&#xff09; 一、UIAbility是什么&#xff1f; Stage模型中的組件類型名&#xff0c;即UIAbility組件&#xff0c;包含UI&#xff0c;提供展示UI的能力&#xff0c;主要用于和用戶交互。 UIAbility類似于傳統移動開發An…