開發(fā)實(shí)戰(zhàn):從數(shù)據(jù)模型到部署的安全加固全鏈路)
SpringBoot做管理系統(tǒng)算是Java后端里最扎實(shí)的“吃飯手藝”了。不管你是做畢業(yè)設(shè)計(jì)、企業(yè)內(nèi)部后臺(tái)還是接手外包項(xiàng)目幾乎天天都要和它打交道。我這些年陸陸續(xù)續(xù)用SpringBoot做過學(xué)生選課、倉(cāng)庫(kù)出入庫(kù)、資產(chǎn)盤點(diǎn)、新聞發(fā)布之類的系統(tǒng)從最早手寫SSM配一堆XML到后來SpringBoot Vue 3前后端分離踩過的坑比寫過的接口還多。這篇文章想把這套東西從需求分析、表結(jié)構(gòu)設(shè)計(jì)、工程搭建、安全加固到部署的完整鏈路按照我自己的實(shí)際操作經(jīng)驗(yàn)從頭理一遍給正在做管理系統(tǒng)或者準(zhǔn)備拿它做畢設(shè)的同學(xué)一個(gè)能直接參考的底稿。先說清楚一件事網(wǎng)上搜“springboot管理系統(tǒng)”能出來一大堆開源項(xiàng)目仿佛這東西就是幾個(gè)增刪改查頁(yè)面拼一起。但真到自己從零設(shè)計(jì)開發(fā)的時(shí)候你才會(huì)發(fā)現(xiàn)光是一個(gè)權(quán)限模型就能讓人改到懷疑人生。所以這篇文章不會(huì)只貼代碼我會(huì)把每個(gè)關(guān)鍵決定背后的原因、踩坑記錄、排查思路全部寫出來爭(zhēng)取讓你看完之后能真正獨(dú)立把一個(gè)管理系統(tǒng)做出來、跑起來、部署出去而不是停留在“能啟動(dòng)”的層面。1. 先想清楚管理系統(tǒng)的本質(zhì)與整體架構(gòu)設(shè)計(jì)1.1 管理系統(tǒng)真不只是CRUD我見過太多人一拿到“XX管理系統(tǒng)”題目就開始建表寫接口結(jié)果做到一半發(fā)現(xiàn)業(yè)務(wù)邏輯繞成一團(tuán)。其實(shí)管理系統(tǒng)的本質(zhì)是把線下的業(yè)務(wù)流程搬到線上讓數(shù)據(jù)流轉(zhuǎn)變得可追蹤、可統(tǒng)計(jì)、可控制。就拿最簡(jiǎn)單的學(xué)生選課管理系統(tǒng)來說表面上是學(xué)生選課、老師開課、管理員管數(shù)據(jù)但背后是課程容量校驗(yàn)、選課時(shí)間窗口、退課規(guī)則、成績(jī)錄入權(quán)限分配這一連串業(yè)務(wù)規(guī)則。所以動(dòng)工之前我強(qiáng)烈建議先畫一張業(yè)務(wù)流程圖把角色、動(dòng)作、狀態(tài)列清楚。比如庫(kù)存出入庫(kù)管理系統(tǒng)你要回答幾個(gè)問題入庫(kù)單由誰創(chuàng)建審核通過后庫(kù)存才能增加嗎出庫(kù)時(shí)庫(kù)存不夠怎么辦盤點(diǎn)差異怎么處理這些問題沒想清楚后面返工成本極高。另一個(gè)容易被忽略的點(diǎn)是“狀態(tài)機(jī)”。很多管理系統(tǒng)的核心表都有一個(gè)狀態(tài)字段比如訂單有待審核、已審核、已駁回、已完成資產(chǎn)申請(qǐng)有借用中、已歸還。狀態(tài)流轉(zhuǎn)不是隨意跳轉(zhuǎn)的每個(gè)狀態(tài)能執(zhí)行什么操作、由哪個(gè)角色觸發(fā)在設(shè)計(jì)階段就要定義清楚。我習(xí)慣用一張狀態(tài)流轉(zhuǎn)表把a(bǔ)llowed transitions寫出來代碼里再做狀態(tài)校驗(yàn)而不是每個(gè)接口都寫一遍if else。1.2 技術(shù)選型為什么是SpringBoot為什么前后端分離不少初學(xué)者會(huì)問現(xiàn)在還有人用SSM嗎要不要學(xué)Spring Cloud我的建議很直接單體管理系統(tǒng)就用SpringBoot別想太多。SSM時(shí)代最痛苦的就是那一堆繁瑣的XML配置數(shù)據(jù)源、事務(wù)、MyBatis映射要來回調(diào)。SpringBoot把自動(dòng)配置、內(nèi)嵌Tomcat、起步依賴這些做成了開箱即用的東西一個(gè)注解就能啟動(dòng)項(xiàng)目這對(duì)于管理系統(tǒng)這種業(yè)務(wù)密集、技術(shù)復(fù)雜度不高的場(chǎng)景來說開發(fā)效率提升是非常明顯的。前后端分離我強(qiáng)烈推薦SpringBoot Vue 3的組合。后端只提供RESTful API前端負(fù)責(zé)頁(yè)面渲染和交互兩邊通過JSON通信。這樣做的好處不只是分工清晰更重要的是部署靈活后端可以單獨(dú)打包成jar跑在服務(wù)器上前端build完扔到Nginx里就行后續(xù)要加微信小程序端或者App端后端接口可以直接復(fù)用。我第一次做前后端分離項(xiàng)目時(shí)還不太習(xí)慣總覺得模板引擎Thymeleaf省事但經(jīng)歷過一次前端頁(yè)面頻繁改版之后我徹底倒向分離架構(gòu)了——接口穩(wěn)定頁(yè)面隨便改心里踏實(shí)。當(dāng)然技術(shù)選型也不要走極端。如果你只是做一個(gè)內(nèi)部幾十人用的小工具單體單庫(kù)完全夠用不要為了簡(jiǎn)歷好看硬上微服務(wù)、消息隊(duì)列那只會(huì)給自己挖坑。管理系統(tǒng)的核心矛盾永遠(yuǎn)在業(yè)務(wù)模型和數(shù)據(jù)一致性上不在架構(gòu)規(guī)模上。2. 數(shù)據(jù)模型設(shè)計(jì)權(quán)限模型、主鍵策略與自動(dòng)建表2.1 RBAC權(quán)限模型管理系統(tǒng)通用底座管理系統(tǒng)幾乎都逃不開用戶、角色、權(quán)限這套模型業(yè)界最成熟的就是RBAC基于角色的訪問控制。簡(jiǎn)單說不直接給用戶分配權(quán)限而是給角色分配權(quán)限再把角色賦予用戶。這么做的好處是當(dāng)公司有新人入職時(shí)管理員只需要給他配一個(gè)“普通員工”角色他自然就有了該角色下的一批菜單和按鈕權(quán)限而不是一條條去勾選。我常用的RBAC表結(jié)構(gòu)是5張表用戶表、角色表、權(quán)限表、用戶角色關(guān)聯(lián)表、角色權(quán)限關(guān)聯(lián)表。用戶表存賬號(hào)密碼、姓名、狀態(tài)、部門ID這些基本信息角色表就是管理員、老師、學(xué)生這類權(quán)限表可以細(xì)分到菜單權(quán)限、按鈕權(quán)限比如“用戶管理-新增按鈕”就是一個(gè)權(quán)限記錄。查詢用戶權(quán)限的時(shí)候通過用戶ID關(guān)聯(lián)出角色I(xiàn)D再通過角色I(xiàn)D關(guān)聯(lián)出權(quán)限集合最后在Spring Security里做權(quán)限判斷。這里有個(gè)設(shè)計(jì)經(jīng)驗(yàn)權(quán)限表里最好加上權(quán)限類型字段區(qū)分目錄、菜單、按鈕。前端動(dòng)態(tài)路由要根據(jù)菜單權(quán)限生成側(cè)邊欄后端接口要校驗(yàn)按鈕權(quán)限兩者共用同一份權(quán)限數(shù)據(jù)就不容易出現(xiàn)“菜單能看到但接口調(diào)不通”的尷尬情況。權(quán)限數(shù)據(jù)變更后可以通過Redis緩存用戶權(quán)限避免每次請(qǐng)求都查一次數(shù)據(jù)庫(kù)。2.2 主鍵生成策略與公共字段主鍵ID這塊我需要單獨(dú)說一下因?yàn)楹芏嘈率种苯舆x了數(shù)據(jù)庫(kù)自增ID結(jié)果做分庫(kù)分表或者數(shù)據(jù)遷移時(shí)后悔不已。管理系統(tǒng)的數(shù)據(jù)量雖然不大但為了以后擴(kuò)展和省心我建議使用MyBatis-Plus內(nèi)置的雪花算法生成ID。雪花ID是Long類型趨勢(shì)遞增不像UUID那樣無序?qū)е滤饕阅芟陆刀胰治ㄒ徊灰蕾嚁?shù)據(jù)庫(kù)自增。另外每張業(yè)務(wù)表我都強(qiáng)烈建議加上幾個(gè)公共字段create_time創(chuàng)建時(shí)間、update_time更新時(shí)間、deleted邏輯刪除標(biāo)記0未刪1已刪。特別是邏輯刪除管理系統(tǒng)里很多數(shù)據(jù)不能物理刪除比如訂單要留痕、用戶注銷要保留審計(jì)日志用邏輯刪除可以隨時(shí)恢復(fù)數(shù)據(jù)。配合MyBatis-Plus的邏輯刪除插件所有查詢自動(dòng)帶上deleted0條件刪除操作自動(dòng)變成update非常省事。說到時(shí)間字段我踩過一個(gè)坑MySQL的datetime用Java的LocalDateTime映射沒問題但如果用默認(rèn)的CST時(shí)區(qū)連接串?dāng)?shù)據(jù)庫(kù)存的時(shí)間和頁(yè)面顯示的時(shí)間可能差8小時(shí)。我現(xiàn)在的習(xí)慣是連接串統(tǒng)一加serverTimezoneAsia/Shanghai實(shí)體類用LocalDateTime前端統(tǒng)一顯示格式化后的字符串避免各種時(shí)區(qū)換算的坑。2.3 通過MyBatis-Plus實(shí)現(xiàn)表不存在自動(dòng)建表管理系統(tǒng)開發(fā)過程中最煩的就是頻繁改表結(jié)構(gòu)每次都要打開Navicat手動(dòng)執(zhí)行SQL。尤其是團(tuán)隊(duì)協(xié)作時(shí)別人改了表忘了同步代碼跑起來直接報(bào)“Table doesnt exist”。后來我搜到一個(gè)解決辦法就是利用MyBatis-Plus的DDL自動(dòng)建表能力在項(xiàng)目啟動(dòng)時(shí)讀取建表SQL腳本發(fā)現(xiàn)表不存在就自動(dòng)執(zhí)行。具體實(shí)現(xiàn)思路是在resources目錄下維護(hù)一個(gè)schema.sql里面用CREATE TABLE IF NOT EXISTS語句建表然后寫一個(gè)ApplicationRunner在SpringBoot啟動(dòng)完成后執(zhí)行這個(gè)腳本。這里要用到spring.sql.init.schema-locations配置或者自己用JdbcTemplate執(zhí)行ScriptUtils。需要注意的一點(diǎn)是如果表結(jié)構(gòu)后續(xù)有變更CREATE TABLE IF NOT EXISTS不會(huì)幫你加字段只能兼容首次建表。字段變更我會(huì)用Flyway來做版本化遷移把V1__init.sql、V2__add_column.sql這種腳本按順序執(zhí)行。注意自動(dòng)建表只適合開發(fā)環(huán)境和demo演示正式環(huán)境務(wù)必關(guān)閉自動(dòng)執(zhí)行或者只允許通過遷移腳本變更否則線上數(shù)據(jù)被誤操作就很危險(xiǎn)。3. 工程搭建、配置加密與統(tǒng)一返回3.1 創(chuàng)建項(xiàng)目的版本選擇與踩坑如果你打算用IDEA從Spring Initializr創(chuàng)建SpringBoot項(xiàng)目網(wǎng)絡(luò)不好時(shí)可能會(huì)出現(xiàn)創(chuàng)建超時(shí)。這時(shí)候可以手動(dòng)修改創(chuàng)建URL為阿里云鏡像地址https://start.aliyun.com速度會(huì)快很多這是我當(dāng)時(shí)折騰半天總結(jié)出來的經(jīng)驗(yàn)。版本選擇上Spring Boot 3.x要求JDK 17而很多學(xué)校或者企業(yè)還在用JDK 8如果你對(duì)版本兼容性沒把握我建議用Spring Boot 2.7.x。這個(gè)版本是目前兼容性最穩(wěn)的既能用JDK 8也能平滑升級(jí)到2.x的最后一版網(wǎng)上資料也最多。等后續(xù)確實(shí)需要升級(jí)到3.x再慢慢遷移。熱詞里有人搜“springboot版本太高”導(dǎo)致的問題多半就是JDK版本不匹配要么降Boot版本要么升JDK二選一。創(chuàng)建項(xiàng)目時(shí)依賴選擇上管理系統(tǒng)一般需要這幾個(gè)Spring Web、Spring Security、MyBatis-Plus用MyBatis的選MyBatis Framework、MySQL Driver、Lombok、Validation。Redis看情況需要緩存和分布式鎖就加Spring Data Redis。3.2 yml配置文件中的敏感信息加密不知道你有沒有這種經(jīng)歷把項(xiàng)目發(fā)到Git倉(cāng)庫(kù)或者給同事演示代碼時(shí)數(shù)據(jù)庫(kù)密碼、Redis密碼、第三方密鑰全明文寫在application.yml里。這確實(shí)是巨大的安全隱患熱詞里也有人在搜“springboot yml密文”。我用的是Jasypt對(duì)敏感配置做加密。使用步驟分三步第一步引入jasypt-spring-boot-starter依賴第二步配置一個(gè)加密密鑰比如jasypt.encryptor.password第三步在yml里把明文密碼替換成ENC(密文)格式。這里的邏輯是頁(yè)面顯示的是密文項(xiàng)目啟動(dòng)時(shí)Jasypt用密鑰解密成明文再傳給數(shù)據(jù)源??梢詫懸粋€(gè)簡(jiǎn)單的測(cè)試類用StringEncryptor加密你的數(shù)據(jù)庫(kù)密碼然后把生成結(jié)果粘到y(tǒng)ml里。稍微要注意的是加密密鑰本身不要硬編碼在代碼里可以通過環(huán)境變量或者啟動(dòng)參數(shù)注入比如-Djasypt.encryptor.password你的密鑰這樣就算拿到配置文件也解不開密文。提示如果yml里密碼中包含特殊字符比如#、:一定要加上單引號(hào)否則會(huì)被YAML解析成注釋或者報(bào)錯(cuò)這個(gè)細(xì)節(jié)很坑但很容易遇到。3.3 統(tǒng)一返回結(jié)構(gòu)與全局異常處理前后端分離之后接口要約定數(shù)據(jù)格式不然前端拿到一個(gè)報(bào)錯(cuò)不知道是自己參數(shù)傳錯(cuò)了還是后端炸了。我的統(tǒng)一返回結(jié)構(gòu)長(zhǎng)這樣{ code: 200, message: 操作成功, data: { } }code是業(yè)務(wù)狀態(tài)碼200成功400參數(shù)錯(cuò)誤401未認(rèn)證403無權(quán)限500服務(wù)器異常。后端封裝一個(gè)Result類提供靜態(tài)方法success()和error()Controller里直接返回Result.success(data)即可。這樣前端拿到結(jié)果后先判斷code再?zèng)Q定是渲染數(shù)據(jù)還是彈出錯(cuò)誤提示。全局異常處理也很有必要。我在項(xiàng)目里寫了一個(gè)RestControllerAdvice修飾的GlobalExceptionHandler把業(yè)務(wù)異常、參數(shù)校驗(yàn)異常、未知異常分別處理統(tǒng)一轉(zhuǎn)成上面這個(gè)結(jié)構(gòu)。尤其是參數(shù)校驗(yàn)用Valid注解加上實(shí)體類字段的NotBlank、NotNull注解比自己在Controller里寫一堆if判斷干凈太多。異常日志一定要用log.error全量打印堆棧不然線上問題根本沒法排查。4. 認(rèn)證授權(quán)、列表分頁(yè)與文件處理4.1 Spring Security JWT做登錄授權(quán)管理系統(tǒng)的登錄認(rèn)證我推薦Spring Security JWT這套組合。為什么不用Session因?yàn)榍昂蠖朔蛛x后后端服務(wù)是無狀態(tài)的用戶的登錄狀態(tài)不能一直放在服務(wù)器內(nèi)存里否則水平擴(kuò)展時(shí)Session不共享用戶一會(huì)被踢下線。JWT把用戶ID、用戶名、過期時(shí)間這些信息簽成一個(gè)token返回給前端前端每次請(qǐng)求在Header帶上Authorization: Bearer token后端校驗(yàn)簽名和過期時(shí)間即可。具體實(shí)現(xiàn)流程是用戶提交賬號(hào)密碼后端用PasswordEncoder比對(duì)密碼密碼在數(shù)據(jù)庫(kù)里必須是BCrypt加密存儲(chǔ)絕對(duì)不能是明文比對(duì)成功用JWT工具類生成token返回。前端拿到token存到localStorage后續(xù)請(qǐng)求通過axios攔截器自動(dòng)攜帶。后端寫一個(gè)JwtAuthenticationFilter繼承OncePerRequestFilter在請(qǐng)求進(jìn)入到Controller之前解析token、加載用戶權(quán)限、放入SecurityContext。這里有兩個(gè)容易踩的坑第一個(gè)是token過期時(shí)間我一般設(shè)置2小時(shí)前端在響應(yīng)攔截器里遇到401就跳轉(zhuǎn)登錄頁(yè)讓用戶重新登錄第二個(gè)是密碼加密一定用BCryptPasswordEncoder每次加密結(jié)果都帶隨機(jī)鹽同樣密碼兩次加密結(jié)果不一樣但matches校驗(yàn)?zāi)芡ㄟ^比MD5安全很多。4.2 列表查詢、分頁(yè)與統(tǒng)計(jì)管理系統(tǒng)八成以上的頁(yè)面都是列表用戶列表、訂單列表、課程列表。列表頁(yè)最基礎(chǔ)的能力是分頁(yè)和條件篩選。我用MyBatis-Plus的Page對(duì)象前端傳current和size兩個(gè)參數(shù)再傳一些篩選條件后端構(gòu)造LambdaQueryWrapper把條件拼接進(jìn)去返回總記錄數(shù)和當(dāng)前頁(yè)數(shù)據(jù)。有個(gè)細(xì)節(jié)要注意篩選條件傳空字符串時(shí)要過濾掉否則MyBatis-Plus會(huì)一直帶上這個(gè)條件導(dǎo)致查不到數(shù)據(jù)。統(tǒng)計(jì)功能是管理系統(tǒng)的另一個(gè)重頭戲。比如首頁(yè)要展示“今日新增用戶”“本月訂單金額”“各分類商品銷量”這些用聚合查詢。以訂單表為例按天統(tǒng)計(jì)訂單數(shù)量SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS total FROM orders WHERE create_time #{startTime} GROUP BY day ORDER BY day;執(zhí)行統(tǒng)計(jì)查詢時(shí)要注意SQL的性能。我遇到過一張表幾百萬數(shù)據(jù)統(tǒng)計(jì)接口每次都全表掃前端等了十幾秒才開始轉(zhuǎn)圈。后來加上了create_time的索引并且把統(tǒng)計(jì)范圍限制在最近30天問題就解決了。列表查詢也一樣排序字段和篩選字段要記得建索引但不要無腦建索引太多會(huì)影響寫入性能。4.3 文件上傳與Excel導(dǎo)入導(dǎo)出管理系統(tǒng)里文件上傳、Excel導(dǎo)入導(dǎo)出是高頻功能比如批量導(dǎo)入學(xué)生名單、導(dǎo)出商品報(bào)表。文件上傳我一般用Nginx/本地磁盤作為存儲(chǔ)上傳時(shí)保存一份原始文件名并生成一個(gè)新的文件名比如UUID避免文件名沖突和中文亂碼。數(shù)據(jù)庫(kù)只存文件路徑不存二進(jìn)制內(nèi)容。Excel處理我用的是EasyExcel它比Apache POI更省內(nèi)存因?yàn)榈讓邮荢AX方式逐行解析不會(huì)一次性把整個(gè)文件加載進(jìn)內(nèi)存。導(dǎo)入時(shí)先讀取表頭做校驗(yàn)再逐行解析成對(duì)象批量插入數(shù)據(jù)庫(kù)。有一個(gè)很重要的經(jīng)驗(yàn)如果Excel有幾萬行不要一條條insert一定要用批量插入比如每500條執(zhí)行一次批量insert速度能快幾十倍。導(dǎo)出時(shí)如果數(shù)據(jù)量大絕對(duì)不能在內(nèi)存里把所有數(shù)據(jù)構(gòu)造完再寫文件那樣很容易OOM。我遇到過一次導(dǎo)出5萬條記錄用EasyExcel的分批寫前端下載沒卡后端內(nèi)存也穩(wěn)住了。5. 業(yè)務(wù)場(chǎng)景實(shí)戰(zhàn)選課管理與庫(kù)存出入庫(kù)5.1 學(xué)生選課管理系統(tǒng)事務(wù)與并發(fā)控制拿學(xué)生選課這個(gè)經(jīng)典場(chǎng)景來說看上去就是“學(xué)生選課、課程減一容量”但并發(fā)情況下問題非常多。比如一門課只剩1個(gè)名額兩個(gè)學(xué)生同時(shí)提交選課如果邏輯是先查容量再判斷再減一極有可能兩個(gè)請(qǐng)求都查到剩余名額是1然后都通過了課程實(shí)際選了2個(gè)人超賣。解決辦法有幾種最常用的是在課程表上加一個(gè)樂觀鎖版本號(hào)字段version更新時(shí)帶上UPDATE course SET selected_count selected_count 1, version version 1 WHERE id ? AND version ?影響行數(shù)為0說明版本沖突提示用戶“課程容量已滿請(qǐng)重新選擇”。如果并發(fā)特別高還可以用Redis的分布式鎖把課程ID作為鎖key選課前先獲取鎖選完釋放。除了并發(fā)選課系統(tǒng)的事務(wù)控制也很關(guān)鍵。選課成功時(shí)要同時(shí)完成兩件事插入選課記錄、課程容量加一這兩步必須在一個(gè)事務(wù)里要么都成功要么都失敗。我在Service方法上加Transactional(rollbackFor Exception.class)注意rollbackFor一定要指定否則RuntimeException才能觸發(fā)回滾普通的異常不會(huì)回滾這是很多新手最容易忽略的點(diǎn)。5.2 倉(cāng)庫(kù)出入庫(kù)/資產(chǎn)管理系統(tǒng)庫(kù)存扣減與流水記錄倉(cāng)庫(kù)出入庫(kù)管理系統(tǒng)核心是庫(kù)存賬和流水賬。每次入庫(kù)庫(kù)存表數(shù)量增加并生成一條入庫(kù)流水每次出庫(kù)庫(kù)存表數(shù)量減少并生成一條出庫(kù)流水。流水一旦生成就不能修改和刪除只能做沖銷單來糾正錯(cuò)誤這是財(cái)務(wù)審計(jì)的基本要求。庫(kù)存扣減同樣有并發(fā)問題我的做法是在庫(kù)存表里直接用SQL原子更新避免“先查后改”UPDATE inventory SET stock stock - #{num} WHERE sku_id #{skuId} AND stock #{num};如果更新影響行數(shù)為0說明庫(kù)存不足或商品不存在直接給前端返回“庫(kù)存不足”。這樣利用數(shù)據(jù)庫(kù)行鎖保證了并發(fā)安全比用Java代碼加鎖更簡(jiǎn)單可靠。如果是高端一點(diǎn)的資產(chǎn)管理系統(tǒng)還會(huì)涉及資產(chǎn)借還流程資產(chǎn)表有狀態(tài)字段在庫(kù)、借用中、維修中、已報(bào)廢。借出時(shí)要把資產(chǎn)狀態(tài)改成借用中歸還時(shí)改回在庫(kù)同時(shí)記錄借用人、借用時(shí)間、預(yù)計(jì)歸還時(shí)間。這個(gè)流程用狀態(tài)機(jī)做約束能避免很多操作上的誤判比如已報(bào)廢的資產(chǎn)不能被借出、借用中的資產(chǎn)不能被再次借出。6. 部署、安全與性能加固6.1 Docker部署與配置分離管理系統(tǒng)開發(fā)完了最終要部署到服務(wù)器。我現(xiàn)在的標(biāo)準(zhǔn)交付方式是Docker Compose編排一套環(huán)境包含MySQL、Redis、后端應(yīng)用。后端Dockerfile很簡(jiǎn)單基于openjdk鏡像把jar復(fù)制進(jìn)去暴露端口啟動(dòng)命令是java -jar。數(shù)據(jù)庫(kù)和Redis用docker-compose里的服務(wù)名訪問比如jdbc:mysql://mysql:3306/dbname容器間通信直接用服務(wù)名做主機(jī)名。配置分離是部署時(shí)的重要原則。開發(fā)環(huán)境、測(cè)試環(huán)境、生產(chǎn)環(huán)境的數(shù)據(jù)庫(kù)地址、Redis地址、密鑰都不一樣不應(yīng)該每次部署都改配置重新打包。我的做法是在Docker Compose里用environment注入環(huán)境變量SpringBoot的yml里使用${DB_HOST}、${DB_PASSWORD}這種占位符如果環(huán)境變量沒設(shè)置本地開發(fā)還能用默認(rèn)值兜底spring: datasource: url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/admin_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: ${DB_USERNAME:root} password: ${DB_PASSWORD:root}這樣一套代碼可以無修改部署到任意環(huán)境配合CI/CD流水線非常順暢。6.2 接口越權(quán)與HeapDump信息泄露管理系統(tǒng)做完了安全檢查一定不能跳過。先說越權(quán)問題這是后臺(tái)系統(tǒng)最常見的高危漏洞。很多系統(tǒng)只判斷了“用戶是否登錄”但沒有判斷“這個(gè)用戶是否有權(quán)限操作這條數(shù)據(jù)”。比如用戶A登錄后直接調(diào)用接口把訂單ID改成B的訂單ID結(jié)果把B的訂單刪了。這種漏洞叫水平越權(quán)。我的經(jīng)驗(yàn)是在Service層必須校驗(yàn)數(shù)據(jù)歸屬根據(jù)當(dāng)前登錄用戶ID和資源ID同時(shí)查詢查不到就拋“無權(quán)操作”。還有一個(gè)最近被頻繁提到的問題就是Spring Boot Actuator暴露出來的heapdump接口。如果生產(chǎn)環(huán)境開啟了management.endpoints.web.exposure.include*別人可以直接訪問/actuator/heapdump下載JVM堆內(nèi)存快照再用工具分析數(shù)據(jù)庫(kù)密碼、Redis密碼、token等敏感信息全部裸奔。我處理這個(gè)問題的方式是生產(chǎn)環(huán)境只暴露health和info兩個(gè)端點(diǎn)其他全部關(guān)閉如果實(shí)在需要監(jiān)控也要加上Spring Security權(quán)限校驗(yàn)絕不能裸奔。6.3 慢SQL優(yōu)化與緩存管理系統(tǒng)數(shù)據(jù)量大了之后最先扛不住的就是數(shù)據(jù)庫(kù)。我排查慢SQL時(shí)通常先在MySQL里開啟慢查詢?nèi)罩驹侔褕?zhí)行時(shí)間超過1秒的SQL語句撈出來用EXPLAIN分析執(zhí)行計(jì)劃。常見的性能問題就幾種表沒加索引、查詢條件沒走索引、不必要的全表掃、并發(fā)重復(fù)查詢相同數(shù)據(jù)。緩存是提升性能最直接的手段。對(duì)于熱點(diǎn)數(shù)據(jù)比如系統(tǒng)配置項(xiàng)、首頁(yè)統(tǒng)計(jì)數(shù)據(jù)、字典數(shù)據(jù)我會(huì)用Redis先緩存一份設(shè)置合理的過期時(shí)間。緩存更新的策略我用Cache Aside模式讀的時(shí)候先查緩存緩存沒有則查數(shù)據(jù)庫(kù)再回填寫的時(shí)候先更新數(shù)據(jù)庫(kù)再刪除緩存避免數(shù)據(jù)不一致。別看簡(jiǎn)單這個(gè)模式在工作里非常實(shí)用。這里有一個(gè)緩存穿透的小坑如果查詢的ID根本不存在緩存和數(shù)據(jù)庫(kù)都沒有每一次請(qǐng)求都會(huì)打到數(shù)據(jù)庫(kù)。解決辦法是緩存空值并設(shè)置較短的過期時(shí)間比如5分鐘或者用布隆過濾器先做一層過濾。實(shí)操中緩存空值更簡(jiǎn)單我一般用這個(gè)方案。7. 實(shí)戰(zhàn)中的常見問題排查7.1 環(huán)境、版本與配置類問題這里整理一下我實(shí)際開發(fā)中經(jīng)常遇到的問題以及我的解決思路。問題一IDEA創(chuàng)建SpringBoot項(xiàng)目超時(shí)。原因是Spring Initializr默認(rèn)訪問國(guó)外地址經(jīng)常連不上。解決方法是改為阿里云鏡像地址https://start.aliyun.com再不行就直接從官網(wǎng)start.spring.io手動(dòng)下載壓縮包導(dǎo)入。問題二項(xiàng)目啟動(dòng)報(bào)版本錯(cuò)誤比如Unable to start embedded Tomcat。大概率是SpringBoot版本和JDK版本不搭配。SpringBoot 2.x要求JDK 8或以上SpringBoot 3.x要求JDK 17或以上啟動(dòng)失敗先檢查java -version。問題三yml配置文件報(bào)錯(cuò)。YAML對(duì)縮進(jìn)和冒號(hào)非常敏感同一層級(jí)必須對(duì)齊鍵值冒號(hào)后面必須要空格。出現(xiàn)錯(cuò)誤時(shí)看IDEA下方的Error信息通常能直接定位到第幾行的具體問題。用錯(cuò)了一份舊配置最好的辦法是先把配置內(nèi)容全部撤銷從已知能跑的版本一點(diǎn)點(diǎn)加。問題四數(shù)據(jù)庫(kù)連接串亂碼。MySQL連接URL一定要加上useUnicodetruecharacterEncodingutf8不然插入中文到數(shù)據(jù)庫(kù)變成問號(hào)。同時(shí)確保數(shù)據(jù)庫(kù)表本身就是utf8mb4字符集。問題五MyBatis-Plus的分頁(yè)不生效。MyBatis-Plus分頁(yè)需要配置PaginationInnerInterceptor插件很多人忘了配結(jié)果分頁(yè)查出來的是全量數(shù)據(jù)。還要確認(rèn)前端傳的pageNum、pageSize和代碼里的參數(shù)名對(duì)得上。7.2 數(shù)據(jù)與邏輯類問題問題一邏輯刪除字段沒生效。MyBatis-Plus邏輯刪除需要兩個(gè)配置全局邏輯刪除字段名和值以及實(shí)體類字段上標(biāo)注TableLogic。漏掉任何一處刪除操作就變成物理刪除數(shù)據(jù)找不回來這是很危險(xiǎn)的情況。問題二日期時(shí)間差8小時(shí)。前端傳過來的時(shí)間戳被后端轉(zhuǎn)成LocalDateTime后和北京時(shí)間差了8小時(shí)多半是Jackson和數(shù)據(jù)庫(kù)連接的時(shí)區(qū)不一致。我統(tǒng)一在application.yml里配置spring.jackson.time-zoneGMT8數(shù)據(jù)庫(kù)連接串帶serverTimezoneAsia/Shanghai問題解決。問題三事務(wù)不生效。寫了個(gè)帶Transactional的方法數(shù)據(jù)出錯(cuò)了還是沒有回滾。常見原因有兩個(gè)一是方法被同類中的另一個(gè)方法調(diào)用導(dǎo)致代理失效要把事務(wù)方法放到獨(dú)立Service里二是異常被try catch吞了事務(wù)感知不到異常這種情況要么異常拋出要么手動(dòng)標(biāo)記rollback我建議不做特殊的“異常繼續(xù)執(zhí)行”處理就讓它往外拋。問題四Excel導(dǎo)入數(shù)據(jù)總是失敗??赡苁亲侄胃袷絾栴}比如數(shù)字單元格讀進(jìn)來變成科學(xué)計(jì)數(shù)法或者日期格式不統(tǒng)一。我在EasyExcel里對(duì)每一列都顯式指定類型轉(zhuǎn)換器并在轉(zhuǎn)換失敗時(shí)記錄行號(hào)和錯(cuò)誤原因最后把錯(cuò)誤信息一起返回給前端讓用戶下載錯(cuò)誤報(bào)告體驗(yàn)好很多。8. 寫在最后幾條開發(fā)管理系統(tǒng)的誠(chéng)心建議結(jié)合這些年的實(shí)際項(xiàng)目經(jīng)驗(yàn)最后再嘮叨幾句。做管理系統(tǒng)不要把精力全花在技術(shù)框架上SpringBoot再花哨也只是工具真正值錢的是你對(duì)業(yè)務(wù)的理解和數(shù)據(jù)模型的設(shè)計(jì)。動(dòng)手之前把角色權(quán)限、業(yè)務(wù)流程、狀態(tài)流轉(zhuǎn)這些理清楚比多寫一百個(gè)接口都有用。第二點(diǎn)是安全意識(shí)和規(guī)范意識(shí)要早培養(yǎng)。邏輯刪除、密碼加密、接口越權(quán)校驗(yàn)、敏感配置加密這些不是可選項(xiàng)而是必選項(xiàng)。等系統(tǒng)上線被人掃出漏洞再補(bǔ)救代價(jià)成倍增加我親眼見過因?yàn)閔eapdump信息泄露導(dǎo)致整個(gè)后臺(tái)被拿下的案例場(chǎng)面非常慘烈。第三點(diǎn)是遇到問題先學(xué)會(huì)看日志。啟動(dòng)失敗看堆棧第一行接口報(bào)錯(cuò)看異常類型和消息數(shù)據(jù)不對(duì)看SQL語句和入?yún)?。不要拿著?bào)錯(cuò)信息不管三七二十一就去復(fù)制粘貼搜答案先讀懂它再判斷方向。這套排查思路比任何框架知識(shí)都有用能陪你走很久很久。如果你正在做一個(gè)SpringBoot管理系統(tǒng)或者準(zhǔn)備拿它當(dāng)畢業(yè)設(shè)計(jì)希望這篇文章能幫你少走一些彎路。按照我寫的內(nèi)容把項(xiàng)目跑起來之后再自己去擴(kuò)展業(yè)務(wù)模塊你會(huì)發(fā)現(xiàn)所謂的“管理系統(tǒng)開發(fā)”真沒那么玄乎核心就是把簡(jiǎn)單的事情踏實(shí)做對(duì)一遍一遍地打磨細(xì)節(jié)而已。