[{"data":1,"prerenderedAt":3403},["ShallowReactive",2],{"post:\u002Fposts\u002Fjavaspring\u002Fspring-notes-09":3,"posts:ordered":3126},{"id":4,"title":5,"body":6,"category":3110,"cover":3111,"date":3112,"description":3113,"extension":3114,"featured":3115,"meta":3116,"navigation":318,"path":3117,"seo":3118,"series":3119,"seriesOrder":421,"stem":3120,"tags":3121,"__hash__":3125},"posts\u002Fposts\u002Fjavaspring\u002Fspring-notes-09.md","Spring 学习笔记 09：拦截器 HandlerInterceptor",{"type":7,"value":8,"toc":3097},"minimark",[9,16,21,24,163,175,184,188,191,279,285,289,296,463,541,544,589,593,922,925,964,968,1326,1339,1343,1358,1912,1918,1921,1969,1973,1986,2077,2092,2096,2103,2147,2465,2476,2526,2533,2537,2540,2781,2784,2822,2838,2842,3005,3009,3086,3093],[10,11,12],"blockquote",{},[13,14,15],"p",{},"这是一份个人学习笔记：概念部分是我按自己的理解写的，示例代码都是自己敲的，参考的是\nSpring 官方文档。理解有偏差的地方欢迎指出。",[17,18,20],"h2",{"id":19},"_0-每个控制器都抄一遍的检查","0 每个控制器都抄一遍的检查",[13,22,23],{},"登录之后，很多页面要「只有登录用户能看」。最笨的做法是在每个方法开头写：",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-java shiki shiki-themes github-light github-dark","@GetMapping(\"\u002F{id}\")\npublic String detail(@PathVariable Long id, @SessionAttribute(name = \"loginUser\", required = false) User user,\n                     Model model) {\n    if (user == null) {\n        return \"redirect:\u002Faccount\u002Flogin\";\n    }\n    \u002F\u002F ...\n}\n","java","",[32,33,34,57,108,114,132,144,150,157],"code",{"__ignoreMap":30},[35,36,39,43,47,50,54],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sVt8B","@",[35,44,46],{"class":45},"szBVR","GetMapping",[35,48,49],{"class":41},"(",[35,51,53],{"class":52},"sZZnC","\"\u002F{id}\"",[35,55,56],{"class":41},")\n",[35,58,60,63,66,70,73,76,79,82,84,88,91,94,97,100,102,105],{"class":37,"line":59},2,[35,61,62],{"class":45},"public",[35,64,65],{"class":41}," String ",[35,67,69],{"class":68},"sScJk","detail",[35,71,72],{"class":41},"(@",[35,74,75],{"class":45},"PathVariable",[35,77,78],{"class":41}," Long id, @",[35,80,81],{"class":45},"SessionAttribute",[35,83,49],{"class":41},[35,85,87],{"class":86},"sj4cs","name",[35,89,90],{"class":45}," =",[35,92,93],{"class":52}," \"loginUser\"",[35,95,96],{"class":41},", ",[35,98,99],{"class":86},"required",[35,101,90],{"class":45},[35,103,104],{"class":86}," false",[35,106,107],{"class":41},") User user,\n",[35,109,111],{"class":37,"line":110},3,[35,112,113],{"class":41},"                     Model model) {\n",[35,115,117,120,123,126,129],{"class":37,"line":116},4,[35,118,119],{"class":45},"    if",[35,121,122],{"class":41}," (user ",[35,124,125],{"class":45},"==",[35,127,128],{"class":86}," null",[35,130,131],{"class":41},") {\n",[35,133,135,138,141],{"class":37,"line":134},5,[35,136,137],{"class":45},"        return",[35,139,140],{"class":52}," \"redirect:\u002Faccount\u002Flogin\"",[35,142,143],{"class":41},";\n",[35,145,147],{"class":37,"line":146},6,[35,148,149],{"class":41},"    }\n",[35,151,153],{"class":37,"line":152},7,[35,154,156],{"class":155},"sJ8bj","    \u002F\u002F ...\n",[35,158,160],{"class":37,"line":159},8,[35,161,162],{"class":41},"}\n",[13,164,165,166,170,171,174],{},"两个问题：",[167,168,169],"strong",{},"重复","（几十个方法都要抄）、",[167,172,173],{},"容易漏","（新加一个接口忘了加判断，就是一个漏洞）。\n这类「和业务无关、但每个请求都要做」的逻辑，需要一个集中入口。",[13,176,177,178,183],{},"Spring MVC 给的入口是 ",[167,179,180],{},[32,181,182],{},"HandlerInterceptor","。",[17,185,187],{"id":186},"_1-拦截器与过滤器的分工","1 拦截器与过滤器的分工",[13,189,190],{},"先在概念上分清两个容易混的东西：",[192,193,194,215],"table",{},[195,196,197],"thead",{},[198,199,200,204,209],"tr",{},[201,202,203],"th",{},"对比项",[201,205,206,207],{},"拦截器 ",[32,208,182],{},[201,210,211,212],{},"过滤器 ",[32,213,214],{},"Filter",[216,217,218,230,243,257,268],"tbody",{},[198,219,220,224,227],{},[221,222,223],"td",{},"归属",[221,225,226],{},"Spring MVC 的组件",[221,228,229],{},"Servlet 容器的组件",[198,231,232,235,240],{},[221,233,234],{},"作用范围",[221,236,237],{},[167,238,239],{},"只作用于交给 Controller 的请求",[221,241,242],{},"所有经过容器的请求（含静态资源）",[198,244,245,248,254],{},[221,246,247],{},"能拿到的东西",[221,249,250,251],{},"处理方法、注解、",[32,252,253],{},"ModelAndView",[221,255,256],{},"只有原始请求与响应",[198,258,259,262,265],{},[221,260,261],{},"是否 Spring Bean",[221,263,264],{},"是，可以注入其他 Bean",[221,266,267],{},"注册为 Bean 时可以，否则不能",[198,269,270,273,276],{},[221,271,272],{},"典型用途",[221,274,275],{},"登录校验、权限、埋点、给视图塞公共数据",[221,277,278],{},"字符编码、CORS、请求体日志、压缩",[13,280,281,282],{},"判断标准很简单：",[167,283,284],{},"需要知道「哪个处理方法」的信息，用拦截器；只想在 Servlet 层做点事，用过滤器。",[17,286,288],{"id":287},"_2-三个回调三种时机","2 三个回调，三种时机",[13,290,291],{},[292,293],"img",{"alt":294,"src":295},"拦截器执行顺序：preHandle 按注册顺序执行，Controller 之后 postHandle 逆序执行，视图渲染完成后 afterCompletion 再逆序执行","\u002Fimages\u002Fspring-notes-09-interceptor-flow.svg",[25,297,299],{"className":27,"code":298,"language":29,"meta":30,"style":30},"public interface HandlerInterceptor {\n\n    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)\n            throws Exception { return true; }\n\n    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,\n                            ModelAndView modelAndView) throws Exception { }\n\n    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,\n                                 Exception ex) throws Exception { }\n}\n",[32,300,301,314,320,352,369,373,398,415,419,443,458],{"__ignoreMap":30},[35,302,303,305,308,311],{"class":37,"line":38},[35,304,62],{"class":45},[35,306,307],{"class":45}," interface",[35,309,310],{"class":68}," HandlerInterceptor",[35,312,313],{"class":41}," {\n",[35,315,316],{"class":37,"line":59},[35,317,319],{"emptyLinePlaceholder":318},true,"\n",[35,321,322,325,328,331,334,338,341,344,347,350],{"class":37,"line":110},[35,323,324],{"class":45},"    default",[35,326,327],{"class":45}," boolean",[35,329,330],{"class":68}," preHandle",[35,332,333],{"class":41},"(HttpServletRequest ",[35,335,337],{"class":336},"s4XuR","request",[35,339,340],{"class":41},", HttpServletResponse ",[35,342,343],{"class":336},"response",[35,345,346],{"class":41},", Object ",[35,348,349],{"class":336},"handler",[35,351,56],{"class":41},[35,353,354,357,360,363,366],{"class":37,"line":116},[35,355,356],{"class":45},"            throws",[35,358,359],{"class":41}," Exception { ",[35,361,362],{"class":45},"return",[35,364,365],{"class":86}," true",[35,367,368],{"class":41},"; }\n",[35,370,371],{"class":37,"line":134},[35,372,319],{"emptyLinePlaceholder":318},[35,374,375,377,380,383,385,387,389,391,393,395],{"class":37,"line":146},[35,376,324],{"class":45},[35,378,379],{"class":45}," void",[35,381,382],{"class":68}," postHandle",[35,384,333],{"class":41},[35,386,337],{"class":336},[35,388,340],{"class":41},[35,390,343],{"class":336},[35,392,346],{"class":41},[35,394,349],{"class":336},[35,396,397],{"class":41},",\n",[35,399,400,403,406,409,412],{"class":37,"line":152},[35,401,402],{"class":41},"                            ModelAndView ",[35,404,405],{"class":336},"modelAndView",[35,407,408],{"class":41},") ",[35,410,411],{"class":45},"throws",[35,413,414],{"class":41}," Exception { }\n",[35,416,417],{"class":37,"line":159},[35,418,319],{"emptyLinePlaceholder":318},[35,420,422,424,426,429,431,433,435,437,439,441],{"class":37,"line":421},9,[35,423,324],{"class":45},[35,425,379],{"class":45},[35,427,428],{"class":68}," afterCompletion",[35,430,333],{"class":41},[35,432,337],{"class":336},[35,434,340],{"class":41},[35,436,343],{"class":336},[35,438,346],{"class":41},[35,440,349],{"class":336},[35,442,397],{"class":41},[35,444,446,449,452,454,456],{"class":37,"line":445},10,[35,447,448],{"class":41},"                                 Exception ",[35,450,451],{"class":336},"ex",[35,453,408],{"class":41},[35,455,411],{"class":45},[35,457,414],{"class":41},[35,459,461],{"class":37,"line":460},11,[35,462,162],{"class":41},[192,464,465,478],{},[195,466,467],{},[198,468,469,472,475],{},[201,470,471],{},"回调",[201,473,474],{},"时机",[201,476,477],{},"能做什么",[216,479,480,503,522],{},[198,481,482,487,493],{},[221,483,484],{},[32,485,486],{},"preHandle",[221,488,489,490],{},"交给 Controller ",[167,491,492],{},"之前",[221,494,495,496],{},"校验、拦截、放数据到请求属性；",[167,497,498,499,502],{},"返回 ",[32,500,501],{},"false"," 即中断",[198,504,505,510,516],{},[221,506,507],{},[32,508,509],{},"postHandle",[221,511,512,513],{},"Controller 返回后、",[167,514,515],{},"视图渲染之前",[221,517,518,519,521],{},"往 ",[32,520,253],{}," 里加公共数据",[198,523,524,529,535],{},[221,525,526],{},[32,527,528],{},"afterCompletion",[221,530,531,534],{},[167,532,533],{},"视图渲染之后","（无论成功还是异常）",[221,536,537,538,540],{},"记录耗时、清理资源；异常会通过 ",[32,539,451],{}," 传进来",[13,542,543],{},"两个必须记住的性质：",[545,546,547,563],"ul",{},[548,549,550,553,554,556,557,559,560,562],"li",{},[167,551,552],{},"顺序不对称","：",[32,555,486],{}," 按注册顺序执行（A → B → C），",[32,558,509],{}," 与 ",[32,561,528],{},"\n按相反顺序执行（C → B → A）。原因是它们构成「进栈、出栈」的结构，最先进入的最晚收尾；",[548,564,565,568,569,571,572,574,575,585,586,588],{},[167,566,567],{},"中断规则","：某个 ",[32,570,486],{}," 返回 ",[32,573,501],{},"，后面的拦截器与控制器都不会执行，但",[167,576,577,578,581,582,584],{},"已经返回过\n",[32,579,580],{},"true"," 的拦截器的 ",[32,583,528],{}," 仍会被调用","。所以清理逻辑放 ",[32,587,528],{}," 里最安全。",[17,590,592],{"id":591},"_3-实战一登录拦截","3 实战一：登录拦截",[25,594,596],{"className":27,"code":595,"language":29,"meta":30,"style":30},"package com.example.library.web.interceptor;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport jakarta.servlet.http.HttpSession;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport java.io.IOException;\n\n@Component\npublic class LoginInterceptor implements HandlerInterceptor {\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)\n            throws IOException {\n\n        \u002F\u002F 静态资源等非控制器请求走的是别的 Handler，直接放行\n        if (!(handler instanceof HandlerMethod)) {\n            return true;\n        }\n\n        HttpSession session = request.getSession(false);   \u002F\u002F 不要创建新会话\n        if (session != null && session.getAttribute(\"loginUser\") != null) {\n            return true;\n        }\n\n        response.sendRedirect(request.getContextPath() + \"\u002Faccount\u002Flogin\");\n        return false;    \u002F\u002F 中断请求，别往下走\n    }\n}\n",[32,597,598,606,610,618,625,632,639,646,653,657,664,668,676,694,699,708,732,740,745,751,772,782,788,793,818,853,862,867,872,899,912,917],{"__ignoreMap":30},[35,599,600,603],{"class":37,"line":38},[35,601,602],{"class":45},"package",[35,604,605],{"class":41}," com.example.library.web.interceptor;\n",[35,607,608],{"class":37,"line":59},[35,609,319],{"emptyLinePlaceholder":318},[35,611,612,615],{"class":37,"line":110},[35,613,614],{"class":45},"import",[35,616,617],{"class":41}," jakarta.servlet.http.HttpServletRequest;\n",[35,619,620,622],{"class":37,"line":116},[35,621,614],{"class":45},[35,623,624],{"class":41}," jakarta.servlet.http.HttpServletResponse;\n",[35,626,627,629],{"class":37,"line":134},[35,628,614],{"class":45},[35,630,631],{"class":41}," jakarta.servlet.http.HttpSession;\n",[35,633,634,636],{"class":37,"line":146},[35,635,614],{"class":45},[35,637,638],{"class":41}," org.springframework.stereotype.Component;\n",[35,640,641,643],{"class":37,"line":152},[35,642,614],{"class":45},[35,644,645],{"class":41}," org.springframework.web.method.HandlerMethod;\n",[35,647,648,650],{"class":37,"line":159},[35,649,614],{"class":45},[35,651,652],{"class":41}," org.springframework.web.servlet.HandlerInterceptor;\n",[35,654,655],{"class":37,"line":421},[35,656,319],{"emptyLinePlaceholder":318},[35,658,659,661],{"class":37,"line":445},[35,660,614],{"class":45},[35,662,663],{"class":41}," java.io.IOException;\n",[35,665,666],{"class":37,"line":460},[35,667,319],{"emptyLinePlaceholder":318},[35,669,671,673],{"class":37,"line":670},12,[35,672,42],{"class":41},[35,674,675],{"class":45},"Component\n",[35,677,679,681,684,687,690,692],{"class":37,"line":678},13,[35,680,62],{"class":45},[35,682,683],{"class":45}," class",[35,685,686],{"class":68}," LoginInterceptor",[35,688,689],{"class":45}," implements",[35,691,310],{"class":68},[35,693,313],{"class":41},[35,695,697],{"class":37,"line":696},14,[35,698,319],{"emptyLinePlaceholder":318},[35,700,702,705],{"class":37,"line":701},15,[35,703,704],{"class":41},"    @",[35,706,707],{"class":45},"Override\n",[35,709,711,714,716,718,720,722,724,726,728,730],{"class":37,"line":710},16,[35,712,713],{"class":45},"    public",[35,715,327],{"class":45},[35,717,330],{"class":68},[35,719,333],{"class":41},[35,721,337],{"class":336},[35,723,340],{"class":41},[35,725,343],{"class":336},[35,727,346],{"class":41},[35,729,349],{"class":336},[35,731,56],{"class":41},[35,733,735,737],{"class":37,"line":734},17,[35,736,356],{"class":45},[35,738,739],{"class":41}," IOException {\n",[35,741,743],{"class":37,"line":742},18,[35,744,319],{"emptyLinePlaceholder":318},[35,746,748],{"class":37,"line":747},19,[35,749,750],{"class":155},"        \u002F\u002F 静态资源等非控制器请求走的是别的 Handler，直接放行\n",[35,752,754,757,760,763,766,769],{"class":37,"line":753},20,[35,755,756],{"class":45},"        if",[35,758,759],{"class":41}," (",[35,761,762],{"class":45},"!",[35,764,765],{"class":41},"(handler ",[35,767,768],{"class":45},"instanceof",[35,770,771],{"class":41}," HandlerMethod)) {\n",[35,773,775,778,780],{"class":37,"line":774},21,[35,776,777],{"class":45},"            return",[35,779,365],{"class":86},[35,781,143],{"class":41},[35,783,785],{"class":37,"line":784},22,[35,786,787],{"class":41},"        }\n",[35,789,791],{"class":37,"line":790},23,[35,792,319],{"emptyLinePlaceholder":318},[35,794,796,799,802,805,808,810,812,815],{"class":37,"line":795},24,[35,797,798],{"class":41},"        HttpSession session ",[35,800,801],{"class":45},"=",[35,803,804],{"class":41}," request.",[35,806,807],{"class":68},"getSession",[35,809,49],{"class":41},[35,811,501],{"class":86},[35,813,814],{"class":41},");   ",[35,816,817],{"class":155},"\u002F\u002F 不要创建新会话\n",[35,819,821,823,826,829,831,834,837,840,842,845,847,849,851],{"class":37,"line":820},25,[35,822,756],{"class":45},[35,824,825],{"class":41}," (session ",[35,827,828],{"class":45},"!=",[35,830,128],{"class":86},[35,832,833],{"class":45}," &&",[35,835,836],{"class":41}," session.",[35,838,839],{"class":68},"getAttribute",[35,841,49],{"class":41},[35,843,844],{"class":52},"\"loginUser\"",[35,846,408],{"class":41},[35,848,828],{"class":45},[35,850,128],{"class":86},[35,852,131],{"class":41},[35,854,856,858,860],{"class":37,"line":855},26,[35,857,777],{"class":45},[35,859,365],{"class":86},[35,861,143],{"class":41},[35,863,865],{"class":37,"line":864},27,[35,866,787],{"class":41},[35,868,870],{"class":37,"line":869},28,[35,871,319],{"emptyLinePlaceholder":318},[35,873,875,878,881,884,887,890,893,896],{"class":37,"line":874},29,[35,876,877],{"class":41},"        response.",[35,879,880],{"class":68},"sendRedirect",[35,882,883],{"class":41},"(request.",[35,885,886],{"class":68},"getContextPath",[35,888,889],{"class":41},"() ",[35,891,892],{"class":45},"+",[35,894,895],{"class":52}," \"\u002Faccount\u002Flogin\"",[35,897,898],{"class":41},");\n",[35,900,902,904,906,909],{"class":37,"line":901},30,[35,903,137],{"class":45},[35,905,104],{"class":86},[35,907,908],{"class":41},";    ",[35,910,911],{"class":155},"\u002F\u002F 中断请求，别往下走\n",[35,913,915],{"class":37,"line":914},31,[35,916,149],{"class":41},[35,918,920],{"class":37,"line":919},32,[35,921,162],{"class":41},[13,923,924],{},"三个细节都能决定这个拦截器是否可用：",[926,927,928,937,952],"ol",{},[548,929,930,936],{},[167,931,932,935],{},[32,933,934],{},"handler instanceof HandlerMethod"," 的判断","：静态资源的请求交给的不是控制器方法，没有这层\n判断时，拦截器会把图片、样式也一起拦下来重定向——页面会莫名样式全丢；",[548,938,939,553,944,947,948,951],{},[167,940,941],{},[32,942,943],{},"request.getSession(false)",[32,945,946],{},"getSession()"," 在没有会话时会",[167,949,950],{},"创建一个新的","，等于给每个\n匿名访问者都建会话，白白占内存；",[548,953,954,963],{},[167,955,956,959,960],{},[32,957,958],{},"response.sendRedirect(...)"," 之后必须 ",[32,961,962],{},"return false","。忘了返回，请求会继续走到控制器，\n重定向之后又渲染了一遍页面，行为完全错乱。",[17,965,967],{"id":966},"_4-实战二请求耗时统计","4 实战二：请求耗时统计",[25,969,971],{"className":27,"code":970,"language":29,"meta":30,"style":30},"package com.example.library.web.interceptor;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\n@Component\npublic class TimingInterceptor implements HandlerInterceptor {\n\n    private static final Logger log = LoggerFactory.getLogger(TimingInterceptor.class);\n    private static final String START = TimingInterceptor.class.getName() + \".start\";\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {\n        request.setAttribute(START, System.nanoTime());\n        return true;\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,\n                                Object handler, Exception ex) {\n        Object start = request.getAttribute(START);\n        if (start != null) {\n            \u002F\u002F 注意：Object 不能直接强转成基本类型，要先转成 Long 再自动拆箱；\n            \u002F\u002F 写成 (long) start 是编译不过的\n            long costMillis = (System.nanoTime() - (Long) start) \u002F 1_000_000;\n            log.info(\"{} {} 状态 {} 耗时 {} ms\",\n                    request.getMethod(), request.getRequestURI(), response.getStatus(), costMillis);\n        }\n    }\n}\n",[32,972,973,979,983,989,995,1002,1009,1015,1021,1025,1031,1046,1050,1075,1103,1107,1113,1135,1152,1160,1164,1168,1174,1192,1206,1220,1233,1238,1243,1274,1289,1312,1316,1321],{"__ignoreMap":30},[35,974,975,977],{"class":37,"line":38},[35,976,602],{"class":45},[35,978,605],{"class":41},[35,980,981],{"class":37,"line":59},[35,982,319],{"emptyLinePlaceholder":318},[35,984,985,987],{"class":37,"line":110},[35,986,614],{"class":45},[35,988,617],{"class":41},[35,990,991,993],{"class":37,"line":116},[35,992,614],{"class":45},[35,994,624],{"class":41},[35,996,997,999],{"class":37,"line":134},[35,998,614],{"class":45},[35,1000,1001],{"class":41}," org.slf4j.Logger;\n",[35,1003,1004,1006],{"class":37,"line":146},[35,1005,614],{"class":45},[35,1007,1008],{"class":41}," org.slf4j.LoggerFactory;\n",[35,1010,1011,1013],{"class":37,"line":152},[35,1012,614],{"class":45},[35,1014,638],{"class":41},[35,1016,1017,1019],{"class":37,"line":159},[35,1018,614],{"class":45},[35,1020,652],{"class":41},[35,1022,1023],{"class":37,"line":421},[35,1024,319],{"emptyLinePlaceholder":318},[35,1026,1027,1029],{"class":37,"line":445},[35,1028,42],{"class":41},[35,1030,675],{"class":45},[35,1032,1033,1035,1037,1040,1042,1044],{"class":37,"line":460},[35,1034,62],{"class":45},[35,1036,683],{"class":45},[35,1038,1039],{"class":68}," TimingInterceptor",[35,1041,689],{"class":45},[35,1043,310],{"class":68},[35,1045,313],{"class":41},[35,1047,1048],{"class":37,"line":670},[35,1049,319],{"emptyLinePlaceholder":318},[35,1051,1052,1055,1058,1061,1064,1066,1069,1072],{"class":37,"line":678},[35,1053,1054],{"class":45},"    private",[35,1056,1057],{"class":45}," static",[35,1059,1060],{"class":45}," final",[35,1062,1063],{"class":41}," Logger log ",[35,1065,801],{"class":45},[35,1067,1068],{"class":41}," LoggerFactory.",[35,1070,1071],{"class":68},"getLogger",[35,1073,1074],{"class":41},"(TimingInterceptor.class);\n",[35,1076,1077,1079,1081,1083,1086,1088,1091,1094,1096,1098,1101],{"class":37,"line":696},[35,1078,1054],{"class":45},[35,1080,1057],{"class":45},[35,1082,1060],{"class":45},[35,1084,1085],{"class":41}," String START ",[35,1087,801],{"class":45},[35,1089,1090],{"class":41}," TimingInterceptor.class.",[35,1092,1093],{"class":68},"getName",[35,1095,889],{"class":41},[35,1097,892],{"class":45},[35,1099,1100],{"class":52}," \".start\"",[35,1102,143],{"class":41},[35,1104,1105],{"class":37,"line":701},[35,1106,319],{"emptyLinePlaceholder":318},[35,1108,1109,1111],{"class":37,"line":710},[35,1110,704],{"class":41},[35,1112,707],{"class":45},[35,1114,1115,1117,1119,1121,1123,1125,1127,1129,1131,1133],{"class":37,"line":734},[35,1116,713],{"class":45},[35,1118,327],{"class":45},[35,1120,330],{"class":68},[35,1122,333],{"class":41},[35,1124,337],{"class":336},[35,1126,340],{"class":41},[35,1128,343],{"class":336},[35,1130,346],{"class":41},[35,1132,349],{"class":336},[35,1134,131],{"class":41},[35,1136,1137,1140,1143,1146,1149],{"class":37,"line":742},[35,1138,1139],{"class":41},"        request.",[35,1141,1142],{"class":68},"setAttribute",[35,1144,1145],{"class":41},"(START, System.",[35,1147,1148],{"class":68},"nanoTime",[35,1150,1151],{"class":41},"());\n",[35,1153,1154,1156,1158],{"class":37,"line":747},[35,1155,137],{"class":45},[35,1157,365],{"class":86},[35,1159,143],{"class":41},[35,1161,1162],{"class":37,"line":753},[35,1163,149],{"class":41},[35,1165,1166],{"class":37,"line":774},[35,1167,319],{"emptyLinePlaceholder":318},[35,1169,1170,1172],{"class":37,"line":784},[35,1171,704],{"class":41},[35,1173,707],{"class":45},[35,1175,1176,1178,1180,1182,1184,1186,1188,1190],{"class":37,"line":790},[35,1177,713],{"class":45},[35,1179,379],{"class":45},[35,1181,428],{"class":68},[35,1183,333],{"class":41},[35,1185,337],{"class":336},[35,1187,340],{"class":41},[35,1189,343],{"class":336},[35,1191,397],{"class":41},[35,1193,1194,1197,1199,1202,1204],{"class":37,"line":795},[35,1195,1196],{"class":41},"                                Object ",[35,1198,349],{"class":336},[35,1200,1201],{"class":41},", Exception ",[35,1203,451],{"class":336},[35,1205,131],{"class":41},[35,1207,1208,1211,1213,1215,1217],{"class":37,"line":820},[35,1209,1210],{"class":41},"        Object start ",[35,1212,801],{"class":45},[35,1214,804],{"class":41},[35,1216,839],{"class":68},[35,1218,1219],{"class":41},"(START);\n",[35,1221,1222,1224,1227,1229,1231],{"class":37,"line":855},[35,1223,756],{"class":45},[35,1225,1226],{"class":41}," (start ",[35,1228,828],{"class":45},[35,1230,128],{"class":86},[35,1232,131],{"class":41},[35,1234,1235],{"class":37,"line":864},[35,1236,1237],{"class":155},"            \u002F\u002F 注意：Object 不能直接强转成基本类型，要先转成 Long 再自动拆箱；\n",[35,1239,1240],{"class":37,"line":869},[35,1241,1242],{"class":155},"            \u002F\u002F 写成 (long) start 是编译不过的\n",[35,1244,1245,1248,1251,1253,1256,1258,1260,1263,1266,1269,1272],{"class":37,"line":874},[35,1246,1247],{"class":45},"            long",[35,1249,1250],{"class":41}," costMillis ",[35,1252,801],{"class":45},[35,1254,1255],{"class":41}," (System.",[35,1257,1148],{"class":68},[35,1259,889],{"class":41},[35,1261,1262],{"class":45},"-",[35,1264,1265],{"class":41}," (Long) start) ",[35,1267,1268],{"class":45},"\u002F",[35,1270,1271],{"class":86}," 1_000_000",[35,1273,143],{"class":41},[35,1275,1276,1279,1282,1284,1287],{"class":37,"line":901},[35,1277,1278],{"class":41},"            log.",[35,1280,1281],{"class":68},"info",[35,1283,49],{"class":41},[35,1285,1286],{"class":52},"\"{} {} 状态 {} 耗时 {} ms\"",[35,1288,397],{"class":41},[35,1290,1291,1294,1297,1300,1303,1306,1309],{"class":37,"line":914},[35,1292,1293],{"class":41},"                    request.",[35,1295,1296],{"class":68},"getMethod",[35,1298,1299],{"class":41},"(), request.",[35,1301,1302],{"class":68},"getRequestURI",[35,1304,1305],{"class":41},"(), response.",[35,1307,1308],{"class":68},"getStatus",[35,1310,1311],{"class":41},"(), costMillis);\n",[35,1313,1314],{"class":37,"line":919},[35,1315,787],{"class":41},[35,1317,1319],{"class":37,"line":1318},33,[35,1320,149],{"class":41},[35,1322,1324],{"class":37,"line":1323},34,[35,1325,162],{"class":41},[13,1327,1328,1329,1331,1332,1334,1335,1338],{},"放在 ",[32,1330,528],{}," 里而不是 ",[32,1333,509],{}," 里，是因为",[167,1336,1337],{},"渲染模板的时间也要算进去","——只统计\n控制器耗时会低估响应时间。",[17,1340,1342],{"id":1341},"_5-注册拦截器","5 注册拦截器",[13,1344,1345,1346,1349,1350,1353,1354,1357],{},"拦截器只标 ",[32,1347,1348],{},"@Component"," ",[167,1351,1352],{},"不会生效","，必须在 ",[32,1355,1356],{},"WebMvcConfigurer"," 里注册，并指定路径模式：",[25,1359,1361],{"className":27,"code":1360,"language":29,"meta":30,"style":30},"package com.example.library.config;\n\nimport com.example.library.web.interceptor.AdminInterceptor;\nimport com.example.library.web.interceptor.LoginInterceptor;\nimport com.example.library.web.interceptor.TimingInterceptor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.LocaleResolver;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\nimport org.springframework.web.servlet.i18n.LocaleChangeInterceptor;\n\nimport java.util.Locale;\n\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n    private final LoginInterceptor loginInterceptor;\n    private final AdminInterceptor adminInterceptor;\n    private final TimingInterceptor timingInterceptor;\n\n    public WebConfig(LoginInterceptor loginInterceptor,\n                     AdminInterceptor adminInterceptor,\n                     TimingInterceptor timingInterceptor) {\n        this.loginInterceptor = loginInterceptor;\n        this.adminInterceptor = adminInterceptor;\n        this.timingInterceptor = timingInterceptor;\n    }\n\n    \u002F** 语言解析器（第 07 篇的 WhitelistLocaleResolver） *\u002F\n    @Bean\n    public LocaleResolver localeResolver() {\n        WhitelistLocaleResolver resolver = new WhitelistLocaleResolver();\n        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);\n        return resolver;\n    }\n\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        \u002F\u002F 先注册的先执行\n        registry.addInterceptor(timingInterceptor)\n                .addPathPatterns(\"\u002F**\")\n                .excludePathPatterns(\"\u002Fcss\u002F**\", \"\u002Fjs\u002F**\", \"\u002Fimages\u002F**\", \"\u002Ferror\", \"\u002Fh2-console\u002F**\");\n\n        registry.addInterceptor(loginInterceptor)\n                .addPathPatterns(\"\u002F**\")\n                .excludePathPatterns(\"\u002Faccount\u002F**\", \"\u002Fapi\u002F**\", \"\u002Ferror\", \"\u002Fh2-console\u002F**\");\n\n        \u002F\u002F 权限拦截器只对带 @RequireAdmin 的方法生效，所以路径不用排除\n        registry.addInterceptor(adminInterceptor)\n                .addPathPatterns(\"\u002F**\");\n\n        \u002F\u002F 支持 ?lang=en 切换语言（第 07 篇）\n        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();\n        localeChangeInterceptor.setParamName(\"lang\");\n        registry.addInterceptor(localeChangeInterceptor).addPathPatterns(\"\u002F**\");\n    }\n}\n",[32,1362,1363,1370,1374,1381,1388,1395,1402,1409,1416,1423,1430,1437,1441,1448,1452,1459,1475,1479,1488,1497,1506,1510,1524,1534,1544,1557,1569,1581,1585,1589,1594,1601,1614,1630,1641,1649,1654,1659,1666,1684,1690,1702,1718,1753,1758,1768,1781,1808,1813,1819,1829,1842,1847,1853,1868,1884,1902,1907],{"__ignoreMap":30},[35,1364,1365,1367],{"class":37,"line":38},[35,1366,602],{"class":45},[35,1368,1369],{"class":41}," com.example.library.config;\n",[35,1371,1372],{"class":37,"line":59},[35,1373,319],{"emptyLinePlaceholder":318},[35,1375,1376,1378],{"class":37,"line":110},[35,1377,614],{"class":45},[35,1379,1380],{"class":41}," com.example.library.web.interceptor.AdminInterceptor;\n",[35,1382,1383,1385],{"class":37,"line":116},[35,1384,614],{"class":45},[35,1386,1387],{"class":41}," com.example.library.web.interceptor.LoginInterceptor;\n",[35,1389,1390,1392],{"class":37,"line":134},[35,1391,614],{"class":45},[35,1393,1394],{"class":41}," com.example.library.web.interceptor.TimingInterceptor;\n",[35,1396,1397,1399],{"class":37,"line":146},[35,1398,614],{"class":45},[35,1400,1401],{"class":41}," org.springframework.context.annotation.Bean;\n",[35,1403,1404,1406],{"class":37,"line":152},[35,1405,614],{"class":45},[35,1407,1408],{"class":41}," org.springframework.context.annotation.Configuration;\n",[35,1410,1411,1413],{"class":37,"line":159},[35,1412,614],{"class":45},[35,1414,1415],{"class":41}," org.springframework.web.servlet.LocaleResolver;\n",[35,1417,1418,1420],{"class":37,"line":421},[35,1419,614],{"class":45},[35,1421,1422],{"class":41}," org.springframework.web.servlet.config.annotation.InterceptorRegistry;\n",[35,1424,1425,1427],{"class":37,"line":445},[35,1426,614],{"class":45},[35,1428,1429],{"class":41}," org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n",[35,1431,1432,1434],{"class":37,"line":460},[35,1433,614],{"class":45},[35,1435,1436],{"class":41}," org.springframework.web.servlet.i18n.LocaleChangeInterceptor;\n",[35,1438,1439],{"class":37,"line":670},[35,1440,319],{"emptyLinePlaceholder":318},[35,1442,1443,1445],{"class":37,"line":678},[35,1444,614],{"class":45},[35,1446,1447],{"class":41}," java.util.Locale;\n",[35,1449,1450],{"class":37,"line":696},[35,1451,319],{"emptyLinePlaceholder":318},[35,1453,1454,1456],{"class":37,"line":701},[35,1455,42],{"class":41},[35,1457,1458],{"class":45},"Configuration\n",[35,1460,1461,1463,1465,1468,1470,1473],{"class":37,"line":710},[35,1462,62],{"class":45},[35,1464,683],{"class":45},[35,1466,1467],{"class":68}," WebConfig",[35,1469,689],{"class":45},[35,1471,1472],{"class":68}," WebMvcConfigurer",[35,1474,313],{"class":41},[35,1476,1477],{"class":37,"line":734},[35,1478,319],{"emptyLinePlaceholder":318},[35,1480,1481,1483,1485],{"class":37,"line":742},[35,1482,1054],{"class":45},[35,1484,1060],{"class":45},[35,1486,1487],{"class":41}," LoginInterceptor loginInterceptor;\n",[35,1489,1490,1492,1494],{"class":37,"line":747},[35,1491,1054],{"class":45},[35,1493,1060],{"class":45},[35,1495,1496],{"class":41}," AdminInterceptor adminInterceptor;\n",[35,1498,1499,1501,1503],{"class":37,"line":753},[35,1500,1054],{"class":45},[35,1502,1060],{"class":45},[35,1504,1505],{"class":41}," TimingInterceptor timingInterceptor;\n",[35,1507,1508],{"class":37,"line":774},[35,1509,319],{"emptyLinePlaceholder":318},[35,1511,1512,1514,1516,1519,1522],{"class":37,"line":784},[35,1513,713],{"class":45},[35,1515,1467],{"class":68},[35,1517,1518],{"class":41},"(LoginInterceptor ",[35,1520,1521],{"class":336},"loginInterceptor",[35,1523,397],{"class":41},[35,1525,1526,1529,1532],{"class":37,"line":790},[35,1527,1528],{"class":41},"                     AdminInterceptor ",[35,1530,1531],{"class":336},"adminInterceptor",[35,1533,397],{"class":41},[35,1535,1536,1539,1542],{"class":37,"line":795},[35,1537,1538],{"class":41},"                     TimingInterceptor ",[35,1540,1541],{"class":336},"timingInterceptor",[35,1543,131],{"class":41},[35,1545,1546,1549,1552,1554],{"class":37,"line":820},[35,1547,1548],{"class":86},"        this",[35,1550,1551],{"class":41},".loginInterceptor ",[35,1553,801],{"class":45},[35,1555,1556],{"class":41}," loginInterceptor;\n",[35,1558,1559,1561,1564,1566],{"class":37,"line":855},[35,1560,1548],{"class":86},[35,1562,1563],{"class":41},".adminInterceptor ",[35,1565,801],{"class":45},[35,1567,1568],{"class":41}," adminInterceptor;\n",[35,1570,1571,1573,1576,1578],{"class":37,"line":864},[35,1572,1548],{"class":86},[35,1574,1575],{"class":41},".timingInterceptor ",[35,1577,801],{"class":45},[35,1579,1580],{"class":41}," timingInterceptor;\n",[35,1582,1583],{"class":37,"line":869},[35,1584,149],{"class":41},[35,1586,1587],{"class":37,"line":874},[35,1588,319],{"emptyLinePlaceholder":318},[35,1590,1591],{"class":37,"line":901},[35,1592,1593],{"class":155},"    \u002F** 语言解析器（第 07 篇的 WhitelistLocaleResolver） *\u002F\n",[35,1595,1596,1598],{"class":37,"line":914},[35,1597,704],{"class":41},[35,1599,1600],{"class":45},"Bean\n",[35,1602,1603,1605,1608,1611],{"class":37,"line":919},[35,1604,713],{"class":45},[35,1606,1607],{"class":41}," LocaleResolver ",[35,1609,1610],{"class":68},"localeResolver",[35,1612,1613],{"class":41},"() {\n",[35,1615,1616,1619,1621,1624,1627],{"class":37,"line":1318},[35,1617,1618],{"class":41},"        WhitelistLocaleResolver resolver ",[35,1620,801],{"class":45},[35,1622,1623],{"class":45}," new",[35,1625,1626],{"class":68}," WhitelistLocaleResolver",[35,1628,1629],{"class":41},"();\n",[35,1631,1632,1635,1638],{"class":37,"line":1323},[35,1633,1634],{"class":41},"        resolver.",[35,1636,1637],{"class":68},"setDefaultLocale",[35,1639,1640],{"class":41},"(Locale.SIMPLIFIED_CHINESE);\n",[35,1642,1644,1646],{"class":37,"line":1643},35,[35,1645,137],{"class":45},[35,1647,1648],{"class":41}," resolver;\n",[35,1650,1652],{"class":37,"line":1651},36,[35,1653,149],{"class":41},[35,1655,1657],{"class":37,"line":1656},37,[35,1658,319],{"emptyLinePlaceholder":318},[35,1660,1662,1664],{"class":37,"line":1661},38,[35,1663,704],{"class":41},[35,1665,707],{"class":45},[35,1667,1669,1671,1673,1676,1679,1682],{"class":37,"line":1668},39,[35,1670,713],{"class":45},[35,1672,379],{"class":45},[35,1674,1675],{"class":68}," addInterceptors",[35,1677,1678],{"class":41},"(InterceptorRegistry ",[35,1680,1681],{"class":336},"registry",[35,1683,131],{"class":41},[35,1685,1687],{"class":37,"line":1686},40,[35,1688,1689],{"class":155},"        \u002F\u002F 先注册的先执行\n",[35,1691,1693,1696,1699],{"class":37,"line":1692},41,[35,1694,1695],{"class":41},"        registry.",[35,1697,1698],{"class":68},"addInterceptor",[35,1700,1701],{"class":41},"(timingInterceptor)\n",[35,1703,1705,1708,1711,1713,1716],{"class":37,"line":1704},42,[35,1706,1707],{"class":41},"                .",[35,1709,1710],{"class":68},"addPathPatterns",[35,1712,49],{"class":41},[35,1714,1715],{"class":52},"\"\u002F**\"",[35,1717,56],{"class":41},[35,1719,1721,1723,1726,1728,1731,1733,1736,1738,1741,1743,1746,1748,1751],{"class":37,"line":1720},43,[35,1722,1707],{"class":41},[35,1724,1725],{"class":68},"excludePathPatterns",[35,1727,49],{"class":41},[35,1729,1730],{"class":52},"\"\u002Fcss\u002F**\"",[35,1732,96],{"class":41},[35,1734,1735],{"class":52},"\"\u002Fjs\u002F**\"",[35,1737,96],{"class":41},[35,1739,1740],{"class":52},"\"\u002Fimages\u002F**\"",[35,1742,96],{"class":41},[35,1744,1745],{"class":52},"\"\u002Ferror\"",[35,1747,96],{"class":41},[35,1749,1750],{"class":52},"\"\u002Fh2-console\u002F**\"",[35,1752,898],{"class":41},[35,1754,1756],{"class":37,"line":1755},44,[35,1757,319],{"emptyLinePlaceholder":318},[35,1759,1761,1763,1765],{"class":37,"line":1760},45,[35,1762,1695],{"class":41},[35,1764,1698],{"class":68},[35,1766,1767],{"class":41},"(loginInterceptor)\n",[35,1769,1771,1773,1775,1777,1779],{"class":37,"line":1770},46,[35,1772,1707],{"class":41},[35,1774,1710],{"class":68},[35,1776,49],{"class":41},[35,1778,1715],{"class":52},[35,1780,56],{"class":41},[35,1782,1784,1786,1788,1790,1793,1795,1798,1800,1802,1804,1806],{"class":37,"line":1783},47,[35,1785,1707],{"class":41},[35,1787,1725],{"class":68},[35,1789,49],{"class":41},[35,1791,1792],{"class":52},"\"\u002Faccount\u002F**\"",[35,1794,96],{"class":41},[35,1796,1797],{"class":52},"\"\u002Fapi\u002F**\"",[35,1799,96],{"class":41},[35,1801,1745],{"class":52},[35,1803,96],{"class":41},[35,1805,1750],{"class":52},[35,1807,898],{"class":41},[35,1809,1811],{"class":37,"line":1810},48,[35,1812,319],{"emptyLinePlaceholder":318},[35,1814,1816],{"class":37,"line":1815},49,[35,1817,1818],{"class":155},"        \u002F\u002F 权限拦截器只对带 @RequireAdmin 的方法生效，所以路径不用排除\n",[35,1820,1822,1824,1826],{"class":37,"line":1821},50,[35,1823,1695],{"class":41},[35,1825,1698],{"class":68},[35,1827,1828],{"class":41},"(adminInterceptor)\n",[35,1830,1832,1834,1836,1838,1840],{"class":37,"line":1831},51,[35,1833,1707],{"class":41},[35,1835,1710],{"class":68},[35,1837,49],{"class":41},[35,1839,1715],{"class":52},[35,1841,898],{"class":41},[35,1843,1845],{"class":37,"line":1844},52,[35,1846,319],{"emptyLinePlaceholder":318},[35,1848,1850],{"class":37,"line":1849},53,[35,1851,1852],{"class":155},"        \u002F\u002F 支持 ?lang=en 切换语言（第 07 篇）\n",[35,1854,1856,1859,1861,1863,1866],{"class":37,"line":1855},54,[35,1857,1858],{"class":41},"        LocaleChangeInterceptor localeChangeInterceptor ",[35,1860,801],{"class":45},[35,1862,1623],{"class":45},[35,1864,1865],{"class":68}," LocaleChangeInterceptor",[35,1867,1629],{"class":41},[35,1869,1871,1874,1877,1879,1882],{"class":37,"line":1870},55,[35,1872,1873],{"class":41},"        localeChangeInterceptor.",[35,1875,1876],{"class":68},"setParamName",[35,1878,49],{"class":41},[35,1880,1881],{"class":52},"\"lang\"",[35,1883,898],{"class":41},[35,1885,1887,1889,1891,1894,1896,1898,1900],{"class":37,"line":1886},56,[35,1888,1695],{"class":41},[35,1890,1698],{"class":68},[35,1892,1893],{"class":41},"(localeChangeInterceptor).",[35,1895,1710],{"class":68},[35,1897,49],{"class":41},[35,1899,1715],{"class":52},[35,1901,898],{"class":41},[35,1903,1905],{"class":37,"line":1904},57,[35,1906,149],{"class":41},[35,1908,1910],{"class":37,"line":1909},58,[35,1911,162],{"class":41},[13,1913,1914,1917],{},[167,1915,1916],{},"注册顺序就是执行顺序","，这一点在多个拦截器有依赖关系时必须留意（比如「记录操作日志」要依赖\n「已经解析出当前用户」）。",[13,1919,1920],{},"路径模式的坑：",[545,1922,1923,1942,1948,1956],{},[548,1924,1925,1926,1929,1930,1933,1934,1937,1938,1941],{},"模式是 Ant 风格：",[32,1927,1928],{},"\u002Fbooks\u002F*"," 只匹配一级（",[32,1931,1932],{},"\u002Fbooks\u002F1","），",[32,1935,1936],{},"\u002Fbooks\u002F**"," 匹配多级\n（",[32,1939,1940],{},"\u002Fbooks\u002F1\u002Fchapters","）；",[548,1943,1944,1947],{},[167,1945,1946],{},"模式是从应用路径之后开始匹配的","，不含 context path；",[548,1949,1950,1952,1953,1955],{},[32,1951,1725],{}," 的优先级高于 ",[32,1954,1710],{},"，排除写全了比写包含更保险；",[548,1957,1958,1964,1965,1968],{},[167,1959,1960,1961,1963],{},"排除静态资源路径后，还需要 ",[32,1962,349],{}," 类型判断","兜底：",[32,1966,1967],{},"\u002Ferror"," 这类内部跳转、以及一些没被\n路径模式覆盖的资源请求，仍可能走进拦截器。",[17,1970,1972],{"id":1971},"_6-拦截器与控制器之间传数据","6 拦截器与控制器之间传数据",[13,1974,1975,1977,1978,1981,1982,1985],{},[32,1976,486],{}," 里查到的数据放在",[167,1979,1980],{},"请求属性","里，控制器用 ",[32,1983,1984],{},"@RequestAttribute"," 取：",[25,1987,1989],{"className":27,"code":1988,"language":29,"meta":30,"style":30},"\u002F\u002F preHandle 中\nrequest.setAttribute(\"currentUser\", user);\n\n\u002F\u002F 控制器中\n@GetMapping\npublic String list(@RequestAttribute(\"currentUser\") User user, Model model) {\n    model.addAttribute(\"user\", user);\n    return \"books\u002Flist\";\n}\n",[32,1990,1991,1996,2011,2015,2020,2027,2048,2063,2073],{"__ignoreMap":30},[35,1992,1993],{"class":37,"line":38},[35,1994,1995],{"class":155},"\u002F\u002F preHandle 中\n",[35,1997,1998,2001,2003,2005,2008],{"class":37,"line":59},[35,1999,2000],{"class":41},"request.",[35,2002,1142],{"class":68},[35,2004,49],{"class":41},[35,2006,2007],{"class":52},"\"currentUser\"",[35,2009,2010],{"class":41},", user);\n",[35,2012,2013],{"class":37,"line":110},[35,2014,319],{"emptyLinePlaceholder":318},[35,2016,2017],{"class":37,"line":116},[35,2018,2019],{"class":155},"\u002F\u002F 控制器中\n",[35,2021,2022,2024],{"class":37,"line":134},[35,2023,42],{"class":41},[35,2025,2026],{"class":45},"GetMapping\n",[35,2028,2029,2031,2033,2036,2038,2041,2043,2045],{"class":37,"line":146},[35,2030,62],{"class":45},[35,2032,65],{"class":41},[35,2034,2035],{"class":68},"list",[35,2037,72],{"class":41},[35,2039,2040],{"class":45},"RequestAttribute",[35,2042,49],{"class":41},[35,2044,2007],{"class":52},[35,2046,2047],{"class":41},") User user, Model model) {\n",[35,2049,2050,2053,2056,2058,2061],{"class":37,"line":152},[35,2051,2052],{"class":41},"    model.",[35,2054,2055],{"class":68},"addAttribute",[35,2057,49],{"class":41},[35,2059,2060],{"class":52},"\"user\"",[35,2062,2010],{"class":41},[35,2064,2065,2068,2071],{"class":37,"line":159},[35,2066,2067],{"class":45},"    return",[35,2069,2070],{"class":52}," \"books\u002Flist\"",[35,2072,143],{"class":41},[35,2074,2075],{"class":37,"line":421},[35,2076,162],{"class":41},[13,2078,2079,2080,2087,2088,2091],{},"注意区别：",[167,2081,2082,2083,2086],{},"请求属性（",[32,2084,2085],{},"request.setAttribute","）只在本次请求内有效","，会话属性（",[32,2089,2090],{},"session.setAttribute","）\n跨请求存在。拦截器里放临时数据用请求属性，别往会话里塞。",[17,2093,2095],{"id":2094},"_7-用注解驱动拦截逻辑","7 用注解驱动拦截逻辑",[13,2097,2098,2099,2102],{},"拦截器能拿到 ",[32,2100,2101],{},"HandlerMethod","，因此可以读取方法上的注解，把「哪些接口要检查什么」交给注解声明：",[25,2104,2106],{"className":27,"code":2105,"language":29,"meta":30,"style":30},"@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface RequireAdmin {\n}\n",[32,2107,2108,2118,2128,2143],{"__ignoreMap":30},[35,2109,2110,2112,2115],{"class":37,"line":38},[35,2111,42],{"class":41},[35,2113,2114],{"class":45},"Target",[35,2116,2117],{"class":41},"(ElementType.METHOD)\n",[35,2119,2120,2122,2125],{"class":37,"line":59},[35,2121,42],{"class":41},[35,2123,2124],{"class":45},"Retention",[35,2126,2127],{"class":41},"(RetentionPolicy.RUNTIME)\n",[35,2129,2130,2132,2135,2138,2141],{"class":37,"line":110},[35,2131,62],{"class":45},[35,2133,2134],{"class":41}," @",[35,2136,2137],{"class":45},"interface",[35,2139,2140],{"class":45}," RequireAdmin",[35,2142,313],{"class":41},[35,2144,2145],{"class":37,"line":116},[35,2146,162],{"class":41},[25,2148,2150],{"className":27,"code":2149,"language":29,"meta":30,"style":30},"package com.example.library.web.interceptor;\n\nimport com.example.library.domain.User;\nimport com.example.library.web.RequireAdmin;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport jakarta.servlet.http.HttpSession;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport java.io.IOException;\n\n@Component\npublic class AdminInterceptor implements HandlerInterceptor {\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)\n            throws IOException {\n\n        if (handler instanceof HandlerMethod method\n                && method.getMethodAnnotation(RequireAdmin.class) != null\n                && !isAdmin(request)) {\n            response.sendError(HttpServletResponse.SC_FORBIDDEN);\n            return false;\n        }\n        return true;\n    }\n\n    private boolean isAdmin(HttpServletRequest request) {\n        HttpSession session = request.getSession(false);\n        Object loginUser = session == null ? null : session.getAttribute(\"loginUser\");\n        return loginUser instanceof User user && user.isAdmin();\n    }\n}\n",[32,2151,2152,2158,2162,2169,2176,2182,2188,2194,2200,2206,2212,2216,2222,2226,2232,2247,2251,2257,2279,2285,2289,2301,2320,2333,2344,2352,2356,2364,2368,2372,2387,2403,2435,2457,2461],{"__ignoreMap":30},[35,2153,2154,2156],{"class":37,"line":38},[35,2155,602],{"class":45},[35,2157,605],{"class":41},[35,2159,2160],{"class":37,"line":59},[35,2161,319],{"emptyLinePlaceholder":318},[35,2163,2164,2166],{"class":37,"line":110},[35,2165,614],{"class":45},[35,2167,2168],{"class":41}," com.example.library.domain.User;\n",[35,2170,2171,2173],{"class":37,"line":116},[35,2172,614],{"class":45},[35,2174,2175],{"class":41}," com.example.library.web.RequireAdmin;\n",[35,2177,2178,2180],{"class":37,"line":134},[35,2179,614],{"class":45},[35,2181,617],{"class":41},[35,2183,2184,2186],{"class":37,"line":146},[35,2185,614],{"class":45},[35,2187,624],{"class":41},[35,2189,2190,2192],{"class":37,"line":152},[35,2191,614],{"class":45},[35,2193,631],{"class":41},[35,2195,2196,2198],{"class":37,"line":159},[35,2197,614],{"class":45},[35,2199,638],{"class":41},[35,2201,2202,2204],{"class":37,"line":421},[35,2203,614],{"class":45},[35,2205,645],{"class":41},[35,2207,2208,2210],{"class":37,"line":445},[35,2209,614],{"class":45},[35,2211,652],{"class":41},[35,2213,2214],{"class":37,"line":460},[35,2215,319],{"emptyLinePlaceholder":318},[35,2217,2218,2220],{"class":37,"line":670},[35,2219,614],{"class":45},[35,2221,663],{"class":41},[35,2223,2224],{"class":37,"line":678},[35,2225,319],{"emptyLinePlaceholder":318},[35,2227,2228,2230],{"class":37,"line":696},[35,2229,42],{"class":41},[35,2231,675],{"class":45},[35,2233,2234,2236,2238,2241,2243,2245],{"class":37,"line":701},[35,2235,62],{"class":45},[35,2237,683],{"class":45},[35,2239,2240],{"class":68}," AdminInterceptor",[35,2242,689],{"class":45},[35,2244,310],{"class":68},[35,2246,313],{"class":41},[35,2248,2249],{"class":37,"line":710},[35,2250,319],{"emptyLinePlaceholder":318},[35,2252,2253,2255],{"class":37,"line":734},[35,2254,704],{"class":41},[35,2256,707],{"class":45},[35,2258,2259,2261,2263,2265,2267,2269,2271,2273,2275,2277],{"class":37,"line":742},[35,2260,713],{"class":45},[35,2262,327],{"class":45},[35,2264,330],{"class":68},[35,2266,333],{"class":41},[35,2268,337],{"class":336},[35,2270,340],{"class":41},[35,2272,343],{"class":336},[35,2274,346],{"class":41},[35,2276,349],{"class":336},[35,2278,56],{"class":41},[35,2280,2281,2283],{"class":37,"line":747},[35,2282,356],{"class":45},[35,2284,739],{"class":41},[35,2286,2287],{"class":37,"line":753},[35,2288,319],{"emptyLinePlaceholder":318},[35,2290,2291,2293,2296,2298],{"class":37,"line":774},[35,2292,756],{"class":45},[35,2294,2295],{"class":41}," (handler ",[35,2297,768],{"class":45},[35,2299,2300],{"class":41}," HandlerMethod method\n",[35,2302,2303,2306,2309,2312,2315,2317],{"class":37,"line":784},[35,2304,2305],{"class":45},"                &&",[35,2307,2308],{"class":41}," method.",[35,2310,2311],{"class":68},"getMethodAnnotation",[35,2313,2314],{"class":41},"(RequireAdmin.class) ",[35,2316,828],{"class":45},[35,2318,2319],{"class":86}," null\n",[35,2321,2322,2324,2327,2330],{"class":37,"line":790},[35,2323,2305],{"class":45},[35,2325,2326],{"class":45}," !",[35,2328,2329],{"class":68},"isAdmin",[35,2331,2332],{"class":41},"(request)) {\n",[35,2334,2335,2338,2341],{"class":37,"line":795},[35,2336,2337],{"class":41},"            response.",[35,2339,2340],{"class":68},"sendError",[35,2342,2343],{"class":41},"(HttpServletResponse.SC_FORBIDDEN);\n",[35,2345,2346,2348,2350],{"class":37,"line":820},[35,2347,777],{"class":45},[35,2349,104],{"class":86},[35,2351,143],{"class":41},[35,2353,2354],{"class":37,"line":855},[35,2355,787],{"class":41},[35,2357,2358,2360,2362],{"class":37,"line":864},[35,2359,137],{"class":45},[35,2361,365],{"class":86},[35,2363,143],{"class":41},[35,2365,2366],{"class":37,"line":869},[35,2367,149],{"class":41},[35,2369,2370],{"class":37,"line":874},[35,2371,319],{"emptyLinePlaceholder":318},[35,2373,2374,2376,2378,2381,2383,2385],{"class":37,"line":901},[35,2375,1054],{"class":45},[35,2377,327],{"class":45},[35,2379,2380],{"class":68}," isAdmin",[35,2382,333],{"class":41},[35,2384,337],{"class":336},[35,2386,131],{"class":41},[35,2388,2389,2391,2393,2395,2397,2399,2401],{"class":37,"line":914},[35,2390,798],{"class":41},[35,2392,801],{"class":45},[35,2394,804],{"class":41},[35,2396,807],{"class":68},[35,2398,49],{"class":41},[35,2400,501],{"class":86},[35,2402,898],{"class":41},[35,2404,2405,2408,2410,2413,2415,2417,2420,2422,2425,2427,2429,2431,2433],{"class":37,"line":919},[35,2406,2407],{"class":41},"        Object loginUser ",[35,2409,801],{"class":45},[35,2411,2412],{"class":41}," session ",[35,2414,125],{"class":45},[35,2416,128],{"class":86},[35,2418,2419],{"class":45}," ?",[35,2421,128],{"class":86},[35,2423,2424],{"class":45}," :",[35,2426,836],{"class":41},[35,2428,839],{"class":68},[35,2430,49],{"class":41},[35,2432,844],{"class":52},[35,2434,898],{"class":41},[35,2436,2437,2439,2442,2444,2447,2450,2453,2455],{"class":37,"line":1318},[35,2438,137],{"class":45},[35,2440,2441],{"class":41}," loginUser ",[35,2443,768],{"class":45},[35,2445,2446],{"class":41}," User user ",[35,2448,2449],{"class":45},"&&",[35,2451,2452],{"class":41}," user.",[35,2454,2329],{"class":68},[35,2456,1629],{"class":41},[35,2458,2459],{"class":37,"line":1323},[35,2460,149],{"class":41},[35,2462,2463],{"class":37,"line":1643},[35,2464,162],{"class":41},[13,2466,2467,2468,2471,2472,2475],{},"注册时不用排除任何路径：没标 ",[32,2469,2470],{},"@RequireAdmin"," 的方法会被直接放行（见第 5 节的 ",[32,2473,2474],{},"WebConfig","）。",[25,2477,2479],{"className":27,"code":2478,"language":29,"meta":30,"style":30},"@RequireAdmin\n@PostMapping(\"\u002F{id}\u002Fdelete\")\npublic String delete(@PathVariable Long id) {\n    \u002F\u002F ...\n}\n",[32,2480,2481,2488,2502,2518,2522],{"__ignoreMap":30},[35,2482,2483,2485],{"class":37,"line":38},[35,2484,42],{"class":41},[35,2486,2487],{"class":45},"RequireAdmin\n",[35,2489,2490,2492,2495,2497,2500],{"class":37,"line":59},[35,2491,42],{"class":41},[35,2493,2494],{"class":45},"PostMapping",[35,2496,49],{"class":41},[35,2498,2499],{"class":52},"\"\u002F{id}\u002Fdelete\"",[35,2501,56],{"class":41},[35,2503,2504,2506,2508,2511,2513,2515],{"class":37,"line":110},[35,2505,62],{"class":45},[35,2507,65],{"class":41},[35,2509,2510],{"class":68},"delete",[35,2512,72],{"class":41},[35,2514,75],{"class":45},[35,2516,2517],{"class":41}," Long id) {\n",[35,2519,2520],{"class":37,"line":116},[35,2521,156],{"class":155},[35,2523,2524],{"class":37,"line":134},[35,2525,162],{"class":41},[13,2527,2528,2529,2532],{},"这套「注解 + 拦截器」的组合比在拦截器里硬编码路径列表更好维护：",[167,2530,2531],{},"规则写在方法自己身上","，\n新增接口时不容易漏。这也是很多框架（包括 Spring Security 的方法级注解）的思路。",[17,2534,2536],{"id":2535},"_8-什么时候改用过滤器","8 什么时候改用过滤器",[13,2538,2539],{},"过滤器的场景更贴近 Servlet 层：",[25,2541,2543],{"className":27,"code":2542,"language":29,"meta":30,"style":30},"package com.example.library.web.filter;\n\nimport jakarta.servlet.FilterChain;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.filter.OncePerRequestFilter;\n\nimport java.io.IOException;\n\n@Component\npublic class RequestLoggingFilter extends OncePerRequestFilter {\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,\n                                    FilterChain filterChain) throws ServletException, IOException {\n        long start = System.nanoTime();\n        try {\n            filterChain.doFilter(request, response);\n        } finally {\n            long costMillis = (System.nanoTime() - start) \u002F 1_000_000;\n            log.info(\"{} {} 耗时 {} ms\", request.getMethod(), request.getRequestURI(), costMillis);\n        }\n    }\n}\n",[32,2544,2545,2552,2556,2563,2570,2576,2582,2588,2595,2599,2605,2609,2615,2632,2636,2642,2662,2677,2694,2701,2712,2722,2747,2769,2773,2777],{"__ignoreMap":30},[35,2546,2547,2549],{"class":37,"line":38},[35,2548,602],{"class":45},[35,2550,2551],{"class":41}," com.example.library.web.filter;\n",[35,2553,2554],{"class":37,"line":59},[35,2555,319],{"emptyLinePlaceholder":318},[35,2557,2558,2560],{"class":37,"line":110},[35,2559,614],{"class":45},[35,2561,2562],{"class":41}," jakarta.servlet.FilterChain;\n",[35,2564,2565,2567],{"class":37,"line":116},[35,2566,614],{"class":45},[35,2568,2569],{"class":41}," jakarta.servlet.ServletException;\n",[35,2571,2572,2574],{"class":37,"line":134},[35,2573,614],{"class":45},[35,2575,617],{"class":41},[35,2577,2578,2580],{"class":37,"line":146},[35,2579,614],{"class":45},[35,2581,624],{"class":41},[35,2583,2584,2586],{"class":37,"line":152},[35,2585,614],{"class":45},[35,2587,638],{"class":41},[35,2589,2590,2592],{"class":37,"line":159},[35,2591,614],{"class":45},[35,2593,2594],{"class":41}," org.springframework.web.filter.OncePerRequestFilter;\n",[35,2596,2597],{"class":37,"line":421},[35,2598,319],{"emptyLinePlaceholder":318},[35,2600,2601,2603],{"class":37,"line":445},[35,2602,614],{"class":45},[35,2604,663],{"class":41},[35,2606,2607],{"class":37,"line":460},[35,2608,319],{"emptyLinePlaceholder":318},[35,2610,2611,2613],{"class":37,"line":670},[35,2612,42],{"class":41},[35,2614,675],{"class":45},[35,2616,2617,2619,2621,2624,2627,2630],{"class":37,"line":678},[35,2618,62],{"class":45},[35,2620,683],{"class":45},[35,2622,2623],{"class":68}," RequestLoggingFilter",[35,2625,2626],{"class":45}," extends",[35,2628,2629],{"class":68}," OncePerRequestFilter",[35,2631,313],{"class":41},[35,2633,2634],{"class":37,"line":696},[35,2635,319],{"emptyLinePlaceholder":318},[35,2637,2638,2640],{"class":37,"line":701},[35,2639,704],{"class":41},[35,2641,707],{"class":45},[35,2643,2644,2647,2649,2652,2654,2656,2658,2660],{"class":37,"line":710},[35,2645,2646],{"class":45},"    protected",[35,2648,379],{"class":45},[35,2650,2651],{"class":68}," doFilterInternal",[35,2653,333],{"class":41},[35,2655,337],{"class":336},[35,2657,340],{"class":41},[35,2659,343],{"class":336},[35,2661,397],{"class":41},[35,2663,2664,2667,2670,2672,2674],{"class":37,"line":734},[35,2665,2666],{"class":41},"                                    FilterChain ",[35,2668,2669],{"class":336},"filterChain",[35,2671,408],{"class":41},[35,2673,411],{"class":45},[35,2675,2676],{"class":41}," ServletException, IOException {\n",[35,2678,2679,2682,2685,2687,2690,2692],{"class":37,"line":742},[35,2680,2681],{"class":45},"        long",[35,2683,2684],{"class":41}," start ",[35,2686,801],{"class":45},[35,2688,2689],{"class":41}," System.",[35,2691,1148],{"class":68},[35,2693,1629],{"class":41},[35,2695,2696,2699],{"class":37,"line":747},[35,2697,2698],{"class":45},"        try",[35,2700,313],{"class":41},[35,2702,2703,2706,2709],{"class":37,"line":753},[35,2704,2705],{"class":41},"            filterChain.",[35,2707,2708],{"class":68},"doFilter",[35,2710,2711],{"class":41},"(request, response);\n",[35,2713,2714,2717,2720],{"class":37,"line":774},[35,2715,2716],{"class":41},"        } ",[35,2718,2719],{"class":45},"finally",[35,2721,313],{"class":41},[35,2723,2724,2726,2728,2730,2732,2734,2736,2738,2741,2743,2745],{"class":37,"line":784},[35,2725,1247],{"class":45},[35,2727,1250],{"class":41},[35,2729,801],{"class":45},[35,2731,1255],{"class":41},[35,2733,1148],{"class":68},[35,2735,889],{"class":41},[35,2737,1262],{"class":45},[35,2739,2740],{"class":41}," start) ",[35,2742,1268],{"class":45},[35,2744,1271],{"class":86},[35,2746,143],{"class":41},[35,2748,2749,2751,2753,2755,2758,2761,2763,2765,2767],{"class":37,"line":790},[35,2750,1278],{"class":41},[35,2752,1281],{"class":68},[35,2754,49],{"class":41},[35,2756,2757],{"class":52},"\"{} {} 耗时 {} ms\"",[35,2759,2760],{"class":41},", request.",[35,2762,1296],{"class":68},[35,2764,1299],{"class":41},[35,2766,1302],{"class":68},[35,2768,1311],{"class":41},[35,2770,2771],{"class":37,"line":795},[35,2772,787],{"class":41},[35,2774,2775],{"class":37,"line":820},[35,2776,149],{"class":41},[35,2778,2779],{"class":37,"line":855},[35,2780,162],{"class":41},[13,2782,2783],{},"常见的过滤器需求：",[545,2785,2786,2796,2806,2816],{},[548,2787,2788,2791,2792,2795],{},[167,2789,2790],{},"字符编码","（Boot 已经配好 ",[32,2793,2794],{},"CharacterEncodingFilter","，一般不用自己写）；",[548,2797,2798,2801,2802,2805],{},[167,2799,2800],{},"CORS 预检","（虽有 ",[32,2803,2804],{},"WebMvcConfigurer#addCorsMappings","，但跨服务场景常用过滤器）；",[548,2807,2808,2811,2812,2815],{},[167,2809,2810],{},"请求体\u002F响应体日志","（需要包装 ",[32,2813,2814],{},"HttpServletRequest","，这是过滤器才方便做的事）；",[548,2817,2818,2821],{},[167,2819,2820],{},"响应压缩、安全响应头","（如 Content-Security-Policy）。",[13,2823,2824,2830,2831,2834,2835,2837],{},[167,2825,2826,2829],{},[32,2827,2828],{},"OncePerRequestFilter"," 是首选基类","：它保证一次请求只执行一次（转发、",[32,2832,2833],{},"include"," 时不会重复进入），\n手写 ",[32,2836,214],{}," 很容易在这里出错。",[17,2839,2841],{"id":2840},"_9-容易踩的坑","9 容易踩的坑",[192,2843,2844,2857],{},[195,2845,2846],{},[198,2847,2848,2851,2854],{},[201,2849,2850],{},"现象",[201,2852,2853],{},"原因",[201,2855,2856],{},"处理",[216,2858,2859,2880,2896,2913,2930,2948,2965,2976,2994],{},[198,2860,2861,2864,2873],{},[221,2862,2863],{},"拦截器完全不生效",[221,2865,2866,2867,2869,2870,2872],{},"只标了 ",[32,2868,1348],{},"，没在 ",[32,2871,1356],{}," 里注册",[221,2874,2875,2876,2879],{},"实现 ",[32,2877,2878],{},"addInterceptors"," 并注册",[198,2881,2882,2885,2888],{},[221,2883,2884],{},"页面样式和图片全丢，被重定向到登录页",[221,2886,2887],{},"拦截器把静态资源请求也拦了",[221,2889,2890,2892,2893,2895],{},[32,2891,1725],{}," 排除，并用 ",[32,2894,934],{}," 兜底",[198,2897,2898,2901,2908],{},[221,2899,2900],{},"重定向后页面又渲染了一遍，或响应内容错乱",[221,2902,2903,2905,2906],{},[32,2904,486],{}," 里重定向了但没 ",[32,2907,962],{},[221,2909,2910,2911],{},"拦截并处理完必须 ",[32,2912,962],{},[198,2914,2915,2918,2924],{},[221,2916,2917],{},"匿名访问也创建了一堆会话",[221,2919,2920,2921],{},"用了 ",[32,2922,2923],{},"request.getSession()",[221,2925,2926,2927],{},"只读场景用 ",[32,2928,2929],{},"getSession(false)",[198,2931,2932,2938,2945],{},[221,2933,2934,2935],{},"拦截器里注入的 Bean 是 ",[32,2936,2937],{},"null",[221,2939,2940,2941,2944],{},"用 ",[32,2942,2943],{},"new"," 手动创建拦截器，没交给容器",[221,2946,2947],{},"让拦截器成为 Spring Bean，注册时注入实例",[198,2949,2950,2953,2959],{},[221,2951,2952],{},"耗时统计总是偏小",[221,2954,2955,2956,2958],{},"在 ",[32,2957,509],{}," 里统计，没算模板渲染",[221,2960,2961,2962,2964],{},"放到 ",[32,2963,528],{}," 里",[198,2966,2967,2970,2973],{},[221,2968,2969],{},"多个拦截器执行顺序不符合预期",[221,2971,2972],{},"注册顺序决定顺序",[221,2974,2975],{},"按依赖关系调整注册顺序",[198,2977,2978,2984,2989],{},[221,2979,2980,2981,2983],{},"抛异常时 ",[32,2982,528],{}," 里的资源没释放",[221,2985,2986,2987],{},"清理逻辑写在了 ",[32,2988,509],{},[221,2990,2991,2993],{},[32,2992,528],{}," 无论成功失败都会调用",[198,2995,2996,2999,3002],{},[221,2997,2998],{},"权限判断写在模板里被绕过",[221,3000,3001],{},"模板只负责显示，不负责授权",[221,3003,3004],{},"服务端拦截（拦截器或安全框架）才是防线",[17,3006,3008],{"id":3007},"_10-小结","10 小结",[545,3010,3011,3019,3034,3047,3058,3066,3076],{},[548,3012,3013,3014,3018],{},"横切逻辑用 ",[167,3015,3016],{},[32,3017,182],{}," 集中处理，别在每个控制器里抄；",[548,3020,3021,3022,3033],{},"三个回调：",[167,3023,3024,3026,3027,3029,3030,3032],{},[32,3025,486],{},"（可中断，正序）→ Controller → ",[32,3028,509],{},"（逆序）→ 视图渲染 →\n",[32,3031,528],{},"（逆序，必调用）","；",[548,3035,3036,3037,3033],{},"登录拦截要写对三处：",[167,3038,3039,3041,3042,3044,3045],{},[32,3040,349],{}," 类型判断、",[32,3043,2929],{},"、重定向后 ",[32,3046,962],{},[548,3048,3049,3050,3053,3054,3057],{},"拦截器",[167,3051,3052],{},"必须注册","（",[32,3055,3056],{},"WebMvcConfigurer#addInterceptors","），顺序即注册顺序，静态资源要排除；",[548,3059,3060,3061,3063,3064,3033],{},"拦截器与控制器传数据用",[167,3062,1980],{}," + ",[32,3065,1984],{},[548,3067,3068,3069,3075],{},"规则会变的权限检查，用",[167,3070,3071,3072],{},"注解 + ",[32,3073,3074],{},"HandlerMethod.getMethodAnnotation"," 声明在方法上；",[548,3077,3078,3079,183],{},"需要操作 Servlet 层（编码、CORS、请求体日志）时用 ",[167,3080,3081,3083,3084],{},[32,3082,214],{}," \u002F ",[32,3085,2828],{},[13,3087,3088,3089,3092],{},"到这里请求的「横向」问题基本解决：谁都能访问、访问顺序、进入前检查什么。接下来回到业务层，\n处理另一个维度的横切问题——",[167,3090,3091],{},"事务","：一组数据库操作要么全成功，要么全失败。",[3094,3095,3096],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":30,"searchDepth":110,"depth":110,"links":3098},[3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109],{"id":19,"depth":59,"text":20},{"id":186,"depth":59,"text":187},{"id":287,"depth":59,"text":288},{"id":591,"depth":59,"text":592},{"id":966,"depth":59,"text":967},{"id":1341,"depth":59,"text":1342},{"id":1971,"depth":59,"text":1972},{"id":2094,"depth":59,"text":2095},{"id":2535,"depth":59,"text":2536},{"id":2840,"depth":59,"text":2841},{"id":3007,"depth":59,"text":3008},"学习",null,"2026-10-15","登录检查、权限判断、请求耗时统计这类横切逻辑该放哪：HandlerInterceptor 的三个回调各在什么时机执行，怎么注册与排除路径，以及它和过滤器的分工。","md",false,{},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-09",{"title":5,"description":3113},"Spring 学习笔记","posts\u002Fjavaspring\u002Fspring-notes-09",[3122,3123,3124],"Spring","Spring MVC","学习笔记","sKt_B92hN26-zX4AxaNxochsy75TwNeppr1c59CK7AI",[3127,3133,3138,3143,3148,3153,3158,3163,3168,3173,3178,3183,3188,3193,3198,3203,3208,3213,3218,3223,3228,3233,3238,3243,3248,3253,3258,3263,3268,3272,3276,3280,3284,3288,3292,3296,3300,3301,3305,3309,3313,3319,3323,3329,3333,3337,3341,3345,3349,3353,3357,3362,3366,3370,3375,3380,3386,3390,3395,3399],{"path":3128,"title":3129,"description":3130,"date":3131,"series":3132,"seriesOrder":869},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-28","软件分析与设计笔记 28：draw.io 作图手册：五张图的符号位置、落笔顺序与自检","不模拟软件界面，只讲清三件事：每个 UML 符号在 draw.io 里搜什么词、一张图该按什么顺序落笔、画完拿什么标准自查。覆盖用例图、活动图、顺序图、类图、界面草图五种图纸，末尾附「最容易画反的十二笔」总检表。","2026-11-19","软件分析与设计笔记",{"path":3134,"title":3135,"description":3136,"date":3137,"series":3132,"seriesOrder":864},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-27","软件分析与设计笔记 27：Paper 1 应试：从读案例到交卷的一次完整演算","拿一份完整的案例，从头到尾走一遍：怎么读案例列角色、怎么在四个小问之间分配时间、每一张图从哪一句话推出来、两张表怎么填、交卷前拿什么清单自查。附 C25 真题与样卷的题型分值对照。","2026-11-18",{"path":3139,"title":3140,"description":3141,"date":3142,"series":3132,"seriesOrder":855},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-26","软件分析与设计笔记 26：总结：把整条链路串起来","系列收尾：从需求到代码的完整链路回顾、每一环的产出与验收方式、六条贯穿全系列的原则，以及一份可以直接拿去做项目自查的清单。","2026-11-17",{"path":3144,"title":3145,"description":3146,"date":3147,"series":3132,"seriesOrder":820},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-25","软件分析与设计笔记 25：代码重构：坏味道与手法","重构不改变外部行为、只改善内部结构：四种常见坏味道与对应手法、为什么动手之前必须先有测试、什么时候该重构，以及重构和前面那些设计模型是什么关系。","2026-11-16",{"path":3149,"title":3150,"description":3151,"date":3152,"series":3132,"seriesOrder":795},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-24","软件分析与设计笔记 24：设计原则：内聚、耦合与多态","好设计怎么衡量：内聚与耦合分别在说什么、一次从「差设计」到「多态重设计」的完整对照、抽象类与接口的选择依据，以及复用为什么会从优势变成风险。","2026-11-15",{"path":3154,"title":3155,"description":3156,"date":3157,"series":3132,"seriesOrder":790},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-23","软件分析与设计笔记 23：设计建模：从对象模型到能实现的方案","分析模型能用但还不能直接开工：对象模型怎么映射到关系模型、继承映射的三种方案怎么选、数据访问层与 Data Broker 解决什么问题，以及三层架构与 MVC 的分工。","2026-11-14",{"path":3159,"title":3160,"description":3161,"date":3162,"series":3132,"seriesOrder":784},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-22","软件分析与设计笔记 22：分析建模：边界、实体与控制对象","不经过分析直接写代码会踩什么坑、三种分析对象各管一段、怎么用顺序图完成一次用例实现，以及从顺序图导出类的属性、操作与关系的方法。","2026-11-13",{"path":3164,"title":3165,"description":3166,"date":3167,"series":3132,"seriesOrder":774},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-21","软件分析与设计笔记 21：UML：类图与顺序图","为什么需要一种共同的画法：类符号三格怎么画、五种关系的符号与设计含义、每种关系落到 Java 是什么写法，顺序图的三要素怎么摆；以及这三种最容易找不到的符号在 draw.io 里各搜什么词。","2026-11-12",{"path":3169,"title":3170,"description":3171,"date":3172,"series":3132,"seriesOrder":753},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-20","软件分析与设计笔记 20：面向对象概念：对象、关系与多态","面向对象不是「用某个语言写代码」：对象与类的区别、封装真正在保护什么、五种关系（依赖、关联、聚合、组合、继承）各自何时用，以及多态为什么能减少条件判断。","2026-11-11",{"path":3174,"title":3175,"description":3176,"date":3177,"series":3132,"seriesOrder":747},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-19","软件分析与设计笔记 19：规范化与逻辑数据模型","一次改地址引发的数据不一致，讲清规范化的真正动机；UNF 到 3NF 每一步在解决什么问题，派生字段为什么不能存，优化为什么和规范化方向相反，以及逻辑数据模型在 3NF 之上补了什么。","2026-11-10",{"path":3179,"title":3180,"description":3181,"date":3182,"series":3132,"seriesOrder":742},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-18","软件分析与设计笔记 18：数据库分析：实体、关系与数据字典","为什么不能拿到需求就动手建表：实体与关系怎么判定、基数为什么必须由业务方确认、数据命名要遵守什么规则，以及数据字典四列里最容易被空着的那一列。","2026-11-09",{"path":3184,"title":3185,"description":3186,"date":3187,"series":3132,"seriesOrder":734},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-17","软件分析与设计笔记 17：用生成式 AI 做设计与开发：能用的场景与要防的坑","生成式 AI 在工作流里真正站得住的三个场景 —— 造示例数据、解释陌生代码、起草初稿；它为什么会复现社会偏见、为什么会编得像真的，以及哪些内容绝不能直接采用。","2026-11-08",{"path":3189,"title":3190,"description":3191,"date":3192,"series":3132,"seriesOrder":710},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-16","软件分析与设计笔记 16：从 persona 到 URS：一份交付物的完整骨架","把前面十几篇串成一份能交出去的东西：用户需求规格说明书的九个部分、每一章来自哪个环节、由谁确认，以及面向决策者的提案该讲清哪三件事。","2026-11-07",{"path":3194,"title":3195,"description":3196,"date":3197,"series":3132,"seriesOrder":701},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-15","软件分析与设计笔记 15：非功能需求：把「快、稳、安全」变成数字","为什么「系统要快」不是一条需求：非功能需求怎么量化，决定性能的用户量、交易量、数据量三张表怎么估，安全需求要写清什么，以及可靠性的「不可用」该怎么定义。","2026-11-06",{"path":3199,"title":3200,"description":3201,"date":3202,"series":3132,"seriesOrder":696},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-14","软件分析与设计笔记 14：软件测试：层级、方法与等价类划分","「我们测过了」这句话问题在哪：为什么静态评审和动态测试缺一不可，五个测试层级各自验证什么，白盒与黑盒的差别，以及用等价类划分和边界值把无限输入变成有限用例的方法。","2026-11-05",{"path":3204,"title":3205,"description":3206,"date":3207,"series":3132,"seriesOrder":678},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-13","软件分析与设计笔记 13：需求获取：五种调查方法与一场访谈","需求从哪里来：五种事实调查方法各自适合回答什么、代价是什么，访谈从准备到记录的全过程，问卷设计里最容易犯的错，以及访谈记录必须写下来的那几项信息。","2026-11-04",{"path":3209,"title":3210,"description":3211,"date":3212,"series":3132,"seriesOrder":670},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-12","软件分析与设计笔记 12：前端设计：把流程变成屏幕","界面设计不是「画得好看」：从活动图走到屏幕的三步，CRUD 任务流怎么当检查表，界面必须回答的三个问题，信息架构与五种导航类型，视觉一致性与留白，以及移动端与 Web 端的差别。","2026-11-03",{"path":3214,"title":3215,"description":3216,"date":3217,"series":3132,"seriesOrder":460},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-11","软件分析与设计笔记 11：Agentic AI 与业务流程自动化","AI agent 到底是什么：五项能力与四个核心组件，什么场景该用定时任务、什么该用程序、什么才值得用 agent，两种实现路径的差别，必须提前防住的四类失败模式，以及设计方法从提示词到编排的演进。","2026-11-02",{"path":3219,"title":3220,"description":3221,"date":3222,"series":3132,"seriesOrder":445},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-10","软件分析与设计笔记 10：精炼用例模型：include、extend 与泛化","三种精炼用例关系的判断标准：include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题，以及精炼到什么程度就该停手。","2026-11-01",{"path":3224,"title":3225,"description":3226,"date":3227,"series":3132,"seriesOrder":421},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-09","软件分析与设计笔记 09：功能需求（二）：从流程到业务实体","活动图画完之后，怎么从流程里筛出真正的业务实体：三条判断标准、哪些名词其实不该建实体、属性从哪里来，以及领域模型和后面的数据模型分别负责什么。","2026-10-31",{"path":3229,"title":3230,"description":3231,"date":3232,"series":3132,"seriesOrder":159},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-08","软件分析与设计笔记 08：功能需求（一）：用例模型与活动图","从用户故事到用例的翻译过程：什么才算「好」的参与者，系统边界怎么划，用例描述必须写清哪四件事，活动图怎么把分支、并行与泳道画清楚，以及这两个图在 draw.io 里各用哪个形状、最容易画错哪一笔。","2026-10-30",{"path":3234,"title":3235,"description":3236,"date":3237,"series":3132,"seriesOrder":152},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-07","软件分析与设计笔记 07：用户故事：把需求写成可验收的一句话","用户故事的三段结构，怎么从用户画像和数据地图里把它提取出来，一条故事多大算合适、时序为什么也是需求的一部分，以及验收标准怎么写才算真的可验收。","2026-10-29",{"path":3239,"title":3240,"description":3241,"date":3242,"series":3132,"seriesOrder":146},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-06","软件分析与设计笔记 06：范围界定（数据侧）：用数据校验你的判断","访谈结论和数字对不上时该怎么办：分析的四个层次分别在回答什么问题、为什么不能跳级、数据准备与搜集怎么做成一张应用地图，以及数据最容易在哪几个地方骗你。","2026-10-28",{"path":3244,"title":3245,"description":3246,"date":3247,"series":3132,"seriesOrder":134},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-05","软件分析与设计笔记 05：范围界定（用户侧）：用户画像与客户旅程图","为什么「所有用户」是一个陷阱，项目边界该怎么划，用户画像要写哪五个要素、最容易写成什么废纸，客户旅程图怎么把阶段、想法和情绪画在同一张纸上，以及怎么从这两样东西一步步推到可验证的需求。","2026-10-27",{"path":3249,"title":3250,"description":3251,"date":3252,"series":3132,"seriesOrder":116},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-04","软件分析与设计笔记 04：接手陌生案例的第一步：三问与一张表","拿到一个完全陌生的业务案例，怎么从零开始不跑偏：只问 Who、What、Why 三个问题，把「问题—事实—假设—理由」记成一张四列表，说清为什么假设必须写明理由，以及带着问题进访谈为什么比空手去有效得多。","2026-10-26",{"path":3254,"title":3255,"description":3256,"date":3257,"series":3132,"seriesOrder":110},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-03","软件分析与设计笔记 03：Agile 与 Scrum：迭代、角色与产出物","从「三周做完交付，客户说这不是我要的」讲起，说清瀑布式与迭代式的根本差别，Scrum 的三个角色和三种产出物，一个 sprint 里四次固定活动各自在解决什么问题，以及敏捷里最常见的几个误解。","2026-10-25",{"path":3259,"title":3260,"description":3261,"date":3262,"series":3132,"seriesOrder":59},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-02","软件分析与设计笔记 02：设计思维、用户驱动与数据驱动","从「客服要一个能一次看完所有排期的界面」这个翻车案例讲起，说清设计思维到底是什么、用户驱动与数据驱动各自能看见什么又看不见什么、为什么真正的落点是把两者合起来的 data-informed design，以及访谈中怎么问出有用的信息。","2026-10-24",{"path":3264,"title":3265,"description":3266,"date":3267,"series":3132,"seriesOrder":38},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-01","软件分析与设计笔记 01：课程地图与从需求到设计的主干","这一篇不写具体方法，先把整条链路铺开：软件分析与设计到底要解决什么问题，从需求到设计分成哪两段，中间有哪十几个环节，一份完整的交付物里应该有什么，以及后面 27 篇笔记怎么排、新人该按什么顺序读。","2026-10-23",{"path":3269,"title":3270,"description":3271,"date":3112,"series":3119,"seriesOrder":38},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-01","Spring 学习笔记 01：IoC、依赖注入与第一个 Spring Boot 应用","从「手写 new 出来的三层结构」的痛点讲起，说清 IoC 容器、Bean 与依赖注入是怎么回事，再用 Maven 与 Spring Initializr 跑起第一个能返回 JSON 的应用。",{"path":3273,"title":3274,"description":3275,"date":3112,"series":3119,"seriesOrder":59},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-02","Spring 学习笔记 02：Spring Data JPA 基础——实体映射与 Repository","用 ORM 把类映射到表：@Entity、@Id、@GeneratedValue 怎么用，CrudRepository 三层接口怎么选，H2 与 MySQL 怎么配，以及 ddl-auto 到底该写哪个值。",{"path":3277,"title":3278,"description":3279,"date":3112,"series":3119,"seriesOrder":110},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-03","Spring 学习笔记 03：JPA 查询——派生方法、JPQL 与分页排序","Repository 自带的方法只够按主键查，这篇讲三种自己造查询的方式：方法名派生、@Query 写 JPQL、原生 SQL，以及 Pageable 分页排序和投影返回。",{"path":3281,"title":3282,"description":3283,"date":3112,"series":3119,"seriesOrder":116},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-04","Spring 学习笔记 04：JPA 关联映射与仓储测试","一对多、多对多怎么映射，谁负责维护外键，FetchType 与 CascadeType 该怎么选，顺带用 @DataJpaTest 与 AssertJ 把仓储层测起来。",{"path":3285,"title":3286,"description":3287,"date":3112,"series":3119,"seriesOrder":134},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-05","Spring 学习笔记 05：Spring MVC 与 Thymeleaf 入门","一次页面请求在 Spring MVC 里走了哪几步，@Controller 怎么把数据交给模板，@PathVariable 与 @RequestParam 怎么取参数，以及控制器为什么必须是无状态的。",{"path":3289,"title":3290,"description":3291,"date":3112,"series":3119,"seriesOrder":146},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-06","Spring 学习笔记 06：表单绑定与服务端校验","把表单字段绑到对象上，用 Bean Validation 注解做服务端校验，把错误信息显示回用户刚填的那一页，再补上自定义校验注解与全局异常处理。",{"path":3293,"title":3294,"description":3295,"date":3112,"series":3119,"seriesOrder":152},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-07","Spring 学习笔记 07：Thymeleaf 片段复用与国际化","用 th:fragment 与 th:insert\u002Fth:replace 把导航栏、页脚抽成片段，页面套用统一布局，再用 messages.properties 与 LocaleResolver 做中英文切换。",{"path":3297,"title":3298,"description":3299,"date":3112,"series":3119,"seriesOrder":159},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-08","Spring 学习笔记 08：HTTP Session 与会话状态管理","HTTP 无状态带来的问题，Cookie 与 Session 怎么分工，Spring 里读写会话的几种方式，以及会话固定、超时、多实例共享这些必须想清楚的事。",{"path":3117,"title":5,"description":3113,"date":3112,"series":3119,"seriesOrder":421},{"path":3302,"title":3303,"description":3304,"date":3112,"series":3119,"seriesOrder":445},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-10","Spring 学习笔记 10：Service 层与事务","一组数据库操作要么全成功要么全失败，靠 @Transactional 怎么做到；传播行为、隔离级别、回滚规则怎么选，以及自调用失效、乐观锁悲观锁这些实战里的坑。",{"path":3306,"title":3307,"description":3308,"date":3112,"series":3119,"seriesOrder":460},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-11","Spring 学习笔记 11：REST API 基础——把 Service 暴露成接口","同一套 Service 怎么同时服务页面和客户端：@RestController 的路由与方法语义、状态码怎么给、DTO 怎么定，以及 MockMvc 端到端和切片测试的两种写法与各自的坑。",{"path":3310,"title":3311,"description":3312,"date":3112,"series":3119,"seriesOrder":670},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-12","Spring 学习笔记 12：REST 进阶——版本、更新、跨域与文档化","接口能跑通之后要处理的几件事：URL 与响应结构怎么定、版本怎么演进、更新怎么做到可安全重试、CORS 怎么配、HATEOAS 与 Spring Data REST 该不该用。",{"path":3314,"title":3315,"description":3316,"date":3317,"series":3318,"seriesOrder":116},"\u002Fposts\u002Fjava\u002Fjava-functionalprogramming-notes","Java 函数式编程笔记：Lambda、函数式接口与 Streams","学习 Java 函数式编程时的个人笔记：Lambda 表达式的五步简写与方法引用、常见函数式接口、Stream 流水线与惰性求值，以及创建流、中间操作、终止操作与收集器的用法。示例代码均为自写，运行结果来自本地实测。","2026-10-13","Java 学习笔记",{"path":3320,"title":3321,"description":3322,"date":3317,"series":3318,"seriesOrder":110},"\u002Fposts\u002Fjava\u002Fjava-generics-notes","Java 泛型笔记：泛型方法、有界类型与泛型类","学习 Java 泛型时的个人笔记：从重复的重载方法与多态尝试讲起，再到泛型方法与类型推断、有界泛型类型、Comparable 与 Number 边界，以及泛型类在集合中的用法。示例代码均为自写，运行结果来自本地实测。",{"path":3324,"title":3325,"description":3326,"date":3327,"series":3328,"seriesOrder":116},"\u002Fposts\u002Fmysql\u002Fmysql-learning-04","MySQL 学习笔记 04：数据操纵（增删改）","MySQL 学习笔记第四篇：INSERT 插入（含批量插入与 INSERT ... SELECT）、UPDATE 更新、DELETE 删除，以及各自的坑。","2026-10-10","SQL 学习笔记",{"path":3330,"title":3331,"description":3332,"date":3327,"series":3328,"seriesOrder":134},"\u002Fposts\u002Fmysql\u002Fmysql-learning-05","MySQL 学习笔记 05：DDL 概览","MySQL 学习笔记第五篇：数据定义语言概览——Schema、Table、View、Index、Constraints 这几类对象分别对应哪些 CREATE \u002F ALTER \u002F DROP 命令。",{"path":3334,"title":3335,"description":3336,"date":3327,"series":3328,"seriesOrder":146},"\u002Fposts\u002Fmysql\u002Fmysql-learning-06","MySQL 学习笔记 06：CREATE TABLE 与数据类型","MySQL 学习笔记第六篇：CREATE TABLE 的完整语法、常用数据类型的选型建议，以及外键的两种定义方式。",{"path":3338,"title":3339,"description":3340,"date":3327,"series":3328,"seriesOrder":152},"\u002Fposts\u002Fmysql\u002Fmysql-learning-07","MySQL 学习笔记 07：索引","MySQL 学习笔记第七篇：索引的作用与 B+ 树结构、单列与复合索引的创建、索引的代价与适用场景。",{"path":3342,"title":3343,"description":3344,"date":3327,"series":3328,"seriesOrder":159},"\u002Fposts\u002Fmysql\u002Fmysql-learning-08","MySQL 学习笔记 08：DROP 与 ALTER","MySQL 学习笔记第八篇：DROP 删除数据库对象、ALTER 修改表结构，以及常见的坑。",{"path":3346,"title":3347,"description":3348,"date":3327,"series":3328,"seriesOrder":421},"\u002Fposts\u002Fmysql\u002Fmysql-learning-09","MySQL 学习笔记 09：约束与数据完整性","MySQL 学习笔记第九篇：SQL 约束——NOT NULL、CHECK、PRIMARY KEY、FOREIGN KEY 等如何保证数据完整性，以及它们的对比。",{"path":3350,"title":3351,"description":3352,"date":3327,"series":3328,"seriesOrder":445},"\u002Fposts\u002Fmysql\u002Fmysql-learning-10","MySQL 学习笔记 10：视图","MySQL 学习笔记第十篇：视图（Views）的定义与特性、和表的对比、创建与使用，以及可更新视图的限制。",{"path":3354,"title":3355,"description":3356,"date":3327,"series":3328,"seriesOrder":460},"\u002Fposts\u002Fmysql\u002Fmysql-learning-11","MySQL 学习笔记 11：存储过程","MySQL 学习笔记第十一篇：存储过程（Stored Procedures）的概念、创建与调用、带参数的写法，以及优缺点。",{"path":3358,"title":3359,"description":3360,"date":3361,"series":3328,"seriesOrder":38},"\u002Fposts\u002Fmysql\u002Fmysql-learning-01","MySQL 学习笔记 01：MySQL 基础知识","MySQL 学习笔记第一篇：MySQL Server 与 Workbench 的安装，数据库与 DBMS 的基本概念，关系模型、ACID 事务、常见数据类型，以及后续的学习路线。","2026-10-09",{"path":3363,"title":3364,"description":3365,"date":3361,"series":3328,"seriesOrder":59},"\u002Fposts\u002Fmysql\u002Fmysql-learning-02","MySQL 学习笔记 02：SELECT 查询与过滤","MySQL 学习笔记第二篇：SELECT 的基本语法与逻辑执行顺序，WHERE 过滤（比较、LIKE、IN、BETWEEN、NULL）、各种 JOIN、UNION 与子查询。",{"path":3367,"title":3368,"description":3369,"date":3361,"series":3328,"seriesOrder":110},"\u002Fposts\u002Fmysql\u002Fmysql-learning-03","MySQL 学习笔记 03：聚合、分组与输出控制","MySQL 学习笔记第三篇：COUNT\u002FSUM\u002FAVG 等聚合函数、GROUP BY 分组、HAVING 过滤、ORDER BY 与 LIMIT，以及一条 SELECT 的完整逻辑执行顺序。",{"path":3371,"title":3372,"description":3373,"date":3374,"series":3318,"seriesOrder":59},"\u002Fposts\u002Fjava\u002Fjava-oop-notes","Java 面向对象笔记：类、继承、多态与异常","学习 Java 面向对象编程时的个人笔记：类与对象、封装与访问修饰符、继承与多态、集合、抽象类与接口、异常处理。示例代码均为自写，运行结果来自本地实测。","2026-10-06",{"path":3376,"title":3377,"description":3378,"date":3379,"series":3318,"seriesOrder":38},"\u002Fposts\u002Fjava\u002Fjava-basics-notes","Java 基础语法笔记：从环境安装到方法与数组","学习 Java 基础语法时的个人笔记：JDK 安装、输入输出、变量与运算符、条件与循环、字符串、数组与方法。示例代码均为自写，运行结果来自本地实测。","2026-10-05",{"path":3381,"title":3382,"description":3383,"date":3384,"series":3385,"seriesOrder":38},"\u002Fposts\u002Fgit\u002Fgit-install-and-basics","Git 安装与基本使用","从装好 Git 到跑通日常提交流程，把最常用的几条命令和它们背后的三个区域讲清楚。","2026-10-04","Git 入门",{"path":3387,"title":3388,"description":3389,"date":3384,"series":3385,"seriesOrder":59},"\u002Fposts\u002Fgit\u002Flocal-remote-repository","本地远程仓库的部署","用自己的机器当 Git 服务器：建一个裸仓库、走 SSH 连上去，把本地提交推上去。",{"path":3391,"title":3392,"description":3393,"date":3384,"series":3394,"seriesOrder":38},"\u002Fposts\u002Fguide\u002Fguide-file-layout","文件都放在哪","文章、图片、封面分别放在仓库的什么位置，哪些目录可以放手改、哪些碰了会出问题。","博客使用指南",{"path":3396,"title":3397,"description":3398,"date":3384,"series":3394,"seriesOrder":59},"\u002Fposts\u002Fguide\u002Fguide-frontmatter","frontmatter 每个字段是什么意思","文章开头那段被横线包住的字段，逐个说明作用、默认行为和写错的后果。",{"path":3400,"title":3401,"description":3402,"date":3384,"series":3394,"seriesOrder":110},"\u002Fposts\u002Fguide\u002Fguide-markdown","正文怎么写 —— Markdown 与排版效果","标题、链接、图片、代码、表格分别怎么写，以及本站对它们做了哪些自定义处理。",1793268185843]