前言

其实我之前都是使用FastAPI进行后端开发的,但是嘛...败在Python可维护性太差。

一周前我写的代码,如果一周不去管他,第二周就不认识了

我觉得很大程度上是由于Python的弱类型的特性导致的。

写的爽,维护难

我对比的是C#以及Rust这两门强类型语言。

因为这两门语言的代码我放一个月后再去看我也是能看懂的...

于是便想来学一下Java这门强类型语言的Web开发了,上来接触的是SpringBoot。

实际上这篇记录也是在我接触了SpringBoot后的一段时间才开始写的,因为自己博客没什么特别有价值的东西,所以干脆水一点记录算了。

这次记录中大量使用了AI,我顺便把我这次记录中使用的AI页记录一下:

  • 规划模型:Deepseek R1
  • 对话模型:Gemini-2.0-Pro-Exp
  • 思考模型:Gemini-2.0-Flash-Thinking-Exp

接下来就让我们正式开始吧。

项目式学习

我之前学习的SpringBoot为什么说是“接触”呢?

因为我想尝试一点不一样的学习方法——以知识点的方式进行学习。

说实话,我学习编程以来一直都是用的项目式学习,速度很块、学习成效也高、反馈也强。

“但这是不是唯一的途径呢?”我就在想。

遂利用AI来尝试列一下知识点,得到了如下的表格:

周数 天数 学习主题 核心概念 ​& 学习目标 实践任务 ​& 输出标准 学习资源 ​& 辅助材料
第一周 Day 1 Spring Boot 环境搭建与快速入门 目标: 理解 Spring Boot 核心优势,快速搭建开发环境并启动应用。
概念: Spring Boot 优势 (简化配置、快速开发), Spring Initializr, Maven/Gradle 构建基础。
产出: 能够独立创建 Spring Boot 项目,配置 pom.xml/build.gradle,运行并访问默认端口。
任务: 1. 安装 JDK, Maven/Gradle, IDE (IntelliJ IDEA 或 VS Code)。 2. 使用 Spring Initializr 创建 Spring Boot Web 项目。 3. 添加 spring-boot-starter-web 依赖。 4. 编写简单的 Controller 返回 "Hello, Spring Boot!"。 5. 运行项目并通过浏览器访问。
输出: 成功启动 Spring Boot Web 应用,浏览器访问看到 "Hello, Spring Boot!"。
资源: 1. 官方文档: Spring Boot 官方网站, Spring Initializr。 2. 官方指南: Spring Boot ​"Getting Started​" 指南。 3. 视频教程: 搜索 "Spring Boot 快速入门 tutorial"。
Day 2 依赖注入 (Dependency Injection - DI) & IoC 目标: 掌握 IoC 和 DI 的核心概念,理解 Bean 的生命周期。
概念: IoC (控制反转) 原理, DI (依赖注入) 方式, Bean 概念与作用域, @Component, @Service, @Repository, @Controller 注解, @Autowired 使用。
产出: 理解 Spring 如何管理 Bean,能够使用注解配置和注入 Bean。
任务: 1. 创建多个 Bean 类,分别使用 @Component, @Service, @Repository 注解。 2. 创建 UserService 依赖 UserRepository,使用 @Autowired 进行注入。 3. 在 Controller 中注入 UserService。 4. 编写单元测试,验证依赖注入是否成功。
输出: 成功实现 Bean 的创建和注入,单元测试验证依赖关系。
资源: 1. Spring Framework 文档: 依赖注入部分。 2. 视频教程: 搜索 "Spring Dependency Injection tutorial", "IoC container tutorial"。 3. 博客: 搜索 "Spring IoC DI 详解", "Spring Bean 生命周期"。
Day 3 Spring Boot 核心注解与配置 目标: 掌握 Spring Boot 常用注解,理解外部化配置。
概念: @SpringBootApplication, @Configuration, @Bean, @Value 注解, application.properties/.yml 配置文件, Profiles 环境配置, 配置文件的优先级。
产出: 能够灵活配置 Spring Boot 应用,实现不同环境的配置切换。
任务: 1. 修改 application.properties/.yml 文件,配置端口号、应用名称等。 2. 创建 @Configuration 类和使用 @Bean 定义 Bean。 3. 使用 @Value 读取配置文件信息。 4. 练习使用不同的 Profile 环境配置 (dev, prod)。 5. 掌握配置文件的加载优先级。
输出: 能够通过配置文件修改应用配置,实现不同环境的配置隔离。
资源: 1. Spring Boot 文档: 配置部分, Profiles 部分。 2. 教程: 搜索 "Spring Boot Configuration Properties tutorial", "Spring Boot Profiles example"。 3. 博客: 搜索 "Spring Boot 配置文件加载顺序", "Spring Boot 多环境配置"。
Day 4 Spring MVC 基础:Controller 与请求处理 目标: 掌握 Spring MVC 请求处理流程,构建 RESTful API。
概念: Spring MVC 请求处理流程 (DispatcherServlet, HandlerMapping, Controller, ViewResolver), @Controller vs. @RestController, @RequestMapping 及其变体 (@GetMapping, @PostMapping 等), 请求参数获取 (@RequestParam, @PathVariable, @RequestBody), 返回值处理 (@ResponseBody, ResponseEntity)。
产出: 能够创建 RESTful API 接口,处理 GET 和 POST 请求,返回 JSON 数据。
任务: 1. 创建 @RestController,定义多个请求处理方法。 2. 处理 GET 请求,使用 @RequestParam@PathVariable 获取参数。 3. 处理 POST 请求,使用 @RequestBody 接收 JSON 数据。 4. 返回字符串、JSON 数据和 ResponseEntity 对象。 5. 使用浏览器/Postman 测试 API 接口。
输出: 成功创建多个 REST API 接口,使用 Postman 可以正常请求和接收数据。
资源: 1. Spring MVC 文档: Controller 部分。 2. 教程: 搜索 "Spring MVC REST Controller tutorial", "RESTful API tutorial Spring Boot"。 3. 工具: PostmanInsomnia (API 测试工具)。
Day 5 AOP 基础 (面向切面编程 - 概念与简单应用) 目标: 理解 AOP 的概念和作用,能够使用 AOP 进行简单的方法拦截。
概念: AOP 概念和作用 (解耦、横切关注点), 切面 (Aspect), 连接点 (Join Point), 切入点 (Pointcut), 通知 (Advice) 类型 (@Before, @After, @Around, @AfterReturning, @AfterThrowing), @Aspect, @Before, @After, @Around 注解。
产出: 能够创建简单的日志切面,实现方法执行前后的日志记录。
任务: 1. 创建 @Aspect 类,定义切面。 2. 创建 @Before 通知,在方法执行前打印日志。 3. 在 ControllerService 方法上应用切面。 4. 观察日志输出,理解 AOP 的执行流程。 5. 选做: 尝试使用 @AfterReturning@Around 通知。
输出: 成功创建日志切面,方法执行前后可以看到日志输出。
资源: 1. Spring AOP 文档: AOP 部分。 2. 教程: 搜索 "Spring AOP tutorial beginner", "Spring AOP introduction"。 3. 博客: 搜索 "Spring AOP 例子", "AOP 日志切面实现"。
周末 第一周知识复习与巩固 &迷你项目一:简易 ​"Hello API​" 目标: 系统复习第一周所学知识,通过迷你项目巩固核心概念。
概念: 回顾 Spring Boot 核心优势、DI/IoC、核心注解、Controller、REST API 基本概念、AOP 基础概念。
产出: 完成一个简单的 "Hello World" REST API 项目,包含 GET /hello, GET /hello/{name}, POST /hello 接口,并使用 Postman/curl 测试所有接口。
任务: 1. 复习第一周所有学习内容,回顾关键概念和注解用法。 2. 完成 迷你项目一:简易 ​"Hello API​" : * 创建 Spring Boot 项目,包含 REST Controller。 * 实现三个接口: /hello (返回 "Hello"), /hello/{name} (返回 "Hello, {name}"), POST /hello (接收 JSON { "name": "xxx" },返回 "Hello, xxx")。 * 编写简单的单元测试(可选)。 * 使用 Postman/curl 测试所有接口。
输出: 完成并测试通过 "Hello API" 迷你项目,能够独立运用第一周所学知识。
资源: 复习第一周的学习资源。 迷你项目可参考第一周的示例代码和教程。
第二周 Day 6 数据库连接与 Spring Data JPA 基础 目标: 掌握 Spring Data JPA 的基础使用,连接数据库并进行简单操作。
概念: 关系型数据库基础 (RDBMS), JDBC (了解), Spring Data JPA 概念和优势 (简化数据访问), spring-boot-starter-data-jpa 依赖, 数据库连接配置, 实体类 (Entity)/JPA 注解 (@Entity, @Id, @GeneratedValue, @Column)。
产出: 能够配置数据库连接,创建实体类并映射数据库表。
任务: 1. 选择数据库 (H2 内存数据库 或 MySQL)。 2. 添加 spring-boot-starter-data-jpa 和数据库驱动依赖。 3. 配置数据库连接信息 (application.properties/yml)。 4. 创建实体类 (Entity),使用 JPA 注解映射到数据库表。 5. 启动项目,观察自动创建数据库表 (如果使用 H2,访问 h2-console 查看)。
输出: 成功连接数据库,实体类成功映射到数据库表。
资源: 1. Spring Data JPA 文档: Spring Data JPA。 2. Spring Boot 文档: Data JPA 部分。 3. 教程: 搜索 "Spring Data JPA tutorial beginner", "Spring Boot JPA example"。 4. 数据库: H2 Database (快速入门内存数据库)。
Day 7 Spring Data JPA 核心接口:JpaRepository 目标: 掌握 JpaRepository 的使用,实现基本的 CRUD 操作和自定义查询。
概念: JpaRepository 作用 (CRUD 和基本查询), 定义 Repository 接口 (继承 JpaRepository<T, ID>), 使用默认方法 (save(), findById(), findAll(), deleteById()), 自定义查询方法 (方法名约定查询, @Query 注解)。
产出: 能够使用 JpaRepository 进行数据增删改查,并实现简单的自定义查询。
任务: 1. 为实体类创建 Repository 接口,继承 JpaRepository<EntityType, IDType>。 2. 在 Service/Controller 中注入 Repository。 3. 使用 Repository 提供的方法进行 CRUD 操作。 4. 尝试使用方法名约定查询 (例如 findByProductName)。 5. 选做: 使用 @Query 注解自定义更复杂的查询方法。
输出: 能够使用 JpaRepository 完成基本的数据库操作和查询。
资源: 1. Spring Data JPA 文档: JpaRepository 部分。 2. 教程: 搜索 "Spring Data JPA JpaRepository tutorial", "Spring Data JPA query methods"。 3. 博客: 搜索 "Spring Data JPA 方法名约定查询规则", "Spring Data JPA @Query 使用"。
Day 8 Service 层设计与数据操作实践 目标: 理解 Service 层的作用,进行业务逻辑处理和数据操作。
概念: Service 层作用 (业务逻辑、事务管理), Service 层调用 Repository 数据操作, Controller 调用 Service, 简单数据验证 (了解 Bean Validation), 异常处理 (@ExceptionHandler 基础)。
产出: 能够设计 Service 层,实现业务逻辑和数据操作,并进行简单的数据验证和异常处理。
任务: 1. 创建 ProductService 类,注入 ProductRepository。 2. 在 ProductService 中编写业务逻辑方法 (CRUD 操作)。 3. 修改 ProductController 调用 ProductService 方法。 4. 在 Service 层进行简单的数据验证 (例如非空判断)。 5. 选做: 尝试使用 @ExceptionHandler 处理 Controller 异常。
输出: 成功设计 Service 层,Controller 通过 Service 操作数据,API 能够完成业务逻辑。
资源: 1. 教程: 搜索 "Spring Boot Service Layer best practices", "Spring Boot Service Layer tutorial"。 2. Spring MVC 文档: 异常处理部分。 3. 博客: 搜索 "Spring Boot Service 层设计模式", "Spring Boot 全局异常处理"。
Day 9 表单数据处理与验证 (简化版 Bean Validation) 目标: 掌握使用 Bean Validation 进行基本的数据验证。
概念: 前端传递 JSON (@RequestBody), 后端接收 JSON 映射到 Java 对象 (DTO), Bean Validation 框架 (JSR 380), @Valid 注解, 验证注解 (@NotNull, @NotEmpty, @NotBlank, @Size, @Min, @Max 等), BindingResult 获取验证结果, 返回错误信息 (JSON 响应)。
产出: 能够使用 Bean Validation 对请求数据进行验证,并返回验证错误信息。
任务: 1. 创建 DTO 类 (Data Transfer Object),添加 Bean Validation 注解 (例如 @NotNull, @Size)。 2. Controller 中使用 @RequestBody 接收 DTO 对象,并使用 @Valid 注解触发验证。 3. 在 Controller 方法中通过 BindingResult 获取验证结果。 4. 验证失败时,构建包含错误信息的 JSON 响应 (返回 400 Bad Request 状态码)。 5. 使用 Postman/curl 测试数据验证接口。
输出: 能够实现请求数据验证,并在验证失败时返回包含错误信息的 JSON 响应。
资源: 1. 教程: 搜索 "Spring Boot request body validation", "Spring Boot Bean Validation tutorial"。 2. Bean Validation (JSR 380) 文档: 搜索 "Bean Validation 3.0 JSR 380"。 3. 博客: 搜索 "Spring Boot 参数校验", "Bean Validation 常用注解"。
Day 10 单元测试与集成测试 (基础 MockMvc) 目标: 理解单元测试和集成测试的区别,掌握使用 JUnit 和 Spring Test 进行基础测试的方法。
概念: 单元测试/集成测试概念, JUnit (JUnit 5) 和 Spring Test (spring-boot-starter-test), @SpringBootTest 注解, @Autowired 注入 Bean, MockMvc Controller 集成测试 (模拟 HTTP 请求), Mockito (简单了解 Mock)。
产出: 能够为 Service 层编写单元测试,为 Controller 层编写集成测试。
任务: 1. 为 ProductService 编写单元测试,测试 Service 方法的业务逻辑。 2. 为 ProductController 编写集成测试 (MockMvc),模拟 HTTP 请求测试 API 接口。 3. 至少为每个 Service/Controller 编写 2-3 个测试用例 (正常情况和异常情况)。 4. (可选) 简单了解 Mockito 的使用,进行 Mock 对象创建。
输出: 能够编写单元测试和集成测试,测试覆盖核心业务逻辑和 API 接口。
资源: 1. Spring Boot 文档: 测试部分。 2. 教程: 搜索 "Spring Boot testing tutorial beginner", "Spring Boot MockMvc tutorial"。 3. JUnit 5 文档: JUnit 5 User Guide。 4. Mockito 文档: Mockito (简单了解)。
周末 第二周知识复习与巩固 &迷你项目二:简易 ​"Product API​" 目标: 系统复习第二周所学知识,通过迷你项目巩固 Spring Data JPA 和 REST API 开发。
概念: 回顾 Spring Data JPA、JpaRepository、实体类、Repository、Service & Controller 层数据交互、数据验证 & 基础测试。
产出: 完成一个简单的 "Product API" 项目,实现 Product 实体类的 CRUD RESTful API,包含单元测试和集成测试。
任务: 1. 复习第二周所有学习内容,回顾 JPA、Service、Controller、数据验证和测试相关知识。 2. 完成 迷你项目二:简易 ​"Product API​" : * 创建 Product 实体类 (包含 id, name, description, price 等字段)。 * 创建 ProductRepository, ProductService, ProductController。 * 实现 Product 的 CRUD RESTful API 接口。 * 使用 Bean Validation 进行数据验证。 * 为 Service 和 Controller 编写单元测试和集成测试。
输出: 完成并测试通过 "Product API" 迷你项目,能够熟练运用第二周所学知识,搭建基本的 RESTful API 应用。
资源: 复习第二周的学习资源。 迷你项目可参考第二周示例代码和教程,以及第一周的 "Hello API" 项目。

刚开始还好。

第三天开始就学得非常非常难受。

反馈太弱了是一回事,还有一点是确实慢。

我接触过和自己写过不少项目了,因此我不认为自己会学不懂,而且实际上我学习的成效也很高,但问题就出在确实是反馈实在是太慢太弱了。

于是便弃了这种方法,没学完,所以说是“接触过”。

接下来就还是采用我熟悉的方法——项目式学习。


立项

立项是通过跟AI交流完成的,先做点简单的来熟悉一下Spring框架算了。

AI的提示词是我自己写的,自上次有AI猫娘来辅导我学习高数后便一发不可收拾了(唉,下头)。

最后立项的结果就是这样了:

然后交给R1进行项目规划:

前期准备

按照步骤,我们应该先搭建Java SpringBoot的开发环境。

因为我之前是接触过SpringBoot的,因此这里环境也就提前准备好了:

JDK=21
IDE=Visual Studio
Database=PostgreSQL

随后是初始化SpringBoot后端项目:

初始化React前端项目,这里我使用的是Vite。

常用前端库的安装:

正常启动前端与后端,spring第一次运行的时候还把字母拼错了。

配置跨域请求:

接下来就可以正式开始了。

数据库设计和实体类映射

正如这句我说的格言所说(噗),我是喜欢追问自己认知以外的所有代码的,即假如一段代码我不理解其背后的原理我是不会拿来用的——即使它不是AI写的。

我之前说过自己虽然只是接触,但是成效尤为明显。因此不会追问在SpringBoot中过于基础的部分,比如Java语法或SpringBoot如何工作之类的。我现在更多的是会追问一些自己没见过或使用过的注解。

我尤其喜欢Lobechat中的这个创建子话题的功能,一个是目前市面上支持创建分支对话的ChatUI确实不多,另一个是分支对话让我可以对一个点进行深入的追问而不影响主对话。

首先来看一下数据库的设计吧:

这里我比较疑惑的点是:为什么任务的优先级不使用枚举类型而是使用VARCHAT(10)的类型?

解释的很清楚,最关键的是数据库兼容的问题,除此之外也优化了灵活性和扩展性。

除此之外,在利用AI来学习编程的时候,我还有一个习惯——追问是否有更优的实现方式?

大多数读者都应该知道,AI是有幻觉的,即使是Deepseek R1这样的推理模型也不例外,更不用说对话模型了。

因此绝大多数AI都是尽可能的给你一个“能跑就行”的代码,而不是最优代码。在学习的时候就容易认为这是最优代码,但实际上可能就只是能跑就行。

在进行提问后,会发现这些注解的更多更优用法:

当然也是受精度限制的,一般来说第一次学习我只会进行一次的优化提问,后续在项目基本完成后我会对某一小段代码进行多次提问是否有更优解。以此来学习更优化的代码。

中途还追问了一下为什么需要一个无参构造方法:

总之,我们最终实体类的映射代码如下:

// src/main/java/cc/kispace/KiTasks/entity/Task.java
package cc.kispace.KiTasks.entity;

import jakarta.persistence.*;
import java.util.List;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "username", unique = true, nullable = false, length = 50)
    private String username;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "email", length = 50)
    private String email;

    @OneToMany(mappedBy = "user")
    private List<Task> tasks;

    public User() {}

    public User(Long id, String username, String password, String email, List<Task> tasks) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.tasks = tasks;
    }

    // Getter 和 Setter 方法
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public List<Task> getTasks() {
        return tasks;
    }

    public void setTasks(List<Task> tasks) {
        this.tasks = tasks;
    }
}
// src/main/java/cc/kispace/KiTasks/entity/User.java
package cc.kispace.KiTasks.entity;

import jakarta.persistence.*;
import java.util.List;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "username", unique = true, nullable = false, length = 50)
    private String username;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "email", length = 50)
    private String email;

    @OneToMany(mappedBy = "user")
    private List<Task> tasks;

    public User() {}

    public User(Long id, String username, String password, String email, List<Task> tasks) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.tasks = tasks;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public List<Task> getTasks() {
        return tasks;
    }

    public void setTasks(List<Task> tasks) {
        this.tasks = tasks;
    }
}