[{"data":1,"prerenderedAt":4576},["ShallowReactive",2],{"post:\u002Fposts\u002Fjavaspring\u002Fspring-notes-06":3,"posts:ordered":4299},{"id":4,"title":5,"body":6,"category":4282,"cover":4283,"date":4284,"description":4285,"extension":4286,"featured":4287,"meta":4288,"navigation":92,"path":4289,"seo":4290,"series":4291,"seriesOrder":121,"stem":4292,"tags":4293,"__hash__":4298},"posts\u002Fposts\u002Fjavaspring\u002Fspring-notes-06.md","Spring 学习笔记 06：表单绑定与服务端校验",{"type":7,"value":8,"toc":4266},"minimark",[9,16,21,24,46,53,57,65,814,817,850,860,1153,1162,1254,1523,1526,1623,1644,1866,1872,1921,1924,1928,2442,2452,2527,2541,2544,2581,2585,2590,2642,2656,2660,2816,2819,2844,2848,2865,2873,2938,2944,2948,2953,2981,2984,3040,3044,3051,3054,3086,3090,3093,3303,3447,3450,3468,3471,3516,3520,3526,3682,3689,3830,3840,3843,3896,3945,3952,3956,4158,4162,4251,4262],[10,11,12],"blockquote",{},[13,14,15],"p",{},"这是一份个人学习笔记：概念部分是我按自己的理解写的，示例代码都是自己敲的，参考的是\nSpring 官方文档、Jakarta Bean Validation 规范与 Thymeleaf 官方文档。理解有偏差的地方\n欢迎指出。",[17,18,20],"h2",{"id":19},"_0-表单为什么比查询难","0 表单为什么比查询难",[13,22,23],{},"「新增一本书」这个功能，要把事情串起来：",[25,26,27,36,43],"ol",{},[28,29,30,31,35],"li",{},"打开表单页，",[32,33,34],"strong",{},"显示一个空表单","（下拉框里还要有分类列表）；",[28,37,38,39,42],{},"用户填错提交，",[32,40,41],{},"回到同一页，保留他刚填的内容，并指出哪里填错了","；",[28,44,45],{},"全部合法才存库，然后跳转到一个干净的地址（避免刷新重复提交）。",[13,47,48,49,52],{},"查询只要「取数据 → 渲染」，表单要处理",[32,50,51],{},"双向的数据流","：从对象到页面（回填），再从页面到对象\n（绑定）。这一篇把这条链路走完。",[17,54,56],{"id":55},"_1-表单对象与-modelattribute","1 表单对象与 @ModelAttribute",[13,58,59,60,64],{},"用专门的对象接收表单，而不是一堆 ",[61,62,63],"code",{},"@RequestParam","：",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-java shiki shiki-themes github-light github-dark","package com.example.library.dto;\n\nimport com.example.library.validation.PastOrCurrentYear;\nimport jakarta.validation.constraints.Max;\nimport jakarta.validation.constraints.Min;\nimport jakarta.validation.constraints.NotBlank;\nimport jakarta.validation.constraints.NotNull;\nimport jakarta.validation.constraints.Pattern;\nimport jakarta.validation.constraints.Size;\n\npublic class BookForm {\n\n    @NotBlank(message = \"书名不能为空\")\n    @Size(max = 200, message = \"书名不能超过 200 个字符\")\n    private String title;\n\n    @NotBlank(message = \"作者不能为空\")\n    private String author;\n\n    @PastOrCurrentYear\n    @Min(value = 1000, message = \"出版年份看起来不对\")\n    @Max(value = 2100, message = \"出版年份看起来不对\")\n    private Integer publishedYear;\n\n    @Pattern(regexp = \"\\\\d{13}\", message = \"ISBN 必须是 13 位数字\")\n    private String isbn;\n\n    @NotNull(message = \"请选择分类\")\n    private Long categoryId;\n\n    \u002F\u002F 下面这些 getter \u002F setter 一个都不能少：\n    \u002F\u002F 绑定靠 setter，回填靠 getter，缺任何一个都会「绑定不上」或「值显示不出来」\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = author;\n    }\n\n    public Integer getPublishedYear() {\n        return publishedYear;\n    }\n\n    public void setPublishedYear(Integer publishedYear) {\n        this.publishedYear = publishedYear;\n    }\n\n    public String getIsbn() {\n        return isbn;\n    }\n\n    public void setIsbn(String isbn) {\n        this.isbn = isbn;\n    }\n\n    public Long getCategoryId() {\n        return categoryId;\n    }\n\n    public void setCategoryId(Long categoryId) {\n        this.categoryId = categoryId;\n    }\n}\n","java","",[61,73,74,87,94,103,111,119,127,135,143,151,156,172,177,203,233,242,247,265,273,278,286,315,342,350,355,390,398,403,422,430,435,442,448,453,468,477,483,488,509,523,528,533,545,553,558,563,580,592,597,602,615,623,628,633,651,663,668,673,685,693,698,703,720,732,737,742,755,763,768,773,791,803,808],{"__ignoreMap":71},[75,76,79,83],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"szBVR","package",[75,84,86],{"class":85},"sVt8B"," com.example.library.dto;\n",[75,88,90],{"class":77,"line":89},2,[75,91,93],{"emptyLinePlaceholder":92},true,"\n",[75,95,97,100],{"class":77,"line":96},3,[75,98,99],{"class":81},"import",[75,101,102],{"class":85}," com.example.library.validation.PastOrCurrentYear;\n",[75,104,106,108],{"class":77,"line":105},4,[75,107,99],{"class":81},[75,109,110],{"class":85}," jakarta.validation.constraints.Max;\n",[75,112,114,116],{"class":77,"line":113},5,[75,115,99],{"class":81},[75,117,118],{"class":85}," jakarta.validation.constraints.Min;\n",[75,120,122,124],{"class":77,"line":121},6,[75,123,99],{"class":81},[75,125,126],{"class":85}," jakarta.validation.constraints.NotBlank;\n",[75,128,130,132],{"class":77,"line":129},7,[75,131,99],{"class":81},[75,133,134],{"class":85}," jakarta.validation.constraints.NotNull;\n",[75,136,138,140],{"class":77,"line":137},8,[75,139,99],{"class":81},[75,141,142],{"class":85}," jakarta.validation.constraints.Pattern;\n",[75,144,146,148],{"class":77,"line":145},9,[75,147,99],{"class":81},[75,149,150],{"class":85}," jakarta.validation.constraints.Size;\n",[75,152,154],{"class":77,"line":153},10,[75,155,93],{"emptyLinePlaceholder":92},[75,157,159,162,165,169],{"class":77,"line":158},11,[75,160,161],{"class":81},"public",[75,163,164],{"class":81}," class",[75,166,168],{"class":167},"sScJk"," BookForm",[75,170,171],{"class":85}," {\n",[75,173,175],{"class":77,"line":174},12,[75,176,93],{"emptyLinePlaceholder":92},[75,178,180,183,186,189,193,196,200],{"class":77,"line":179},13,[75,181,182],{"class":85},"    @",[75,184,185],{"class":81},"NotBlank",[75,187,188],{"class":85},"(",[75,190,192],{"class":191},"sj4cs","message",[75,194,195],{"class":81}," =",[75,197,199],{"class":198},"sZZnC"," \"书名不能为空\"",[75,201,202],{"class":85},")\n",[75,204,206,208,211,213,216,218,221,224,226,228,231],{"class":77,"line":205},14,[75,207,182],{"class":85},[75,209,210],{"class":81},"Size",[75,212,188],{"class":85},[75,214,215],{"class":191},"max",[75,217,195],{"class":81},[75,219,220],{"class":191}," 200",[75,222,223],{"class":85},", ",[75,225,192],{"class":191},[75,227,195],{"class":81},[75,229,230],{"class":198}," \"书名不能超过 200 个字符\"",[75,232,202],{"class":85},[75,234,236,239],{"class":77,"line":235},15,[75,237,238],{"class":81},"    private",[75,240,241],{"class":85}," String title;\n",[75,243,245],{"class":77,"line":244},16,[75,246,93],{"emptyLinePlaceholder":92},[75,248,250,252,254,256,258,260,263],{"class":77,"line":249},17,[75,251,182],{"class":85},[75,253,185],{"class":81},[75,255,188],{"class":85},[75,257,192],{"class":191},[75,259,195],{"class":81},[75,261,262],{"class":198}," \"作者不能为空\"",[75,264,202],{"class":85},[75,266,268,270],{"class":77,"line":267},18,[75,269,238],{"class":81},[75,271,272],{"class":85}," String author;\n",[75,274,276],{"class":77,"line":275},19,[75,277,93],{"emptyLinePlaceholder":92},[75,279,281,283],{"class":77,"line":280},20,[75,282,182],{"class":85},[75,284,285],{"class":81},"PastOrCurrentYear\n",[75,287,289,291,294,296,299,301,304,306,308,310,313],{"class":77,"line":288},21,[75,290,182],{"class":85},[75,292,293],{"class":81},"Min",[75,295,188],{"class":85},[75,297,298],{"class":191},"value",[75,300,195],{"class":81},[75,302,303],{"class":191}," 1000",[75,305,223],{"class":85},[75,307,192],{"class":191},[75,309,195],{"class":81},[75,311,312],{"class":198}," \"出版年份看起来不对\"",[75,314,202],{"class":85},[75,316,318,320,323,325,327,329,332,334,336,338,340],{"class":77,"line":317},22,[75,319,182],{"class":85},[75,321,322],{"class":81},"Max",[75,324,188],{"class":85},[75,326,298],{"class":191},[75,328,195],{"class":81},[75,330,331],{"class":191}," 2100",[75,333,223],{"class":85},[75,335,192],{"class":191},[75,337,195],{"class":81},[75,339,312],{"class":198},[75,341,202],{"class":85},[75,343,345,347],{"class":77,"line":344},23,[75,346,238],{"class":81},[75,348,349],{"class":85}," Integer publishedYear;\n",[75,351,353],{"class":77,"line":352},24,[75,354,93],{"emptyLinePlaceholder":92},[75,356,358,360,363,365,368,370,373,376,379,381,383,385,388],{"class":77,"line":357},25,[75,359,182],{"class":85},[75,361,362],{"class":81},"Pattern",[75,364,188],{"class":85},[75,366,367],{"class":191},"regexp",[75,369,195],{"class":81},[75,371,372],{"class":198}," \"",[75,374,375],{"class":191},"\\\\",[75,377,378],{"class":198},"d{13}\"",[75,380,223],{"class":85},[75,382,192],{"class":191},[75,384,195],{"class":81},[75,386,387],{"class":198}," \"ISBN 必须是 13 位数字\"",[75,389,202],{"class":85},[75,391,393,395],{"class":77,"line":392},26,[75,394,238],{"class":81},[75,396,397],{"class":85}," String isbn;\n",[75,399,401],{"class":77,"line":400},27,[75,402,93],{"emptyLinePlaceholder":92},[75,404,406,408,411,413,415,417,420],{"class":77,"line":405},28,[75,407,182],{"class":85},[75,409,410],{"class":81},"NotNull",[75,412,188],{"class":85},[75,414,192],{"class":191},[75,416,195],{"class":81},[75,418,419],{"class":198}," \"请选择分类\"",[75,421,202],{"class":85},[75,423,425,427],{"class":77,"line":424},29,[75,426,238],{"class":81},[75,428,429],{"class":85}," Long categoryId;\n",[75,431,433],{"class":77,"line":432},30,[75,434,93],{"emptyLinePlaceholder":92},[75,436,438],{"class":77,"line":437},31,[75,439,441],{"class":440},"sJ8bj","    \u002F\u002F 下面这些 getter \u002F setter 一个都不能少：\n",[75,443,445],{"class":77,"line":444},32,[75,446,447],{"class":440},"    \u002F\u002F 绑定靠 setter，回填靠 getter，缺任何一个都会「绑定不上」或「值显示不出来」\n",[75,449,451],{"class":77,"line":450},33,[75,452,93],{"emptyLinePlaceholder":92},[75,454,456,459,462,465],{"class":77,"line":455},34,[75,457,458],{"class":81},"    public",[75,460,461],{"class":85}," String ",[75,463,464],{"class":167},"getTitle",[75,466,467],{"class":85},"() {\n",[75,469,471,474],{"class":77,"line":470},35,[75,472,473],{"class":81},"        return",[75,475,476],{"class":85}," title;\n",[75,478,480],{"class":77,"line":479},36,[75,481,482],{"class":85},"    }\n",[75,484,486],{"class":77,"line":485},37,[75,487,93],{"emptyLinePlaceholder":92},[75,489,491,493,496,499,502,506],{"class":77,"line":490},38,[75,492,458],{"class":81},[75,494,495],{"class":81}," void",[75,497,498],{"class":167}," setTitle",[75,500,501],{"class":85},"(String ",[75,503,505],{"class":504},"s4XuR","title",[75,507,508],{"class":85},") {\n",[75,510,512,515,518,521],{"class":77,"line":511},39,[75,513,514],{"class":191},"        this",[75,516,517],{"class":85},".title ",[75,519,520],{"class":81},"=",[75,522,476],{"class":85},[75,524,526],{"class":77,"line":525},40,[75,527,482],{"class":85},[75,529,531],{"class":77,"line":530},41,[75,532,93],{"emptyLinePlaceholder":92},[75,534,536,538,540,543],{"class":77,"line":535},42,[75,537,458],{"class":81},[75,539,461],{"class":85},[75,541,542],{"class":167},"getAuthor",[75,544,467],{"class":85},[75,546,548,550],{"class":77,"line":547},43,[75,549,473],{"class":81},[75,551,552],{"class":85}," author;\n",[75,554,556],{"class":77,"line":555},44,[75,557,482],{"class":85},[75,559,561],{"class":77,"line":560},45,[75,562,93],{"emptyLinePlaceholder":92},[75,564,566,568,570,573,575,578],{"class":77,"line":565},46,[75,567,458],{"class":81},[75,569,495],{"class":81},[75,571,572],{"class":167}," setAuthor",[75,574,501],{"class":85},[75,576,577],{"class":504},"author",[75,579,508],{"class":85},[75,581,583,585,588,590],{"class":77,"line":582},47,[75,584,514],{"class":191},[75,586,587],{"class":85},".author ",[75,589,520],{"class":81},[75,591,552],{"class":85},[75,593,595],{"class":77,"line":594},48,[75,596,482],{"class":85},[75,598,600],{"class":77,"line":599},49,[75,601,93],{"emptyLinePlaceholder":92},[75,603,605,607,610,613],{"class":77,"line":604},50,[75,606,458],{"class":81},[75,608,609],{"class":85}," Integer ",[75,611,612],{"class":167},"getPublishedYear",[75,614,467],{"class":85},[75,616,618,620],{"class":77,"line":617},51,[75,619,473],{"class":81},[75,621,622],{"class":85}," publishedYear;\n",[75,624,626],{"class":77,"line":625},52,[75,627,482],{"class":85},[75,629,631],{"class":77,"line":630},53,[75,632,93],{"emptyLinePlaceholder":92},[75,634,636,638,640,643,646,649],{"class":77,"line":635},54,[75,637,458],{"class":81},[75,639,495],{"class":81},[75,641,642],{"class":167}," setPublishedYear",[75,644,645],{"class":85},"(Integer ",[75,647,648],{"class":504},"publishedYear",[75,650,508],{"class":85},[75,652,654,656,659,661],{"class":77,"line":653},55,[75,655,514],{"class":191},[75,657,658],{"class":85},".publishedYear ",[75,660,520],{"class":81},[75,662,622],{"class":85},[75,664,666],{"class":77,"line":665},56,[75,667,482],{"class":85},[75,669,671],{"class":77,"line":670},57,[75,672,93],{"emptyLinePlaceholder":92},[75,674,676,678,680,683],{"class":77,"line":675},58,[75,677,458],{"class":81},[75,679,461],{"class":85},[75,681,682],{"class":167},"getIsbn",[75,684,467],{"class":85},[75,686,688,690],{"class":77,"line":687},59,[75,689,473],{"class":81},[75,691,692],{"class":85}," isbn;\n",[75,694,696],{"class":77,"line":695},60,[75,697,482],{"class":85},[75,699,701],{"class":77,"line":700},61,[75,702,93],{"emptyLinePlaceholder":92},[75,704,706,708,710,713,715,718],{"class":77,"line":705},62,[75,707,458],{"class":81},[75,709,495],{"class":81},[75,711,712],{"class":167}," setIsbn",[75,714,501],{"class":85},[75,716,717],{"class":504},"isbn",[75,719,508],{"class":85},[75,721,723,725,728,730],{"class":77,"line":722},63,[75,724,514],{"class":191},[75,726,727],{"class":85},".isbn ",[75,729,520],{"class":81},[75,731,692],{"class":85},[75,733,735],{"class":77,"line":734},64,[75,736,482],{"class":85},[75,738,740],{"class":77,"line":739},65,[75,741,93],{"emptyLinePlaceholder":92},[75,743,745,747,750,753],{"class":77,"line":744},66,[75,746,458],{"class":81},[75,748,749],{"class":85}," Long ",[75,751,752],{"class":167},"getCategoryId",[75,754,467],{"class":85},[75,756,758,760],{"class":77,"line":757},67,[75,759,473],{"class":81},[75,761,762],{"class":85}," categoryId;\n",[75,764,766],{"class":77,"line":765},68,[75,767,482],{"class":85},[75,769,771],{"class":77,"line":770},69,[75,772,93],{"emptyLinePlaceholder":92},[75,774,776,778,780,783,786,789],{"class":77,"line":775},70,[75,777,458],{"class":81},[75,779,495],{"class":81},[75,781,782],{"class":167}," setCategoryId",[75,784,785],{"class":85},"(Long ",[75,787,788],{"class":504},"categoryId",[75,790,508],{"class":85},[75,792,794,796,799,801],{"class":77,"line":793},71,[75,795,514],{"class":191},[75,797,798],{"class":85},".categoryId ",[75,800,520],{"class":81},[75,802,762],{"class":85},[75,804,806],{"class":77,"line":805},72,[75,807,482],{"class":85},[75,809,811],{"class":77,"line":810},73,[75,812,813],{"class":85},"}\n",[13,815,816],{},"三处细节值得单独说：",[818,819,820,833,839],"ul",{},[28,821,822,824,825,828,829,832],{},[61,823,648],{}," 用 ",[61,826,827],{},"Integer"," 而不是 ",[61,830,831],{},"int","。数字框留空时请求参数是空串，用基本类型会让字段绑定\n失败并留下一个「必需字段缺失」的错误，用包装类型就没有这个副作用；",[28,834,835,838],{},[61,836,837],{},"@PastOrCurrentYear"," 是这一篇第 5 节要写的自定义校验注解，「不能填未来年份」这种规则内置\n注解表达不了；",[28,840,841,842,845,846,849],{},"表单对象放在 ",[61,843,844],{},"dto"," 包，Service 也直接用它（见下面的 ",[61,847,848],{},"create","），这样不用在 controller 和\nservice 之间再来一次拷贝。",[13,851,852,853,856,857,64],{},"控制器就是在 05 篇那个 ",[61,854,855],{},"BookViewController"," 上加两个方法，并多注入一个 ",[61,858,859],{},"CategoryService",[66,861,863],{"className":68,"code":862,"language":70,"meta":71,"style":71},"    private final CategoryService categoryService;\n\n    public BookViewController(BookService bookService, CategoryService categoryService) {\n        this.bookService = bookService;\n        this.categoryService = categoryService;\n    }\n\n    @GetMapping(\"\u002Fnew\")\n    public String form(Model model) {\n        model.addAttribute(\"form\", new BookForm());        \u002F\u002F 空对象，th:object 靠它\n        model.addAttribute(\"categories\", categoryService.findAll());\n        return \"books\u002Fform\";\n    }\n\n    @PostMapping\n    public String create(@Valid @ModelAttribute(\"form\") BookForm form,\n                         BindingResult bindingResult,\n                         Model model,\n                         RedirectAttributes redirectAttributes) {\n        if (bindingResult.hasErrors()) {\n            model.addAttribute(\"categories\", categoryService.findAll());  \u002F\u002F 下拉框数据要重新准备\n            return \"books\u002Fform\";\n        }\n        bookService.create(form);\n        redirectAttributes.addFlashAttribute(\"message\", \"新增成功\");\n        return \"redirect:\u002Fbooks\";\n    }\n",[61,864,865,875,879,889,901,913,917,921,935,947,973,993,1003,1007,1011,1018,1045,1050,1055,1060,1074,1095,1104,1109,1119,1140,1149],{"__ignoreMap":71},[75,866,867,869,872],{"class":77,"line":78},[75,868,238],{"class":81},[75,870,871],{"class":81}," final",[75,873,874],{"class":85}," CategoryService categoryService;\n",[75,876,877],{"class":77,"line":89},[75,878,93],{"emptyLinePlaceholder":92},[75,880,881,883,886],{"class":77,"line":96},[75,882,458],{"class":81},[75,884,885],{"class":167}," BookViewController",[75,887,888],{"class":85},"(BookService bookService, CategoryService categoryService) {\n",[75,890,891,893,896,898],{"class":77,"line":105},[75,892,514],{"class":191},[75,894,895],{"class":85},".bookService ",[75,897,520],{"class":81},[75,899,900],{"class":85}," bookService;\n",[75,902,903,905,908,910],{"class":77,"line":113},[75,904,514],{"class":191},[75,906,907],{"class":85},".categoryService ",[75,909,520],{"class":81},[75,911,912],{"class":85}," categoryService;\n",[75,914,915],{"class":77,"line":121},[75,916,482],{"class":85},[75,918,919],{"class":77,"line":129},[75,920,93],{"emptyLinePlaceholder":92},[75,922,923,925,928,930,933],{"class":77,"line":137},[75,924,182],{"class":85},[75,926,927],{"class":81},"GetMapping",[75,929,188],{"class":85},[75,931,932],{"class":198},"\"\u002Fnew\"",[75,934,202],{"class":85},[75,936,937,939,941,944],{"class":77,"line":145},[75,938,458],{"class":81},[75,940,461],{"class":85},[75,942,943],{"class":167},"form",[75,945,946],{"class":85},"(Model model) {\n",[75,948,949,952,955,957,960,962,965,967,970],{"class":77,"line":153},[75,950,951],{"class":85},"        model.",[75,953,954],{"class":167},"addAttribute",[75,956,188],{"class":85},[75,958,959],{"class":198},"\"form\"",[75,961,223],{"class":85},[75,963,964],{"class":81},"new",[75,966,168],{"class":167},[75,968,969],{"class":85},"());        ",[75,971,972],{"class":440},"\u002F\u002F 空对象，th:object 靠它\n",[75,974,975,977,979,981,984,987,990],{"class":77,"line":158},[75,976,951],{"class":85},[75,978,954],{"class":167},[75,980,188],{"class":85},[75,982,983],{"class":198},"\"categories\"",[75,985,986],{"class":85},", categoryService.",[75,988,989],{"class":167},"findAll",[75,991,992],{"class":85},"());\n",[75,994,995,997,1000],{"class":77,"line":174},[75,996,473],{"class":81},[75,998,999],{"class":198}," \"books\u002Fform\"",[75,1001,1002],{"class":85},";\n",[75,1004,1005],{"class":77,"line":179},[75,1006,482],{"class":85},[75,1008,1009],{"class":77,"line":205},[75,1010,93],{"emptyLinePlaceholder":92},[75,1012,1013,1015],{"class":77,"line":235},[75,1014,182],{"class":85},[75,1016,1017],{"class":81},"PostMapping\n",[75,1019,1020,1022,1024,1026,1029,1032,1035,1038,1040,1042],{"class":77,"line":244},[75,1021,458],{"class":81},[75,1023,461],{"class":85},[75,1025,848],{"class":167},[75,1027,1028],{"class":85},"(@",[75,1030,1031],{"class":81},"Valid",[75,1033,1034],{"class":85}," @",[75,1036,1037],{"class":81},"ModelAttribute",[75,1039,188],{"class":85},[75,1041,959],{"class":198},[75,1043,1044],{"class":85},") BookForm form,\n",[75,1046,1047],{"class":77,"line":249},[75,1048,1049],{"class":85},"                         BindingResult bindingResult,\n",[75,1051,1052],{"class":77,"line":267},[75,1053,1054],{"class":85},"                         Model model,\n",[75,1056,1057],{"class":77,"line":275},[75,1058,1059],{"class":85},"                         RedirectAttributes redirectAttributes) {\n",[75,1061,1062,1065,1068,1071],{"class":77,"line":280},[75,1063,1064],{"class":81},"        if",[75,1066,1067],{"class":85}," (bindingResult.",[75,1069,1070],{"class":167},"hasErrors",[75,1072,1073],{"class":85},"()) {\n",[75,1075,1076,1079,1081,1083,1085,1087,1089,1092],{"class":77,"line":288},[75,1077,1078],{"class":85},"            model.",[75,1080,954],{"class":167},[75,1082,188],{"class":85},[75,1084,983],{"class":198},[75,1086,986],{"class":85},[75,1088,989],{"class":167},[75,1090,1091],{"class":85},"());  ",[75,1093,1094],{"class":440},"\u002F\u002F 下拉框数据要重新准备\n",[75,1096,1097,1100,1102],{"class":77,"line":317},[75,1098,1099],{"class":81},"            return",[75,1101,999],{"class":198},[75,1103,1002],{"class":85},[75,1105,1106],{"class":77,"line":344},[75,1107,1108],{"class":85},"        }\n",[75,1110,1111,1114,1116],{"class":77,"line":352},[75,1112,1113],{"class":85},"        bookService.",[75,1115,848],{"class":167},[75,1117,1118],{"class":85},"(form);\n",[75,1120,1121,1124,1127,1129,1132,1134,1137],{"class":77,"line":357},[75,1122,1123],{"class":85},"        redirectAttributes.",[75,1125,1126],{"class":167},"addFlashAttribute",[75,1128,188],{"class":85},[75,1130,1131],{"class":198},"\"message\"",[75,1133,223],{"class":85},[75,1135,1136],{"class":198},"\"新增成功\"",[75,1138,1139],{"class":85},");\n",[75,1141,1142,1144,1147],{"class":77,"line":392},[75,1143,473],{"class":81},[75,1145,1146],{"class":198}," \"redirect:\u002Fbooks\"",[75,1148,1002],{"class":85},[75,1150,1151],{"class":77,"line":400},[75,1152,482],{"class":85},[13,1154,1155,1157,1158,1161],{},[61,1156,859],{}," 是这一篇新引入的（04 篇只给了 ",[61,1159,1160],{},"Category"," 实体），接口和实现都很短：",[66,1163,1165],{"className":68,"code":1164,"language":70,"meta":71,"style":71},"package com.example.library.service;\n\nimport com.example.library.domain.Category;\n\nimport java.util.List;\n\npublic interface CategoryService {\n\n    List\u003CCategory> findAll();\n\n    Category findByName(String name);\n}\n",[61,1166,1167,1174,1178,1185,1189,1196,1200,1212,1216,1231,1235,1250],{"__ignoreMap":71},[75,1168,1169,1171],{"class":77,"line":78},[75,1170,82],{"class":81},[75,1172,1173],{"class":85}," com.example.library.service;\n",[75,1175,1176],{"class":77,"line":89},[75,1177,93],{"emptyLinePlaceholder":92},[75,1179,1180,1182],{"class":77,"line":96},[75,1181,99],{"class":81},[75,1183,1184],{"class":85}," com.example.library.domain.Category;\n",[75,1186,1187],{"class":77,"line":105},[75,1188,93],{"emptyLinePlaceholder":92},[75,1190,1191,1193],{"class":77,"line":113},[75,1192,99],{"class":81},[75,1194,1195],{"class":85}," java.util.List;\n",[75,1197,1198],{"class":77,"line":121},[75,1199,93],{"emptyLinePlaceholder":92},[75,1201,1202,1204,1207,1210],{"class":77,"line":129},[75,1203,161],{"class":81},[75,1205,1206],{"class":81}," interface",[75,1208,1209],{"class":167}," CategoryService",[75,1211,171],{"class":85},[75,1213,1214],{"class":77,"line":137},[75,1215,93],{"emptyLinePlaceholder":92},[75,1217,1218,1221,1223,1226,1228],{"class":77,"line":145},[75,1219,1220],{"class":85},"    List\u003C",[75,1222,1160],{"class":81},[75,1224,1225],{"class":85},"> ",[75,1227,989],{"class":167},[75,1229,1230],{"class":85},"();\n",[75,1232,1233],{"class":77,"line":153},[75,1234,93],{"emptyLinePlaceholder":92},[75,1236,1237,1240,1243,1245,1248],{"class":77,"line":158},[75,1238,1239],{"class":85},"    Category ",[75,1241,1242],{"class":167},"findByName",[75,1244,501],{"class":85},[75,1246,1247],{"class":504},"name",[75,1249,1139],{"class":85},[75,1251,1252],{"class":77,"line":174},[75,1253,813],{"class":85},[66,1255,1257],{"className":68,"code":1256,"language":70,"meta":71,"style":71},"package com.example.library.service;\n\nimport com.example.library.domain.Category;\nimport com.example.library.repository.CategoryRepository;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.util.List;\nimport java.util.NoSuchElementException;\n\n@Service\n@Transactional(readOnly = true)\npublic class CategoryServiceImpl implements CategoryService {\n\n    private final CategoryRepository categoryRepository;\n\n    public CategoryServiceImpl(CategoryRepository categoryRepository) {\n        this.categoryRepository = categoryRepository;\n    }\n\n    @Override\n    public List\u003CCategory> findAll() {\n        return categoryRepository.findAll();\n    }\n\n    @Override\n    public Category findByName(String name) {\n        return categoryRepository.findByName(name)\n                .orElseThrow(() -> new NoSuchElementException(\"分类不存在：\" + name));\n    }\n}\n",[61,1258,1259,1265,1269,1275,1282,1289,1296,1300,1306,1313,1317,1325,1344,1360,1364,1373,1377,1391,1403,1407,1411,1418,1433,1444,1448,1452,1458,1473,1484,1515,1519],{"__ignoreMap":71},[75,1260,1261,1263],{"class":77,"line":78},[75,1262,82],{"class":81},[75,1264,1173],{"class":85},[75,1266,1267],{"class":77,"line":89},[75,1268,93],{"emptyLinePlaceholder":92},[75,1270,1271,1273],{"class":77,"line":96},[75,1272,99],{"class":81},[75,1274,1184],{"class":85},[75,1276,1277,1279],{"class":77,"line":105},[75,1278,99],{"class":81},[75,1280,1281],{"class":85}," com.example.library.repository.CategoryRepository;\n",[75,1283,1284,1286],{"class":77,"line":113},[75,1285,99],{"class":81},[75,1287,1288],{"class":85}," org.springframework.stereotype.Service;\n",[75,1290,1291,1293],{"class":77,"line":121},[75,1292,99],{"class":81},[75,1294,1295],{"class":85}," org.springframework.transaction.annotation.Transactional;\n",[75,1297,1298],{"class":77,"line":129},[75,1299,93],{"emptyLinePlaceholder":92},[75,1301,1302,1304],{"class":77,"line":137},[75,1303,99],{"class":81},[75,1305,1195],{"class":85},[75,1307,1308,1310],{"class":77,"line":145},[75,1309,99],{"class":81},[75,1311,1312],{"class":85}," java.util.NoSuchElementException;\n",[75,1314,1315],{"class":77,"line":153},[75,1316,93],{"emptyLinePlaceholder":92},[75,1318,1319,1322],{"class":77,"line":158},[75,1320,1321],{"class":85},"@",[75,1323,1324],{"class":81},"Service\n",[75,1326,1327,1329,1332,1334,1337,1339,1342],{"class":77,"line":174},[75,1328,1321],{"class":85},[75,1330,1331],{"class":81},"Transactional",[75,1333,188],{"class":85},[75,1335,1336],{"class":191},"readOnly",[75,1338,195],{"class":81},[75,1340,1341],{"class":191}," true",[75,1343,202],{"class":85},[75,1345,1346,1348,1350,1353,1356,1358],{"class":77,"line":179},[75,1347,161],{"class":81},[75,1349,164],{"class":81},[75,1351,1352],{"class":167}," CategoryServiceImpl",[75,1354,1355],{"class":81}," implements",[75,1357,1209],{"class":167},[75,1359,171],{"class":85},[75,1361,1362],{"class":77,"line":205},[75,1363,93],{"emptyLinePlaceholder":92},[75,1365,1366,1368,1370],{"class":77,"line":235},[75,1367,238],{"class":81},[75,1369,871],{"class":81},[75,1371,1372],{"class":85}," CategoryRepository categoryRepository;\n",[75,1374,1375],{"class":77,"line":244},[75,1376,93],{"emptyLinePlaceholder":92},[75,1378,1379,1381,1383,1386,1389],{"class":77,"line":249},[75,1380,458],{"class":81},[75,1382,1352],{"class":167},[75,1384,1385],{"class":85},"(CategoryRepository ",[75,1387,1388],{"class":504},"categoryRepository",[75,1390,508],{"class":85},[75,1392,1393,1395,1398,1400],{"class":77,"line":267},[75,1394,514],{"class":191},[75,1396,1397],{"class":85},".categoryRepository ",[75,1399,520],{"class":81},[75,1401,1402],{"class":85}," categoryRepository;\n",[75,1404,1405],{"class":77,"line":275},[75,1406,482],{"class":85},[75,1408,1409],{"class":77,"line":280},[75,1410,93],{"emptyLinePlaceholder":92},[75,1412,1413,1415],{"class":77,"line":288},[75,1414,182],{"class":85},[75,1416,1417],{"class":81},"Override\n",[75,1419,1420,1422,1425,1427,1429,1431],{"class":77,"line":317},[75,1421,458],{"class":81},[75,1423,1424],{"class":85}," List\u003C",[75,1426,1160],{"class":81},[75,1428,1225],{"class":85},[75,1430,989],{"class":167},[75,1432,467],{"class":85},[75,1434,1435,1437,1440,1442],{"class":77,"line":344},[75,1436,473],{"class":81},[75,1438,1439],{"class":85}," categoryRepository.",[75,1441,989],{"class":167},[75,1443,1230],{"class":85},[75,1445,1446],{"class":77,"line":352},[75,1447,482],{"class":85},[75,1449,1450],{"class":77,"line":357},[75,1451,93],{"emptyLinePlaceholder":92},[75,1453,1454,1456],{"class":77,"line":392},[75,1455,182],{"class":85},[75,1457,1417],{"class":81},[75,1459,1460,1462,1465,1467,1469,1471],{"class":77,"line":400},[75,1461,458],{"class":81},[75,1463,1464],{"class":85}," Category ",[75,1466,1242],{"class":167},[75,1468,501],{"class":85},[75,1470,1247],{"class":504},[75,1472,508],{"class":85},[75,1474,1475,1477,1479,1481],{"class":77,"line":405},[75,1476,473],{"class":81},[75,1478,1439],{"class":85},[75,1480,1242],{"class":167},[75,1482,1483],{"class":85},"(name)\n",[75,1485,1486,1489,1492,1495,1498,1501,1504,1506,1509,1512],{"class":77,"line":424},[75,1487,1488],{"class":85},"                .",[75,1490,1491],{"class":167},"orElseThrow",[75,1493,1494],{"class":85},"(() ",[75,1496,1497],{"class":81},"->",[75,1499,1500],{"class":81}," new",[75,1502,1503],{"class":167}," NoSuchElementException",[75,1505,188],{"class":85},[75,1507,1508],{"class":198},"\"分类不存在：\"",[75,1510,1511],{"class":81}," +",[75,1513,1514],{"class":85}," name));\n",[75,1516,1517],{"class":77,"line":432},[75,1518,482],{"class":85},[75,1520,1521],{"class":77,"line":437},[75,1522,813],{"class":85},[13,1524,1525],{},"它对应的仓储接口（04 篇只给了实体，这里补上）：",[66,1527,1529],{"className":68,"code":1528,"language":70,"meta":71,"style":71},"package com.example.library.repository;\n\nimport com.example.library.domain.Category;\nimport org.springframework.data.jpa.repository.JpaRepository;\n\nimport java.util.Optional;\n\npublic interface CategoryRepository extends JpaRepository\u003CCategory, Long> {\n\n    Optional\u003CCategory> findByName(String name);\n}\n",[61,1530,1531,1538,1542,1548,1555,1559,1566,1570,1598,1602,1619],{"__ignoreMap":71},[75,1532,1533,1535],{"class":77,"line":78},[75,1534,82],{"class":81},[75,1536,1537],{"class":85}," com.example.library.repository;\n",[75,1539,1540],{"class":77,"line":89},[75,1541,93],{"emptyLinePlaceholder":92},[75,1543,1544,1546],{"class":77,"line":96},[75,1545,99],{"class":81},[75,1547,1184],{"class":85},[75,1549,1550,1552],{"class":77,"line":105},[75,1551,99],{"class":81},[75,1553,1554],{"class":85}," org.springframework.data.jpa.repository.JpaRepository;\n",[75,1556,1557],{"class":77,"line":113},[75,1558,93],{"emptyLinePlaceholder":92},[75,1560,1561,1563],{"class":77,"line":121},[75,1562,99],{"class":81},[75,1564,1565],{"class":85}," java.util.Optional;\n",[75,1567,1568],{"class":77,"line":129},[75,1569,93],{"emptyLinePlaceholder":92},[75,1571,1572,1574,1576,1579,1582,1585,1588,1590,1592,1595],{"class":77,"line":137},[75,1573,161],{"class":81},[75,1575,1206],{"class":81},[75,1577,1578],{"class":167}," CategoryRepository",[75,1580,1581],{"class":81}," extends",[75,1583,1584],{"class":167}," JpaRepository",[75,1586,1587],{"class":85},"\u003C",[75,1589,1160],{"class":81},[75,1591,223],{"class":85},[75,1593,1594],{"class":81},"Long",[75,1596,1597],{"class":85},"> {\n",[75,1599,1600],{"class":77,"line":145},[75,1601,93],{"emptyLinePlaceholder":92},[75,1603,1604,1607,1609,1611,1613,1615,1617],{"class":77,"line":153},[75,1605,1606],{"class":85},"    Optional\u003C",[75,1608,1160],{"class":81},[75,1610,1225],{"class":85},[75,1612,1242],{"class":167},[75,1614,501],{"class":85},[75,1616,1247],{"class":504},[75,1618,1139],{"class":85},[75,1620,1621],{"class":77,"line":158},[75,1622,813],{"class":85},[13,1624,1625,1626,1629,1630,1632,1633,1636,1637,1640,1641,64],{},"最后是 ",[61,1627,1628],{},"BookService"," 新增的 ",[61,1631,848],{},"。接口加一行 ",[61,1634,1635],{},"Book create(BookForm form);","，\n",[61,1638,1639],{},"BookServiceImpl"," 相应多注入一个 ",[61,1642,1643],{},"CategoryRepository",[66,1645,1647],{"className":68,"code":1646,"language":70,"meta":71,"style":71},"    private final BookRepository bookRepository;\n    private final CategoryRepository categoryRepository;\n\n    public BookServiceImpl(BookRepository bookRepository, CategoryRepository categoryRepository) {\n        this.bookRepository = bookRepository;\n        this.categoryRepository = categoryRepository;\n    }\n\n    @Override\n    @Transactional\n    public Book create(BookForm form) {\n        Category category = categoryRepository.findById(form.getCategoryId())\n                .orElseThrow(() -> new NoSuchElementException(\"分类不存在：\" + form.getCategoryId()));\n\n        Book book = new Book(form.getTitle(), category);   \u002F\u002F 02 \u002F 04 篇那个「带分类」的构造器\n        book.setAuthor(form.getAuthor());\n        book.setIsbn(form.getIsbn());\n        book.setPublishedYear(form.getPublishedYear());\n        return bookRepository.save(book);\n    }\n",[61,1648,1649,1658,1666,1670,1680,1692,1702,1706,1710,1716,1723,1735,1755,1783,1787,1809,1823,1836,1849,1862],{"__ignoreMap":71},[75,1650,1651,1653,1655],{"class":77,"line":78},[75,1652,238],{"class":81},[75,1654,871],{"class":81},[75,1656,1657],{"class":85}," BookRepository bookRepository;\n",[75,1659,1660,1662,1664],{"class":77,"line":89},[75,1661,238],{"class":81},[75,1663,871],{"class":81},[75,1665,1372],{"class":85},[75,1667,1668],{"class":77,"line":96},[75,1669,93],{"emptyLinePlaceholder":92},[75,1671,1672,1674,1677],{"class":77,"line":105},[75,1673,458],{"class":81},[75,1675,1676],{"class":167}," BookServiceImpl",[75,1678,1679],{"class":85},"(BookRepository bookRepository, CategoryRepository categoryRepository) {\n",[75,1681,1682,1684,1687,1689],{"class":77,"line":113},[75,1683,514],{"class":191},[75,1685,1686],{"class":85},".bookRepository ",[75,1688,520],{"class":81},[75,1690,1691],{"class":85}," bookRepository;\n",[75,1693,1694,1696,1698,1700],{"class":77,"line":121},[75,1695,514],{"class":191},[75,1697,1397],{"class":85},[75,1699,520],{"class":81},[75,1701,1402],{"class":85},[75,1703,1704],{"class":77,"line":129},[75,1705,482],{"class":85},[75,1707,1708],{"class":77,"line":137},[75,1709,93],{"emptyLinePlaceholder":92},[75,1711,1712,1714],{"class":77,"line":145},[75,1713,182],{"class":85},[75,1715,1417],{"class":81},[75,1717,1718,1720],{"class":77,"line":153},[75,1719,182],{"class":85},[75,1721,1722],{"class":81},"Transactional\n",[75,1724,1725,1727,1730,1732],{"class":77,"line":158},[75,1726,458],{"class":81},[75,1728,1729],{"class":85}," Book ",[75,1731,848],{"class":167},[75,1733,1734],{"class":85},"(BookForm form) {\n",[75,1736,1737,1740,1742,1744,1747,1750,1752],{"class":77,"line":174},[75,1738,1739],{"class":85},"        Category category ",[75,1741,520],{"class":81},[75,1743,1439],{"class":85},[75,1745,1746],{"class":167},"findById",[75,1748,1749],{"class":85},"(form.",[75,1751,752],{"class":167},[75,1753,1754],{"class":85},"())\n",[75,1756,1757,1759,1761,1763,1765,1767,1769,1771,1773,1775,1778,1780],{"class":77,"line":179},[75,1758,1488],{"class":85},[75,1760,1491],{"class":167},[75,1762,1494],{"class":85},[75,1764,1497],{"class":81},[75,1766,1500],{"class":81},[75,1768,1503],{"class":167},[75,1770,188],{"class":85},[75,1772,1508],{"class":198},[75,1774,1511],{"class":81},[75,1776,1777],{"class":85}," form.",[75,1779,752],{"class":167},[75,1781,1782],{"class":85},"()));\n",[75,1784,1785],{"class":77,"line":205},[75,1786,93],{"emptyLinePlaceholder":92},[75,1788,1789,1792,1794,1796,1799,1801,1803,1806],{"class":77,"line":235},[75,1790,1791],{"class":85},"        Book book ",[75,1793,520],{"class":81},[75,1795,1500],{"class":81},[75,1797,1798],{"class":167}," Book",[75,1800,1749],{"class":85},[75,1802,464],{"class":167},[75,1804,1805],{"class":85},"(), category);   ",[75,1807,1808],{"class":440},"\u002F\u002F 02 \u002F 04 篇那个「带分类」的构造器\n",[75,1810,1811,1814,1817,1819,1821],{"class":77,"line":244},[75,1812,1813],{"class":85},"        book.",[75,1815,1816],{"class":167},"setAuthor",[75,1818,1749],{"class":85},[75,1820,542],{"class":167},[75,1822,992],{"class":85},[75,1824,1825,1827,1830,1832,1834],{"class":77,"line":249},[75,1826,1813],{"class":85},[75,1828,1829],{"class":167},"setIsbn",[75,1831,1749],{"class":85},[75,1833,682],{"class":167},[75,1835,992],{"class":85},[75,1837,1838,1840,1843,1845,1847],{"class":77,"line":267},[75,1839,1813],{"class":85},[75,1841,1842],{"class":167},"setPublishedYear",[75,1844,1749],{"class":85},[75,1846,612],{"class":167},[75,1848,992],{"class":85},[75,1850,1851,1853,1856,1859],{"class":77,"line":275},[75,1852,473],{"class":81},[75,1854,1855],{"class":85}," bookRepository.",[75,1857,1858],{"class":167},"save",[75,1860,1861],{"class":85},"(book);\n",[75,1863,1864],{"class":77,"line":280},[75,1865,482],{"class":85},[13,1867,1868,1871],{},[61,1869,1870],{},"@ModelAttribute"," 做了三件事：",[818,1873,1874,1888,1910],{},[28,1875,1876,1879,1880,1883,1884,1887],{},[32,1877,1878],{},"把请求参数按名字写进对象属性","（",[61,1881,1882],{},"title=重构"," → ",[61,1885,1886],{},"form.setTitle(\"重构\")","）；",[28,1889,1890,1896,1897,1883,1900,1903,1904,1907,1908,42],{},[32,1891,1892,1893],{},"把对象放进 ",[61,1894,1895],{},"Model","，键名默认是「类名首字母小写」（",[61,1898,1899],{},"BookForm",[61,1901,1902],{},"bookForm","），用\n",[61,1905,1906],{},"@ModelAttribute(\"form\")"," 可以改成 ",[61,1909,943],{},[28,1911,1912,1913,1916,1917,1920],{},"因此表单页 ",[32,1914,1915],{},"GET 时也要放一个同名的空对象","，否则 ",[61,1918,1919],{},"th:object=\"${form}\""," 会报错。",[13,1922,1923],{},"为什么用专门的表单对象，而不是直接绑定 JPA 实体？因为两者关心的东西不同：实体关心持久化\n（主键、版本、关联），表单关心输入（字段是否必填、格式）。混在一起，改一个就动全身。",[17,1925,1927],{"id":1926},"_2-模板侧的-thobject-与-thfield","2 模板侧的 th:object 与 th:field",[66,1929,1933],{"className":1930,"code":1931,"language":1932,"meta":71,"style":71},"language-html shiki shiki-themes github-light github-dark","\u003Cform th:action=\"@{\u002Fbooks}\" th:object=\"${form}\" method=\"post\">\n\n    \u003Clabel for=\"title\">书名\u003C\u002Flabel>\n    \u003Cinput type=\"text\" id=\"title\" th:field=\"*{title}\" th:errorclass=\"is-invalid\">\n    \u003Cspan class=\"error\" th:if=\"${#fields.hasErrors('title')}\" th:errors=\"*{title}\">书名不能为空\u003C\u002Fspan>\n\n    \u003Clabel for=\"author\">作者\u003C\u002Flabel>\n    \u003Cinput type=\"text\" id=\"author\" th:field=\"*{author}\">\n    \u003Cspan class=\"error\" th:if=\"${#fields.hasErrors('author')}\" th:errors=\"*{author}\">作者不能为空\u003C\u002Fspan>\n\n    \u003Clabel for=\"publishedYear\">出版年份\u003C\u002Flabel>\n    \u003Cinput type=\"number\" id=\"publishedYear\" th:field=\"*{publishedYear}\">\n\n    \u003Clabel for=\"isbn\">ISBN\u003C\u002Flabel>\n    \u003Cinput type=\"text\" id=\"isbn\" th:field=\"*{isbn}\">\n\n    \u003Clabel for=\"categoryId\">分类\u003C\u002Flabel>\n    \u003Cselect id=\"categoryId\" th:field=\"*{categoryId}\">\n        \u003Coption value=\"\">请选择\u003C\u002Foption>\n        \u003Coption th:each=\"category : ${categories}\"\n                th:value=\"${category.id}\"\n                th:text=\"${category.name}\">小说\u003C\u002Foption>\n    \u003C\u002Fselect>\n    \u003Cspan class=\"error\" th:if=\"${#fields.hasErrors('categoryId')}\" th:errors=\"*{categoryId}\">请选择分类\u003C\u002Fspan>\n\n    \u003Cbutton type=\"submit\">保存\u003C\u002Fbutton>\n\u003C\u002Fform>\n","html",[61,1934,1935,1969,1973,1996,2036,2071,2075,2095,2122,2154,2158,2178,2206,2210,2230,2257,2261,2281,2303,2326,2340,2350,2367,2376,2408,2412,2433],{"__ignoreMap":71},[75,1936,1937,1939,1942,1945,1947,1950,1953,1955,1958,1961,1963,1966],{"class":77,"line":78},[75,1938,1587],{"class":85},[75,1940,943],{"class":1941},"s9eBZ",[75,1943,1944],{"class":167}," th:action",[75,1946,520],{"class":85},[75,1948,1949],{"class":198},"\"@{\u002Fbooks}\"",[75,1951,1952],{"class":167}," th:object",[75,1954,520],{"class":85},[75,1956,1957],{"class":198},"\"${form}\"",[75,1959,1960],{"class":167}," method",[75,1962,520],{"class":85},[75,1964,1965],{"class":198},"\"post\"",[75,1967,1968],{"class":85},">\n",[75,1970,1971],{"class":77,"line":89},[75,1972,93],{"emptyLinePlaceholder":92},[75,1974,1975,1978,1981,1984,1986,1989,1992,1994],{"class":77,"line":96},[75,1976,1977],{"class":85},"    \u003C",[75,1979,1980],{"class":1941},"label",[75,1982,1983],{"class":167}," for",[75,1985,520],{"class":85},[75,1987,1988],{"class":198},"\"title\"",[75,1990,1991],{"class":85},">书名\u003C\u002F",[75,1993,1980],{"class":1941},[75,1995,1968],{"class":85},[75,1997,1998,2000,2003,2006,2008,2011,2014,2016,2018,2021,2023,2026,2029,2031,2034],{"class":77,"line":105},[75,1999,1977],{"class":85},[75,2001,2002],{"class":1941},"input",[75,2004,2005],{"class":167}," type",[75,2007,520],{"class":85},[75,2009,2010],{"class":198},"\"text\"",[75,2012,2013],{"class":167}," id",[75,2015,520],{"class":85},[75,2017,1988],{"class":198},[75,2019,2020],{"class":167}," th:field",[75,2022,520],{"class":85},[75,2024,2025],{"class":198},"\"*{title}\"",[75,2027,2028],{"class":167}," th:errorclass",[75,2030,520],{"class":85},[75,2032,2033],{"class":198},"\"is-invalid\"",[75,2035,1968],{"class":85},[75,2037,2038,2040,2042,2044,2046,2049,2052,2054,2057,2060,2062,2064,2067,2069],{"class":77,"line":113},[75,2039,1977],{"class":85},[75,2041,75],{"class":1941},[75,2043,164],{"class":167},[75,2045,520],{"class":85},[75,2047,2048],{"class":198},"\"error\"",[75,2050,2051],{"class":167}," th:if",[75,2053,520],{"class":85},[75,2055,2056],{"class":198},"\"${#fields.hasErrors('title')}\"",[75,2058,2059],{"class":167}," th:errors",[75,2061,520],{"class":85},[75,2063,2025],{"class":198},[75,2065,2066],{"class":85},">书名不能为空\u003C\u002F",[75,2068,75],{"class":1941},[75,2070,1968],{"class":85},[75,2072,2073],{"class":77,"line":121},[75,2074,93],{"emptyLinePlaceholder":92},[75,2076,2077,2079,2081,2083,2085,2088,2091,2093],{"class":77,"line":129},[75,2078,1977],{"class":85},[75,2080,1980],{"class":1941},[75,2082,1983],{"class":167},[75,2084,520],{"class":85},[75,2086,2087],{"class":198},"\"author\"",[75,2089,2090],{"class":85},">作者\u003C\u002F",[75,2092,1980],{"class":1941},[75,2094,1968],{"class":85},[75,2096,2097,2099,2101,2103,2105,2107,2109,2111,2113,2115,2117,2120],{"class":77,"line":137},[75,2098,1977],{"class":85},[75,2100,2002],{"class":1941},[75,2102,2005],{"class":167},[75,2104,520],{"class":85},[75,2106,2010],{"class":198},[75,2108,2013],{"class":167},[75,2110,520],{"class":85},[75,2112,2087],{"class":198},[75,2114,2020],{"class":167},[75,2116,520],{"class":85},[75,2118,2119],{"class":198},"\"*{author}\"",[75,2121,1968],{"class":85},[75,2123,2124,2126,2128,2130,2132,2134,2136,2138,2141,2143,2145,2147,2150,2152],{"class":77,"line":145},[75,2125,1977],{"class":85},[75,2127,75],{"class":1941},[75,2129,164],{"class":167},[75,2131,520],{"class":85},[75,2133,2048],{"class":198},[75,2135,2051],{"class":167},[75,2137,520],{"class":85},[75,2139,2140],{"class":198},"\"${#fields.hasErrors('author')}\"",[75,2142,2059],{"class":167},[75,2144,520],{"class":85},[75,2146,2119],{"class":198},[75,2148,2149],{"class":85},">作者不能为空\u003C\u002F",[75,2151,75],{"class":1941},[75,2153,1968],{"class":85},[75,2155,2156],{"class":77,"line":153},[75,2157,93],{"emptyLinePlaceholder":92},[75,2159,2160,2162,2164,2166,2168,2171,2174,2176],{"class":77,"line":158},[75,2161,1977],{"class":85},[75,2163,1980],{"class":1941},[75,2165,1983],{"class":167},[75,2167,520],{"class":85},[75,2169,2170],{"class":198},"\"publishedYear\"",[75,2172,2173],{"class":85},">出版年份\u003C\u002F",[75,2175,1980],{"class":1941},[75,2177,1968],{"class":85},[75,2179,2180,2182,2184,2186,2188,2191,2193,2195,2197,2199,2201,2204],{"class":77,"line":174},[75,2181,1977],{"class":85},[75,2183,2002],{"class":1941},[75,2185,2005],{"class":167},[75,2187,520],{"class":85},[75,2189,2190],{"class":198},"\"number\"",[75,2192,2013],{"class":167},[75,2194,520],{"class":85},[75,2196,2170],{"class":198},[75,2198,2020],{"class":167},[75,2200,520],{"class":85},[75,2202,2203],{"class":198},"\"*{publishedYear}\"",[75,2205,1968],{"class":85},[75,2207,2208],{"class":77,"line":179},[75,2209,93],{"emptyLinePlaceholder":92},[75,2211,2212,2214,2216,2218,2220,2223,2226,2228],{"class":77,"line":205},[75,2213,1977],{"class":85},[75,2215,1980],{"class":1941},[75,2217,1983],{"class":167},[75,2219,520],{"class":85},[75,2221,2222],{"class":198},"\"isbn\"",[75,2224,2225],{"class":85},">ISBN\u003C\u002F",[75,2227,1980],{"class":1941},[75,2229,1968],{"class":85},[75,2231,2232,2234,2236,2238,2240,2242,2244,2246,2248,2250,2252,2255],{"class":77,"line":235},[75,2233,1977],{"class":85},[75,2235,2002],{"class":1941},[75,2237,2005],{"class":167},[75,2239,520],{"class":85},[75,2241,2010],{"class":198},[75,2243,2013],{"class":167},[75,2245,520],{"class":85},[75,2247,2222],{"class":198},[75,2249,2020],{"class":167},[75,2251,520],{"class":85},[75,2253,2254],{"class":198},"\"*{isbn}\"",[75,2256,1968],{"class":85},[75,2258,2259],{"class":77,"line":244},[75,2260,93],{"emptyLinePlaceholder":92},[75,2262,2263,2265,2267,2269,2271,2274,2277,2279],{"class":77,"line":249},[75,2264,1977],{"class":85},[75,2266,1980],{"class":1941},[75,2268,1983],{"class":167},[75,2270,520],{"class":85},[75,2272,2273],{"class":198},"\"categoryId\"",[75,2275,2276],{"class":85},">分类\u003C\u002F",[75,2278,1980],{"class":1941},[75,2280,1968],{"class":85},[75,2282,2283,2285,2288,2290,2292,2294,2296,2298,2301],{"class":77,"line":267},[75,2284,1977],{"class":85},[75,2286,2287],{"class":1941},"select",[75,2289,2013],{"class":167},[75,2291,520],{"class":85},[75,2293,2273],{"class":198},[75,2295,2020],{"class":167},[75,2297,520],{"class":85},[75,2299,2300],{"class":198},"\"*{categoryId}\"",[75,2302,1968],{"class":85},[75,2304,2305,2308,2311,2314,2316,2319,2322,2324],{"class":77,"line":275},[75,2306,2307],{"class":85},"        \u003C",[75,2309,2310],{"class":1941},"option",[75,2312,2313],{"class":167}," value",[75,2315,520],{"class":85},[75,2317,2318],{"class":198},"\"\"",[75,2320,2321],{"class":85},">请选择\u003C\u002F",[75,2323,2310],{"class":1941},[75,2325,1968],{"class":85},[75,2327,2328,2330,2332,2335,2337],{"class":77,"line":280},[75,2329,2307],{"class":85},[75,2331,2310],{"class":1941},[75,2333,2334],{"class":167}," th:each",[75,2336,520],{"class":85},[75,2338,2339],{"class":198},"\"category : ${categories}\"\n",[75,2341,2342,2345,2347],{"class":77,"line":288},[75,2343,2344],{"class":167},"                th:value",[75,2346,520],{"class":85},[75,2348,2349],{"class":198},"\"${category.id}\"\n",[75,2351,2352,2355,2357,2360,2363,2365],{"class":77,"line":317},[75,2353,2354],{"class":167},"                th:text",[75,2356,520],{"class":85},[75,2358,2359],{"class":198},"\"${category.name}\"",[75,2361,2362],{"class":85},">小说\u003C\u002F",[75,2364,2310],{"class":1941},[75,2366,1968],{"class":85},[75,2368,2369,2372,2374],{"class":77,"line":344},[75,2370,2371],{"class":85},"    \u003C\u002F",[75,2373,2287],{"class":1941},[75,2375,1968],{"class":85},[75,2377,2378,2380,2382,2384,2386,2388,2390,2392,2395,2397,2399,2401,2404,2406],{"class":77,"line":352},[75,2379,1977],{"class":85},[75,2381,75],{"class":1941},[75,2383,164],{"class":167},[75,2385,520],{"class":85},[75,2387,2048],{"class":198},[75,2389,2051],{"class":167},[75,2391,520],{"class":85},[75,2393,2394],{"class":198},"\"${#fields.hasErrors('categoryId')}\"",[75,2396,2059],{"class":167},[75,2398,520],{"class":85},[75,2400,2300],{"class":198},[75,2402,2403],{"class":85},">请选择分类\u003C\u002F",[75,2405,75],{"class":1941},[75,2407,1968],{"class":85},[75,2409,2410],{"class":77,"line":357},[75,2411,93],{"emptyLinePlaceholder":92},[75,2413,2414,2416,2419,2421,2423,2426,2429,2431],{"class":77,"line":392},[75,2415,1977],{"class":85},[75,2417,2418],{"class":1941},"button",[75,2420,2005],{"class":167},[75,2422,520],{"class":85},[75,2424,2425],{"class":198},"\"submit\"",[75,2427,2428],{"class":85},">保存\u003C\u002F",[75,2430,2418],{"class":1941},[75,2432,1968],{"class":85},[75,2434,2435,2438,2440],{"class":77,"line":400},[75,2436,2437],{"class":85},"\u003C\u002F",[75,2439,943],{"class":1941},[75,2441,1968],{"class":85},[13,2443,2444,2447,2448,2451],{},[61,2445,2446],{},"th:field=\"*{title}\""," 一个属性干了四件事，这是它比手写 ",[61,2449,2450],{},"th:value"," 好用的地方：",[2453,2454,2455,2468],"table",{},[2456,2457,2458],"thead",{},[2459,2460,2461,2465],"tr",{},[2462,2463,2464],"th",{},"它做的事",[2462,2466,2467],{},"效果",[2469,2470,2471,2485,2502,2511],"tbody",{},[2459,2472,2473,2479],{},[2474,2475,2476,2477],"td",{},"生成 ",[61,2478,1247],{},[2474,2480,2481,2484],{},[61,2482,2483],{},"name=\"title\"","，绑定靠的就是这个名字",[2459,2486,2487,2492],{},[2474,2488,2476,2489],{},[61,2490,2491],{},"id",[2474,2493,2494,2497,2498,2501],{},[61,2495,2496],{},"id=\"title\"","，",[61,2499,2500],{},"\u003Clabel for=\"title\">"," 才对得上",[2459,2503,2504,2508],{},[2474,2505,2476,2506],{},[61,2507,298],{},[2474,2509,2510],{},"校验失败回填时，把用户上次填的值放回输入框",[2459,2512,2513,2516],{},[2474,2514,2515],{},"按类型自动处理",[2474,2517,2518,2519,2522,2523,2526],{},"单选\u002F复选\u002F下拉自动选中正确项（",[61,2520,2521],{},"selected"," \u002F ",[61,2524,2525],{},"checked","）",[13,2528,2529,2532,2533,2536,2537,2540],{},[61,2530,2531],{},"*{...}"," 是「选中对象上的表达式」，作用范围是 ",[61,2534,2535],{},"th:object"," 指定的那个对象，等价于\n",[61,2538,2539],{},"${form.title}","，但省去了重复前缀。",[13,2542,2543],{},"几个细节：",[818,2545,2546,2558,2569,2575],{},[28,2547,2548,2557],{},[32,2549,2550,2553,2554,2556],{},[61,2551,2552],{},"th:field"," 与 ",[61,2555,1247],{}," 不要同时手写","，会冲突；",[28,2559,2560,2561,2564,2565,2568],{},"错误提示用 ",[61,2562,2563],{},"th:errors=\"*{title}\"","，它会把该字段所有校验消息拼接输出；配合\n",[61,2566,2567],{},"th:if=\"${#fields.hasErrors('title')}\""," 可以控制空元素的显示；",[28,2570,2571,2574],{},[61,2572,2573],{},"th:errorclass=\"is-invalid\""," 在字段出错时追加 CSS class，前端样式直接用得上；",[28,2576,2577,2580],{},[32,2578,2579],{},"绑定失败的字段（比如数字框里填了字母）不会回填","——数据已经无效了，回填等于把脏值还给\n用户，所以 Thymeleaf 会留空。",[17,2582,2584],{"id":2583},"_3-服务端校验bean-validation","3 服务端校验：Bean Validation",[2586,2587,2589],"h3",{"id":2588},"_31-先加依赖","3.1 先加依赖",[66,2591,2595],{"className":2592,"code":2593,"language":2594,"meta":71,"style":71},"language-xml shiki shiki-themes github-light github-dark","\u003Cdependency>\n    \u003CgroupId>org.springframework.boot\u003C\u002FgroupId>\n    \u003CartifactId>spring-boot-starter-validation\u003C\u002FartifactId>\n\u003C\u002Fdependency>\n","xml",[61,2596,2597,2606,2620,2634],{"__ignoreMap":71},[75,2598,2599,2601,2604],{"class":77,"line":78},[75,2600,1587],{"class":85},[75,2602,2603],{"class":1941},"dependency",[75,2605,1968],{"class":85},[75,2607,2608,2610,2613,2616,2618],{"class":77,"line":89},[75,2609,1977],{"class":85},[75,2611,2612],{"class":1941},"groupId",[75,2614,2615],{"class":85},">org.springframework.boot\u003C\u002F",[75,2617,2612],{"class":1941},[75,2619,1968],{"class":85},[75,2621,2622,2624,2627,2630,2632],{"class":77,"line":96},[75,2623,1977],{"class":85},[75,2625,2626],{"class":1941},"artifactId",[75,2628,2629],{"class":85},">spring-boot-starter-validation\u003C\u002F",[75,2631,2626],{"class":1941},[75,2633,1968],{"class":85},[75,2635,2636,2638,2640],{"class":77,"line":105},[75,2637,2437],{"class":85},[75,2639,2603],{"class":1941},[75,2641,1968],{"class":85},[13,2643,2644,2647,2648,2651,2652,2655],{},[32,2645,2646],{},"这一步很容易漏","：Spring Boot 从 2.3 起不再默认包含校验实现，只写注解不加这个依赖，\n",[61,2649,2650],{},"@Valid"," 会",[32,2653,2654],{},"静默失效","——没有报错，校验就是不执行。这是本系列里最值得记住的坑之一。",[2586,2657,2659],{"id":2658},"_32-常用约束注解","3.2 常用约束注解",[2453,2661,2662,2672],{},[2456,2663,2664],{},[2459,2665,2666,2669],{},[2462,2667,2668],{},"注解",[2462,2670,2671],{},"作用",[2469,2673,2674,2688,2701,2711,2721,2734,2747,2760,2770,2780,2790,2803],{},[2459,2675,2676,2681],{},[2474,2677,2678],{},[61,2679,2680],{},"@NotNull",[2474,2682,2683,2684,2687],{},"不能为 ",[61,2685,2686],{},"null","（空字符串可以）",[2459,2689,2690,2695],{},[2474,2691,2692],{},[61,2693,2694],{},"@NotBlank",[2474,2696,2697,2698,2700],{},"字符串不能为 ",[61,2699,2686],{},"、空串、全空白",[2459,2702,2703,2708],{},[2474,2704,2705],{},[61,2706,2707],{},"@NotEmpty",[2474,2709,2710],{},"字符串\u002F集合\u002F数组不能为空（可以是空白字符串）",[2459,2712,2713,2718],{},[2474,2714,2715],{},[61,2716,2717],{},"@Size(min, max)",[2474,2719,2720],{},"长度或集合大小范围",[2459,2722,2723,2731],{},[2474,2724,2725,2522,2728],{},[61,2726,2727],{},"@Min",[61,2729,2730],{},"@Max",[2474,2732,2733],{},"数值范围",[2459,2735,2736,2744],{},[2474,2737,2738,2522,2741],{},[61,2739,2740],{},"@Positive",[61,2742,2743],{},"@Negative",[2474,2745,2746],{},"正数 \u002F 负数",[2459,2748,2749,2757],{},[2474,2750,2751,2522,2754],{},[61,2752,2753],{},"@DecimalMin",[61,2755,2756],{},"@DecimalMax",[2474,2758,2759],{},"带小数的范围，可指定是否含边界",[2459,2761,2762,2767],{},[2474,2763,2764],{},[61,2765,2766],{},"@Digits(integer, fraction)",[2474,2768,2769],{},"整数位与小数位上限",[2459,2771,2772,2777],{},[2474,2773,2774],{},[61,2775,2776],{},"@Pattern(regexp)",[2474,2778,2779],{},"正则匹配",[2459,2781,2782,2787],{},[2474,2783,2784],{},[61,2785,2786],{},"@Email",[2474,2788,2789],{},"邮箱格式",[2459,2791,2792,2800],{},[2474,2793,2794,2522,2797],{},[61,2795,2796],{},"@Past",[61,2798,2799],{},"@Future",[2474,2801,2802],{},"日期在过去 \u002F 将来",[2459,2804,2805,2813],{},[2474,2806,2807,2522,2810],{},[61,2808,2809],{},"@AssertTrue",[61,2811,2812],{},"@AssertFalse",[2474,2814,2815],{},"布尔断言",[13,2817,2818],{},"选择上的两个提醒：",[818,2820,2821,2831],{},[28,2822,2823,2830],{},[32,2824,2825,2826,828,2828],{},"字符串一律优先 ",[61,2827,2694],{},[61,2829,2680],{},"，否则用户提交一个空格就能过；",[28,2832,2833,2836,2837,2839,2840,2843],{},[61,2834,2835],{},"@Pattern"," 的正则里反斜杠要写成 ",[61,2838,375],{},"（Java 字符串转义），且尽量用",[32,2841,2842],{},"锚定","的写法，别指望\n「部分匹配」。",[2586,2845,2847],{"id":2846},"_33-校验在什么时候触发","3.3 校验在什么时候触发",[13,2849,2850,2851,2860,2861,2864],{},"只有在参数上",[32,2852,2853,2854,2856,2857,2526],{},"明确写了 ",[61,2855,2650],{},"（或 ",[61,2858,2859],{},"@Validated","，校验才会跑。触发时机是在数据绑定之后、\n方法体执行之前，失败的结果全部记进紧跟其后的 ",[61,2862,2863],{},"BindingResult","。",[13,2866,2867,2872],{},[32,2868,2869,2871],{},[61,2870,2863],{}," 必须紧挨着被校验的参数","，中间不能插别的参数：",[66,2874,2876],{"className":68,"code":2875,"language":70,"meta":71,"style":71},"\u002F\u002F 对：BindingResult 紧接着 form\npublic String create(@Valid @ModelAttribute(\"form\") BookForm form, BindingResult bindingResult, Model model)\n\n\u002F\u002F 错：中间隔了 Model，框架会直接抛 MethodArgumentNotValidException\npublic String create(@Valid @ModelAttribute(\"form\") BookForm form, Model model, BindingResult bindingResult)\n",[61,2877,2878,2883,2906,2910,2915],{"__ignoreMap":71},[75,2879,2880],{"class":77,"line":78},[75,2881,2882],{"class":440},"\u002F\u002F 对：BindingResult 紧接着 form\n",[75,2884,2885,2887,2889,2891,2893,2895,2897,2899,2901,2903],{"class":77,"line":89},[75,2886,161],{"class":81},[75,2888,461],{"class":85},[75,2890,848],{"class":167},[75,2892,1028],{"class":85},[75,2894,1031],{"class":81},[75,2896,1034],{"class":85},[75,2898,1037],{"class":81},[75,2900,188],{"class":85},[75,2902,959],{"class":198},[75,2904,2905],{"class":85},") BookForm form, BindingResult bindingResult, Model model)\n",[75,2907,2908],{"class":77,"line":96},[75,2909,93],{"emptyLinePlaceholder":92},[75,2911,2912],{"class":77,"line":105},[75,2913,2914],{"class":440},"\u002F\u002F 错：中间隔了 Model，框架会直接抛 MethodArgumentNotValidException\n",[75,2916,2917,2919,2921,2923,2925,2927,2929,2931,2933,2935],{"class":77,"line":113},[75,2918,161],{"class":81},[75,2920,461],{"class":85},[75,2922,848],{"class":167},[75,2924,1028],{"class":85},[75,2926,1031],{"class":81},[75,2928,1034],{"class":85},[75,2930,1037],{"class":81},[75,2932,188],{"class":85},[75,2934,959],{"class":198},[75,2936,2937],{"class":85},") BookForm form, Model model, BindingResult bindingResult)\n",[13,2939,2940,2941,2943],{},"写错的后果不难观察：请求直接 400，",[61,2942,1895],{}," 里的错误信息再也没机会渲染。",[2586,2945,2947],{"id":2946},"_34-校验结果长什么样","3.4 校验结果长什么样",[13,2949,2950,2952],{},[61,2951,2863],{}," 里有两类问题，都算「有错」：",[818,2954,2955,2964],{},[28,2956,2957,1879,2960,2963],{},[32,2958,2959],{},"字段错误",[61,2961,2962],{},"FieldError","）：某个字段不满足约束；",[28,2965,2966,1879,2969,2972,2973,2976,2977,2980],{},[32,2967,2968],{},"全局错误",[61,2970,2971],{},"ObjectError","）：不针对具体字段（比如「两次输入的密码不一致」）。用\n",[61,2974,2975],{},"bindingResult.rejectValue(\"field\", \"code\", \"消息\")"," 和 ",[61,2978,2979],{},"bindingResult.reject(\"code\", \"消息\")","\n手动添加。",[13,2982,2983],{},"要看整体结果，模板里可以用：",[66,2985,2987],{"className":1930,"code":2986,"language":1932,"meta":71,"style":71},"\u003Cul th:if=\"${#fields.hasGlobalErrors()}\">\n    \u003Cli th:each=\"err : ${#fields.globalErrors()}\" th:text=\"${err}\">两次输入不一致\u003C\u002Fli>\n\u003C\u002Ful>\n",[61,2988,2989,3004,3032],{"__ignoreMap":71},[75,2990,2991,2993,2995,2997,2999,3002],{"class":77,"line":78},[75,2992,1587],{"class":85},[75,2994,818],{"class":1941},[75,2996,2051],{"class":167},[75,2998,520],{"class":85},[75,3000,3001],{"class":198},"\"${#fields.hasGlobalErrors()}\"",[75,3003,1968],{"class":85},[75,3005,3006,3008,3010,3012,3014,3017,3020,3022,3025,3028,3030],{"class":77,"line":89},[75,3007,1977],{"class":85},[75,3009,28],{"class":1941},[75,3011,2334],{"class":167},[75,3013,520],{"class":85},[75,3015,3016],{"class":198},"\"err : ${#fields.globalErrors()}\"",[75,3018,3019],{"class":167}," th:text",[75,3021,520],{"class":85},[75,3023,3024],{"class":198},"\"${err}\"",[75,3026,3027],{"class":85},">两次输入不一致\u003C\u002F",[75,3029,28],{"class":1941},[75,3031,1968],{"class":85},[75,3033,3034,3036,3038],{"class":77,"line":96},[75,3035,2437],{"class":85},[75,3037,818],{"class":1941},[75,3039,1968],{"class":85},[17,3041,3043],{"id":3042},"_4-完整的流程","4 完整的流程",[13,3045,3046],{},[3047,3048],"img",{"alt":3049,"src":3050},"表单提交后的校验流程：数据绑定、@Valid 校验，失败则带错误回到表单视图，通过则调用 Service 后重定向","\u002Fimages\u002Fspring-notes-06-validation-flow.svg",[13,3052,3053],{},"对照图看几个容易忽略的点：",[818,3055,3056,3073,3079],{},[28,3057,3058,3061,3062,3065,3066,3069,3070,3072],{},[32,3059,3060],{},"下拉框的数据要在两条路径上都准备","。校验失败返回 ",[61,3063,3064],{},"books\u002Fform"," 时，",[61,3067,3068],{},"categories"," 不在\n",[61,3071,1895],{}," 里了（上一次请求的模型不会自动带过来），必须重新放。忘了这一步，页面上的下拉框会\n变空——这是「校验失败后页面缺数据」最常见的原因；",[28,3074,3075,3078],{},[32,3076,3077],{},"校验失败时数据库里什么都没发生","，所以不需要事务回滚；",[28,3080,3081,3082,3085],{},"校验通过后走 ",[61,3083,3084],{},"redirect:","，页面和 URL 都干净。",[17,3087,3089],{"id":3088},"_5-自定义校验注解","5 自定义校验注解",[13,3091,3092],{},"内置注解不够用时（比如「出版年份不能晚于今年」这种依赖上下文的规则），自己写一个：",[66,3094,3096],{"className":68,"code":3095,"language":70,"meta":71,"style":71},"package com.example.library.validation;\n\nimport jakarta.validation.Constraint;\nimport jakarta.validation.Payload;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Documented\n@Constraint(validatedBy = PastOrCurrentYearValidator.class)\n@Target({ ElementType.FIELD })\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface PastOrCurrentYear {\n\n    String message() default \"年份不能晚于当前年份\";\n\n    Class\u003C?>[] groups() default {};\n\n    Class\u003C? extends Payload>[] payload() default {};\n}\n",[61,3097,3098,3105,3109,3116,3123,3127,3134,3141,3148,3155,3162,3166,3173,3190,3200,3210,3224,3228,3246,3250,3271,3275,3299],{"__ignoreMap":71},[75,3099,3100,3102],{"class":77,"line":78},[75,3101,82],{"class":81},[75,3103,3104],{"class":85}," com.example.library.validation;\n",[75,3106,3107],{"class":77,"line":89},[75,3108,93],{"emptyLinePlaceholder":92},[75,3110,3111,3113],{"class":77,"line":96},[75,3112,99],{"class":81},[75,3114,3115],{"class":85}," jakarta.validation.Constraint;\n",[75,3117,3118,3120],{"class":77,"line":105},[75,3119,99],{"class":81},[75,3121,3122],{"class":85}," jakarta.validation.Payload;\n",[75,3124,3125],{"class":77,"line":113},[75,3126,93],{"emptyLinePlaceholder":92},[75,3128,3129,3131],{"class":77,"line":121},[75,3130,99],{"class":81},[75,3132,3133],{"class":85}," java.lang.annotation.Documented;\n",[75,3135,3136,3138],{"class":77,"line":129},[75,3137,99],{"class":81},[75,3139,3140],{"class":85}," java.lang.annotation.ElementType;\n",[75,3142,3143,3145],{"class":77,"line":137},[75,3144,99],{"class":81},[75,3146,3147],{"class":85}," java.lang.annotation.Retention;\n",[75,3149,3150,3152],{"class":77,"line":145},[75,3151,99],{"class":81},[75,3153,3154],{"class":85}," java.lang.annotation.RetentionPolicy;\n",[75,3156,3157,3159],{"class":77,"line":153},[75,3158,99],{"class":81},[75,3160,3161],{"class":85}," java.lang.annotation.Target;\n",[75,3163,3164],{"class":77,"line":158},[75,3165,93],{"emptyLinePlaceholder":92},[75,3167,3168,3170],{"class":77,"line":174},[75,3169,1321],{"class":85},[75,3171,3172],{"class":81},"Documented\n",[75,3174,3175,3177,3180,3182,3185,3187],{"class":77,"line":179},[75,3176,1321],{"class":85},[75,3178,3179],{"class":81},"Constraint",[75,3181,188],{"class":85},[75,3183,3184],{"class":191},"validatedBy",[75,3186,195],{"class":81},[75,3188,3189],{"class":85}," PastOrCurrentYearValidator.class)\n",[75,3191,3192,3194,3197],{"class":77,"line":205},[75,3193,1321],{"class":85},[75,3195,3196],{"class":81},"Target",[75,3198,3199],{"class":85},"({ ElementType.FIELD })\n",[75,3201,3202,3204,3207],{"class":77,"line":235},[75,3203,1321],{"class":85},[75,3205,3206],{"class":81},"Retention",[75,3208,3209],{"class":85},"(RetentionPolicy.RUNTIME)\n",[75,3211,3212,3214,3216,3219,3222],{"class":77,"line":244},[75,3213,161],{"class":81},[75,3215,1034],{"class":85},[75,3217,3218],{"class":81},"interface",[75,3220,3221],{"class":81}," PastOrCurrentYear",[75,3223,171],{"class":85},[75,3225,3226],{"class":77,"line":249},[75,3227,93],{"emptyLinePlaceholder":92},[75,3229,3230,3233,3235,3238,3241,3244],{"class":77,"line":267},[75,3231,3232],{"class":85},"    String ",[75,3234,192],{"class":167},[75,3236,3237],{"class":85},"() ",[75,3239,3240],{"class":81},"default",[75,3242,3243],{"class":198}," \"年份不能晚于当前年份\"",[75,3245,1002],{"class":85},[75,3247,3248],{"class":77,"line":275},[75,3249,93],{"emptyLinePlaceholder":92},[75,3251,3252,3255,3258,3261,3264,3266,3268],{"class":77,"line":280},[75,3253,3254],{"class":85},"    Class",[75,3256,3257],{"class":81},"\u003C?>",[75,3259,3260],{"class":85},"[] ",[75,3262,3263],{"class":167},"groups",[75,3265,3237],{"class":85},[75,3267,3240],{"class":81},[75,3269,3270],{"class":85}," {};\n",[75,3272,3273],{"class":77,"line":288},[75,3274,93],{"emptyLinePlaceholder":92},[75,3276,3277,3279,3282,3285,3288,3290,3293,3295,3297],{"class":77,"line":317},[75,3278,3254],{"class":85},[75,3280,3281],{"class":81},"\u003C?",[75,3283,3284],{"class":85}," extends Payload",[75,3286,3287],{"class":81},">",[75,3289,3260],{"class":85},[75,3291,3292],{"class":167},"payload",[75,3294,3237],{"class":85},[75,3296,3240],{"class":81},[75,3298,3270],{"class":85},[75,3300,3301],{"class":77,"line":344},[75,3302,813],{"class":85},[66,3304,3306],{"className":68,"code":3305,"language":70,"meta":71,"style":71},"package com.example.library.validation;\n\nimport jakarta.validation.ConstraintValidator;\nimport jakarta.validation.ConstraintValidatorContext;\n\nimport java.time.Year;\n\npublic class PastOrCurrentYearValidator implements ConstraintValidator\u003CPastOrCurrentYear, Integer> {\n\n    @Override\n    public boolean isValid(Integer value, ConstraintValidatorContext context) {\n        return value == null || value \u003C= Year.now().getValue();\n    }\n}\n",[61,3307,3308,3314,3318,3325,3332,3336,3343,3347,3372,3376,3382,3404,3439,3443],{"__ignoreMap":71},[75,3309,3310,3312],{"class":77,"line":78},[75,3311,82],{"class":81},[75,3313,3104],{"class":85},[75,3315,3316],{"class":77,"line":89},[75,3317,93],{"emptyLinePlaceholder":92},[75,3319,3320,3322],{"class":77,"line":96},[75,3321,99],{"class":81},[75,3323,3324],{"class":85}," jakarta.validation.ConstraintValidator;\n",[75,3326,3327,3329],{"class":77,"line":105},[75,3328,99],{"class":81},[75,3330,3331],{"class":85}," jakarta.validation.ConstraintValidatorContext;\n",[75,3333,3334],{"class":77,"line":113},[75,3335,93],{"emptyLinePlaceholder":92},[75,3337,3338,3340],{"class":77,"line":121},[75,3339,99],{"class":81},[75,3341,3342],{"class":85}," java.time.Year;\n",[75,3344,3345],{"class":77,"line":129},[75,3346,93],{"emptyLinePlaceholder":92},[75,3348,3349,3351,3353,3356,3358,3361,3363,3366,3368,3370],{"class":77,"line":137},[75,3350,161],{"class":81},[75,3352,164],{"class":81},[75,3354,3355],{"class":167}," PastOrCurrentYearValidator",[75,3357,1355],{"class":81},[75,3359,3360],{"class":167}," ConstraintValidator",[75,3362,1587],{"class":85},[75,3364,3365],{"class":81},"PastOrCurrentYear",[75,3367,223],{"class":85},[75,3369,827],{"class":81},[75,3371,1597],{"class":85},[75,3373,3374],{"class":77,"line":145},[75,3375,93],{"emptyLinePlaceholder":92},[75,3377,3378,3380],{"class":77,"line":153},[75,3379,182],{"class":85},[75,3381,1417],{"class":81},[75,3383,3384,3386,3389,3392,3394,3396,3399,3402],{"class":77,"line":158},[75,3385,458],{"class":81},[75,3387,3388],{"class":81}," boolean",[75,3390,3391],{"class":167}," isValid",[75,3393,645],{"class":85},[75,3395,298],{"class":504},[75,3397,3398],{"class":85},", ConstraintValidatorContext ",[75,3400,3401],{"class":504},"context",[75,3403,508],{"class":85},[75,3405,3406,3408,3411,3414,3417,3420,3422,3425,3428,3431,3434,3437],{"class":77,"line":174},[75,3407,473],{"class":81},[75,3409,3410],{"class":85}," value ",[75,3412,3413],{"class":81},"==",[75,3415,3416],{"class":191}," null",[75,3418,3419],{"class":81}," ||",[75,3421,3410],{"class":85},[75,3423,3424],{"class":81},"\u003C=",[75,3426,3427],{"class":85}," Year.",[75,3429,3430],{"class":167},"now",[75,3432,3433],{"class":85},"().",[75,3435,3436],{"class":167},"getValue",[75,3438,1230],{"class":85},[75,3440,3441],{"class":77,"line":179},[75,3442,482],{"class":85},[75,3444,3445],{"class":77,"line":205},[75,3446,813],{"class":85},[13,3448,3449],{},"用起来和内置注解一样：",[66,3451,3453],{"className":68,"code":3452,"language":70,"meta":71,"style":71},"@PastOrCurrentYear\nprivate Integer publishedYear;\n",[61,3454,3455,3461],{"__ignoreMap":71},[75,3456,3457,3459],{"class":77,"line":78},[75,3458,1321],{"class":85},[75,3460,285],{"class":81},[75,3462,3463,3466],{"class":77,"line":89},[75,3464,3465],{"class":81},"private",[75,3467,349],{"class":85},[13,3469,3470],{},"三个约定必须遵守，否则校验不会生效：",[25,3472,3473,3479,3500],{},[28,3474,3475,3476,42],{},"注解上要有 ",[61,3477,3478],{},"@Constraint(validatedBy = ...)",[28,3480,3481,3482,3485,3486,3485,3489,3492,3493,1887],{},"注解里必须有 ",[61,3483,3484],{},"message()","、",[61,3487,3488],{},"groups()",[61,3490,3491],{},"payload()"," 三个成员（",[32,3494,3495,2976,3497,3499],{},[61,3496,3263],{},[61,3498,3292],{}," 是规范\n要求，缺了会报错",[28,3501,3502,3503,2497,3506,3512,3513,3515],{},"校验器实现 ",[61,3504,3505],{},"ConstraintValidator\u003C注解, 字段类型>",[32,3507,3508,3509,3511],{},"把 ",[61,3510,2686],{}," 当作合法","——「是否必填」交给\n",[61,3514,2680],{}," 那一组注解管，两件事不要混在一个校验器里。",[17,3517,3519],{"id":3518},"_6-全局异常处理","6 全局异常处理",[13,3521,3522,3523,64],{},"校验错误之外的问题（资源不存在、权限不足、数据库异常）如果每个控制器都 try-catch，\n代码会很难看。集中处理用 ",[61,3524,3525],{},"@ControllerAdvice",[66,3527,3529],{"className":68,"code":3528,"language":70,"meta":71,"style":71},"package com.example.library.web;\n\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\n\nimport java.util.NoSuchElementException;\n\n\u002F** 页面型控制器的统一异常处理 *\u002F\n@ControllerAdvice(assignableTypes = { BookViewController.class, BookWizardController.class })\npublic class GlobalExceptionHandler {\n\n    @ExceptionHandler(NoSuchElementException.class)\n    public String handleNotFound(NoSuchElementException ex, Model model) {\n        model.addAttribute(\"message\", ex.getMessage());\n        return \"error\u002Fnot-found\";\n    }\n}\n",[61,3530,3531,3538,3542,3549,3556,3563,3567,3573,3577,3582,3599,3610,3614,3624,3647,3665,3674,3678],{"__ignoreMap":71},[75,3532,3533,3535],{"class":77,"line":78},[75,3534,82],{"class":81},[75,3536,3537],{"class":85}," com.example.library.web;\n",[75,3539,3540],{"class":77,"line":89},[75,3541,93],{"emptyLinePlaceholder":92},[75,3543,3544,3546],{"class":77,"line":96},[75,3545,99],{"class":81},[75,3547,3548],{"class":85}," org.springframework.ui.Model;\n",[75,3550,3551,3553],{"class":77,"line":105},[75,3552,99],{"class":81},[75,3554,3555],{"class":85}," org.springframework.web.bind.annotation.ControllerAdvice;\n",[75,3557,3558,3560],{"class":77,"line":113},[75,3559,99],{"class":81},[75,3561,3562],{"class":85}," org.springframework.web.bind.annotation.ExceptionHandler;\n",[75,3564,3565],{"class":77,"line":121},[75,3566,93],{"emptyLinePlaceholder":92},[75,3568,3569,3571],{"class":77,"line":129},[75,3570,99],{"class":81},[75,3572,1312],{"class":85},[75,3574,3575],{"class":77,"line":137},[75,3576,93],{"emptyLinePlaceholder":92},[75,3578,3579],{"class":77,"line":145},[75,3580,3581],{"class":440},"\u002F** 页面型控制器的统一异常处理 *\u002F\n",[75,3583,3584,3586,3589,3591,3594,3596],{"class":77,"line":153},[75,3585,1321],{"class":85},[75,3587,3588],{"class":81},"ControllerAdvice",[75,3590,188],{"class":85},[75,3592,3593],{"class":191},"assignableTypes",[75,3595,195],{"class":81},[75,3597,3598],{"class":85}," { BookViewController.class, BookWizardController.class })\n",[75,3600,3601,3603,3605,3608],{"class":77,"line":158},[75,3602,161],{"class":81},[75,3604,164],{"class":81},[75,3606,3607],{"class":167}," GlobalExceptionHandler",[75,3609,171],{"class":85},[75,3611,3612],{"class":77,"line":174},[75,3613,93],{"emptyLinePlaceholder":92},[75,3615,3616,3618,3621],{"class":77,"line":179},[75,3617,182],{"class":85},[75,3619,3620],{"class":81},"ExceptionHandler",[75,3622,3623],{"class":85},"(NoSuchElementException.class)\n",[75,3625,3626,3628,3630,3633,3636,3639,3642,3645],{"class":77,"line":205},[75,3627,458],{"class":81},[75,3629,461],{"class":85},[75,3631,3632],{"class":167},"handleNotFound",[75,3634,3635],{"class":85},"(NoSuchElementException ",[75,3637,3638],{"class":504},"ex",[75,3640,3641],{"class":85},", Model ",[75,3643,3644],{"class":504},"model",[75,3646,508],{"class":85},[75,3648,3649,3651,3653,3655,3657,3660,3663],{"class":77,"line":235},[75,3650,951],{"class":85},[75,3652,954],{"class":167},[75,3654,188],{"class":85},[75,3656,1131],{"class":198},[75,3658,3659],{"class":85},", ex.",[75,3661,3662],{"class":167},"getMessage",[75,3664,992],{"class":85},[75,3666,3667,3669,3672],{"class":77,"line":244},[75,3668,473],{"class":81},[75,3670,3671],{"class":198}," \"error\u002Fnot-found\"",[75,3673,1002],{"class":85},[75,3675,3676],{"class":77,"line":249},[75,3677,482],{"class":85},[75,3679,3680],{"class":77,"line":267},[75,3681,813],{"class":85},[13,3683,3684,3685,3688],{},"返回 JSON 的接口用 ",[61,3686,3687],{},"@RestControllerAdvice","，直接返回响应体：",[66,3690,3692],{"className":68,"code":3691,"language":70,"meta":71,"style":71},"package com.example.library.web;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ProblemDetail;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n@RestControllerAdvice\npublic class ApiExceptionHandler {\n\n    @ExceptionHandler(NoSuchElementException.class)\n    public ProblemDetail handleNotFound(NoSuchElementException ex) {\n        ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());\n        detail.setTitle(\"资源不存在\");\n        return detail;\n    }\n}\n",[61,3693,3694,3700,3704,3711,3718,3724,3731,3735,3742,3753,3757,3765,3780,3800,3815,3822,3826],{"__ignoreMap":71},[75,3695,3696,3698],{"class":77,"line":78},[75,3697,82],{"class":81},[75,3699,3537],{"class":85},[75,3701,3702],{"class":77,"line":89},[75,3703,93],{"emptyLinePlaceholder":92},[75,3705,3706,3708],{"class":77,"line":96},[75,3707,99],{"class":81},[75,3709,3710],{"class":85}," org.springframework.http.HttpStatus;\n",[75,3712,3713,3715],{"class":77,"line":105},[75,3714,99],{"class":81},[75,3716,3717],{"class":85}," org.springframework.http.ProblemDetail;\n",[75,3719,3720,3722],{"class":77,"line":113},[75,3721,99],{"class":81},[75,3723,3562],{"class":85},[75,3725,3726,3728],{"class":77,"line":121},[75,3727,99],{"class":81},[75,3729,3730],{"class":85}," org.springframework.web.bind.annotation.RestControllerAdvice;\n",[75,3732,3733],{"class":77,"line":129},[75,3734,93],{"emptyLinePlaceholder":92},[75,3736,3737,3739],{"class":77,"line":137},[75,3738,1321],{"class":85},[75,3740,3741],{"class":81},"RestControllerAdvice\n",[75,3743,3744,3746,3748,3751],{"class":77,"line":145},[75,3745,161],{"class":81},[75,3747,164],{"class":81},[75,3749,3750],{"class":167}," ApiExceptionHandler",[75,3752,171],{"class":85},[75,3754,3755],{"class":77,"line":153},[75,3756,93],{"emptyLinePlaceholder":92},[75,3758,3759,3761,3763],{"class":77,"line":158},[75,3760,182],{"class":85},[75,3762,3620],{"class":81},[75,3764,3623],{"class":85},[75,3766,3767,3769,3772,3774,3776,3778],{"class":77,"line":174},[75,3768,458],{"class":81},[75,3770,3771],{"class":85}," ProblemDetail ",[75,3773,3632],{"class":167},[75,3775,3635],{"class":85},[75,3777,3638],{"class":504},[75,3779,508],{"class":85},[75,3781,3782,3785,3787,3790,3793,3796,3798],{"class":77,"line":179},[75,3783,3784],{"class":85},"        ProblemDetail detail ",[75,3786,520],{"class":81},[75,3788,3789],{"class":85}," ProblemDetail.",[75,3791,3792],{"class":167},"forStatusAndDetail",[75,3794,3795],{"class":85},"(HttpStatus.NOT_FOUND, ex.",[75,3797,3662],{"class":167},[75,3799,992],{"class":85},[75,3801,3802,3805,3808,3810,3813],{"class":77,"line":205},[75,3803,3804],{"class":85},"        detail.",[75,3806,3807],{"class":167},"setTitle",[75,3809,188],{"class":85},[75,3811,3812],{"class":198},"\"资源不存在\"",[75,3814,1139],{"class":85},[75,3816,3817,3819],{"class":77,"line":235},[75,3818,473],{"class":81},[75,3820,3821],{"class":85}," detail;\n",[75,3823,3824],{"class":77,"line":244},[75,3825,482],{"class":85},[75,3827,3828],{"class":77,"line":249},[75,3829,813],{"class":85},[13,3831,3832,3835,3836,3839],{},[61,3833,3834],{},"ProblemDetail"," 是 Spring 6 引入的、对应 RFC 7807 的标准错误体格式，比自定义一个\n",[61,3837,3838],{},"{\"code\":..,\"msg\":..}"," 更容易被客户端通用处理。",[13,3841,3842],{},"几点要注意：",[818,3844,3845,3859,3875,3893],{},[28,3846,3847,3850,3851,3854,3855,3858],{},[32,3848,3849],{},"异常处理方法要尽量具体","。如果同时存在 ",[61,3852,3853],{},"@ExceptionHandler(Exception.class)"," 和更具体的\n处理方法，Spring 会选最匹配的那个，但一个兜底的 ",[61,3856,3857],{},"Exception"," 处理器会把 400\u002F404 也吞掉，\n变成「所有错误都返回 500」，反而更难排查；",[28,3860,3861,3863,3864,3867,3868,3871,3872,42],{},[61,3862,3525],{}," 可以限定生效范围：",[61,3865,3866],{},"@ControllerAdvice(assignableTypes = BookViewController.class)","\n（示例工程用的就是这种）、",[61,3869,3870],{},"basePackages = \"com.example.library.web\"","，或按注解\n",[61,3873,3874],{},"@ControllerAdvice(annotations = RestController.class)",[28,3876,3877,2864,3880,3882,3883,824,3886,3888,3889,3892],{},[32,3878,3879],{},"页面型和接口型的 advice 一定要分开限定",[61,3881,3687],{}," 默认作用于所有控制器，\n不加限定的话，页面控制器抛异常时会返回一段 JSON 而不是错误页，用户看到的是一屏花括号。\n示例工程里 ",[61,3884,3885],{},"GlobalExceptionHandler",[61,3887,3593],{}," 只盯页面控制器，第 11 篇的\n",[61,3890,3891],{},"ApiExceptionHandler"," 只盯 REST 控制器，两边互不干扰；",[28,3894,3895],{},"它还能给所有控制器统一注入公共模型数据：",[66,3897,3899],{"className":68,"code":3898,"language":70,"meta":71,"style":71},"@ModelAttribute(\"currentYear\")\npublic int currentYear() {\n    return Year.now().getValue();\n}\n",[61,3900,3901,3914,3926,3941],{"__ignoreMap":71},[75,3902,3903,3905,3907,3909,3912],{"class":77,"line":78},[75,3904,1321],{"class":85},[75,3906,1037],{"class":81},[75,3908,188],{"class":85},[75,3910,3911],{"class":198},"\"currentYear\"",[75,3913,202],{"class":85},[75,3915,3916,3918,3921,3924],{"class":77,"line":89},[75,3917,161],{"class":81},[75,3919,3920],{"class":81}," int",[75,3922,3923],{"class":167}," currentYear",[75,3925,467],{"class":85},[75,3927,3928,3931,3933,3935,3937,3939],{"class":77,"line":96},[75,3929,3930],{"class":81},"    return",[75,3932,3427],{"class":85},[75,3934,3430],{"class":167},[75,3936,3433],{"class":85},[75,3938,3436],{"class":167},[75,3940,1230],{"class":85},[75,3942,3943],{"class":77,"line":105},[75,3944,813],{"class":85},[13,3946,3947,3948,3951],{},"这样每个页面都能直接用 ",[61,3949,3950],{},"${currentYear}","，不用每个控制器都塞一遍。",[17,3953,3955],{"id":3954},"_7-容易踩的坑","7 容易踩的坑",[2453,3957,3958,3971],{},[2456,3959,3960],{},[2459,3961,3962,3965,3968],{},[2462,3963,3964],{},"现象",[2462,3966,3967],{},"原因",[2462,3969,3970],{},"处理",[2469,3972,3973,3988,4009,4026,4046,4060,4075,4086,4106,4127,4145],{},[2459,3974,3975,3978,3985],{},[2474,3976,3977],{},"校验注解写了但完全不生效，也不报错",[2474,3979,3980,3981,3984],{},"少了 ",[61,3982,3983],{},"spring-boot-starter-validation"," 依赖",[2474,3986,3987],{},"加上依赖重启",[2459,3989,3990,3993,4002],{},[2474,3991,3992],{},"提交非法数据直接 400，页面没显示错误",[2474,3994,3995,3996,3998,3999,4001],{},"参数上没写 ",[61,3997,2650],{},"，或 ",[61,4000,2863],{}," 位置不对",[2474,4003,4004,2553,4006,4008],{},[61,4005,2650],{},[61,4007,2863],{}," 必须相邻",[2459,4010,4011,4016,4021],{},[2474,4012,4013,4015],{},[61,4014,1919],{}," 报空指针",[2474,4017,4018,4019],{},"GET 进入表单页时没把空对象放进 ",[61,4020,1895],{},[2474,4022,4023],{},[61,4024,4025],{},"model.addAttribute(\"form\", new BookForm())",[2459,4027,4028,4031,4037],{},[2474,4029,4030],{},"字段一直绑不上，值永远是 null",[2474,4032,4033,4034,4036],{},"input 的 ",[61,4035,1247],{}," 与对象属性名不一致",[2474,4038,4039,4040,4042,4043,4045],{},"用 ",[61,4041,2552],{}," 生成 ",[61,4044,1247],{},"，别手写",[2459,4047,4048,4051,4054],{},[2474,4049,4050],{},"校验失败后下拉框空了",[2474,4052,4053],{},"返回视图时没重新放下拉数据",[2474,4055,4056,4057],{},"失败分支里重新 ",[61,4058,4059],{},"addAttribute(\"categories\", ...)",[2459,4061,4062,4069,4072],{},[2474,4063,4064,4065,4068],{},"表单对象是 ",[61,4066,4067],{},"record","，绑定直接失败",[2474,4070,4071],{},"绑定需要无参构造器 + setter",[2474,4073,4074],{},"表单对象用普通 class",[2459,4076,4077,4080,4083],{},[2474,4078,4079],{},"数字字段填了字母，结果字段是空的",[2474,4081,4082],{},"类型转换失败会作为字段错误记录，不回填",[2474,4084,4085],{},"这是预期行为；提示文案写清楚该填什么",[2459,4087,4088,4094,4097],{},[2474,4089,4090,4091],{},"错误信息显示的是 ",[61,4092,4093],{},"{jakarta.validation.constraints...}",[2474,4095,4096],{},"找不到对应语言的校验消息",[2474,4098,4099,4100,4103,4104],{},"在 ",[61,4101,4102],{},"messages.properties"," 里提供对应键，或每个注解都写 ",[61,4105,192],{},[2459,4107,4108,4111,4124],{},[2474,4109,4110],{},"自定义校验注解不生效",[2474,4112,4113,4114,4117,4118,4120,4121,4123],{},"缺 ",[61,4115,4116],{},"@Constraint","、缺 ",[61,4119,3263],{},"\u002F",[61,4122,3292],{},"，或校验器没注册",[2474,4125,4126],{},"按第 5 节三个约定逐条检查",[2459,4128,4129,4132,4138],{},[2474,4130,4131],{},"所有 404 都变成 500",[2474,4133,4134,4135,4137],{},"兜底的 ",[61,4136,3857],{}," 处理器把所有异常都接走了",[2474,4139,4140,4141,4144],{},"兜底处理器里显式处理 ",[61,4142,4143],{},"NoHandlerFoundException"," 等，或不要兜底",[2459,4146,4147,4150,4153],{},[2474,4148,4149],{},"提交成功刷新页面又提交一次",[2474,4151,4152],{},"没有走重定向",[2474,4154,4155,4156],{},"成功后 ",[61,4157,3084],{},[17,4159,4161],{"id":4160},"_8-小结","8 小结",[818,4163,4164,4171,4187,4203,4210,4216,4224,4239],{},[28,4165,4166,4167,4170],{},"表单用",[32,4168,4169],{},"专门的表单对象","接，不直接绑实体；对象要有无参构造器与 setter；",[28,4172,4173,4174,4181,4182,3485,4184,4186],{},"模板侧用 ",[32,4175,4176,4178,4179],{},[61,4177,2535],{}," + ",[61,4180,2552],{},"：一个属性负责 ",[61,4183,1247],{},[61,4185,2491],{},"、回填和选中；",[28,4188,4189,4190,4193,4194,4199,4200,4202],{},"服务端校验用",[32,4191,4192],{},"Bean Validation 注解","，并且",[32,4195,4196,4197],{},"必须引入 ",[61,4198,3983],{},"，\n否则 ",[61,4201,2650],{}," 会静默失效；",[28,4204,4205,42],{},[32,4206,4207,4209],{},[61,4208,2863],{}," 必须紧跟被校验的参数",[28,4211,4212,4213,42],{},"校验失败返回表单视图时要",[32,4214,4215],{},"重新准备下拉数据",[28,4217,3081,4218,4223],{},[32,4219,4220,4221,2526],{},"PRG（",[61,4222,3084],{},"，一次性提示用 Flash 属性；",[28,4225,4226,4227,1879,4230,4178,4232,4235,4236,4238],{},"内置注解不够就",[32,4228,4229],{},"自定义约束",[61,4231,4116],{},[61,4233,4234],{},"ConstraintValidator","），记得 ",[61,4237,2686],{}," 视为合法；",[28,4240,4241,4242,4248,4249,2864],{},"错误统一用 ",[32,4243,4244,2522,4246],{},[61,4245,3525],{},[61,4247,3687],{}," 处理，REST 用 ",[61,4250,3834],{},[13,4252,4253,4254,4257,4258,4261],{},"页面的骨架已经有了，但每个页面重复写导航栏、页脚会很痛苦。下一篇讲 Thymeleaf 的",[32,4255,4256],{},"片段复用","\n与",[32,4259,4260],{},"国际化","，把模板也「组件化」。",[4263,4264,4265],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":71,"searchDepth":96,"depth":96,"links":4267},[4268,4269,4270,4271,4277,4278,4279,4280,4281],{"id":19,"depth":89,"text":20},{"id":55,"depth":89,"text":56},{"id":1926,"depth":89,"text":1927},{"id":2583,"depth":89,"text":2584,"children":4272},[4273,4274,4275,4276],{"id":2588,"depth":96,"text":2589},{"id":2658,"depth":96,"text":2659},{"id":2846,"depth":96,"text":2847},{"id":2946,"depth":96,"text":2947},{"id":3042,"depth":89,"text":3043},{"id":3088,"depth":89,"text":3089},{"id":3518,"depth":89,"text":3519},{"id":3954,"depth":89,"text":3955},{"id":4160,"depth":89,"text":4161},"学习",null,"2026-10-15","把表单字段绑到对象上，用 Bean Validation 注解做服务端校验，把错误信息显示回用户刚填的那一页，再补上自定义校验注解与全局异常处理。","md",false,{},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-06",{"title":5,"description":4285},"Spring 学习笔记","posts\u002Fjavaspring\u002Fspring-notes-06",[4294,4295,4296,4297],"Spring","Spring MVC","校验","学习笔记","oHmlTPpLTaT2km8MYVQRWzzGK7eBCLBqrhZe0feG1IQ",[4300,4306,4311,4316,4321,4326,4331,4336,4341,4346,4351,4356,4361,4366,4371,4376,4381,4386,4391,4396,4401,4406,4411,4416,4421,4426,4431,4436,4441,4445,4449,4453,4457,4461,4462,4466,4470,4474,4478,4482,4486,4492,4496,4502,4506,4510,4514,4518,4522,4526,4530,4535,4539,4543,4548,4553,4559,4563,4568,4572],{"path":4301,"title":4302,"description":4303,"date":4304,"series":4305,"seriesOrder":405},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-28","软件分析与设计笔记 28：draw.io 作图手册：五张图的符号位置、落笔顺序与自检","不模拟软件界面，只讲清三件事：每个 UML 符号在 draw.io 里搜什么词、一张图该按什么顺序落笔、画完拿什么标准自查。覆盖用例图、活动图、顺序图、类图、界面草图五种图纸，末尾附「最容易画反的十二笔」总检表。","2026-11-19","软件分析与设计笔记",{"path":4307,"title":4308,"description":4309,"date":4310,"series":4305,"seriesOrder":400},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-27","软件分析与设计笔记 27：Paper 1 应试：从读案例到交卷的一次完整演算","拿一份完整的案例，从头到尾走一遍：怎么读案例列角色、怎么在四个小问之间分配时间、每一张图从哪一句话推出来、两张表怎么填、交卷前拿什么清单自查。附 C25 真题与样卷的题型分值对照。","2026-11-18",{"path":4312,"title":4313,"description":4314,"date":4315,"series":4305,"seriesOrder":392},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-26","软件分析与设计笔记 26：总结：把整条链路串起来","系列收尾：从需求到代码的完整链路回顾、每一环的产出与验收方式、六条贯穿全系列的原则，以及一份可以直接拿去做项目自查的清单。","2026-11-17",{"path":4317,"title":4318,"description":4319,"date":4320,"series":4305,"seriesOrder":357},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-25","软件分析与设计笔记 25：代码重构：坏味道与手法","重构不改变外部行为、只改善内部结构：四种常见坏味道与对应手法、为什么动手之前必须先有测试、什么时候该重构，以及重构和前面那些设计模型是什么关系。","2026-11-16",{"path":4322,"title":4323,"description":4324,"date":4325,"series":4305,"seriesOrder":352},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-24","软件分析与设计笔记 24：设计原则：内聚、耦合与多态","好设计怎么衡量：内聚与耦合分别在说什么、一次从「差设计」到「多态重设计」的完整对照、抽象类与接口的选择依据，以及复用为什么会从优势变成风险。","2026-11-15",{"path":4327,"title":4328,"description":4329,"date":4330,"series":4305,"seriesOrder":344},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-23","软件分析与设计笔记 23：设计建模：从对象模型到能实现的方案","分析模型能用但还不能直接开工：对象模型怎么映射到关系模型、继承映射的三种方案怎么选、数据访问层与 Data Broker 解决什么问题，以及三层架构与 MVC 的分工。","2026-11-14",{"path":4332,"title":4333,"description":4334,"date":4335,"series":4305,"seriesOrder":317},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-22","软件分析与设计笔记 22：分析建模：边界、实体与控制对象","不经过分析直接写代码会踩什么坑、三种分析对象各管一段、怎么用顺序图完成一次用例实现，以及从顺序图导出类的属性、操作与关系的方法。","2026-11-13",{"path":4337,"title":4338,"description":4339,"date":4340,"series":4305,"seriesOrder":288},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-21","软件分析与设计笔记 21：UML：类图与顺序图","为什么需要一种共同的画法：类符号三格怎么画、五种关系的符号与设计含义、每种关系落到 Java 是什么写法，顺序图的三要素怎么摆；以及这三种最容易找不到的符号在 draw.io 里各搜什么词。","2026-11-12",{"path":4342,"title":4343,"description":4344,"date":4345,"series":4305,"seriesOrder":280},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-20","软件分析与设计笔记 20：面向对象概念：对象、关系与多态","面向对象不是「用某个语言写代码」：对象与类的区别、封装真正在保护什么、五种关系（依赖、关联、聚合、组合、继承）各自何时用，以及多态为什么能减少条件判断。","2026-11-11",{"path":4347,"title":4348,"description":4349,"date":4350,"series":4305,"seriesOrder":275},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-19","软件分析与设计笔记 19：规范化与逻辑数据模型","一次改地址引发的数据不一致，讲清规范化的真正动机；UNF 到 3NF 每一步在解决什么问题，派生字段为什么不能存，优化为什么和规范化方向相反，以及逻辑数据模型在 3NF 之上补了什么。","2026-11-10",{"path":4352,"title":4353,"description":4354,"date":4355,"series":4305,"seriesOrder":267},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-18","软件分析与设计笔记 18：数据库分析：实体、关系与数据字典","为什么不能拿到需求就动手建表：实体与关系怎么判定、基数为什么必须由业务方确认、数据命名要遵守什么规则，以及数据字典四列里最容易被空着的那一列。","2026-11-09",{"path":4357,"title":4358,"description":4359,"date":4360,"series":4305,"seriesOrder":249},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-17","软件分析与设计笔记 17：用生成式 AI 做设计与开发：能用的场景与要防的坑","生成式 AI 在工作流里真正站得住的三个场景 —— 造示例数据、解释陌生代码、起草初稿；它为什么会复现社会偏见、为什么会编得像真的，以及哪些内容绝不能直接采用。","2026-11-08",{"path":4362,"title":4363,"description":4364,"date":4365,"series":4305,"seriesOrder":244},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-16","软件分析与设计笔记 16：从 persona 到 URS：一份交付物的完整骨架","把前面十几篇串成一份能交出去的东西：用户需求规格说明书的九个部分、每一章来自哪个环节、由谁确认，以及面向决策者的提案该讲清哪三件事。","2026-11-07",{"path":4367,"title":4368,"description":4369,"date":4370,"series":4305,"seriesOrder":235},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-15","软件分析与设计笔记 15：非功能需求：把「快、稳、安全」变成数字","为什么「系统要快」不是一条需求：非功能需求怎么量化，决定性能的用户量、交易量、数据量三张表怎么估，安全需求要写清什么，以及可靠性的「不可用」该怎么定义。","2026-11-06",{"path":4372,"title":4373,"description":4374,"date":4375,"series":4305,"seriesOrder":205},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-14","软件分析与设计笔记 14：软件测试：层级、方法与等价类划分","「我们测过了」这句话问题在哪：为什么静态评审和动态测试缺一不可，五个测试层级各自验证什么，白盒与黑盒的差别，以及用等价类划分和边界值把无限输入变成有限用例的方法。","2026-11-05",{"path":4377,"title":4378,"description":4379,"date":4380,"series":4305,"seriesOrder":179},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-13","软件分析与设计笔记 13：需求获取：五种调查方法与一场访谈","需求从哪里来：五种事实调查方法各自适合回答什么、代价是什么，访谈从准备到记录的全过程，问卷设计里最容易犯的错，以及访谈记录必须写下来的那几项信息。","2026-11-04",{"path":4382,"title":4383,"description":4384,"date":4385,"series":4305,"seriesOrder":174},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-12","软件分析与设计笔记 12：前端设计：把流程变成屏幕","界面设计不是「画得好看」：从活动图走到屏幕的三步，CRUD 任务流怎么当检查表，界面必须回答的三个问题，信息架构与五种导航类型，视觉一致性与留白，以及移动端与 Web 端的差别。","2026-11-03",{"path":4387,"title":4388,"description":4389,"date":4390,"series":4305,"seriesOrder":158},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-11","软件分析与设计笔记 11：Agentic AI 与业务流程自动化","AI agent 到底是什么：五项能力与四个核心组件，什么场景该用定时任务、什么该用程序、什么才值得用 agent，两种实现路径的差别，必须提前防住的四类失败模式，以及设计方法从提示词到编排的演进。","2026-11-02",{"path":4392,"title":4393,"description":4394,"date":4395,"series":4305,"seriesOrder":153},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-10","软件分析与设计笔记 10：精炼用例模型：include、extend 与泛化","三种精炼用例关系的判断标准：include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题，以及精炼到什么程度就该停手。","2026-11-01",{"path":4397,"title":4398,"description":4399,"date":4400,"series":4305,"seriesOrder":145},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-09","软件分析与设计笔记 09：功能需求（二）：从流程到业务实体","活动图画完之后，怎么从流程里筛出真正的业务实体：三条判断标准、哪些名词其实不该建实体、属性从哪里来，以及领域模型和后面的数据模型分别负责什么。","2026-10-31",{"path":4402,"title":4403,"description":4404,"date":4405,"series":4305,"seriesOrder":137},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-08","软件分析与设计笔记 08：功能需求（一）：用例模型与活动图","从用户故事到用例的翻译过程：什么才算「好」的参与者，系统边界怎么划，用例描述必须写清哪四件事，活动图怎么把分支、并行与泳道画清楚，以及这两个图在 draw.io 里各用哪个形状、最容易画错哪一笔。","2026-10-30",{"path":4407,"title":4408,"description":4409,"date":4410,"series":4305,"seriesOrder":129},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-07","软件分析与设计笔记 07：用户故事：把需求写成可验收的一句话","用户故事的三段结构，怎么从用户画像和数据地图里把它提取出来，一条故事多大算合适、时序为什么也是需求的一部分，以及验收标准怎么写才算真的可验收。","2026-10-29",{"path":4412,"title":4413,"description":4414,"date":4415,"series":4305,"seriesOrder":121},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-06","软件分析与设计笔记 06：范围界定（数据侧）：用数据校验你的判断","访谈结论和数字对不上时该怎么办：分析的四个层次分别在回答什么问题、为什么不能跳级、数据准备与搜集怎么做成一张应用地图，以及数据最容易在哪几个地方骗你。","2026-10-28",{"path":4417,"title":4418,"description":4419,"date":4420,"series":4305,"seriesOrder":113},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-05","软件分析与设计笔记 05：范围界定（用户侧）：用户画像与客户旅程图","为什么「所有用户」是一个陷阱，项目边界该怎么划，用户画像要写哪五个要素、最容易写成什么废纸，客户旅程图怎么把阶段、想法和情绪画在同一张纸上，以及怎么从这两样东西一步步推到可验证的需求。","2026-10-27",{"path":4422,"title":4423,"description":4424,"date":4425,"series":4305,"seriesOrder":105},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-04","软件分析与设计笔记 04：接手陌生案例的第一步：三问与一张表","拿到一个完全陌生的业务案例，怎么从零开始不跑偏：只问 Who、What、Why 三个问题，把「问题—事实—假设—理由」记成一张四列表，说清为什么假设必须写明理由，以及带着问题进访谈为什么比空手去有效得多。","2026-10-26",{"path":4427,"title":4428,"description":4429,"date":4430,"series":4305,"seriesOrder":96},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-03","软件分析与设计笔记 03：Agile 与 Scrum：迭代、角色与产出物","从「三周做完交付，客户说这不是我要的」讲起，说清瀑布式与迭代式的根本差别，Scrum 的三个角色和三种产出物，一个 sprint 里四次固定活动各自在解决什么问题，以及敏捷里最常见的几个误解。","2026-10-25",{"path":4432,"title":4433,"description":4434,"date":4435,"series":4305,"seriesOrder":89},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-02","软件分析与设计笔记 02：设计思维、用户驱动与数据驱动","从「客服要一个能一次看完所有排期的界面」这个翻车案例讲起，说清设计思维到底是什么、用户驱动与数据驱动各自能看见什么又看不见什么、为什么真正的落点是把两者合起来的 data-informed design，以及访谈中怎么问出有用的信息。","2026-10-24",{"path":4437,"title":4438,"description":4439,"date":4440,"series":4305,"seriesOrder":78},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-01","软件分析与设计笔记 01：课程地图与从需求到设计的主干","这一篇不写具体方法，先把整条链路铺开：软件分析与设计到底要解决什么问题，从需求到设计分成哪两段，中间有哪十几个环节，一份完整的交付物里应该有什么，以及后面 27 篇笔记怎么排、新人该按什么顺序读。","2026-10-23",{"path":4442,"title":4443,"description":4444,"date":4284,"series":4291,"seriesOrder":78},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-01","Spring 学习笔记 01：IoC、依赖注入与第一个 Spring Boot 应用","从「手写 new 出来的三层结构」的痛点讲起，说清 IoC 容器、Bean 与依赖注入是怎么回事，再用 Maven 与 Spring Initializr 跑起第一个能返回 JSON 的应用。",{"path":4446,"title":4447,"description":4448,"date":4284,"series":4291,"seriesOrder":89},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-02","Spring 学习笔记 02：Spring Data JPA 基础——实体映射与 Repository","用 ORM 把类映射到表：@Entity、@Id、@GeneratedValue 怎么用，CrudRepository 三层接口怎么选，H2 与 MySQL 怎么配，以及 ddl-auto 到底该写哪个值。",{"path":4450,"title":4451,"description":4452,"date":4284,"series":4291,"seriesOrder":96},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-03","Spring 学习笔记 03：JPA 查询——派生方法、JPQL 与分页排序","Repository 自带的方法只够按主键查，这篇讲三种自己造查询的方式：方法名派生、@Query 写 JPQL、原生 SQL，以及 Pageable 分页排序和投影返回。",{"path":4454,"title":4455,"description":4456,"date":4284,"series":4291,"seriesOrder":105},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-04","Spring 学习笔记 04：JPA 关联映射与仓储测试","一对多、多对多怎么映射，谁负责维护外键，FetchType 与 CascadeType 该怎么选，顺带用 @DataJpaTest 与 AssertJ 把仓储层测起来。",{"path":4458,"title":4459,"description":4460,"date":4284,"series":4291,"seriesOrder":113},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-05","Spring 学习笔记 05：Spring MVC 与 Thymeleaf 入门","一次页面请求在 Spring MVC 里走了哪几步，@Controller 怎么把数据交给模板，@PathVariable 与 @RequestParam 怎么取参数，以及控制器为什么必须是无状态的。",{"path":4289,"title":5,"description":4285,"date":4284,"series":4291,"seriesOrder":121},{"path":4463,"title":4464,"description":4465,"date":4284,"series":4291,"seriesOrder":129},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-07","Spring 学习笔记 07：Thymeleaf 片段复用与国际化","用 th:fragment 与 th:insert\u002Fth:replace 把导航栏、页脚抽成片段，页面套用统一布局，再用 messages.properties 与 LocaleResolver 做中英文切换。",{"path":4467,"title":4468,"description":4469,"date":4284,"series":4291,"seriesOrder":137},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-08","Spring 学习笔记 08：HTTP Session 与会话状态管理","HTTP 无状态带来的问题，Cookie 与 Session 怎么分工，Spring 里读写会话的几种方式，以及会话固定、超时、多实例共享这些必须想清楚的事。",{"path":4471,"title":4472,"description":4473,"date":4284,"series":4291,"seriesOrder":145},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-09","Spring 学习笔记 09：拦截器 HandlerInterceptor","登录检查、权限判断、请求耗时统计这类横切逻辑该放哪：HandlerInterceptor 的三个回调各在什么时机执行，怎么注册与排除路径，以及它和过滤器的分工。",{"path":4475,"title":4476,"description":4477,"date":4284,"series":4291,"seriesOrder":153},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-10","Spring 学习笔记 10：Service 层与事务","一组数据库操作要么全成功要么全失败，靠 @Transactional 怎么做到；传播行为、隔离级别、回滚规则怎么选，以及自调用失效、乐观锁悲观锁这些实战里的坑。",{"path":4479,"title":4480,"description":4481,"date":4284,"series":4291,"seriesOrder":158},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-11","Spring 学习笔记 11：REST API 基础——把 Service 暴露成接口","同一套 Service 怎么同时服务页面和客户端：@RestController 的路由与方法语义、状态码怎么给、DTO 怎么定，以及 MockMvc 端到端和切片测试的两种写法与各自的坑。",{"path":4483,"title":4484,"description":4485,"date":4284,"series":4291,"seriesOrder":174},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-12","Spring 学习笔记 12：REST 进阶——版本、更新、跨域与文档化","接口能跑通之后要处理的几件事：URL 与响应结构怎么定、版本怎么演进、更新怎么做到可安全重试、CORS 怎么配、HATEOAS 与 Spring Data REST 该不该用。",{"path":4487,"title":4488,"description":4489,"date":4490,"series":4491,"seriesOrder":105},"\u002Fposts\u002Fjava\u002Fjava-functionalprogramming-notes","Java 函数式编程笔记：Lambda、函数式接口与 Streams","学习 Java 函数式编程时的个人笔记：Lambda 表达式的五步简写与方法引用、常见函数式接口、Stream 流水线与惰性求值，以及创建流、中间操作、终止操作与收集器的用法。示例代码均为自写，运行结果来自本地实测。","2026-10-13","Java 学习笔记",{"path":4493,"title":4494,"description":4495,"date":4490,"series":4491,"seriesOrder":96},"\u002Fposts\u002Fjava\u002Fjava-generics-notes","Java 泛型笔记：泛型方法、有界类型与泛型类","学习 Java 泛型时的个人笔记：从重复的重载方法与多态尝试讲起，再到泛型方法与类型推断、有界泛型类型、Comparable 与 Number 边界，以及泛型类在集合中的用法。示例代码均为自写，运行结果来自本地实测。",{"path":4497,"title":4498,"description":4499,"date":4500,"series":4501,"seriesOrder":105},"\u002Fposts\u002Fmysql\u002Fmysql-learning-04","MySQL 学习笔记 04：数据操纵（增删改）","MySQL 学习笔记第四篇：INSERT 插入（含批量插入与 INSERT ... SELECT）、UPDATE 更新、DELETE 删除，以及各自的坑。","2026-10-10","SQL 学习笔记",{"path":4503,"title":4504,"description":4505,"date":4500,"series":4501,"seriesOrder":113},"\u002Fposts\u002Fmysql\u002Fmysql-learning-05","MySQL 学习笔记 05：DDL 概览","MySQL 学习笔记第五篇：数据定义语言概览——Schema、Table、View、Index、Constraints 这几类对象分别对应哪些 CREATE \u002F ALTER \u002F DROP 命令。",{"path":4507,"title":4508,"description":4509,"date":4500,"series":4501,"seriesOrder":121},"\u002Fposts\u002Fmysql\u002Fmysql-learning-06","MySQL 学习笔记 06：CREATE TABLE 与数据类型","MySQL 学习笔记第六篇：CREATE TABLE 的完整语法、常用数据类型的选型建议，以及外键的两种定义方式。",{"path":4511,"title":4512,"description":4513,"date":4500,"series":4501,"seriesOrder":129},"\u002Fposts\u002Fmysql\u002Fmysql-learning-07","MySQL 学习笔记 07：索引","MySQL 学习笔记第七篇：索引的作用与 B+ 树结构、单列与复合索引的创建、索引的代价与适用场景。",{"path":4515,"title":4516,"description":4517,"date":4500,"series":4501,"seriesOrder":137},"\u002Fposts\u002Fmysql\u002Fmysql-learning-08","MySQL 学习笔记 08：DROP 与 ALTER","MySQL 学习笔记第八篇：DROP 删除数据库对象、ALTER 修改表结构，以及常见的坑。",{"path":4519,"title":4520,"description":4521,"date":4500,"series":4501,"seriesOrder":145},"\u002Fposts\u002Fmysql\u002Fmysql-learning-09","MySQL 学习笔记 09：约束与数据完整性","MySQL 学习笔记第九篇：SQL 约束——NOT NULL、CHECK、PRIMARY KEY、FOREIGN KEY 等如何保证数据完整性，以及它们的对比。",{"path":4523,"title":4524,"description":4525,"date":4500,"series":4501,"seriesOrder":153},"\u002Fposts\u002Fmysql\u002Fmysql-learning-10","MySQL 学习笔记 10：视图","MySQL 学习笔记第十篇：视图（Views）的定义与特性、和表的对比、创建与使用，以及可更新视图的限制。",{"path":4527,"title":4528,"description":4529,"date":4500,"series":4501,"seriesOrder":158},"\u002Fposts\u002Fmysql\u002Fmysql-learning-11","MySQL 学习笔记 11：存储过程","MySQL 学习笔记第十一篇：存储过程（Stored Procedures）的概念、创建与调用、带参数的写法，以及优缺点。",{"path":4531,"title":4532,"description":4533,"date":4534,"series":4501,"seriesOrder":78},"\u002Fposts\u002Fmysql\u002Fmysql-learning-01","MySQL 学习笔记 01：MySQL 基础知识","MySQL 学习笔记第一篇：MySQL Server 与 Workbench 的安装，数据库与 DBMS 的基本概念，关系模型、ACID 事务、常见数据类型，以及后续的学习路线。","2026-10-09",{"path":4536,"title":4537,"description":4538,"date":4534,"series":4501,"seriesOrder":89},"\u002Fposts\u002Fmysql\u002Fmysql-learning-02","MySQL 学习笔记 02：SELECT 查询与过滤","MySQL 学习笔记第二篇：SELECT 的基本语法与逻辑执行顺序，WHERE 过滤（比较、LIKE、IN、BETWEEN、NULL）、各种 JOIN、UNION 与子查询。",{"path":4540,"title":4541,"description":4542,"date":4534,"series":4501,"seriesOrder":96},"\u002Fposts\u002Fmysql\u002Fmysql-learning-03","MySQL 学习笔记 03：聚合、分组与输出控制","MySQL 学习笔记第三篇：COUNT\u002FSUM\u002FAVG 等聚合函数、GROUP BY 分组、HAVING 过滤、ORDER BY 与 LIMIT，以及一条 SELECT 的完整逻辑执行顺序。",{"path":4544,"title":4545,"description":4546,"date":4547,"series":4491,"seriesOrder":89},"\u002Fposts\u002Fjava\u002Fjava-oop-notes","Java 面向对象笔记：类、继承、多态与异常","学习 Java 面向对象编程时的个人笔记：类与对象、封装与访问修饰符、继承与多态、集合、抽象类与接口、异常处理。示例代码均为自写，运行结果来自本地实测。","2026-10-06",{"path":4549,"title":4550,"description":4551,"date":4552,"series":4491,"seriesOrder":78},"\u002Fposts\u002Fjava\u002Fjava-basics-notes","Java 基础语法笔记：从环境安装到方法与数组","学习 Java 基础语法时的个人笔记：JDK 安装、输入输出、变量与运算符、条件与循环、字符串、数组与方法。示例代码均为自写，运行结果来自本地实测。","2026-10-05",{"path":4554,"title":4555,"description":4556,"date":4557,"series":4558,"seriesOrder":78},"\u002Fposts\u002Fgit\u002Fgit-install-and-basics","Git 安装与基本使用","从装好 Git 到跑通日常提交流程，把最常用的几条命令和它们背后的三个区域讲清楚。","2026-10-04","Git 入门",{"path":4560,"title":4561,"description":4562,"date":4557,"series":4558,"seriesOrder":89},"\u002Fposts\u002Fgit\u002Flocal-remote-repository","本地远程仓库的部署","用自己的机器当 Git 服务器：建一个裸仓库、走 SSH 连上去，把本地提交推上去。",{"path":4564,"title":4565,"description":4566,"date":4557,"series":4567,"seriesOrder":78},"\u002Fposts\u002Fguide\u002Fguide-file-layout","文件都放在哪","文章、图片、封面分别放在仓库的什么位置，哪些目录可以放手改、哪些碰了会出问题。","博客使用指南",{"path":4569,"title":4570,"description":4571,"date":4557,"series":4567,"seriesOrder":89},"\u002Fposts\u002Fguide\u002Fguide-frontmatter","frontmatter 每个字段是什么意思","文章开头那段被横线包住的字段，逐个说明作用、默认行为和写错的后果。",{"path":4573,"title":4574,"description":4575,"date":4557,"series":4567,"seriesOrder":96},"\u002Fposts\u002Fguide\u002Fguide-markdown","正文怎么写 —— Markdown 与排版效果","标题、链接、图片、代码、表格分别怎么写，以及本站对它们做了哪些自定义处理。",1793268185761]