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