據(jù)庫分頁)
簡介面向Java Web學(xué)習(xí)者的個人微博應(yīng)用項目圍繞會員管理、個人中心、消息發(fā)布等核心模塊系統(tǒng)展示W(wǎng)eb原理與前后端開發(fā)全流程適合課程設(shè)計、畢業(yè)設(shè)計及Servlet/JSP初學(xué)者參考是一次完整的Web應(yīng)用實踐。壓縮包內(nèi)共71個文件以22個Java源碼、24個class編譯文件、5個JSP頁面為主輔以XML配置、CSS樣式、Jar依賴及數(shù)據(jù)庫文件等整體僅3.33MB包含Eclipse工程配置文件導(dǎo)入后即可運行調(diào)試。目前已有223人學(xué)習(xí)。借助完整的工程目錄讀者可對照學(xué)習(xí)用戶注冊登錄、數(shù)據(jù)持久化、接口設(shè)計、實時推送等核心知識還可參考代碼中的權(quán)限管理、單元測試與搜索思路是一份幫助串聯(lián)Web應(yīng)用前后端技術(shù)的實用案例。1. 前端選 Java Web 方向后第一次把瀏覽器請求打到后端時最先撞上的不是代碼 bug而是一個反直覺的事實HTTP 本身完全“健忘”你剛注冊完賬號刷新一次頁面卻發(fā)現(xiàn)時間線里出現(xiàn)了別人發(fā)的微博而且頁面右上角還掛著你自己的頭像。瀏覽器明明只是往服務(wù)器發(fā)了一個普通的 GET 請求服務(wù)器是怎么在“沒有記憶”的 HTTP 協(xié)議上認(rèn)出你、知道你關(guān)注了誰、該給你推什么內(nèi)容的答案不是某個魔法字段而是 Cookie、Session、數(shù)據(jù)庫查詢、Servlet 調(diào)度這四件事串成的一條完整鏈路。搞不懂這條鏈路前端寫的是“頁面死不死”后端寫的則是“登錄態(tài)丟不丟、數(shù)據(jù)亂不亂、連接池崩不崩”。個人微博應(yīng)用恰好是把這些機制全部走一遍的最小完整案例注冊登錄涉及會話管理發(fā)微博涉及數(shù)據(jù)寫入時間線涉及查詢與排序異步刷新涉及 JSON 序列化。把這套機制拆開你會看到一個請求從瀏覽器到數(shù)據(jù)庫再回到瀏覽器的完整旅程——這也是為什么值得專門寫一篇文章把它講透。2. 一次微博刷新背后從 TCP 連接、線程池到 Servlet 的完整調(diào)度鏈2.1 先看清請求的“底層路徑”不是 url 直連 Java 方法很多初學(xué)者以為瀏覽器地址欄里輸入 index.jsp服務(wù)器就會“直接執(zhí)行那個文件”。實際上請求要先經(jīng)過一層網(wǎng)絡(luò)協(xié)議棧、一個監(jiān)聽端口、一個線程池最后才由 Servlet 容器交到你的代碼手里。我一般會這么理解Servlet 容器例如 Tomcat啟動后會在某個端口上持續(xù) accept TCP 連接。瀏覽器發(fā)來的 GET /index.jsp HTTP/1.1 請求是一段純文本容器把這段文本解析成請求行、請求頭、請求體封裝成一個 HttpServletRequest 對象然后從線程池里撈出一個空閑線程在這個線程上執(zhí)行匹配到的組件代碼。這里有個隱蔽但關(guān)鍵的點容器處理請求的工作線程是復(fù)用的。你寫的代碼里如果讓線程 sleep或者在一個請求里反復(fù)創(chuàng)建數(shù)據(jù)庫連接然后不關(guān)消耗的不是“這一個請求”而是整個應(yīng)用的吞吐能力。后面避坑環(huán)節(jié)里連接耗盡的問題根源就在這里。2.2 URL 映射與 Filter 鏈誰先碰這個請求請求進(jìn)入容器后會先經(jīng)過一層 Filter 鏈再到達(dá) Servlet。理解先后順序很重要Filter 的執(zhí)行發(fā)生在 Servlet 的 service 方法之前因此編碼設(shè)置、登錄校驗這類橫切邏輯通常會放在 Filter 里而不是在每個 Servlet 里重復(fù)寫。一個典型個人微博項目的 web.xml 或注解配置里會聲明類似這樣的映射WebFilter(urlPatterns {/api/*, /index.jsp}) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }chain.doFilter 是關(guān)鍵它表示把請求繼續(xù)向后傳遞。如果忘了調(diào)用這一行請求就會在這里被“吞掉”后面的 Servlet 永遠(yuǎn)執(zhí)行不到前端表現(xiàn)就是接口一直 pending。再往下容器根據(jù)請求路徑找到對應(yīng)的 Servlet。對于 /api/login 這個路徑容器會在映射關(guān)系里找到 LoginServlet然后調(diào)用它的 service 方法service 再根據(jù) HTTP 方法分發(fā)到 doGet 或 doPost。所以 doPost 不是“你寫的入口”而是整個調(diào)度鏈上最靠近業(yè)務(wù)邏輯的那一層。2.3 Servlet 三兄弟的生命周期init、service、destroyServlet 不是“每個請求新建一個實例”恰恰相反同一個 Servlet 在容器里通常只有一個實例被所有請求線程共享。這是新人最容易踩的并發(fā)思維誤區(qū)。init 方法容器啟動或首次請求時執(zhí)行一次適合做資源初始化。service 方法每個請求都會執(zhí)行但它內(nèi)部按方法類型分發(fā)給 doGet/doPost。destroy 方法容器關(guān)閉時執(zhí)行一次適合釋放資源。這里就出現(xiàn)一個并發(fā)隱患如果在一個 Servlet 里定義了成員變量并用它存某個登錄用戶的身份信息那么用戶 A 的請求和用戶 B 的請求會同時讀寫同一個變量。正確做法是把每個請求、每個用戶特有的數(shù)據(jù)放進(jìn) request 作用域、session 作用域或方法局部變量而不是 Servlet 成員變量。一個真實場景的登錄 Servlet 大致長這樣WebServlet(/api/login) public class LoginServlet extends HttpServlet { private UserService userService new UserServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user ! null) { HttpSession session req.getSession(true); session.setAttribute(currentUserId, user.getId()); resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(loginError, 用戶名或密碼錯誤); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }注意這里的兩個跳轉(zhuǎn)方式差別非常微妙sendRedirect 是讓瀏覽器再發(fā)一次新請求地址欄會變成 /index.jspforward 是服務(wù)器內(nèi)部直接改渲染目標(biāo)地址欄保持 /api/login 不變。登錄失敗用 forward能把錯誤信息通過 request 屬性帶給登錄頁登錄成功用 redirect能避免用戶刷新頁面時把“登錄提交”再次重放。這段代碼里的 UserService 是業(yè)務(wù)層它的內(nèi)部會調(diào) DAO 去查數(shù)據(jù)庫這就是后面章節(jié)里時間線查詢的入口。3. 會話機制Cookie 里的 JSESSIONID、服務(wù)端 Map 與“假裝記住你”3.1 HttpSession 到底存在哪上一章登錄成功時調(diào)用 req.getSession(true)這里發(fā)生了兩件事服務(wù)器創(chuàng)建或找到一條會話記錄同時在響應(yīng)頭里寫一個 Set-Cookie。會話記錄本身是一個服務(wù)端的內(nèi)存對象本質(zhì)是一個鍵值結(jié)構(gòu)存著這個會話的創(chuàng)建時間、最后訪問時間、失效時間以及你往里塞的各種 attribute。Cookie 里存的是會話的唯一標(biāo)識大多數(shù)容器默認(rèn)叫 JSESSIONID。瀏覽器收到后后續(xù)每次請求都會把這個 Cookie 原樣帶回。服務(wù)器一看 JSESSIONID就能從自己的會話集合里找到對應(yīng)的 HttpSession 對象然后把“當(dāng)前登錄的是誰”這個信息取出來。丟登錄態(tài)最常見的原因就是這條鏈路某一段斷了環(huán)節(jié)常見坑表現(xiàn)Cookie 未設(shè)置 HttpOnly前端腳本能讀到 JSESSIONIDXSS 后可被竊取會話劫持Cookie 未設(shè)置 Path應(yīng)用上下文路徑變化后 Cookie 帶不過來登錄失效Session 超時超過 maxInactiveInterval 無訪問容器自動清理突然要重新登錄容器重啟內(nèi)存會話全部消失重啟后全部掉線3.2 登錄校驗 Filter為什么每個受保護頁面都要過一道關(guān)卡微博應(yīng)用中發(fā)微博、看時間線、改頭像這些操作都不允許匿名訪問。如果每個 Servlet 都手寫一遍“檢查 session 里有沒有 currentUserId”代碼會瞬間失控。標(biāo)準(zhǔn)做法是做一個統(tǒng)一的登錄校驗 Filter只讓登錄過的請求通過。WebFilter(urlPatterns {/api/postWeibo, /api/timeline, /index.jsp}) public class LoginCheckFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq (HttpServletRequest) req; HttpSession session httpReq.getSession(false); if (session ! null session.getAttribute(currentUserId) ! null) { chain.doFilter(req, resp); } else { HttpServletResponse httpResp (HttpServletResponse) resp; httpResp.sendRedirect(httpReq.getContextPath() /login.jsp); } } }這里專門用了 getSession(false)如果當(dāng)前沒有會話直接返回 null而不是新造一個。很多人寫成 getSession(true)導(dǎo)致未登錄用戶每次訪問受保護頁面都會被服務(wù)器創(chuàng)建一個新會話服務(wù)器內(nèi)存里堆滿“幽靈會話”。登錄校驗的嚴(yán)謹(jǐn)程度可以從這一個參數(shù)上看出來。3.3 密碼不能明文存加鹽哈希與登錄流程中容易被跳過的那一步個人微博項目里最常見的重災(zāi)區(qū)是密碼存儲。有人直接明文存有人用 MD5 存一次就當(dāng)作加密。MD5 確實“不是明文”但它經(jīng)不起彩虹表和暴力碰撞。真正常見且可靠的思路是加鹽哈希每個用戶生成一個隨機鹽把鹽拼到密碼后面再進(jìn)行多次哈希然后鹽和哈希結(jié)果一起入庫。public class PasswordUtil { private static final int ITERATIONS 10000; public static String hashWithSalt(String rawPassword, String salt) throws Exception { byte[] result rawPassword.getBytes(UTF-8); for (int i 0; i ITERATIONS; i) { byte[] combined new byte[result.length salt.getBytes(UTF-8).length]; System.arraycopy(result, 0, combined, 0, result.length); System.arraycopy(salt.getBytes(UTF-8), 0, combined, result.length, salt.length()); result MessageDigest.getInstance(SHA-256).digest(combined); } return bytesToHex(result); } }這里的迭代次數(shù)不是隨便寫的。每次循環(huán)都把上一步的摘要結(jié)果再和鹽一起重新哈希目的是把計算成本抬高到“暴力破解一個密碼需要足夠長時間”的量級。與此同時登錄驗證時也要重復(fù)同樣次數(shù)的哈希再比對所以登錄接口天然會比其他接口慢一些這是安全換來的可接受代價。從這一章能看到整個登錄態(tài)的本質(zhì)服務(wù)器并沒有“記住你”它只是保存了一條狀態(tài)記錄并通過一個不可偽造的標(biāo)識和瀏覽器保持約定。你刷新一次頁面看到自己的頭像和別人的微博其實是 session 里的 userId 被拿去查了關(guān)注關(guān)系、查了時間線最終拼出了“好像記得你是誰”的完整體驗。4. 時間線是怎么“算”出來的feed 表結(jié)構(gòu)、拉取式查詢與分頁索引4.1 核心表結(jié)構(gòu)用戶表、微博表、關(guān)注關(guān)系表個人微博應(yīng)用的數(shù)據(jù)模型圍繞三個核心實體用戶、微博內(nèi)容、關(guān)注關(guān)系。微博表是業(yè)務(wù)的主體關(guān)注關(guān)系決定時間線范圍。建表 SQL 里索引設(shè)計決定了查詢上限。CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL ); CREATE TABLE weibo ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, content VARCHAR(280) NOT NULL, created_at DATETIME NOT NULL, INDEX idx_user_time (user_id, created_at DESC), INDEX idx_time (created_at DESC) ); CREATE TABLE follow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, follow_user_id BIGINT NOT NULL, created_at DATETIME NOT NULL, UNIQUE KEY uk_user_follow (user_id, follow_user_id) );這里有兩個索引細(xì)節(jié)值得展開。idx_user_time 支撐“查某個人的所有微博并按時間倒序”這種個人主頁查詢idx_time 支撐“全站最新微博”這種公共時間線查詢。當(dāng) create_at 被加上后MySQL 可以直接按索引順序倒著讀避免做文件排序。4.2 拉取式時間線為什么這種小項目不選擇“推送”時間線設(shè)計有兩條路拉取模式和推送模式。拉取模式是當(dāng)你請求時間線時現(xiàn)場去查你關(guān)注的人最近發(fā)了什么推送模式是發(fā)微博時系統(tǒng)立刻把這條微博復(fù)制到所有粉絲的收件箱里。個人微博項目用拉取有兩個現(xiàn)實原因。一是用戶量小關(guān)注關(guān)系簡單查詢開銷完全扛得住二是推送模式要維護“每個用戶的收件箱表”每次發(fā)微博都是一次扇出寫入粉絲多時要寫入幾千條數(shù)據(jù)還要處理事務(wù)一致性問題。拉取模式把開銷從“寫時”轉(zhuǎn)移到了“讀時”代碼簡單失誤率低。你手上這份資源里如果用 JDBC 手寫 SQL 查詢走的一定是拉取模式因為它不需要引入消息隊列或異步任務(wù)。4.3 時間線 SQLJOIN 與分頁的正確姿勢查詢時間線時最直觀的寫法是查出所有關(guān)注對象的 id再去微博表里查這些人的微博。但逐條查會產(chǎn)生 N1 次 SQL。正確做法是用子查詢加 JOIN 一次性查出完整結(jié)果SELECT w.id, w.content, w.created_at, u.username FROM weibo w JOIN user u ON u.id w.user_id WHERE w.user_id IN ( SELECT follow_user_id FROM follow WHERE user_id ? ) ORDER BY w.created_at DESC LIMIT 20 OFFSET 0;這條 SQL 的執(zhí)行順序和人們通常以為的不一樣不是先執(zhí)行 IN 子查詢再把結(jié)果拿去 JOIN而是優(yōu)化器會嘗試先把 follow 表按 user_id 定位再通過索引找對應(yīng)的 weibo 行。這里占位符 ? 必須用 PreparedStatement 傳參不能直接字符串拼接否則一個包含單引號的微博內(nèi)容就能把整個查詢語法打碎這就是 SQL 注入的典型入口。分頁參數(shù) LIMIT 20 OFFSET 0 表達(dá)的是“每頁 20 條跳過 0 條”。頁碼越大OFFSET 值越大MySQL 需要先把前面所有行全部掃出來再丟棄這就是深翻頁變慢的根源。資源 DAO 層如果提供了“上一頁最后一條微博 id”這種參數(shù)走的其實是鍵集分頁比如 WHERE w.created_at ? ORDER BY w.created_at DESC LIMIT 20它能直接走索引而不需要掃已讀過的數(shù)據(jù)兩種分頁方式的性能差距在數(shù)據(jù)量過萬后會非常明顯。DAO 層把 SQL 查詢結(jié)果封裝成對象的常見思路是先取 ResultSet再逐行構(gòu)建 Weibo 對象組裝成 List 返回給 Service 層Service 層再把時間列表交給 Servlet 響應(yīng)輸出。這一層封裝雖然代碼量大卻是 JDBC 應(yīng)用的骨架。如果資源里使用了連接池配置那連接的生命周期管理就是下一章要說的重災(zāi)區(qū)。5. 前端 Java Web 避坑清單亂碼、連接耗盡、異步數(shù)據(jù)拿不到、會話丟失5.1 中文亂碼編碼設(shè)置的位置比想象中更早現(xiàn)象注冊時輸入“我喜歡寫代碼”存入數(shù)據(jù)庫變成“?????”頁面顯示一串問號。原因Servlet 容器解析請求體時默認(rèn)按 ISO-8859-1 讀取字節(jié)流。你調(diào)用 req.getParameter 的時候參數(shù)已經(jīng)被按錯誤編碼解碼了后面無論怎么 new String 轉(zhuǎn)碼都只能靠“先還原字節(jié)再重解”這步補救而這個還原過程一旦遇到不可逆字符就徹底失敗。解決在最先接觸請求的 Filter 里于讀取任何參數(shù)之前執(zhí)行 req.setCharacterEncoding(UTF-8)同時給 JDBC 連接 URL 加上 characterEncodingUTF-8建表時把字符集設(shè)為 utf8mb4。三處缺一不可Filter 少了請求參數(shù)報錯URL 少了數(shù)據(jù)庫寫入報錯表字符集少了存儲和展示不一致。5.2 數(shù)據(jù)庫連接不釋放一次忘記 close 引發(fā)的雪崩現(xiàn)象應(yīng)用剛啟動一切正常運行二十分鐘后隨便一個接口都卡住不動重啟后恢復(fù)過一段時間又卡死。原因Java 代碼里每次用 DriverManager.getConnection 創(chuàng)建連接用完后沒有 close。數(shù)據(jù)庫的最大連接數(shù)被占滿后新的請求只能排隊等超時。更隱蔽的是即使寫了 close如果出現(xiàn)在 try 塊中途拋異常的分支上close 根本執(zhí)行不到。解決統(tǒng)一使用連接池例如手動實現(xiàn)一個池或直接依賴容器數(shù)據(jù)源并在 finally 里釋放連接。連接的 close 方法在池化場景下不是真正斷開數(shù)據(jù)庫連接而是“歸還”到池中所以每個 Request 的處理都必須保證歸還否則池里的可用連接會越借越少。從 Java 7 開始的 try-with-resources 寫法能把釋放邏輯收斂到一行代碼中是這類資源里最值得優(yōu)先改造的點。5.3 Ajax 拿不到 JSON返回類型和響應(yīng)頭各錯一半現(xiàn)象前端用 fetch 或 XHR 異步請求 /api/timeline控制臺報解析錯誤后端日志沒異常MySQL 查詢也正常執(zhí)行。原因Servlet 往 response 里寫 JSON 字符串時既沒有設(shè)置 Content-Type又順手返回了一個 JSP 頁面。瀏覽器收到的是 text/html 類型把它當(dāng)網(wǎng)頁處理前端卻按 JSON 解析自然解析失敗。另一個常見原因是在 Servlet 里寫了 resp.sendRedirect把異步請求整個重定向到登錄頁前端拿到的是登錄頁的 HTML 片段。解決返回 JSON 的接口統(tǒng)一設(shè)置 resp.setContentType(application/json;charsetUTF-8)用第三方工具完成對象到 JSON 字符串的序列化同時避免在異步接口里使用重定向。前端 fetch 里也要加上請求頭聲明并在 then 里判斷 res.ok 再 res.json()。兩個方向各退半步問題就能定位到具體哪一側(cè)。5.4 Session 突然丟失登錄態(tài)容器重啟和瀏覽器策略各有一套邏輯現(xiàn)象后端代碼一改重啟應(yīng)用服務(wù)器所有用戶被踢下線用戶用無痕窗口打開應(yīng)用每次都要重新登錄。原因HttpSession 默認(rèn)存在容器內(nèi)存里不是存在數(shù)據(jù)庫或磁盤文件中。容器一重啟內(nèi)存清空所有 JSESSIONID 對應(yīng)的會話記錄全部消失。無痕窗口則是瀏覽器不保留會話級 Cookie 的表現(xiàn)因為容器默認(rèn)沒有給 JSESSIONID 設(shè)置 Max-AgeCookie 只存活在瀏覽器進(jìn)程內(nèi)存里窗口一關(guān)就沒了。解決先分清楚這是“預(yù)期行為”還是“產(chǎn)品缺陷”。個人微博項目里容器重啟導(dǎo)致掉線可以接受因為會話數(shù)據(jù)量小、重建成本低。如果產(chǎn)品要求“記住我”七天免登錄就要用持久化的登錄令牌方案登錄成功后生成一個隨機 token 存數(shù)據(jù)庫把 token 寫進(jìn)帶 Max-Age 的 Cookie 里后續(xù)請求用 token 換取用戶身份。這個方案繞開了 Session 內(nèi)存存儲的邊界代價是每次請求多一次數(shù)據(jù)庫查詢。5.5 時間線深翻頁越來越慢LIMIT/OFFSET 的代價被低估現(xiàn)象前幾頁的微博秒開翻到三十頁后接口耗時從 80ms 漲到 800ms。原因數(shù)據(jù)庫要 LIMIT 20 OFFSET 580就得從最新的那條開始一路數(shù)過前六百條再丟掉前五百八十條。翻得越深數(shù)據(jù)庫做的工作越多而且這些被丟掉的行沒法走索引加速。解決改用鍵集分頁。前端把當(dāng)前頁最后一條微博的時間或 id 傳回后端SQL 改成 WHERE w.created_at ? ORDER BY w.created_at DESC LIMIT 20。這個條件下數(shù)據(jù)庫能直接利用索引定位到指定位置隨后順序讀取不管翻到多深掃描范圍都固定在一頁大小內(nèi)。注意時間字段在有數(shù)據(jù)重復(fù)時可能出現(xiàn)同一秒內(nèi)多條記錄被跳過的問題穩(wěn)妥做法是使用 id 作為排序鍵因為 id 在主鍵索引里天然單調(diào)遞增、單調(diào)且唯一。6. 從這門課的設(shè)計取舍再看現(xiàn)代的 Java Web手寫鏈路的價值在框架封裝之后反而更清晰普通 Web 開發(fā)者現(xiàn)在寫接口習(xí)慣是框架一鍵生成前端發(fā)請求后端寫一個 controller 方法加幾個注解返回 JSON完事??雌饋砗?Servlet 時代天差地別但對照前幾章的鏈路你會發(fā)現(xiàn) Spring 這類框架做的每一件事都是在用更抽象的方式重新表達(dá) Servlet 規(guī)范里已有的機制RequestMapping 替代了 web.xml 里的 URL 映射攔截器替代了 Filter 鏈JdbcTemplate 替你把 PreparedStatement 的創(chuàng)建、參數(shù)綁定、ResultSet 遍歷收進(jìn)了一行代碼而控制器方法拿到 HttpSession 參數(shù)的過程本質(zhì)上仍是容器在調(diào)用鏈里解析并注入會話對象。所以我一直覺得先手寫一遍 Servlet、Service、DAO 三層再去看框架效率是最高的。你踩過的亂碼、連接池耗盡、會話丟失、分頁變慢每一個坑對應(yīng)的都是框架底層幫你封裝的某個組件。框架不是魔法它是把你的血淚經(jīng)驗固化成了一批默認(rèn)配置。遇到復(fù)雜的異常時如果腦子里沒有這條手寫鏈路就只能對著堆棧瞎猜。驗證這套理解最直接的方法打開瀏覽器開發(fā)者工具觀察從輸入 URL 到頁面渲染完成的網(wǎng)絡(luò)請求時間線找到那個異步加載時間線的請求點開它的請求頭找到 Cookie 里的 JSESSIONID再去服務(wù)器日志里找到那次請求對應(yīng)的 SQL 日志你可以親眼看到 session 里的 userId 被拼進(jìn)了哪個查詢條件。從那以后我每接手一個 Java Web 項目第一件事不是跑起來而是先把它的請求鏈路圖畫一遍哪個 Filter 先執(zhí)行、哪個 Servlet 處理、Session 從哪里寫入又在何處讀、數(shù)據(jù)庫連接的生命周期覆蓋在哪個調(diào)用范圍內(nèi)。這張圖畫清楚了項目里百分之八十的詭異問題都有了定位方向。希望這份從請求到會話再到數(shù)據(jù)的完整鏈路拆解能幫你在跑通這份資源時少走幾步當(dāng)年我走過的彎路。本文還有配套的精品資源點擊獲取