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