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