尧图精选

从零搭建Spring Boot项目骨架:UNK20 Day1实战复盘

🕒 发布时间:2026/9/2 20:16:00 📁 来源:尧图网络
各位开发者朋友大家好。最近在复盘一个很有意思的技术训练营项目“UNK20”它的 Day1 课程是由 Simon Patterson 主讲的第七期内容编号 VII。这里先解释一下背景UNK20 是一个偏实战的集中式学习项目特点是节奏快、任务重、当天内容必须当天消化。而 Day1 往往决定整个训练营的体验因为它要解决一个最核心的问题如何在最短时间内把一个空白项目变成一套可运行、可扩展、可维护的工程骨架。很多自学 Java 或 Spring Boot 的朋友都有类似经历看教程时每一步都懂但合上教程自己从零建项目却不知道该先建哪个文件、依赖怎么配、代码放哪里、启动报错怎么排查。Simon Patterson 在 Day1 中强调了一个观点“先让代码跑起来再让它变好。”这句话听起来朴素但恰恰是很多初学者和中级开发者最容易忽略的。本文将围绕 UNK20 Day1 的实战思路完整还原一次从零搭建可运行项目的过程包括环境准备、依赖管理、核心代码编写、测试验证、常见报错排查以及工程规范建议。无论你是刚接触后端开发的学生还是工作中需要快速搭建项目骨架的工程师这篇文章都能给你一套可以直接照做的方案。1. 为什么 Day1 必须先解决“项目骨架”1.1 什么是项目骨架项目骨架简单说就是“一个项目最开始的那套基础结构”。它包含统一的目录分层构建工具与依赖描述文件应用入口类基础配置端口、日志、环境标识等健康检查或示例接口测试目录与基础测试用例。很多初学者觉得项目骨架不重要认为“代码能跑就行”。但在训练营场景里项目骨架直接决定了后续几天你写功能时是否顺手。如果骨架乱每加一个功能都要调整目录、改配置、处理依赖冲突一天下来大部分时间都耗在“环境问题”上而不是真正的业务逻辑。1.2 Simon Patterson 在 Day1 强调的核心思路Simon Patterson 在 Day1 的讲解中把第一天拆成了三个阶段最小可运行先不追求完美设计只要项目能启动、能访问、能测试。结构规范化在能运行的基础上按照工程标准调整目录和命名。自动化验证引入测试与静态检查让项目从“能跑”变成“可靠”。这三步对应三种心态初学者心态、工程师心态、质量意识。Day1 的重点不是把每一步做到极致而是让你在一天内同时体验这三种心态。1.3 为什么推荐 Java Spring Boot 作为实践载体UNK20 Day1 使用的是 Java 与 Spring Boot 技术栈原因很实际Java 在企业级后端中主流且稳定资料多、生态成熟Spring Boot 的“约定优于配置”特性非常适合快速搭建骨架Maven/Gradle 的依赖管理能帮助学员建立工程化思维由于 Spring Boot 内置 Tomcat运行体验贴近真实线上服务。本文的示例也以这套技术栈为主但强调一点思路是通用的。即使你用的是 Python FastAPI、Go Gin 或其他框架骨架设计的核心逻辑仍然一致。2. 环境准备与版本说明在开始写代码之前必须先把环境准备好。训练营第一天最容易翻车的就是环境不一致。这里我整理了一套相对稳妥的配置思路。2.1 环境清单以下是本文示例使用的环境读者需要根据自己机器的实际情况调整组件建议版本范围说明JDK8 或 11 或 17不同 Spring Boot 版本对 JDK 要求不同建议先确认Maven3.6 及以上本文使用 MavenGradle 用户可自行转换Spring Boot2.7.x 或 3.x2.7.x 对 JDK 8 更友好3.x 要求 JDK 17IDEIntelliJ IDEA 或 EclipseIDEA 对 Spring Boot 支持更好操作系统Windows / macOS / Linux 均可命令可能需要对应调整版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。如果你本机还没有 JDK可以通过命令行检查java -version mvn -version如果命令不存在需要先安装 JDK 和 Maven。Windows 用户配置环境变量时注意JAVA_HOME要指向 JDK 安装目录而不是jre目录。2.2 推荐的项目目录结构项目骨架的核心之一是目录结构。下面是本文后续实战案例中使用的结构unK20-day1-demo ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── unk20 │ │ │ └── day1 │ │ │ ├── Unk20Day1Application.java │ │ │ ├── controller │ │ │ │ └── HelloController.java │ │ │ ├── service │ │ │ │ └── GreetingService.java │ │ │ └── model │ │ │ └── Greeting.java │ │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── unk20 │ └── day1 │ └── Unk20Day1ApplicationTests.java这个结构看起来很普通但它体现了两个原则按职责分包controller负责接口层service负责业务逻辑model负责数据模型。后续加功能时你知道代码该往哪里放。主类放在根包下Unk20Day1Application.java放在com.unk20.day1这样 Spring Boot 的组件扫描默认范围正好覆盖整个项目。3. 从零创建项目核心步骤拆解3.1 创建 Maven 项目在 IDEA 中可以选择 Spring Initializr 创建项目也可以手动创建 Maven 项目。手动创建能让你更清楚每一个文件的作用所以这里推荐手动方式。首先创建一个空的 Maven 项目在pom.xml中引入 Spring Boot 依赖。文件路径pom.xml?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.unk20/groupId artifactIdunk20-day1-demo/artifactId version1.0.0/version nameunk20-day1-demo/name descriptionUNK20 Day1 Demo Project/description properties java.version8/java.version project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project这里引入的spring-boot-starter-web已经包含了内嵌 Tomcat、Spring MVC 和 Jackson 等基础依赖。也就是说只要引入这一个 starter我们就能启动一个 Web 应用。spring-boot-starter-test是测试专用依赖会在后续编写单元测试时用到。注意它的 scope 是test不会打入最终的生产包。3.2 编写应用入口类Spring Boot 应用需要一个入口类也就是带有main方法的类。文件路径src/main/java/com/unk20/day1/Unk20Day1Application.javapackage com.unk20.day1; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class Unk20Day1Application { public static void main(String[] args) { SpringApplication.run(Unk20Day1Application.class, args); } }SpringBootApplication是一个组合注解它等价于SpringBootConfiguration标识这是一个 Spring Boot 配置类EnableAutoConfiguration开启 Spring Boot 的自动配置ComponentScan默认扫描当前包及其子包下的组件。把入口类放在根包下是为了让组件扫描不出问题。这是新手最容易犯的错误之一主类放在com.unk20下而 Controller 放在com.unk20.day1.controller下结果导致 Controller 扫描不到。3.3 添加基础配置文件Spring Boot 支持application.properties和application.yml两种格式。本文使用application.yml因为它的层级结构更清晰。文件路径src/main/resources/application.ymlserver: port: 8080 spring: application: name: unk20-day1-demo logging: level: root: info com.unk20.day1: debug上面的配置做了三件事指定服务端口为8080。如果你本机端口被占用可以改成8081等。指定应用名称为unk20-day1-demo。这个名称在日志和后续服务注册时会用到。配置日志级别。com.unk20.day1下的日志输出debug级别方便开发时调试。这里需要注意的是spring.application.name不是必填项但建议从一开始就写上。后续接入 Nacos、Consul、Apollo 等配置中心或注册中心时这个名字会成为服务标识。3.4 编写领域模型类按照三层架构的习惯我们先定义一个简单的模型类。文件路径src/main/java/com/unk20/day1/model/Greeting.javapackage com.unk20.day1.model; public class Greeting { private String message; private String author; public Greeting() { } public Greeting(String message, String author) { this.message message; this.author author; } public String getMessage() { return message; } public void setMessage(String message) { this.message message; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author author; } }这个类有两个字段message和author。它会被 Jackson 自动序列化为 JSON 返回给前端。注意必须保留无参构造函数。虽然这里没写序列化框架代码但 Jackson 等工具在反序列化时通常需要无参构造。有了它后续扩展 POST 接口时会省掉很多麻烦。3.5 编写 Service 层Service 层承载业务逻辑。Day1 中我们不写复杂逻辑只负责生成问候语。文件路径src/main/java/com/unk20/day1/service/GreetingService.javapackage com.unk20.day1.service; import com.unk20.day1.model.Greeting; import org.springframework.stereotype.Service; Service public class GreetingService { public Greeting getGreeting(String name) { String defaultName (name null || name.trim().isEmpty()) ? UNK20 : name.trim(); String message Hello, defaultName ! Welcome to UNK20 Day1.; return new Greeting(message, Simon Patterson); } }这里有几个设计细节使用Service注解将该类交给 Spring 容器管理。对入参做了空值处理。如果调用方没有传name就使用默认值UNK20避免接口因入参为null出现不可预期行为。返回的是Greeting对象而不是直接返回字符串。这为后续接口扩展提供空间比如增加时间戳、状态码等字段。3.6 编写 Controller 层Controller 暴露 HTTP 接口。文件路径src/main/java/com/unk20/day1/controller/HelloController.javapackage com.unk20.day1.controller; import com.unk20.day1.model.Greeting; import com.unk20.day1.service.GreetingService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class HelloController { private final GreetingService greetingService; public HelloController(GreetingService greetingService) { this.greetingService greetingService; } GetMapping(/hello) public Greeting hello(RequestParam(value name, required false) String name) { return greetingService.getGreeting(name); } }RestController表示这是一个返回数据而非视图的控制器。GetMapping(/hello)将 HTTP GET 请求映射到hello方法。这里使用的是构造器注入而不是Autowired字段注入。构造器注入更推荐原因有几点依赖关系显式化创建对象时就能确定依赖方便写单元测试可以直接传入 mock 对象避免字段注入在某些场景下导致的空指针问题。3.7 运行项目到这里一个最小可运行项目已经完成了。在命令行项目根目录执行mvn spring-boot:run如果使用 IDEA也可以直接运行Unk20Day1Application主类。启动成功后日志中会出现类似下面的输出Tomcat started on port(s): 8080 (http) Started Unk20Day1Application in 2.5 seconds (JVM running for 3.1)然后在浏览器访问http://localhost:8080/hello?nameCSDN预期返回 JSON{ message: Hello, CSDN! Welcome to UNK20 Day1., author: Simon Patterson }到这里我们已经完成了 Day1 的第一阶段目标让项目跑起来。4. 加入自动化测试让项目从“能跑”变成“可靠”Simon Patterson 在 Day1 中反复强调只做到“能跑”是不够的。一个工程化的项目必须有自动化测试作为保障。测试的作用不是给自己看的而是为了将来修改代码时不至于“改一处崩一片”。4.1 编写单元测试我们为GreetingService编写一个简单的单元测试。单元测试只关注单个类的逻辑不启动 Spring 容器所以速度很快。文件路径src/test/java/com/unk20/day1/service/GreetingServiceTest.javapackage com.unk20.day1.service; import com.unk20.day1.model.Greeting; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class GreetingServiceTest { private final GreetingService greetingService new GreetingService(); Test void shouldReturnDefaultNameWhenNameIsBlank() { Greeting greeting greetingService.getGreeting(null); assertEquals(UNK20, greeting.getMessage().contains(UNK20) ? UNK20 : FAIL); } Test void shouldReturnCustomNameWhenNameIsProvided() { Greeting greeting greetingService.getGreeting(Java); assertTrue(greeting.getMessage().contains(Java)); } }上面的测试有两个用例当name为空时返回的消息包含默认值UNK20当name为Java时返回的消息包含Java。如果你使用的是 Spring Boot 2.7.xspring-boot-starter-test会默认引入 JUnit 5Jupiter所以可以直接使用org.junit.jupiter.api.Test注解。运行测试命令mvn test预期输出中会有类似Tests run: 2, Failures: 0, Errors: 0, Skipped: 04.2 编写 Web 层集成测试单元测试之外我们还可以写一个“启动容器但使用 Mock 环境”的集成测试验证 HTTP 接口是否正常响应。Spring Boot 提供了SpringBootTest和MockMvc来支持这种测试。文件路径src/test/java/com/unk20/day1/controller/HelloControllerTest.javapackage com.unk20.day1.controller; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; SpringBootTest AutoConfigureMockMvc class HelloControllerTest { Autowired private MockMvc mockMvc; Test void shouldReturnGreeting() throws Exception { mockMvc.perform(get(/hello).param(name, CSDN)) .andExpect(status().isOk()) .andExpect(jsonPath($.message).value(Hello, CSDN! Welcome to UNK20 Day1.)) .andExpect(jsonPath($.author).value(Simon Patterson)); } }这个测试的作用是启动完整的 Spring 应用上下文但使用 Mock 的 HTTP 环境发起请求不需要真实占用8080端口。jsonPath($.message)表示从 JSON 响应中取message字段。如果你第一次看到这种写法可以理解为“把 JSON 当作一棵树用路径去取值”。5. 项目体积膨胀后如何配置分层与统一返回结构基础骨架跑通以后UNK20 Day1 花了一些时间讲解“接下来的几天你一定会遇到的扩展问题”。其中最典型的就是当接口变多每个接口返回格式如果不统一前端对接会非常痛苦。所以这里提前引入一个统一响应结构。这是一个非常实用的工程实践建议在项目初期就落实。5.1 定义统一响应类文件路径src/main/java/com/unk20/day1/model/ApiResponse.javapackage com.unk20.day1.model; public class ApiResponseT { private int code; private String message; private T data; public ApiResponse() { } public ApiResponse(int code, String message, T data) { this.code code; this.message message; this.data data; } public static T ApiResponseT success(T data) { return new ApiResponse(200, success, data); } public static T ApiResponseT error(int code, String message) { return new ApiResponse(code, message, null); } public int getCode() { return code; } public void setCode(int code) { this.code code; } public String getMessage() { return message; } public void setMessage(String message) { this.message message; } public T getData() { return data; } public void setData(T data) { this.data data; } }这个类使用了 Java 泛型T代表任意数据类型。success和error两个静态方法让调用方写起来更简洁。5.2 改造 Controller改造HelloController让它返回统一响应结构。package com.unk20.day1.controller; import com.unk20.day1.model.ApiResponse; import com.unk20.day1.model.Greeting; import com.unk20.day1.service.GreetingService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class HelloController { private final GreetingService greetingService; public HelloController(GreetingService greetingService) { this.greetingService greetingService; } GetMapping(/hello) public ApiResponseGreeting hello(RequestParam(value name, required false) String name) { return ApiResponse.success(greetingService.getGreeting(name)); } }此时再次启动项目并访问http://localhost:8080/hello?nameSpringBoot返回结果变为{ code: 200, message: success, data: { message: Hello, SpringBoot! Welcome to UNK20 Day1., author: Simon Patterson } }前端拿到这个结构后不管后端的data是对象还是数组都能按统一的规则处理。这也是 Simon Patterson 强调的“先定义规则再实现功能”在工程上的体现。6. 常见问题与排查思路在 UNK20 Day1 的实操作业中学员遇到的大多数问题都集中在环境、依赖和启动异常上。下面整理一份高频问题清单供大家对照排查。问题现象常见原因解决思路mvn命令不是内部或外部命令Maven 未安装或环境变量未配置重新安装 Maven并配置MAVEN_HOME和PATH启动时报Port 8080 was already in use8080 端口被其他程序占用修改application.yml中server.port或关闭占用进程Controller 接口 404主类包路径与子包不一致组件扫描失败将主类放在根包下例如com.unk20.day1依赖下载特别慢Maven 默认中央仓库网络不稳定配置阿里云 Maven 镜像或在 IDEA 中设置镜像java: 错误: 无效的源发行版项目 JDK 版本与编译级别不一致检查 IDEAProject Structure中 SDK 与pom.xml的java.version类文件具有错误的版本 61.0应为 52.0使用了 JDK 17 编译但运行环境是 JDK 8统一本地 JDK 为 8/11/17并重新构建Autowired注入为 null使用字段注入但类未被 Spring 管理确认类上有Service/Component注解或改用构造器注入6.1 典型问题Maven 依赖下载失败很多新手在第一次mvn spring-boot:run时卡在依赖下载环节日志中经常出现红色的ERROR或超时提示。解决办法通常是更换 Maven 镜像源。在settings.xml中添加阿里云镜像mirror idalimaven/id namealiyun maven/name urlhttps://maven.aliyun.com/repository/public/url mirrorOfcentral/mirrorOf /mirror修改后重新执行mvn clean compile依赖下载速度会有明显提升。6.2 典型问题接口返回 404如果项目启动成功但访问/hello返回 404 或 Whitelabel Error Page大概率是 Controller 没有被扫描到。排查顺序确认HelloController上是否有RestController注解确认主类是否在包的根路径上确认主类上是否有SpringBootApplication注解确认访问路径和方法上的GetMapping路径一致。这里最容易被忽略的是第 2 点主类放在com.unk20包下Controller 放在com.unk20.day1.controller包下默认扫描范围只覆盖com.unk20的当前包及子包所以com.unk20.day1.controller其实是在扫描范围内的。反过来说如果 Controller 放在com.otherpackage下就会扫描不到。6.3 典型问题Tomcat 端口被占用Windows 下查看端口占用netstat -ano | findstr 8080macOS / Linux 下查看端口占用lsof -i :8080找到占用进程的 PID 后确认进程类型再决定是否结束。如果是无关程序占用可以修改端口号继续开发不建议强制结束系统关键进程。7. 工程化建议与最佳实践UNK20 Day1 的内容虽然只是一个入门骨架但已经涉及了很多工程化思维。这里把 Simon Patterson 在课程中强调的一些原则结合我自己的开发经验整理成几条建议。7.1 命名规范从第一天就开始很多人觉得“命名”是很虚的东西等代码写多了再改。实际上命名是项目可维护性的第一道防线。包名统一小写例如com.unk20.day1.controller类名使用大驼峰例如HelloController方法名使用小驼峰例如getGreeting常量使用全大写加下划线例如MAX_RETRY_COUNT避免使用a、b、temp、data1这类无意义命名。统一命名规范后团队协作时不需要花时间猜测别人的代码意图。7.2 使用构造器注入而不是字段注入Spring 开发中依赖注入有三种常见方式字段注入、Setter 注入、构造器注入。字段注入写起来最简洁Autowired private GreetingService greetingService;但这种方式有两个问题依赖关系不直观类与类之间的关系隐藏在字段里单元测试时需要依赖 Spring 容器或者用反射修改私有字段增加了测试成本。构造器注入在 Spring 4.3 中如果类只有一个构造器甚至可以省略Autowired注解。代码更干净也方便测试。7.3 配置信息不要硬编码在application.yml中集中管理配置是基础工程素养。实际项目中下面这些信息都不应该硬编码在 Java 代码里数据库连接地址Redis 地址与密码第三方服务的 API Key业务开关与阈值参数文件存储路径。正确的做法是先放到application.yml然后使用ConfigurationProperties或Value注入到类中。更进一步可以使用 Apollo 或 Nacos 这类配置中心实现动态调整。7.4 日志策略先打印关键链路再优化性能在 Day1 结束时Simon Patterson 给学员留了一个思考题“你的服务出问题时你靠什么定位”答案通常是日志。建议在项目初期就引入统一的日志规范使用 SLF4J 门面记录日志而不是System.out.println在入口方法、调用第三方接口、异常捕获处打印关键日志日志中输出请求参数和耗时但不要输出密码、token 等敏感信息使用logger.debug输出调试信息使用logger.warn输出可恢复的异常使用logger.error输出严重错误。示例private static final Logger logger LoggerFactory.getLogger(HelloController.class); GetMapping(/hello) public ApiResponseGreeting hello(RequestParam(value name, required false) String name) { long start System.currentTimeMillis(); ApiResponseGreeting response ApiResponse.success(greetingService.getGreeting(name)); logger.info(hello接口耗时: {} ms, name{}, System.currentTimeMillis() - start, name); return response; }7.5 尽早引入检查工具正规项目一般会集成 Checkstyle、SpotBugs 或 SonarQube 做代码质量检查。Day1 中不要求配置完整检查流程但至少可以做到在 IDEA 中安装 Checkstyle 插件使用统一的代码格式化配置提交代码前运行mvn test确保测试通过。如果团队开发建议在 Git 提交前配置 pre-commit 钩子或 CI 流水线让每个提交都经过自动化检查。8. 总结与后续学习建议UNK20 Day1 的核心目标不是教会你某一个 API 的用法而是帮助你建立一套“让项目从零开始健康生长”的方法。回顾一下本文的内容了解了项目骨架的含义与重要性从零搭建了一个 Spring Boot 最小可运行项目划分了 controller、service、model 三层的目录结构编写了单元测试与 Web 集成测试引入了统一响应结构为后续多接口开发打好基础整理了常见启动报错的排查方式总结了命名、注入、配置、日志等工程规范。这些内容虽然以 Spring Boot 为例但背后的思路可以迁移到任何后端技术栈。Day1 之后比较推荐的进阶方向是深挖 Spring Boot 自动配置原理为什么引入一个 starter 就能拥有完整 Web 能力spring.factories和AutoConfiguration.imports是怎么生效的掌握统一异常处理在RestControllerAdvice中定义全局异常处理器把业务异常和系统异常转换成统一的响应结构。了解配置中心与分布式环境把application.yml中的配置迁移到 Apollo 或 Nacos体验配置灰度发布与动态刷新。数据库操作在骨架基础上引入 MyBatis-Plus 或 Spring Data JPA把接口数据持久化到 MySQL 中并思考事务与连接池配置。项目瘦身与性能优化学习 Maven 依赖分析排除冗余依赖理解spring-boot-maven-plugin的打包原理。如果你按照本文的步骤成功让第一个接口返回了 JSON那么恭喜你UNK20 最重要的一天已经平稳落地了。接下来的每一天都会在这个骨架的基础上继续加新东西请一定确保你的项目结构和依赖保持整洁。学习完 Day1 后建议手动删掉项目重新建一次不参考任何笔记看看自己能否独立完成从创建项目到接口访问全流程。这个过程能帮你把“看完”变成“会做”。如果遇到任何问题欢迎在评论区留言讨论。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →