0
0

Java Web 笔记

2026-06-14
2026-07-23

随手记

浏览器向 Web 服务器(如 Tomcat)发送一个 HTTP 请求(例如 http://localhost:8080/hello?name=itheima),Tomcat 接收到请求后将其转交给 Spring MVC 的前端控制器 DispatcherServletDispatcherServlet 负责解析 URL,并根据请求路径找到对应的 XxxController(图中并列多个 Controller 表示应用中有多个控制器,但每次只会匹配一个),然后调用其中的具体方法执行业务逻辑,最后将结果封装成 HTTP 响应返回给浏览器。简单说:DispatcherServlet 是“调度中心”,Controller 是“具体干活的”,Tomcat 只负责接收和转发请求。

Spring MVC 是 Spring 框架中处理 Web 请求的模块,它的核心是一个叫 DispatcherServlet 的前端控制器,负责接收所有请求并根据 URL 分发给对应的 Controller 方法。你只需要在方法上加上 @GetMapping@PostMapping 等注解,以及用 @RequestParam@PathVariable@RequestBody 等注解绑定请求参数,方法返回的对象会自动转成 JSON 响应。你现在写的每个 Spring Boot Web 项目,底层都在使用 Spring MVC,只是 Spring Boot 把它自动配置好了,你感觉不到它的存在。

Web 服务器(如 Tomcat)启动后,会在操作系统里注册:我要监听 8080 端口。 操作系统负责接收所有到达 8080 端口的网络数据,然后转给 Tomcat。
Tomcat 再根据请求 URL 转发给你的 Controller。

在传统的 Java Web 开发中,你需要先手动安装一个 Tomcat 服务器,然后把项目打包成 war 包部署到服务器上才能运行;而 Spring Boot 通过 spring-boot-starter-web 依赖在项目中内嵌了一个迷你版的 Tomcat。当你在 @SpringBootApplication 标记的 main 方法中调用 SpringApplication.run() 时,Spring Boot 会做三件事:一、自动启动这个内嵌的 Tomcat 服务器并监听端口(如 8080);二、通过 @ComponentScan 扫描你项目中所有带有 @Controller@RestController 等注解的类;三、将这些 Controller 中定义的请求映射(如 @GetMapping("/hello"))自动注册到 Tomcat 的路由表中。 这样一来,Tomcat 就能把收到的请求转发给对应的 Controller 方法处理。所以你只需要像运行普通 Java 程序一样执行 main 方法,就相当于完成了一个完整 Web 服务器的“启动 + 部署”全过程,无需任何外部服务器软件。这也是 Spring Boot 著名的“直接跑 main 就能运行一个 Web 项目”的核心原理。

Maven 拿到依赖坐标后,先去本地仓库(~/.m2/repository/)找,找到了直接用;找不到再去远程仓库下载,下载后存到本地,下次直接用

Java Web 基础架构

Java Web 开发本质上是在写运行在服务器上的 Java 程序。用户通过浏览器、前端页面、Postman 或其他客户端发送 HTTP 请求,后端服务器接收到请求后,执行对应的 Java 代码,完成参数接收、业务处理、数据库操作,最后把结果返回给客户端。Spring Boot Controller 中写的接口方法,本质上就是在处理一次次 HTTP 请求。

1. Java Web 的整体流程

一个完整的 Java Web 请求大致是这样:

浏览器 / 前端页面 / Postman
        ↓ 发送 HTTP 请求
Tomcat / Spring Boot 内置服务器
        ↓ 找到对应的接口
Controller
        ↓ 调用业务逻辑
Service
        ↓ 操作数据库
Mapper / DAO
        ↓ 返回数据
浏览器 / 前端页面 / Postman

实际开发中,前端一般负责页面展示和用户交互,后端负责接收请求、处理业务、访问数据库、返回数据。比如用户在前端点击“登录”按钮,前端会把账号密码通过 HTTP 请求发送给后端,后端校验账号密码是否正确,然后返回登录成功或失败的信息。

2. 前端是怎么访问后端的

前端访问后端,本质上就是发送 HTTP 请求。以前浏览器直接访问网页时,可能是输入一个地址打开页面;现在前后端分离开发中,前端页面通常通过 JavaScript 调用后端接口。

fetch("/user?name=张三&age=18")
    .then(response => response.json())
    .then(data => console.log(data));

或者使用 Axios:

axios.get("/user", {
    params: {
        name: "张三",
        age: 18
    }
});

如果是提交 JSON 数据,一般是 POST 请求:

axios.post("/user", {
    name: "张三",
    age: 18
});

所以前端并不是“直接调用 Java 方法”,而是通过 HTTP 协议访问后端暴露出来的接口。后端 Controller 方法只是被 Spring Boot 映射成了一个 URL 地址,前端访问这个 URL,后端再执行对应方法。

3. HTTP 协议:前后端通信的规则

HTTP 是浏览器、前端、Postman 和后端服务器之间通信的协议。一次 HTTP 请求通常包含:请求方法、请求路径、请求参数、请求头、请求体。后端接收请求后,会返回 HTTP 响应,响应里通常包含状态码、响应头和响应体。 常见请求方法:

方法 常见用途
GET 查询数据,例如根据 ID 查询用户、查询列表
POST 新增数据,或者提交复杂请求数据
PUT 修改数据,通常表示整体更新
DELETE 删除数据

常见状态码:

状态码 含义
200 请求成功
400 请求参数错误,例如必传参数没传
404 请求地址不存在
405 请求方法不匹配,例如接口只支持 POST,你用了 GET
500 后端程序内部错误

Spring Boot 中的 @GetMapping@PostMapping@PutMapping@DeleteMapping 就是把不同 HTTP 方法的请求映射到不同 Controller 方法上。

@GetMapping("/user/{id}")
public User getUser(@PathVariable Integer id) {
    return userService.getUserById(id);
}

@PostMapping("/user")
public String addUser(@RequestBody User user) {
    userService.addUser(user);
    return "success";
}

4. Tomcat 是什么

Tomcat 是一个 Web 服务器,更准确地说,它是一个 Servlet 容器。Java Web 程序不是像普通 Java 程序那样直接从 main 方法开始处理用户请求,而是需要运行在 Web 容器中,由容器负责监听端口、接收 HTTP 请求、解析请求、创建请求对象、调用对应的 Java 处理逻辑。 以前传统 Java Web 项目通常需要把项目打成 war 包,再部署到外部 Tomcat 中运行。Spring Boot 简化了这件事,它默认内置 Tomcat,所以你运行 Spring Boot 项目时,本质上就已经启动了一个内置的 Tomcat 服务器。

传统 Java Web:
Java Web 项目 → 打成 war 包 → 部署到外部 Tomcat → 启动 Tomcat

Spring Boot:
运行 main 方法 → 自动启动内置 Tomcat → 接收 HTTP 请求

所以可以简单理解:Tomcat 负责让你的 Java Web 程序真正变成一个可以被浏览器或前端访问的服务器程序。

5. Servlet 是什么

Servlet 是 Java Web 最底层的一套请求处理规范。它规定了 Java 程序应该如何接收请求、处理请求、返回响应。Tomcat 实现了 Servlet 规范,所以 Tomcat 能够运行 Servlet 程序。 传统 Servlet 写法大概是这样:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        String name = request.getParameter("name");
        response.getWriter().write("hello " + name);
    }
}

这个代码里,HttpServletRequest 表示请求对象,里面有请求参数、请求头、请求路径等信息;HttpServletResponse 表示响应对象,用来给客户端返回数据。 Spring MVC 和 Spring Boot 并没有抛弃 Servlet,而是在 Servlet 之上做了封装。你现在写的 Controller:

@GetMapping("/hello")
public String hello(String name) {
    return "hello " + name;
}

底层仍然离不开 Servlet,只是 Spring Boot 帮你隐藏了很多重复代码,比如不用手动从 request.getParameter() 取参数,也不用手动写 response.getWriter() 返回结果。

6. Spring MVC / Spring Boot 在 Java Web 中的位置

Spring MVC 是 Spring 提供的 Web 开发框架,它对 Servlet 做了进一步封装,让我们可以用 Controller、注解、参数绑定、JSON 转换等方式更方便地开发接口。 Spring Boot 不是替代 Spring MVC,而是进一步简化 Spring 项目的创建、配置和运行。它帮你自动配置 Spring MVC、内置 Tomcat、JSON 转换器、参数绑定、异常处理等常用功能,所以开发时只需要关注 Controller、Service、Mapper 等业务代码。

HTTP 请求
   ↓
Tomcat 接收请求
   ↓
Servlet 体系处理请求
   ↓
Spring MVC 分发请求
   ↓
Controller 方法执行
   ↓
返回 JSON / 字符串 / 页面

也就是说,Spring Boot 项目里你写的接口虽然看起来只是一个普通 Java 方法,但它能被浏览器访问,是因为底层有 Tomcat、Servlet、Spring MVC 这一整套机制在工作。

7. Controller、Service、Mapper 分层

实际 Java Web 项目通常不会把所有代码都写在 Controller 里,而是分层开发。Controller 负责接收请求和返回响应,Service 负责业务逻辑,Mapper/DAO 负责访问数据库。

Controller:接收请求、校验参数、返回结果
Service:处理业务逻辑
Mapper / DAO:操作数据库
Entity / POJO:封装数据对象

例如查询用户信息:

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Integer id) {
        return userService.getUserById(id);
    }
}
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Integer id) {
        return userMapper.selectById(id);
    }
}

这种分层的好处是职责清晰:Controller 不直接写复杂业务,Service 不直接处理 HTTP 请求,Mapper 不关心业务规则,只负责数据库操作。项目小的时候可以写得简单一点,但正式项目一般都会按这种结构组织代码。

8. Postman 的作用

Postman 就一句话:它是一个专门给后端开发者用的“接口测试工具”,作用是模拟浏览器或前端程序,向你的 Java 后端发送 HTTP 请求,然后把服务器返回的结果显示给你。 比如你写了一个登录接口 /login,正常应该由前端页面把账号密码发给后端;但前端还没写时,你就可以用 Postman 手动填地址、请求方式、参数,然后点 Send 测试后端有没有正常工作。前端发送请求,本质就是:浏览器里的 JavaScript 代码调用一个函数,把 HTTP 请求发给后端地址 更实际地说,你启动 Tomcat 后,Tomcat 会监听 8080 端口;你写的 Servlet 或 Spring Boot Controller 会对应某个访问地址,比如 http://localhost:8080/login。Postman 就是访问这个地址的工具。流程是:Postman 发请求 → Tomcat 收到请求 → 找到你的 Java 代码处理 → Java 代码返回结果 → Postman 显示结果。所以 Postman 不是服务器,不是 Java,也不是 Tomcat,它只是一个“发请求、看返回”的客户端工具。

Postman 是一个接口测试工具。它可以在没有前端页面的情况下,手动发送 HTTP 请求到后端接口,测试接口是否能正常接收参数、返回结果。 比如你写了一个接口:

@GetMapping("/user")
public String getUser(String name, Integer age) {
    return name + ":" + age;
}

就可以在 Postman 中发送:

GET http://localhost:8080/user?name=张三&age=18

如果是 JSON 参数,就在 Postman 里选择 POST 请求,把参数放到 Body 的 raw JSON 中:

{
  "name": "张三",
  "age": 18
}

Postman 的核心作用不是运行后端,而是模拟前端或客户端发送请求,帮助你单独测试后端接口。后端开发时,经常是先用 Postman 把接口测通,再交给前端调用。

9. localhost、端口和接口地址

Spring Boot 项目启动后,通常会监听一个端口,默认是 8080。本机访问时常见地址是:

http://localhost:8080

其中:

部分 含义
http 使用 HTTP 协议
localhost 当前这台电脑
8080 后端服务监听的端口
/user 具体接口路径

例如:

http://localhost:8080/user?name=张三&age=18

表示访问本机 8080 端口上的 /user 接口,并传入 nameage 两个请求参数。如果前端和后端都运行在自己电脑上,前端访问的通常就是 localhost:8080;如果后端部署到服务器上,前端访问的就是服务器 IP 或域名。

10. Java Web 开发中的常见对象

对象 / 概念 作用
HttpServletRequest 表示请求对象,可以获取请求参数、请求头、请求路径等
HttpServletResponse 表示响应对象,可以手动设置响应内容
Controller 接收 HTTP 请求,调用业务逻辑,返回响应
Service 写业务逻辑,例如登录校验、订单计算、用户注册
Mapper / DAO 操作数据库
POJO / Entity 普通 Java 对象,用来封装数据
JSON 前后端最常见的数据交换格式
Tomcat 接收 HTTP 请求并运行 Java Web 程序的服务器/Servlet 容器
Servlet Java Web 底层请求处理规范
Spring MVC 对 Servlet 的封装,提供 Controller、参数绑定、请求映射等能力
Spring Boot 简化 Spring MVC 项目配置和启动,默认内置 Tomcat

Java Web 的核心就是:客户端通过 HTTP 请求访问服务器,Tomcat 接收请求,Servlet 体系处理请求,Spring MVC 把请求分发给 Controller,Controller 调用 Service 和 Mapper 完成业务与数据库操作,最后把结果返回给客户端。 Spring Boot 不是一个全新的 Web 原理,而是把 Tomcat、Servlet、Spring MVC 和常用配置整合起来,让你能更快写后端接口。

Spring MVC 笔记:核心原理、请求链路、常用注解

一、Spring MVC 是什么

Spring MVC 是 Spring 体系中负责 Web 请求处理的框架,核心作用是把浏览器或前端发来的 HTTP 请求,按照 URL、请求方式、参数等条件匹配到后端某个 Controller 方法,然后完成参数绑定、类型转换、业务调用、返回值处理和 JSON 响应。前后端分离项目中,Spring MVC 主要表现为:前端 Axios 发送请求,后端 Controller 接收请求,调用 Service 处理业务,最后返回 JSON 给前端。

Spring MVC 的本质可以记成一句话:把 HTTP 请求自动适配成 Java 方法调用,再把 Java 返回值转换成 HTTP 响应。

在项目分层中,Spring MVC 主要负责 Controller 层以及请求进入 Controller 之前和返回之后的流程。Controller 只负责接收参数、调用 Service、返回结果;Service 负责业务逻辑、事务和规则判断;Mapper/DAO 负责数据库操作。不要把复杂业务和 SQL 写在 Controller 里,否则代码会混乱、难维护。

前端 Vue / Axios
        ↓ HTTP 请求
Controller:接收请求、参数校验、返回结果
        ↓
Service:业务逻辑、事务、规则判断
        ↓
Mapper / DAO:数据库操作
        ↓
MySQL / Redis / ES

二、一次请求的完整链路

一次 Spring MVC 请求可以按下面这条线记:

前端 Axios 请求
→ Tomcat 接收 HTTP 请求
→ 封装 HttpServletRequest / HttpServletResponse
→ Filter 过滤器链
→ DispatcherServlet
→ HandlerMapping 查找 Controller 方法
→ 得到 HandlerExecutionChain
→ Interceptor.preHandle
→ HandlerAdapter 调用 Controller
→ 参数绑定 / 类型转换 / JSON 转对象 / 参数校验
→ Controller 调用 Service
→ Service 调用 Mapper / Redis / 其他组件
→ Controller 返回 Result<T>
→ HttpMessageConverter 把 Java 对象转 JSON
→ Interceptor.postHandle / afterCompletion
→ Tomcat 返回 HTTP 响应
→ 前端拿到 JSON 更新页面

前端发送请求后,请求先到 Tomcat。Tomcat 是 Servlet 容器,负责监听端口、接收网络请求、解析 HTTP 报文,并把请求封装成两个对象:HttpServletRequestHttpServletResponseHttpServletRequest 表示“这次请求”,里面有请求路径、请求方法、请求头、请求参数、Cookie、请求体等信息;HttpServletResponse 表示“这次响应”,后端最终把状态码、响应头、JSON、文件流等内容写入它,再由 Tomcat 返回给浏览器。

请求进入 Spring MVC 前,通常会先经过 Filter。Filter 属于 Servlet 规范,比 Spring MVC 的 Interceptor 更底层,常用于编码处理、跨域、安全过滤、请求包装等。Filter 执行完后,请求进入 DispatcherServlet

DispatcherServlet 是 Spring MVC 的总入口,也叫前端控制器。它不写业务逻辑,只负责调度。它收到请求后,会调用 HandlerMapping 查找哪个 Controller 方法能处理这个请求。HandlerMapping 可以理解成路由匹配器,Spring 启动时会扫描 @Controller@RestController@RequestMapping@GetMapping 等注解,把 URL、请求方法、参数条件和 Controller 方法建立映射关系。请求进来后,就根据这些条件匹配目标方法。

匹配成功后,Spring MVC 得到 HandlerExecutionChain,它里面包含目标 Controller 方法和当前请求需要经过的 Interceptor 拦截器。然后先执行拦截器的 preHandle 方法,常用于登录校验、Token 校验、权限判断、请求日志。如果某个 preHandle 返回 false,请求会被拦截,不再进入 Controller;如果返回 true,继续执行。

接着 DispatcherServlet 调用 HandlerAdapterHandlerAdapter 是执行适配器,负责真正调用 Controller 方法。调用前,Spring MVC 会自动解析参数:@RequestParam 取 URL 参数或表单参数,@PathVariable 取路径变量,@RequestBody 读取请求体 JSON 并转成 Java 对象。同时还会做类型转换,例如把字符串 "1" 转成 IntegerLong。如果参数前加了 @Valid@Validated,还会触发参数校验。

Controller 方法执行后,通常调用 Service 处理业务。Service 处理完成后,Controller 返回一个 Java 对象,比如 Result<UserVO>。如果使用 @RestController@ResponseBody,Spring MVC 会通过 HttpMessageConverter 把 Java 对象转成 JSON 响应体。最后执行拦截器的 postHandleafterCompletion,并由 Tomcat 把响应返回给前端。


三、核心组件理解

DispatcherServlet 是 Spring MVC 的核心入口,所有进入 Spring MVC 的请求都由它统一调度。它本身不处理业务,只负责协调其他组件,比如查找 Controller、执行拦截器、调用方法、处理返回值、处理异常。

HandlerMapping 是处理器映射器,负责根据请求路径、请求方式等条件找到对应的 Controller 方法。它不是每次临时搜索代码,而是在项目启动时根据注解提前建立好映射关系。

HandlerExecutionChain 是执行链,里面包含目标 Controller 方法和一组拦截器。它说明一次请求不仅是执行一个方法,还可能要经过登录校验、权限判断、日志记录等拦截逻辑。

HandlerAdapter 是处理器适配器,负责真正执行 Controller 方法。因为 Spring MVC 支持多种处理器写法,所以需要适配器统一调用方式。常见的注解式 Controller 方法一般由 RequestMappingHandlerAdapter 处理。

HttpMessageConverter 是消息转换器,负责请求体和 Java 对象之间的转换。前端传 JSON 给 @RequestBody 时,它把 JSON 转 Java DTO;Controller 返回对象时,它把 Java 对象转 JSON。Spring Boot 默认常用 Jackson 完成 JSON 序列化和反序列化。

HandlerInterceptor 是 Spring MVC 的拦截器,执行在 Controller 方法前后,常用于登录校验、Token 校验、权限判断、接口日志和耗时统计。它有三个常用方法:preHandle 在 Controller 前执行,返回 true 放行、false 拦截;postHandle 在 Controller 执行后触发;afterCompletion 在整个请求结束后触发,常用于清理资源、记录异常日志。

Filter 是 Servlet 过滤器,执行位置比 Interceptor 更早,在进入 DispatcherServlet 之前执行。Filter 更底层,Interceptor 更贴近 Spring MVC 和 Controller 业务。简单记忆:Filter 管得更早、更底层;Interceptor 管得更靠近接口业务。


四、Controller 相关注解

@Controller 表示普通控制器,常用于传统页面项目。方法返回字符串时,默认会被当成页面名,例如返回 "index" 表示跳转到 index 页面。

@RestController 等价于 @Controller + @ResponseBody,表示这个类中方法的返回值直接写入 HTTP 响应体,通常自动转成 JSON。前后端分离项目基本都用 @RestController

@RestController
@RequestMapping("/users")
public class UserController {
}

@RequestMapping 用来建立请求路径和 Controller 方法之间的映射,可以写在类上,也可以写在方法上。写在类上表示统一路径前缀,写在方法上表示具体接口路径。

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public Result<UserVO> getById(@PathVariable Long id) {
        return Result.success(userService.getById(id));
    }
}

上面接口的完整路径是:

GET /users/{id}

常用请求方法注解如下:

注解 HTTP 方法 常见用途
@GetMapping GET 查询数据,例如列表、详情
@PostMapping POST 新增、登录、注册、提交 JSON
@PutMapping PUT 修改资源,通常表示整体更新
@PatchMapping PATCH 局部修改资源
@DeleteMapping DELETE 删除资源

RESTful 风格里,通常用 URL 表示资源,用 HTTP 方法表示动作。例如 GET /users/1 表示查询用户,POST /users 表示新增用户,PUT /users/1 表示修改用户,DELETE /users/1 表示删除用户。但实际项目不一定完全严格,只要前后端约定一致即可。


五、请求参数接收

Spring MVC 的重要能力是自动把 HTTP 请求参数绑定到 Java 方法参数上。不同位置的数据,用不同注解接收。

1. @RequestParam

@RequestParam 用来接收 URL 查询参数或表单参数。查询参数就是 URL 中 ? 后面的键值对:

GET /users?page=1&size=10&keyword=tom

后端接收:

@GetMapping("/users")
public Result<List<UserVO>> list(@RequestParam(defaultValue = "1") Integer page,
                                 @RequestParam(defaultValue = "10") Integer size,
                                 @RequestParam(required = false) String keyword) {
    return Result.success(userService.list(page, size, keyword));
}

pagesizekeyword 都来自 URL 参数。defaultValue 表示默认值,required = false 表示该参数不是必传。它适合接收分页页码、搜索关键词、状态值等简单参数。注意:@RequestParam 不负责接收 JSON 请求体,前端传 JSON 时应该使用 @RequestBody

2. @PathVariable

@PathVariable 用来接收路径变量,也就是 URL 路径本身的一部分:

GET /users/1001

后端写法:

@GetMapping("/users/{id}")
public Result<UserVO> getById(@PathVariable Long id) {
    return Result.success(userService.getById(id));
}

{id} 会绑定到方法参数 id。它常用于根据 id 查询、修改、删除某个资源。

3. @RequestBody

@RequestBody 用来接收 HTTP 请求体中的 JSON,并把 JSON 转成 Java 对象。前端发送:

{
  "username": "admin",
  "password": "123456"
}

后端接收:

@PostMapping("/login")
public Result<LoginVO> login(@RequestBody LoginDTO dto) {
    return Result.success(userService.login(dto));
}

底层流程是:

HTTP 请求体 JSON
→ HttpMessageConverter
→ Jackson ObjectMapper
→ Java DTO 对象

它适合接收复杂对象,例如登录参数、注册参数、新增对象、修改对象、批量提交数据。一个接口方法里通常只写一个 @RequestBody,因为请求体一般只能读取一次。

4. @RequestHeader@CookieValue

@RequestHeader 用来接收请求头,常见场景是获取 token:

@GetMapping("/profile")
public Result<UserVO> profile(@RequestHeader("Authorization") String token) {
    return Result.success(userService.profile(token));
}

@CookieValue 用来接收 Cookie,例如:

@GetMapping("/test")
public Result test(@CookieValue("JSESSIONID") String sessionId) {
    return Result.success(sessionId);
}

Cookie 是浏览器保存并自动随请求携带的小数据,常用于会话、登录态、追踪信息。

参数接收方式对比:

写法 数据来源 示例 适合场景
@RequestParam URL 参数 / 表单参数 /users?page=1 分页、搜索、简单参数
@PathVariable URL 路径 /users/1 id 查询、id 删除
@RequestBody 请求体 JSON { "name": "tom" } 登录、注册、新增、修改
@RequestHeader 请求头 Authorization: xxx token、客户端信息
@CookieValue Cookie JSESSIONID 会话、登录态
HttpServletRequest 原始请求对象 request.getHeader() 需要底层请求信息

六、返回值与 JSON 转换

@RestController 中,方法返回对象时,Spring MVC 会通过 HttpMessageConverter 自动转成 JSON。

@GetMapping("/users/{id}")
public Result<UserVO> getById(@PathVariable Long id) {
    UserVO user = userService.getById(id);
    return Result.success(user);
}

推荐项目里统一返回格式,例如:

public class Result<T> {
    private Integer code;
    private String message;
    private T data;
}

前端收到的大致是:

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "username": "tom"
  }
}

统一返回格式的好处是前端可以固定判断 code,固定读取 data,不用每个接口单独适配。需要注意,数据库 Entity 不建议直接返回给前端,应该转成 VO,避免把密码、内部状态等敏感字段暴露出去。


七、参数校验与异常处理

DTO 字段上可以写校验注解,但只有 Controller 参数前加了 @Valid@Validated,校验才会触发。

public class LoginDTO {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;
}
@PostMapping("/login")
public Result<LoginVO> login(@Valid @RequestBody LoginDTO dto) {
    return Result.success(userService.login(dto));
}

常见校验注解:@NotNull 表示不能为 null;@NotBlank 表示字符串不能为空且不能全是空格;@NotEmpty 表示字符串或集合不能为空;@Email 校验邮箱格式;@Min@Max 校验数值范围;@Pattern 使用正则校验。

实际项目不要在每个 Controller 里写大量 try-catch,而是用 @RestControllerAdvice + @ExceptionHandler 做全局异常处理。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public Result<Void> handleRuntimeException(RuntimeException e) {
        return Result.fail(e.getMessage());
    }
}

这样 Service 中可以直接抛异常:

if (user == null) {
    throw new RuntimeException("用户不存在");
}

异常最终由全局异常处理器统一转成 JSON,保证前端收到的错误格式一致。


八、拦截器 Interceptor

Interceptor 是 Spring MVC 提供的拦截机制,可以在请求进入 Controller 前后执行额外逻辑。常见用途是登录校验、Token 校验、权限判断、请求日志、接口耗时统计。

执行顺序:

请求
→ Interceptor.preHandle
→ Controller
→ Interceptor.postHandle
→ Interceptor.afterCompletion
→ 响应

preHandle 最常用,返回 true 表示放行,返回 false 表示拦截。示例:

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        String token = request.getHeader("Authorization");

        if (token == null || token.isEmpty()) {
            throw new RuntimeException("未登录");
        }

        return true;
    }
}

注册拦截器:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/users/login", "/users/register");
    }
}

Filter 和 Interceptor 的区别:Filter 属于 Servlet 规范,执行在 DispatcherServlet 之前,更底层,常用于编码、跨域、安全过滤;Interceptor 属于 Spring MVC,执行在 Controller 前后,更适合做登录校验、权限校验、业务日志。


九、Spring Boot 和 Spring MVC 的关系

Spring MVC 是 Web 框架,Spring Boot 是自动配置和快速启动工具。Spring Boot 不替代 Spring MVC,而是帮你自动配置 Spring MVC、Tomcat、Jackson、参数绑定、静态资源、异常处理等。

引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

就会自动拥有 Spring MVC Web 开发能力。所以平时写的 @RestController@GetMapping@PostMapping,本质仍然是 Spring MVC,只是 Spring Boot 帮你省掉了大量配置。


十、最终记忆

Spring MVC 的核心是请求链路:Tomcat 接收 HTTP 请求并封装成 HttpServletRequestHttpServletResponse,请求经过 Filter 后进入 DispatcherServletDispatcherServlet 通过 HandlerMapping 找到 Controller 方法,通过 HandlerAdapter 调用方法,并在调用前完成参数绑定、类型转换、JSON 转对象和参数校验;Controller 调用 Service 处理业务,返回 Java 对象后,HttpMessageConverter 把对象转成 JSON,最后由 Tomcat 返回给前端。常用注解都围绕这条链路展开:@RestController 表示返回 JSON,@RequestMapping 建立路径映射,@RequestParam 取 URL 参数,@PathVariable 取路径变量,@RequestBody 取 JSON 请求体,@Valid 触发校验,@RestControllerAdvice 统一处理异常,Interceptor 做接口级拦截。

Java 注解与 Spring Boot 常见注解

注解(Annotation)是 Java 中一种特殊的标记,写法通常是 @注解名。它本身不是普通代码逻辑,不会像方法一样直接执行,而是用来给类、方法、属性、参数等添加额外说明。框架可以读取这些注解,然后根据注解完成特定功能。Spring Boot 中大量使用注解,就是为了减少 XML 配置,让开发者用简单标记告诉框架:这个类是什么角色、这个方法处理哪个请求、这个参数从哪里来、这个对象要不要交给 Spring 管理。 简单理解:注解就是给代码贴标签,Spring Boot 根据这些标签自动完成配置、扫描、创建对象、注入依赖、处理请求等工作。

1. 注解可以写在哪里

注解可以写在类、方法、属性、方法参数上,不同位置表示不同含义。

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUser(@PathVariable Integer id) {
        return userService.getUserById(id);
    }
}

这里:

注解 写的位置 作用
@RestController 类上 表示这是一个接口控制器
@RequestMapping("/users") 类上 给这个 Controller 统一加请求路径前缀
@Autowired 属性上 让 Spring 自动注入需要的对象
@GetMapping("/{id}") 方法上 表示这个方法处理 GET 请求
@PathVariable 方法参数上 从 URL 路径中取参数

所以注解不是随便写的,写在哪里很重要:写在类上通常表示这个类的角色,写在方法上通常表示这个方法的功能,写在参数上通常表示这个参数从请求的哪个位置获取。

2. Controller 层常见注解

Controller 层主要负责接收 HTTP 请求和返回响应,所以这里最常见的是请求映射和参数接收相关注解。

注解 作用
@RestController 标记当前类是接口控制器,方法返回值一般直接作为响应数据返回,常用于前后端分离
@Controller 标记当前类是控制器,传统项目中常用于返回页面
@RequestMapping 设置请求路径,可以写在类上或方法上
@GetMapping 处理 GET 请求,常用于查询
@PostMapping 处理 POST 请求,常用于新增或提交 JSON
@PutMapping 处理 PUT 请求,常用于修改
@DeleteMapping 处理 DELETE 请求,常用于删除
@RequestParam 接收 URL 查询参数或表单参数
@PathVariable 接收 URL 路径参数
@RequestBody 接收请求体中的 JSON 数据
@DateTimeFormat 指定日期参数的转换格式

示例:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Integer id) {
        return userService.getUserById(id);
    }

    @GetMapping
    public List<User> listUsers(@RequestParam(required = false) String name) {
        return userService.listUsers(name);
    }

    @PostMapping
    public String addUser(@RequestBody User user) {
        userService.addUser(user);
        return "success";
    }
}

这段代码里,@RequestMapping("/users") 给整个类统一加了 /users 前缀;@GetMapping("/{id}") 对应 /users/1 这种路径;@RequestParam 接收 /users?name=张三 这种查询参数;@RequestBody 接收 JSON 请求体。

3. 三层架构中的常见注解

Spring Boot 项目中,Controller、Service、Mapper/DAO 通常会分别使用不同注解标记,让 Spring 知道这些类分别属于哪一层,并把它们创建成 Bean 对象交给 IOC 容器管理。

注解 常用位置 作用
@RestController Controller 层 标记接口控制器
@Service Service 层 标记业务逻辑类
@Repository DAO 层 标记数据访问类
@Mapper MyBatis Mapper 接口 标记 MyBatis 数据库操作接口
@Component 普通组件类 通用组件注解
@Autowired 属性、构造方法、Setter 方法 自动注入依赖对象

示例:

@RestController
public class UserController {

    @Autowired
    private UserService userService;
}
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
}
@Mapper
public interface UserMapper {

    User selectById(Integer id);
}

这里 UserControllerUserServiceUserMapper 都会被框架识别。Controller 需要 Service,Service 需要 Mapper,这些依赖关系可以通过 @Autowired 自动注入,不需要手动 new

4. Bean 相关注解

被 Spring 容器创建和管理的对象叫 Bean。Spring Boot 会扫描特定包下的类,如果发现类上有某些注解,就会把它们创建成 Bean。

常见的 Bean 注解:

@Component
@Service
@RestController
@Controller
@Repository

它们都可以让类成为 Spring 管理的对象,只是语义不同:

注解 语义
@Component 通用组件,不明确属于哪一层时使用
@Service 业务逻辑组件
@Controller 控制层组件
@RestController REST 接口控制层组件
@Repository 数据访问层组件

小提示:@Service@Controller@Repository 本质上都可以理解为更具体语义的 @Component。它们的核心作用都是让 Spring 能扫描到这个类,并创建对象放进 IOC 容器。

5. 参数绑定相关注解

Spring Boot 接收请求参数时,注解主要用来说明“这个参数从 HTTP 请求的哪里来”。

注解 参数来源 示例
@RequestParam URL 查询参数、表单参数 /users?name=张三
@PathVariable URL 路径参数 /users/1
@RequestBody 请求体 JSON { "name": "张三" }
@DateTimeFormat 日期字符串格式转换 2026-06-07

示例:

@GetMapping("/users")
public List<User> list(@RequestParam String name) {
    return userService.listByName(name);
}
@GetMapping("/users/{id}")
public User get(@PathVariable Integer id) {
    return userService.getUserById(id);
}
@PostMapping("/users")
public String add(@RequestBody User user) {
    userService.addUser(user);
    return "success";
}

一句话记忆:?key=value@RequestParam/路径/{变量}@PathVariable,JSON 请求体用 @RequestBody

6. 注解和配置的关系

早期 Java Web 项目经常需要写大量 XML 配置,告诉框架哪些类是 Controller、哪些类是 Service、请求路径怎么映射、对象之间怎么依赖。Spring Boot 更推荐使用注解完成这些配置。

例如以前可能需要在配置文件中声明对象,现在只需要:

@Service
public class UserService {
}

以前可能需要配置 URL 和处理类的对应关系,现在只需要:

@GetMapping("/users/{id}")
public User getUser(@PathVariable Integer id) {
    return userService.getUserById(id);
}

所以 Spring Boot 注解的作用可以理解为:把很多原本写在配置文件里的内容,直接标记在代码上,让框架自动识别和处理。

7. 小总结

注解是 Java 提供的一种元数据标记,Spring Boot 通过读取注解来自动完成对象创建、依赖注入、请求映射、参数绑定、JSON 转换等工作。初学阶段不用深入研究注解底层原理,先记住几个最常用的场景:类上注解表示这个类的角色,例如 @RestController@Service@Mapper;方法上注解表示这个方法处理什么请求,例如 @GetMapping@PostMapping;参数上注解表示这个参数从哪里来,例如 @RequestParam@PathVariable@RequestBody;属性上注解通常用于自动注入对象,例如 @Autowired

HTTP 协议核心知识

HTTP 是前端、浏览器、Postman 和后端服务器之间通信的协议。Java Web 开发中,前端不是直接调用 Java 方法,而是通过 HTTP 请求访问后端暴露出来的接口;后端通过 Controller 接收请求,处理业务后再返回 HTTP 响应。Spring Boot 中的 @GetMapping@PostMapping@RequestParam@RequestBody@PathVariable 等,本质上都是围绕 HTTP 请求展开的。 一次 HTTP 交互可以理解为:客户端发送请求,服务器处理请求并返回响应。

客户端:浏览器 / 前端页面 / Postman
        ↓ 发送 HTTP Request
服务器:Spring Boot / Tomcat
        ↓ 处理请求
客户端:接收 HTTP Response

GET = 查,参数放 URL,用 @RequestParam@PathVariable
POST = 增/改,参数放 Body,用 @RequestBody 查数据用 GET,建数据/改数据/删数据用 POST(虽然删除更常用 DELETE) 请求行标明请求方式、访问地址与协议版本,请求头传递客户端信息与附加参数,请求体存放提交数据(多见于 POST 请求);响应对应分为响应行、响应头、响应体:响应行通过状态码告知请求处理结果,响应头描述返回数据格式、缓存、Cookie 等信息,响应体是服务器返回的实际内容。

HTTP 请求

HTTP 请求通常由四部分组成:请求方法、请求路径、请求头、请求体。

GET /user?id=1 HTTP/1.1
Host: localhost:8080
User-Agent: Chrome
Accept: application/json

如果是 POST 提交 JSON,请求大致是:

POST /user HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
  "name": "张三",
  "age": 18
}

其中,请求方法表示这次请求想做什么;请求路径表示访问哪个接口;请求头保存额外信息,比如数据格式、浏览器信息、登录凭证等;请求体主要用于放比较复杂的数据,比如 JSON。

HTTP 请求方法

HTTP 请求方法表示客户端对服务器资源的操作意图。Spring Boot 中的 @GetMapping@PostMapping 等注解,就是根据 HTTP 请求方法匹配不同接口。

方法 常见用途 Spring Boot 注解
GET 查询数据 @GetMapping
POST 新增数据、提交 JSON @PostMapping
PUT 修改数据,通常表示整体更新 @PutMapping
DELETE 删除数据 @DeleteMapping

例如:

@GetMapping("/users/{id}")
public User getUser(@PathVariable Integer id) {
    return userService.getUserById(id);
}

@PostMapping("/users")
public String addUser(@RequestBody User user) {
    userService.addUser(user);
    return "success";
}

小提示:请求方法不是强制规定业务逻辑的语法规则,而是一种开发约定。比如查询一般用 GET,新增一般用 POST,删除一般用 DELETE。实际项目中遵守这些约定,接口会更清晰。

请求参数的三种常见位置

HTTP 请求中的参数主要有三种常见位置:URL 查询参数、路径参数、请求体参数。不同位置对应 Spring Boot 中不同的接收方式。

参数位置 示例 Spring Boot 接收方式
URL 查询参数 /users?name=张三&age=18 @RequestParam 或普通形参
路径参数 /users/1001 @PathVariable
请求体 JSON { "name": "张三", "age": 18 } @RequestBody

查询参数一般跟在 URL 的 ? 后面,多个参数之间用 & 连接:

GET /users?name=张三&age=18

路径参数是 URL 路径本身的一部分:

GET /users/1001

JSON 请求体一般用于 POST、PUT 等请求:

{
  "name": "张三",
  "age": 18
}

一句话记忆:?key=value@RequestParam/路径/{变量}@PathVariable,JSON 请求体用 @RequestBody

HTTP 请求头

请求头用于携带请求的附加信息。常见请求头包括 Content-TypeAcceptAuthorization 等。

请求头 作用
Content-Type 告诉服务器请求体的数据格式
Accept 告诉服务器客户端希望接收什么格式的数据
Authorization 携带登录认证信息,例如 Token
User-Agent 客户端信息,例如浏览器类型

最常见的是 Content-Type。如果前端提交 JSON,一般需要设置:

Content-Type: application/json

这表示请求体中的数据是 JSON 格式,后端通常用 @RequestBody 接收。如果是普通表单提交,可能是:

Content-Type: application/x-www-form-urlencoded

小提示:@RequestBody 主要看请求体内容,而请求体是什么格式,通常由 Content-Type 告诉后端。

HTTP 响应

服务器处理完请求后,会返回 HTTP 响应。响应一般包括状态码、响应头和响应体。

HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "name": "张三"
  }
}

状态码表示请求处理结果;响应头表示返回数据的附加信息;响应体是真正返回给前端的数据,常见格式是 JSON。

HTTP 状态码

状态码是服务器告诉客户端“这次请求处理结果”的数字。开发中常见状态码如下:

状态码 含义 常见原因
200 请求成功 接口正常返回
400 请求参数错误 必传参数没传、参数格式不对
401 未登录或认证失败 Token 缺失或无效
403 没有权限 已登录但权限不足
404 地址不存在 URL 写错、接口路径不存在
405 请求方法不支持 该接口只支持 POST,却用了 GET
500 服务器内部错误 后端代码异常、数据库异常

例如,@RequestParam 默认 required = true,如果前端没有传这个参数,就可能出现 400;如果 Controller 里只有 @PostMapping("/user"),但你用 GET 访问,就可能出现 405;如果访问了不存在的路径,就会出现 404;如果后端代码空指针异常或数据库报错,就可能出现 500。

GET 和 POST 的区别

GET 和 POST 是最常见的两个请求方法。GET 一般用于查询数据,参数通常放在 URL 后面;POST 一般用于新增数据或提交复杂数据,参数可以放在请求体里,尤其是 JSON。

对比 GET POST
常见用途 查询数据 新增数据、提交复杂数据
参数位置 通常在 URL 后面 通常在请求体中
是否适合 JSON 不常用 常用
Spring Boot 接收 普通形参、@RequestParam@PathVariable @RequestBody、普通表单参数

GET 示例:

GET /users?name=张三&age=18

POST JSON 示例:

POST /users
Content-Type: application/json

{
  "name": "张三",
  "age": 18
}

注意:GET 也可以有请求体,POST 也可以传 URL 参数,但实际开发中一般不这样混着用。初学阶段按常见约定记就够了:查询用 GET,提交 JSON 用 POST。

JSON 是前后端常见数据格式

JSON 是前后端分离开发中最常见的数据交换格式。它本质上是一种字符串格式,但结构很像 JavaScript 对象,也容易映射成 Java 对象。

{
  "id": 1,
  "name": "张三",
  "age": 18
}

Spring Boot 中,如果 Controller 返回一个 Java 对象,通常会自动转换成 JSON 返回给前端;如果前端传来 JSON 请求体,后端可以用 @RequestBody 自动转换成 Java 对象。

@PostMapping("/users")
public User addUser(@RequestBody User user) {
    return user;
}

这里前端传来的 JSON 会被转换成 User 对象,方法返回的 User 对象又会被转换成 JSON 返回给前端。

小总结

HTTP 协议是 Java Web 的通信基础。前端通过 HTTP 请求访问后端接口,后端通过 Controller 接收请求并返回响应。学习 Spring Boot 接收参数时,最重要的是先判断参数在 HTTP 请求的哪个位置:URL 查询参数用普通形参或 @RequestParam,路径中的变量用 @PathVariable,JSON 请求体用 @RequestBody。开发中常见问题也基本和 HTTP 有关:404 是路径不对,405 是请求方法不对,400 是参数问题,500 是后端代码或服务器内部错误。


这章建议保留,但不用再扩展到太深,比如 TCP/IP、HTTPS 握手、HTTP/2 这些现在先不用放。你现在做 Java Web 入门笔记,重点掌握:**请求方法、参数位置、请求头、请求体、响应、状态码、JSON** 就够了。

Spring Boot 接收请求参数

在 Spring Boot 中,Controller 方法里的形参就是用来接收客户端请求数据的变量。不同请求数据来源不同,Spring Boot 主要通过“形参类型 + 参数名 + 注解”完成自动绑定:URL 查询参数、表单参数通常直接绑定到方法形参或实体对象;JSON 数据必须放在请求体中,并使用 @RequestBody 接收;URL 路径中的变量使用 @PathVariable 接收;日期类型需要指定格式,常用 LocalDateLocalDateTime

1. 原始方式:HttpServletRequest 手动获取

最底层的方式是在 Controller 方法参数中声明 HttpServletRequest,然后通过 getParameter("参数名") 手动获取请求参数。

@GetMapping("/user")
public String getUser(HttpServletRequest request) {
    String name = request.getParameter("name");
    String age = request.getParameter("age");
    return name + ":" + age;
}


这种方式能帮助理解底层原理,但实际开发中比较繁琐:所有参数都要手动取,类型也要自己转换,比如 `age` 获取到的是字符串,还要手动转成 `Integer`。Spring Boot 项目中一般不常用,更多是了解机制时使用。

2. 简单参数:直接用方法形参接收

简单参数是最常用的接收方式。只要请求参数名和 Controller 方法形参名一致,Spring Boot 就会自动把请求参数赋值给形参,并完成基本类型转换。

@GetMapping("/user")
public String getUser(String name, Integer age) {
    return name + ":" + age;
}

请求示例:
```text
GET /user?name=张三&age=18

这里 name=张三 会自动赋值给 String nameage=18 会自动转换成 Integer age。这种方式适合参数较少的场景,例如查询条件、分页参数、简单筛选条件等。 如果请求参数名和方法形参名不一致,需要使用 @RequestParam 指定映射关系。

@GetMapping("/user")
public String getUser(@RequestParam("name") String username) {
    return username;
}

请求中传的是 name,方法里接收的是 username,所以需要 @RequestParam("name") 建立对应关系。@RequestParamrequired 默认是 true,表示这个参数必须传;如果不传,Spring Boot 会直接返回 400 错误。可选参数可以写成:

@GetMapping("/user")
public String getUser(@RequestParam(value = "name", required = false) String username) {
    return username;
}

小提示:简单参数接收的是 URL 后面的查询参数,或者普通表单参数,不是 JSON 请求体。

3. 实体参数:多个参数封装成对象

当请求参数比较多时,不适合在方法里写一堆形参,可以定义一个 POJO 实体类,让 Spring Boot 自动把请求参数封装进对象。请求参数名需要和对象属性名一致。

public class User {
    private String name;
    private Integer age;

    // getter / setter
}
@GetMapping("/user")
public String getUser(User user) {
    return user.getName() + ":" + user.getAge();
}

请求示例:

GET /user?name=张三&age=18

Spring Boot 会自动创建 User 对象,并把 nameage 分别赋值给对象中的对应属性。实体参数适合参数较多、结构比较固定的场景,比如用户信息、查询条件对象、分页筛选对象等。 注意:这种方式接收的是普通请求参数,不需要加 @RequestBody。只有当前端传的是 JSON 请求体时,才需要 @RequestBody

4. 数组和集合参数:接收多个同名参数

如果请求中有多个同名参数,可以用数组或集合接收。例如:

GET /user?hobby=唱歌&hobby=跳舞&hobby=篮球

使用数组接收:

@GetMapping("/user")
public String getUser(String[] hobby) {
    return Arrays.toString(hobby);
}

使用集合接收时,一般需要加 @RequestParam

@GetMapping("/user")
public String getUser(@RequestParam List<String> hobby) {
    return hobby.toString();
}

数组和集合适合接收多选框、多个标签、多个 ID 等数据。它们本质上仍然属于请求参数自动绑定,不是 JSON 请求体,因此 GET URL 参数或普通表单参数都可以这样接收。

5. 日期参数:使用 @DateTimeFormat 指定格式

如果请求参数是日期,不能只写 LocalDateLocalDateTime,通常还需要使用 @DateTimeFormat 指定前端传来的日期格式。

@GetMapping("/date")
public String getDate(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
    return date.toString();
}

请求示例:

GET /date?date=2026-06-07

如果是日期时间,可以使用:

@GetMapping("/time")
public String getTime(
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time
) {
    return time.toString();
}

请求示例:

GET /time?time=2026-06-07 18:30:00

小提示:日期参数推荐使用 Java 8 的 LocalDateLocalDateTime,不要优先使用老的 Date 类型。

6. JSON 参数:使用 @RequestBody 接收请求体

如果前端传的是 JSON 格式数据,数据不是放在 URL 后面,而是放在请求体里。这时 Controller 不能靠普通形参自动接收,而是要用 @RequestBody 把 JSON 映射成 Java 对象。

@PostMapping("/user")
public String addUser(@RequestBody User user) {
    return user.getName() + ":" + user.getAge();
}

请求体示例:

{
  "name": "张三",
  "age": 18
}

这里 Spring Boot 会把 JSON 中的 nameage 自动映射到 User 对象的属性上。JSON 参数常用于 POST、PUT 请求,比如新增用户、修改用户、提交复杂表单等。 需要记住:JSON 格式要放在请求体里,并用 @RequestBody 接收;URL 后面的 ?name=张三&age=18 不是 JSON。 如果 JSON 对象里包含日期字段,也需要注意格式转换。普通 URL 参数中的日期常用 @DateTimeFormat,而 JSON 里的日期格式通常还会涉及 Jackson 的 JSON 反序列化规则,实际开发中常配合统一日期格式配置,或者在字段上使用对应的 JSON 日期格式注解。

7. 路径参数:使用 @PathVariable 接收 URL 占位符

路径参数是直接写在 URL 路径中的参数,不是 ?key=value 形式。比如 /user/1001 中的 1001 就是路径参数。

@GetMapping("/user/{id}")
public String getUser(@PathVariable Integer id) {
    return "id = " + id;
}

请求示例:

GET /user/1001

如果路径变量名和形参名不一致,也可以显式指定:

@GetMapping("/user/{id}")
public String getUser(@PathVariable("id") Integer userId) {
    return "id = " + userId;
}

路径参数常用于根据 ID 查询、删除、修改某个资源,例如 /user/1/article/10/order/2026。如果路径参数中包含日期,也可以配合 @DateTimeFormat 使用:

@GetMapping("/order/{date}")
public String getOrder(
    @PathVariable
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    LocalDate date
) {
    return date.toString();
}

8. 总结:看数据放在哪里,再决定怎么接收

Spring Boot 接收参数的核心不是死记注解,而是先判断数据来源。URL 后面的 ?name=张三&age=18 属于普通请求参数,可以用简单形参、@RequestParam、实体对象、数组或集合接收;路径里的 /user/{id}@PathVariable 接收;请求体里的 JSON 数据必须用 @RequestBody 接收。日期类型本身不是字符串,所以要指定格式,常用 @DateTimeFormat 配合 LocalDateLocalDateTime。 常见对应关系可以这样记:

数据位置 示例 接收方式
URL 查询参数 /user?name=张三&age=18 String name, Integer age
参数名不一致 /user?name=张三,形参叫 username @RequestParam("name") String username
多个普通参数 /user?name=张三&age=18 User user
多个同名参数 /user?hobby=唱&hobby=跳 String[] hobby@RequestParam List<String> hobby
JSON 请求体 { "name": "张三", "age": 18 } @RequestBody User user
URL 路径参数 /user/1001 @PathVariable Integer id
日期参数 /date?date=2026-06-07 @DateTimeFormat + LocalDate

一句话记忆:普通参数看参数名,实体对象看属性名,JSON 看请求体和 @RequestBody,路径变量看 @PathVariable,日期类型看格式注解。

==@RequestMapping("/getAddr") 中的 /getAddr 是接口路径,不是网页文件,用来告诉 Spring MVC“什么样的 URL 会触发这个控制器里的方法”==

三层架构

Java Web 项目一般会按照 Controller → Service → DAO/Mapper 的三层结构组织代码。浏览器、前端页面或 Postman 发送 HTTP 请求后,请求先进入 Controller,Controller 负责接收请求参数、调用业务逻辑、返回响应数据;然后 Controller 调用 Service,Service 负责具体业务处理,例如登录校验、用户注册、订单计算等;最后 Service 调用 DAO/Mapper,由 DAO/Mapper 负责和数据库交互,完成数据的增、删、改、查。 三层架构的核心目的不是让代码变复杂,而是让每一层职责清楚:Controller 不直接写复杂业务,Service 不直接处理 HTTP 请求,DAO/Mapper 不关心业务规则,只负责数据访问。这样项目变大后,代码更容易维护、修改和测试。

前端 / 浏览器 / Postman
        ↓
Controller:接收请求、响应数据
        ↓
Service:处理业务逻辑
        ↓
DAO / Mapper:访问数据库
        ↓
数据库

Controller 层

Controller 是控制层,主要负责和前端交互。它接收前端传来的请求参数,例如 nameageid、JSON 数据等,然后调用 Service 层处理业务,最后把结果返回给前端。Controller 一般只做请求接收、参数校验、调用业务、返回结果,不建议在 Controller 中直接写大量业务逻辑或数据库操作。 常见注解:

@RestController
@RequestMapping("/users")
@GetMapping
@PostMapping
@PathVariable
@RequestParam
@RequestBody

示例:

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUser(@PathVariable Integer id) {
        return userService.getUserById(id);
    }
}

Service 层

Service 是业务逻辑层,负责处理真正的业务规则。比如登录时判断账号密码是否正确,注册时判断用户名是否重复,删除用户前判断用户是否存在,这些都应该写在 Service 层。Service 层通常会调用 DAO/Mapper 层获取或修改数据库数据。

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Integer id) {
        return userMapper.selectById(id);
    }
}

Service 层是项目中最容易写出核心逻辑的地方。以后项目复杂后,不要把业务逻辑全部堆在 Controller 里,而是让 Controller 只负责“接请求和回响应”,Service 负责“做事情”。

DAO / Mapper 层

DAO 是 Data Access Object,意思是数据访问对象;在 MyBatis 中更常见的名字是 Mapper。这一层主要负责操作数据库,例如查询用户、添加用户、修改用户、删除用户。

@Mapper
public interface UserMapper {

    User selectById(Integer id);

}

DAO/Mapper 层只负责数据访问,不建议在这里写复杂业务逻辑。比如“根据 ID 查用户”可以写在 Mapper 里,但“用户不存在时如何处理、是否允许删除、删除后还要不要清理其他数据”这些业务规则应该放在 Service 层。

分层解耦、IOC 和 DI

三层架构强调的是代码按职责划分层级(Controller 接请求、Service 写业务、Mapper 操作数据库),而 Spring 的 IOC 和 DI 主要解决的是对象创建和对象依赖装配的问题。以前如果一个类需要使用另一个类,可能会自己 new 对象:

private UserService userService = new UserService();

这种写法的问题是类与类之间强耦合:Controller 自己创建 Service,Service 自己创建 DAO,如果将来要替换实现类、做单元测试、扩展功能,就要改动大量源码,维护成本很高。

Spring 的核心思路是:把所有对象的创建、管理、销毁权,统一交给 Spring IoC 容器。被 Spring 管理的对象叫做 Bean,例如加了 @Controller@RestController@Service@Repository@Component 等注解的类,都会被 Spring 自动扫描、创建并放入容器中统一管理。

💡 一句话理解: 三层架构解决「代码按职责放在哪一层」的问题,IOC/DI 解决「对象由谁创建、依赖怎么装配」的问题。 两者配合实现了代码的分层解耦:层与层之间只依赖接口,不依赖具体实现,替换实现类不用改业务代码。

IOC 与 DI入门

三层架构解决的是 “代码按职责分几层” 的问题,而 IOC 和 DI 解决的是 “这些层的对象由谁创建、对象之间怎么关联” 的问题。比如 Controller 需要调用 Service,Service 需要调用 DAO,如果每一层都自己 new 下一层对象,代码之间会强耦合,不方便后期替换实现类、测试和维护。

Spring 的做法是:把这些类交给 IOC 容器管理,由容器负责创建对象;当某个类需要另一个对象时,再通过 DI 自动注入进来。

@Component 是通用组件注解,写在类上,表示这个类交给 Spring 管理,Spring 启动时会创建它的对象并放入 IOC 容器中,被容器管理的对象称为 Bean。教程里 EmpDaoAEmpServiceA 加了 @Component,意思就是让 Spring 帮我们创建 DAO 和 Service 对象,而不是手动 new

@Component
public class EmpServiceA implements EmpService {
}

@Autowired 表示自动注入依赖对象。比如 EmpController 需要 EmpServiceEmpServiceA 需要 EmpDao,Spring 会从 IOC 容器中找到对应的 Bean,然后自动赋值给这个成员变量。

@Autowired
private EmpService empService;

所以核心逻辑可以理解成:

@Component:把类交给 Spring 管理,实例化后注册为 Bean(放进容器)
@Autowired:从 Spring 容器中取出需要的 Bean,注入到当前类的成员变量中

Controller 依赖 Service
Service 依赖 DAO
所有依赖关系由 Spring 自动装配,不用手动 new 和组装

💡 IOC 与 DI 的关系: IOC(控制反转)是设计思想,DI(依赖注入)是 IOC 的具体实现方式。 IOC 管 “对象创建权交给容器”,DI 管 “容器把依赖对象自动送进来”,两者描述的是同一件事的两个角度。

教程里先用 @Component 讲通用原理,后面实际项目中通常会使用语义更明确的注解:Controller 层用 @RestController,Service 层用 @Service,DAO 层可用 @Repository,MyBatis 接口常用 @Mapper。它们本质上都可以把类交给 Spring 管理,只是表达的分层含义更清楚。

IOC:控制反转

IOC 全称是 Inversion of Control,中文叫控制反转。它的核心是:对象的创建、生命周期管理的控制权,不再由程序员在代码中手动 new 控制,而是反转交给 Spring IoC 容器统一管理。 以前(控制权在开发者):

// 自己创建对象,自己管理生命周期
UserService userService = new UserService();

现在(控制权在 Spring 容器):

@Autowired
private UserService userService;

表面上只是少写了 new,本质上是对象创建方式发生了根本变化:原来是类自己创建依赖对象,现在是 Spring 容器提前创建好单例对象,需要时自动注入进来。

DI:依赖注入

DI 全称是 Dependency Injection,中文叫依赖注入。它是 IOC 的具体实现方式。比如 Controller 需要 Service,Service 需要 Mapper,这些依赖关系不再由自己手动创建,而是由 Spring 自动注入。

@RestController
public class UserController {
    @Autowired
    private UserService userService;

}

这里 UserController 依赖 UserService,Spring 会从 IOC 容器中找到匹配的 UserService Bean 对象,通过反射自动赋值给 userService 变量,这个过程就叫依赖注入。

Bean 对象

Bean 可以理解为被 Spring IoC 容器创建和管理的 Java 对象。普通对象是我们自己 new 出来的,生命周期自己控制;而 Bean 是 Spring 容器帮我们创建、保存、注入、销毁的对象。

1.Bean 的默认名称 不手动指定 Bean 名字时,默认是类名首字母小写,例如 UserService → Bean 名为 userService

特殊规则:如果类名前两个字母都是大写,Bean 名与类名保持一致(如 USerServiceUSerService),遵循 Java Bean 官方命名规范。

2.Bean 的默认作用域 Spring Bean 默认是单例(singleton):整个 IoC 容器中只有一个实例对象,所有地方注入的都是同一个对象。

企业开发绝大多数场景都用默认单例;注意单例 Bean 不要用成员变量存储业务状态,否则会有多线程安全问题。

3.常见 Bean 注解汇总

常见 Bean 注解:

注解 常用位置 说明
@RestController Controller 层 标记接口控制器,返回 JSON 数据,@Controller + @ResponseBody 组合
@Controller Controller 层 标记控制器,常用于返回页面视图
@Service Service 层 标记业务逻辑类,@Component 的派生注解
@Repository DAO 实现层 标记数据访问实现类,@Component 的派生注解
@Component 通用组件类 根注解,通用 Bean 标记,不属于以上三层的通用组件用它
@Mapper MyBatis 接口 MyBatis 专属注解,由 MyBatis 扫描并注册为 Spring Bean;不是 @Component 的派生注解
补充:@Service@Controller@Repository 本质上都包含了 @Component,功能完全一致,只是做了语义分层,方便代码阅读和 AOP 切面匹配。企业开发必须严格按分层使用对应注解,不要全写 @Component

4.依赖注入的注解

  • @Autowired:Spring 官方提供,默认按类型匹配注入

  • 如果同一个类型存在多个 Bean(一个接口多个实现类),直接按类型注入会报错,有三种解决方式:

    1. @Primary:标记其中一个实现类为默认首选
    2. @Autowired + @Qualifier("bean名称"):按名称指定注入哪一个
    3. @Resource(name="bean名称"):直接按名称注入
  • @Resource@Autowired 区别

    • 归属不同:@Autowired 是 Spring 框架提供的注解;@Resource 是 JDK(JSR-250 标准)提供的注解。
    • 匹配规则不同:@Autowired 默认按类型匹配,找不到再按名称;@Resource 默认按名称匹配,找不到再按类型。
    • 支持场景不同:@Autowired 支持构造器、字段、Setter 注入;@Resource 仅支持字段和 Setter 注入。

Spring 三种常见依赖注入方式

Spring 里的“注入”就是:一个类需要用另一个类时,不自己 new,而是让 Spring 容器把已经管理好的对象传进来。 比如 Controller 需要调用 Service:

private UserService userService;

这个 userService 不自己创建,而是交给 Spring 注入。

1. 构造器注入:推荐

构造器注入就是:通过类的构造方法把依赖传进来

原始写法:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
}

意思是:创建 UserController 时,必须传入一个 UserService。Spring 发现构造器需要 UserService,就会从容器里找到 UserService 对象传进去。

项目中常用 Lombok 简化:

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
}

@RequiredArgsConstructor 会自动为 final 字段生成构造器,等价于你手写:

public UserController(UserService userService) {
    this.userService = userService;
}

推荐原因:依赖必须初始化,不能为 null;字段可以用 final,对象更安全;方便测试;Spring 官方也更推荐这种方式。

2. 字段注入:简单但不推荐

字段注入就是直接在成员变量上加 @Autowired

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;
}

意思是:Spring 创建 UserController 后,直接通过反射把 UserService 塞到这个字段里。

优点是写起来最简单。缺点是依赖关系不明显,字段不能很好地用 final,单元测试不方便,也容易隐藏“这个类到底需要哪些依赖”。所以现在项目里不太推荐字段注入。

3. Setter 注入:适合可选依赖

Setter 注入就是通过 set 方法注入依赖:

@RestController
@RequestMapping("/users")
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

意思是:Spring 先创建对象,再调用 setUserService() 把依赖传进去。

Setter 注入适合“可选依赖”或者后续可能变化的依赖,但普通业务代码里用得不如构造器注入多。

对比记忆

注入方式 写法 特点 推荐程度
构造器注入 @RequiredArgsConstructor + private final 依赖明确、不能为 null、适合必需依赖 推荐
字段注入 @Autowired private XxxService xxxService 写法简单,但依赖隐藏、测试不方便 不推荐
Setter 注入 @Autowired setXxx() 适合可选依赖、可后续修改 一般

最终记忆:普通项目优先用构造器注入,也就是 @RequiredArgsConstructor + private final XxxService xxxService;。字段注入虽然简单,但不推荐;Setter 注入主要用于可选依赖。

三层架构和请求参数的关系

前面学的 @RequestParam@RequestBody@PathVariable 主要出现在 Controller 层,因为 Controller 是直接接收 HTTP 请求的地方。Controller 接收到参数后,一般不会自己处理全部逻辑,而是把参数传给 Service 层。

@PostMapping("/users")
public String addUser(@RequestBody User user) {
    userService.addUser(user);
    return "success";
}

这段代码中,@RequestBody User user 负责接收前端 JSON 参数,属于 Controller 层的职责;userService.addUser(user) 才是真正调用业务逻辑。以后写 Spring Boot 项目时,可以按照这个顺序理解:

请求参数 → Controller 接收 → Service 处理 → Mapper 操作数据库 → 返回结果

小总结

三层架构可以简单记成:Controller 接请求,Service 写业务,DAO/Mapper 查数据库。Spring Boot 通过 IOC 容器管理这些类的对象,再通过 DI 把需要的对象自动注入进去,所以我们不用频繁手动 new 对象。实际开发时,请求参数绑定一般写在 Controller,业务规则写在 Service,SQL 或数据库访问写在 Mapper,这样代码结构会更清晰。

MySQL 基础笔记

数据库用于持久化保存数据。Java 程序运行时的数据通常只存在内存中,程序关闭后就会消失;数据库则负责把用户、员工、订单、商品等业务数据长期保存下来。Java Web 项目中,后端接收前端请求后,经常需要通过 Service 调用 Mapper/DAO,再由 Mapper/DAO 操作数据库。 MySQL 是关系型数据库管理系统,数据主要以“库、表、行、列”的形式组织。一个项目通常对应一个数据库,一个业务对象通常对应一张表,例如用户表、员工表、订单表;表中的一行表示一条数据记录,表中的一列表示一个字段。

Java Web 项目不是把数据直接写死在代码里,而是把数据存到 MySQL 中。Controller 接收请求,Service 处理业务,Mapper/DAO 访问数据库,MySQL 负责真正保存数据。

前端 / Postman
        ↓
Controller:接收请求
        ↓
Service:业务逻辑
        ↓
Mapper / DAO:执行 SQL
        ↓
MySQL:保存和查询数据

所以学习 MySQL 不是单独背 SQL,而是为了后面能理解:后端接口如何新增用户、查询员工列表、修改数据、删除数据。

数据库可以理解为一组表的集合,表保存某一类数据;字段是表中的列,记录是表中的一行。SQL 是操作数据库的语言,Java Web 阶段重点掌握 DDL 建表、DML 增删改、DQL 查询,后面 JDBC / MyBatis 本质上也是在 Java 里执行这些 SQL。

1. SQL 分类

分类 作用 常见命令 Java Web 初期重要程度
DDL 定义数据库、表结构 CREATEDROPALTER 重要,主要用于建库建表
DML 操作表中数据 INSERTUPDATEDELETE 很重要,增删改都靠它
DQL 查询表中数据 SELECT 最重要,开发中用得最多
DCL 用户和权限控制 GRANTREVOKE 初期了解即可
TCL 事务控制 COMMITROLLBACK 后面学事务时重点掌握

2. 数据库级操作

操作 SQL 说明
查看所有数据库 SHOW DATABASES; 查看当前 MySQL 中有哪些数据库
创建数据库 CREATE DATABASE javaweb; 创建名为 javaweb 的数据库
创建数据库并指定字符集 CREATE DATABASE javaweb DEFAULT CHARSET utf8mb4; 推荐写法,支持中文和 emoji
使用数据库 USE javaweb; 后续 SQL 都在这个数据库中执行
查看当前数据库 SELECT DATABASE(); 确认当前正在使用哪个库
删除数据库 DROP DATABASE javaweb; 删除整个数据库,真实项目慎用

3. 表结构操作 DDL

操作 SQL 说明
查看当前库所有表 SHOW TABLES; 查看当前数据库里的表
查看表结构 desc 表名; DESC emp; 查看字段、类型、约束
查看建表语句 show create table 表名; SHOW CREATE TABLE emp; 查看完整建表 SQL
删除表 drop table [ if exists ] 表名; DROP TABLE emp; 删除表结构和数据
清空表数据 TRUNCATE TABLE emp; 清空整张表,速度快,慎用
修改表名 rename table 表名 to 新表名; RENAME TABLE emp TO employee; 表重命名
添加字段 alter table 表名 add 字段名 类型(长度) [comment 注释] [约束]; ALTER TABLE emp ADD phone VARCHAR(20); 给表增加一列
修改字段类型 alter table 表名 modify 字段名 新数据类型(长度); ALTER TABLE emp MODIFY phone VARCHAR(30); 修改字段类型或长度
修改字段名和类型 alter table 表名 change 旧字段名 新字段名 类型 (长度) [comment 注释] [约束]; ALTER TABLE emp CHANGE phone mobile VARCHAR(30); phone 改名为 mobile
删除字段 alter table 表名 drop column 字段名; ALTER TABLE emp DROP phone; 删除某一列

建表常用模板:

create table 表名(
   字段1 字段类型 [约束] [comment 字段1注释],
   ......
   字段n 字段类型 [约束] [comment 字段n注释]
)[comment 表注释];


CREATE TABLE emp (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20) NOT NULL,
    age INT,
    gender CHAR(1),
    entry_date DATE,
    dept_id INT
);
约束 含义 示例
PRIMARY KEY 主键,唯一标识一条记录 id INT PRIMARY KEY
AUTO_INCREMENT 自增,一般配合主键 id INT PRIMARY KEY AUTO_INCREMENT
NOT NULL 不能为空 name VARCHAR(20) NOT NULL
UNIQUE 值不能重复 username VARCHAR(20) UNIQUE
DEFAULT 默认值 status INT DEFAULT 1
FOREIGN KEY 外键,建立表关系 FOREIGN KEY(dept_id) REFERENCES dept(id)
概念:约束是作用于表中字段上的规则,用于限制存储在表中的数据。
目的:保证数据库中数据的正确性、有效性和完整性。

4. 常见数据类型

分类 类型 说明 常见场景
整数 TINYINT 小整数,占用小 状态、性别、开关值,如 0/1
整数 INT 普通整数 年龄、数量、普通 ID
整数 BIGINT 大整数 大型 ID、订单号
小数 DOUBLE 浮点小数,可能有精度误差 科学计算,金额不推荐
小数 DECIMAL(m,n) 精确小数 金额、价格,如 DECIMAL(10,2)
字符串 CHAR(n) 定长字符串 性别、状态码、手机号等固定长度数据
字符串 VARCHAR(n) 可变长度字符串 用户名、邮箱、标题等常用文本
字符串 TEXT 长文本 文章内容、评论内容
日期时间 DATE 只保存日期 生日、入职日期,如 2026-06-07
日期时间 TIME 只保存时间 时间值,如 12:30:00
日期时间 DATETIME 日期 + 时间 创建时间、更新时间
日期时间 TIMESTAMP 时间戳,受时区影响 记录时间,了解即可
普通整数用 INT;
大 ID 用 BIGINT;
状态值用 TINYINT;
普通字符串用 VARCHAR;
固定长度字符串用 CHAR;
长文本用 TEXT;
日期用 DATE;
日期时间用 DATETIME;
金额用 DECIMAL,不推荐 DOUBLE。

CHARVARCHAR 区别:

类型 特点 示例
CHAR(10) 固定长度,最多 10 个字符,不足也按固定长度处理 固定编码、手机号
VARCHAR(10) 可变长度,最多 10 个字符,实际多长存多长 用户名、邮箱、标题

DECIMAL(m,n) 说明:

写法 含义
DECIMAL(10,2) 总共最多 10 位数字,其中 2 位小数
示例 最大大约可以存 99999999.99

建表时常见写法:

id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
phone CHAR(11),
status TINYINT DEFAULT 1,
price DECIMAL(10,2),
create_time DATETIME,
update_time DATETIME

记法:VARCHAR(20) 表示最多 20 个字符;DECIMAL(10,2) 表示总共 10 位,其中小数 2 位,例如 99999999.99

5. 增删改 DML

操作 SQL 说明
插入一条数据 INSERT INTO emp(name, age) VALUES ('张三', 18); 自增 id 一般不用手动插入
插入多条数据 INSERT INTO emp(name, age) VALUES ('张三',18),('李四',20); 批量插入
修改数据 UPDATE emp SET age = 19 WHERE id = 1; 必须注意 WHERE
删除数据 DELETE FROM emp WHERE id = 1; 删除满足条件的数据
删除全部数据 DELETE FROM emp; 危险,会删整张表数据
操作 SQL 说明
插入一条数据(指定字段) INSERT INTO 表名(字段1, 字段2) VALUES (值1, 值2); 自增主键不用手动插入
插入一条数据(全部字段) INSERT INTO 表名 VALUES (值1, 值2, ...); 值的顺序必须与表字段顺序一致
插入多条数据(指定字段) INSERT INTO 表名(字段1, 字段2) VALUES (值1, 值2), (值1, 值2); 批量插入,效率更高
插入多条数据(全部字段) INSERT INTO 表名 VALUES (值1, 值2, ...), (值1, 值2, ...); 批量插入全部字段
修改数据 UPDATE 表名 SET 字段1 = 值1 WHERE 条件; ⚠️ 必须注意 WHERE,否则全表更新
删除数据 DELETE FROM 表名 WHERE 条件; 删除满足条件的数据
删除全部数据 DELETE FROM 表名; ⚠️ 危险操作,会删除整张表所有数据
  1. DELETE 语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据。
  2. DELETE 语句不能删除某一个字段的值(如果要操作,可以使用 UPDATE,将该字段的值设为 NULL)。 重点记住:UPDATEDELETE 不加 WHERE 会影响整张表。
-- 危险:修改所有员工年龄
UPDATE emp SET age = 18;

-- 危险:删除整张表所有数据
DELETE FROM emp;

6. 查询

6.1基础查询

  • 查询多个字段:select 字段1, 字段2, 字段3 from 表名;
  • 查询所有字段(通配符):select * from 表名;
  • 设置别名:select 字段1 [as 别名1], 字段2 [as 别名2] from 表名;
  • 去除重复记录:select distinct 字段列表 from 表名;
操作 SQL 说明
查询所有字段 select * from 表名; 查看整张表
查询指定字段 select 字段1, 字段2, 字段3 from 表名; 实际开发更推荐
起别名 select 字段1 [as 别名1], 字段2 [as 别名2] from 表名; AS 可以省略
去重 select distinct 字段列表 from 表名; 去掉重复结果
条件查询 SELECT * FROM 表名 WHERE age > 18; 按条件筛选
多条件查询 SELECT * FROM 表名 WHERE age > 18 AND gender = '男'; 同时满足
或条件查询 SELECT * FROM 表名 WHERE age < 18 OR age > 60; 满足其一
非条件 SELECT * FROM 表名 WHERE NOT gender = '男'; 取反

6.2条件查询

条件查询:select 字段列表 from 表名 where 条件列表;

比较运算符 功能 逻辑运算符 功能
> 大于 and 或 && 并且(多个条件同时成立)
>= 大于等于 or 或 || 或者(多个条件任意一个成立)
< 小于 not 或 ! 非,不是
<= 小于等于
= 等于
<> 或 != 不等于
between … and … 在某个范围之内(含最小、最大值)
in(…) 在in之后的列表中的值,多选一,匹配多个值
like 占位符 模糊匹配(_匹配单个字符,%匹配任意个字符)
is null 判断空值,不能写 = NULL
Iis not null 查询非空数据
  • 模糊查询:WHERE name LIKE '%张%'(包含"张")
  • 前缀匹配:WHERE name LIKE '张%'(以"张"开头)
  • 后缀匹配:WHERE name LIKE '%三'(以"三"结尾)

6.3聚合函数

  • 介绍:将一列数据作为一个整体,进行纵向计算。
  • 语法:select 聚合函数(字段列表) from 表名;
函数 功能 使用示例
count 统计数量 select count(*) from 表名;select count(字段名) from 表名;
max 最大值 select max(字段名) from 表名;
min 最小值 select min(字段名) from 表名;
avg 平均值 select avg(字段名) from 表名;
sum 求和 select sum(字段名) from 表名;

💡 小提示:

  • count(*) 统计整张表的总行数(包含 NULL)
  • count(字段名) 统计该字段非 NULL 值的数量
  • count(常量)就是查询是否为空,不为空就给那一行一个常量,最后统计常量个数
  • 聚合函数会自动忽略 NULL 值(count(*) 除外)

6.4. 分组查询

  • 分组查询:select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤条件];

  • where与having区别

  1. 执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;而having是分组之后对结果进行过滤。
  2. 判断条件不同:where不能对聚合函数进行判断,而having可以。

注意事项

  • 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义。
  • 执行顺序:where > 聚合函数 > having。

6.5. 排序查询

  • 排序查询:select 字段列表 from 表名 [where 条件列表] [group by 分组字段] order by 字段1 排序方式1, 字段2 排序方式2 …; 排序方式
  • ASC:升序(默认值)
  • DESC:降序 注意事项
  • 如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序。

6.6. 分页查询

  • 分页查询:select 字段列表 from 表名 limit 起始索引, 查询记录数; 注意事项
  1. 起始索引从0开始,起始索引 = (查询页码 - 1)* 每页显示记录数。
  2. 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT。
  3. 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 10。

函数:

  • if(表达式, tvalue, fvalue):当表达式为true时,取值tvalue;当表达式为false时,取值fvalue
  • case expr when value1 then result1 [when value2 then result2 ...] [else result] end

7. SQL 语句执行顺序

写 SQL 的顺序:

SELECT ...
FROM ...
WHERE ...
GROUP BY ...
HAVING ...
ORDER BY ...
LIMIT ...

理解执行顺序:

FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT

这个很重要。比如聚合函数结果不能直接写在 WHERE 里,一般要写在 HAVING 里。

8.条件判断函数

IF() 函数

IF(表达式, tvalue, fvalue) 表示:如果表达式成立,返回 tvalue;如果表达式不成立,返回 fvalue。它适合处理简单的二选一判断

SELECT
    name,
    age,
    IF(age >= 18, '成年人', '未成年人') AS age_type
FROM emp;

这条 SQL 会查询员工姓名和年龄,并根据 age >= 18 判断员工是“成年人”还是“未成年人”。如果年龄大于等于 18,age_type 显示为 成年人;否则显示为 未成年人

CASE WHEN 函数

CASE 适合处理多条件、多分支判断,类似 Java 里的 switch 或多重 if...else if...else。常见写法是根据某个字段的不同取值,返回不同结果。

SELECT
    name,
    gender,
    CASE gender
        WHEN '男' THEN '男性'
        WHEN '女' THEN '女性'
        ELSE '未知'
    END AS gender_name
FROM emp;

这条 SQL 会根据 gender 字段转换显示结果:如果 gender,显示 男性;如果是 ,显示 女性;其他情况显示 未知END AS gender_name 表示把这个判断结果起别名为 gender_name

也可以写成条件判断形式:

SELECT
    name,
    age,
    CASE
        WHEN age < 18 THEN '未成年'
        WHEN age BETWEEN 18 AND 60 THEN '成年人'
        ELSE '老年人'
    END AS age_type
FROM emp;

这种写法不是判断某个固定字段等于什么,而是按多个条件依次判断,更适合年龄段、成绩等级、状态分类这类场景。

9. 多表关系

实际项目不会把所有数据都塞进一张表。常见关系有三种: 逻辑外键就是:表里有一个字段用来表示两张表的关系,但数据库层面不真正创建 FOREIGN KEY 约束。

关系 含义 建表 例子
一对一 一条数据对应另一张表一条数据 任意一方添加外键,关联另外一方的主键。 用户表和用户详情表
一对多 一条数据对应多条数据 在多的一方添加外键,关联另外一方的主键。 部门和员工
多对多 多条数据互相关联 通过中间表来维护,中间表的两个外键,分别关联另外两张表的主键。 学生和课程
外键语法
 创建表时指定
  create table 表名(
      字段名 数据类型,
      ...
      [constraint 外键名称] foreign key (外键字段名) references 主表(字段名)
  );
  建完表后,添加外键
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(字段名);`

10 多表查询 JOIN

  • 多表查询:指从多张表中查询数据,输出笛卡尔积
  • 笛卡尔积:笛卡尔乘积是指在数学中,两个集合(A集合和B集合)的所有组合情况(在多表查询时,需要消除无效的笛卡尔积)
  • 连接查询
    • 内连接:相当于查询A、B交集部分数据
    • 外连接
      • 左外连接:查询左表所有数据(包括两张表交集部分数据)
      • 右外连接:查询右表所有数据(包括两张表交集部分数据)
连接方式 作用 常见程度
隐式内连接 select 字段列表 from 表1, 表2 where 条件 …; 只保留两张表都能匹配的数据 常用
显式内连接 select 字段列表 from 表1 [inner] join 表2 on 连接条件 …; 只保留两张表都能匹配的数据 常用
左外连接 select 字段列表 from 表1 left [outer] join 表2 on 连接条件 …; 保留左表全部数据,右表没有则为 NULL 很常用
右外连接 select 字段列表 from 表1 right [outer] join 表2 on 连接条件 …; 保留右表全部数据,左表没有则为 NULL 较少用

查询员工和部门名称:

SELECT e.id, e.name, d.name AS dept_name
FROM emp e
LEFT JOIN dept d ON e.dept_id = d.id;

解释:emp e 表示给员工表起别名 edept d 表示给部门表起别名 dON e.dept_id = d.id 表示两张表通过部门 ID 建立对应关系。

11. 子查询

子查询就是 SQL 里面嵌套 SQL。 子查询能用,但开发中很多场景也可以用 JOIN。简单理解:需要“先查一个结果,再用这个结果继续查”时,可以考虑子查询。

  • SQL语句中嵌套SELECT语句,称为嵌套查询,又称子查询。
  • 形式:SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2 ...)
  • 子查询外部的语句可以是INSERT / UPDATE / DELETE / SELECT的任何一个,最常见的是SELECT。
类型 说明 返回结果
标量子查询 子查询返回的结果为单个值 一行一列
列子查询 子查询返回的结果为一列 多行一列
行子查询 子查询返回的结果为一行 一行多列
表子查询 子查询返回的结果为多行多列 多行多列
📌 子查询分类

🔹 标量子查询(Scalar)

  • 返回:一行一列
  • 本质:单个值
  • 操作符:= <> >= <=

🔹 列子查询(Column)

  • 返回:多行一列
  • 操作符:IN NOT IN

🔹 行子查询(Row)

  • 返回:一行多列
  • 操作符:= <> IN NOT IN

🔹 表子查询(Table)

  • 返回:多行多列
  • 用途:作为临时表参与查询

12. 事务基础

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 事务用于保证一组 SQL 要么全部成功,要么全部失败。典型例子是转账:A 扣钱和 B 加钱必须同时成功。

操作 SQL 说明
开启事务 START TRANSACTION; 开始一组操作
提交事务 COMMIT; 确认生效
回滚事务 ROLLBACK; 撤销本次事务中的修改
默认 MySQL 的事务是自动提交的,也就是说,当执行一条 DML 语句,MySQL 会立即隐式地提交事务。
示例:
START TRANSACTION;

UPDATE account SET money = money - 100 WHERE id = 1;
UPDATE account SET money = money + 100 WHERE id = 2;

COMMIT;

如果中间出错,可以执行:

ROLLBACK;

Java Web 后面学 Spring 事务时,本质上就是帮你管理 commitrollback

事务四大特性(ACID)

特性 英文 说明
原子性 Atomicity 事务是不可分割的最小单元,要么全部成功,要么全部失败
一致性 Consistency 事务完成时,必须使所有的数据都保持一致状态
隔离性 Isolation 数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
持久性 Durability 事务一旦提交或回滚,它对数据库中的数据的改变就是永久的

13.索引

[!summary] 索引 索引(Index)是帮助数据库快速查找数据的数据结构,类似于书籍目录。没有索引时需要遍历整张表(全表扫描);有索引时可以先定位数据位置,再读取对应记录,从而提高查询效率。

优缺点

  1. 优点
  • 提高数据查询的效率,降低数据库的 IO 成本(避免全表扫描,减少磁盘读取次数)
  • 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 消耗(索引本身已排序,无需额外计算)
  1. 缺点
  • 索引会占用存储空间(索引文件独立存储,随数据量增长而增大)
  • 索引大大提高了查询效率,同时也降低了 INSERT、UPDATE、DELETE 的效率(每次数据变更需同步更新索引)
-- 创建索引
create [unique] index 索引名 on 表名 (字段名, ...);

-- 查看索引
show index from 表名;

-- 删除索引
drop index 索引名 on 表名;
  • 主键字段在建表时,会自动创建主键索引,效率最高
  • 添加唯一约束时,数据库实际上会添加唯一索引

[!tip] B+Tree结构

MySQL(InnoDB)默认采用 B+Tree 作为索引结构。

  • 非叶子节点只存储索引值(Key)和指针。
  • 叶子节点存储索引数据,并通过双向链表连接。
  • 树的高度较低,查询时磁盘IO次数少。
  • 天然支持范围查询和排序操作。

查询过程:

根节点
  ↓
非叶子节点
  ↓
叶子节点
  ↓
定位目标数据

[!note] 为什么使用B+Tree

  1. 查询效率稳定,时间复杂度接近 O(logN)
  2. 树高低,减少磁盘IO
  3. 叶子节点形成链表,方便范围查询
  4. 适合数据库大量数据存储场景

14. Java Web 常见建表字段和SQL模版

实际项目建表经常会有这些字段:

字段 类型 说明
id BIGINT / INT 主键 ID
name VARCHAR(50) 名称
username VARCHAR(50) 用户名
password VARCHAR(100) 密码
phone VARCHAR(20) 手机号
email VARCHAR(100) 邮箱
status TINYINT / INT 状态,例如 1 启用,0 禁用
create_time DATETIME 创建时间
update_time DATETIME 更新时间

用户表例子:

CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    nickname VARCHAR(50),
    phone VARCHAR(20),
    status TINYINT DEFAULT 1,
    create_time DATETIME,
    update_time DATETIME
);

[!summary] SQL 高频模板

场景 模板
按 ID 查询 SELECT * FROM emp WHERE id = ?;
条件列表查询 SELECT * FROM emp WHERE name LIKE ? AND gender = ?;
分页查询 SELECT * FROM emp LIMIT ?, ?;
新增数据 INSERT INTO emp(name, age) VALUES (?, ?);
修改数据 UPDATE emp SET name = ?, age = ? WHERE id = ?;
删除数据 DELETE FROM emp WHERE id = ?;
统计数量 SELECT COUNT(*) FROM emp;
多表列表查询 SELECT e.*, d.name AS dept_name FROM emp e LEFT JOIN dept d ON e.dept_id = d.id;

这里的 ? 是后面 JDBC / MyBatis 里常见的参数占位符,表示这个位置的数据由 Java 程序传进来。

Mybatis

1.介绍

MyBatis 是 Java 中常用的持久层框架,主要作用是简化 JDBC 操作。原生 JDBC 需要手动获取连接、创建 SQL、设置参数、执行语句、处理结果集、关闭资源,代码重复且容易出错。MyBatis 把这些重复操作封装起来,开发者只需要关注两件事:写 SQL定义 SQL 对应的 Java 方法。因此 MyBatis 的特点是:SQL 自己写,执行过程和结果封装由框架完成。

MyBatis 常用于三层架构中的 Dao / Mapper 层:

Controller 层:接收请求,返回响应
Service 层:处理业务逻辑
Mapper 层:操作数据库

MyBatis 项目中常见的目录结构如下:

src/main/java/com/jinbai/demo
├── controller
│   └── EmpController.java
├── mapper
│   └── EmpMapper.java
├── pojo
│   └── Emp.java
└── DemoApplication.java

各部分作用:

controller:接收前端请求,返回数据
mapper:操作数据库,编写 SQL
pojo:实体类,用来封装数据库表数据
DemoApplication:Spring Boot 启动类

在 Spring Boot 项目中,通常不会直接写 Dao 实现类,而是写 Mapper 接口。MyBatis 会在运行时为 Mapper 接口生成代理对象,调用 Mapper 方法时,代理对象会找到对应 SQL,执行数据库操作,并把结果封装成 Java 对象返回。MyBatis 对 JDBC 进行了封装,开发者主要负责写 SQL,MyBatis 负责执行 SQL、传递参数、封装结果。

原生 JDBC 操作数据库大致需要这些步骤:

1. 注册数据库驱动
2. 获取数据库连接
3. 编写 SQL
4. 创建 PreparedStatement
5. 设置 SQL 参数
6. 执行 SQL
7. 处理 ResultSet 结果集
8. 释放数据库资源

这些代码大部分都是固定模板,真正和业务相关的只有 SQL、参数和结果映射。MyBatis 的作用就是把连接管理、参数设置、结果封装、资源释放这些重复操作封装起来,让开发者主要关注 SQL 本身。 简单理解:

JDBC:自己写完整数据库操作流程
MyBatis:自己写 SQL,框架帮你执行 SQL 和封装结果

MyBatis 不是完全替代 SQL,而是让 SQL 和 Java 方法建立映射关系。相比 JPA 这类全自动 ORM 框架,MyBatis 更灵活,因为 SQL 由开发者自己控制,更适合复杂查询和需要优化 SQL 的场景。

2. MyBatis 和 JDBC 的关系

JDBC 是 Java 操作数据库的底层标准 API,规定了 Java 程序如何连接数据库、发送 SQL、接收结果。MyBatis 不是脱离 JDBC 的新技术,而是对 JDBC 的进一步封装:底层仍然用 JDBC 连接数据库和执行 SQL,但把大量重复代码交给框架处理 原始 JDBC 查询一条数据通常要写完整流程:

// 1. 定义 SQL,? 是参数占位符,后面再给它赋值
String sql = "select * from emp where id = ?";

// 2. 获取数据库连接:需要数据库地址、用户名、密码
Connection conn = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/db01", "root", "123456");

// 3. 创建 PreparedStatement,用来执行带参数的 SQL
PreparedStatement ps = conn.prepareStatement(sql);

// 4. 给第 1 个 ? 占位符设置参数值,这里 id = 1
ps.setInt(1, 1);

// 5. 执行查询 SQL,返回 ResultSet 结果集
ResultSet rs = ps.executeQuery();

// 6. 遍历查询结果,把数据库中的一行数据封装成 Java 对象
while (rs.next()) {
    Emp emp = new Emp();
    emp.setId(rs.getInt("id"));              // 获取 id 字段
    emp.setUsername(rs.getString("username"));// 获取 username 字段
    emp.setName(rs.getString("name"));      // 获取 name 字段
    emp.setGender(rs.getShort("gender"));   // 获取 gender 字段
}

// 7. 关闭资源,避免连接一直被占用
rs.close();
ps.close();
conn.close();

这段代码里,真正和业务强相关的只有 SQL、参数 id、结果封装成 Emp。其他内容,比如获取连接、创建 PreparedStatement、设置参数、遍历 ResultSet、关闭资源,都是大量重复模板代码。

使用 MyBatis 后,开发者通常只需要写 Mapper 接口和 SQL:

@Mapper // 告诉 Spring / MyBatis:这是一个 Mapper 接口,需要生成代理对象
public interface EmpMapper {

    // 调用 getById 方法时,MyBatis 会执行这条 SQL
    // #{id} 表示获取方法参数 id 的值,并作为 SQL 参数传入
    @Select("select * from emp where id = #{id}")
    Emp getById(Integer id); // 根据 id 查询一条员工数据,返回 Emp 对象
}

调用时直接写:

// empMapper 实际上是 MyBatis 生成的代理对象
// 调用 getById(1),底层会执行:select * from emp where id = 1
Emp emp = empMapper.getById(1);

背后实际执行流程可以理解为:

empMapper.getById(1) → Mapper 代理对象 → 找到 SQL → JDBC 执行 SQL → ResultSet 结果映射 → 返回 Emp 对象

也就是说,MyBatis 帮我们做了 JDBC 中最繁琐的部分:

JDBC:连接数据库 + 设置参数 + 执行 SQL + 处理 ResultSet + 关闭资源都要自己写
MyBatis:开发者写 Mapper 和 SQL,框架负责执行 SQL、传参、封装结果和管理资源

Spring Boot 项目中,数据库连接信息通常写在配置文件中:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8 常用驱动类
    url: jdbc:mysql://localhost:3306/db01       # 数据库连接地址,db01 是数据库名
    username: root                              # 数据库用户名
    password: 123456                            # 数据库密码

MyBatis 的定位可以简单记成:

JDBC 是底层数据库操作标准,MyBatis 是对 JDBC 的封装。

更准确地说:

JDBC 负责“Java 怎么操作数据库”
MyBatis 负责“把 Mapper 方法调用转换成 JDBC SQL 执行,并把结果封装成 Java 对象”

所以 MyBatis 并不是不用 SQL,也不是不用 JDBC,而是让开发者从重复的 JDBC 模板代码中解放出来,主要关注 SQL 本身和 Java 对象之间的映射关系。

3. Java 代理对象

介绍

代理对象就是包在真实对象外面的一层对象。调用方法时,调用者不直接访问真实对象,而是先访问代理对象;代理对象再根据需要调用真实对象,并且可以在调用前后添加额外功能。代理对象的核心价值是:不修改原有业务代码,也能增强方法功能。常见增强功能包括日志记录、权限校验、事务管理、缓存处理、性能统计、异常处理等。 代理对象能成立,底层通常依赖 Java 的多态:代理对象和真实对象实现同一个接口,调用者用接口类型接收对象时,既可以接收真实对象,也可以接收代理对象。

// 业务接口
public interface UserService {
    void save();
}

// 真实对象:只负责核心业务
public class UserServiceImpl implements UserService {
    @Override
    public void save() {
        System.out.println("保存用户");
    }
}

// 代理对象:负责增强功能,并调用真实对象
public class UserServiceProxy implements UserService {
    private UserService target = new UserServiceImpl();

    @Override
    public void save() {
        System.out.println("方法执行前:记录日志");
        target.save(); // 调用真实对象的方法
        System.out.println("方法执行后:记录日志");
    }
}

使用代理对象:

public class Test {
    public static void main(String[] args) {
        // 接口引用指向代理对象,本质上利用了多态
        UserService userService = new UserServiceProxy();

        // 调用的是代理对象的 save() 方法
        userService.save();
    }
}

执行流程:

调用 userService.save()
        ↓
进入代理对象 save()
        ↓
执行前置增强逻辑
        ↓
调用真实对象 save()
        ↓
执行后置增强逻辑

所以代理可以理解为:

代理 = 多态 + 包装真实对象 + 方法增强 + 调用转发

多态解决的是:代理对象为什么能被当成真实对象使用。代理解决的是:如何在不修改真实对象代码的情况下增强方法功能。 代理主要分为两类:

静态代理:自己手写代理类,容易理解,但每个业务类都要写对应代理类,代码量大。
动态代理:程序运行时自动生成代理对象,不需要手写代理类,Spring、MyBatis 常用。

MyBatis 中的 Mapper 动态代理

MyBatis 中的 Mapper 接口就是典型的动态代理。Mapper 接口通常没有实现类:

@Mapper
public interface EmpMapper {
    @Select("select * from emp")
    List<Emp> list();
}

按普通 Java 语法,接口里的方法只有声明,没有方法体,接口本身不能直接创建对象,所以不能这样写:

EmpMapper empMapper = new EmpMapper(); // 错误,接口不能 new

正常情况下,如果没有 MyBatis,就需要自己写实现类:

public class EmpMapperImpl implements EmpMapper {
    @Override
    public List<Emp> list() {
        // 手写 JDBC 代码,连接数据库、执行 SQL、封装结果
    }
}

但 MyBatis 不需要我们手写这个实现类。程序启动时,Spring Boot 会扫描带有 @Mapper 的接口,MyBatis 会读取接口信息、方法信息、SQL 信息和返回值类型,并建立方法和 SQL 的映射关系。

接口名:EmpMapper
方法名:list
SQL:select * from emp
返回类型:List<Emp>

也就是说,MyBatis 会记录:

EmpMapper.list()  --->  select * from emp

运行时,MyBatis 会为 EmpMapper 接口生成一个代理对象。这个代理对象虽然不是我们手写的 EmpMapperImpl,但它实现了 EmpMapper 接口,所以可以赋值给 EmpMapper 类型的变量。

EmpMapper empMapper = MyBatis 生成的代理对象;

这个代理对象大致可以理解成:

public class EmpMapperProxy implements EmpMapper {
    @Override
    public List<Emp> list() {
        // 1. 根据接口名 + 方法名,找到 list() 对应的 SQL
        // 2. 通过 JDBC 执行 SQL:select * from emp
        // 3. 获取数据库查询结果
        // 4. 把每一行数据封装成 Emp 对象
        // 5. 把多个 Emp 对象放入 List 集合并返回
    }
}

注意:上面的 EmpMapperProxy 只是为了方便理解,实际代理类不是我们手写的,而是 MyBatis 在程序运行时动态生成的。 调用:

empMapper.list();

实际流程是:

调用 empMapper.list()
        ↓
empMapper 实际是 MyBatis 生成的代理对象
        ↓
代理对象拦截 list() 方法调用
        ↓
根据 EmpMapper.list 找到对应 SQL
        ↓
通过 JDBC 执行 select * from emp
        ↓
将查询结果封装成 Emp 对象
        ↓
返回 List<Emp>

所以,Mapper 接口没有实现类也能使用,是因为 MyBatis 底层帮我们生成了 Mapper 代理对象。我们只需要写接口和 SQL,MyBatis 负责根据方法调用执行 SQL,并把数据库结果封装成 Java 对象返回。

Spring AOP 和事务中的代理对象

Spring 中的 AOP 和事务也依赖代理对象。例如:

@Transactional
public void save() {
    // 操作数据库
}

表面上看是直接调用 save() 方法,实际上 Spring 可能会给这个类生成一个代理对象。调用方法时,先进入代理对象,由代理对象负责开启事务、调用真实方法、提交事务或回滚事务。

调用代理对象 save()
        ↓
开启事务
        ↓
调用真实对象 save()
        ↓
方法正常结束:提交事务
        ↓
方法出现异常:回滚事务

因此,@Transactional 并不是把事务代码直接插入到你的方法里面,而是通过代理对象在方法执行前后添加事务控制逻辑。

总结

代理对象是真实对象的中间包装层。它通常和真实对象实现同一个接口,所以调用者可以用接口类型接收代理对象。代理对象接收到方法调用后,可以在调用真实方法前后添加增强逻辑,比如日志、权限、事务、缓存等。

真实对象:负责核心业务
代理对象:负责增强功能 + 调用真实对象
调用者:通常只看到接口,不关心底层是真实对象还是代理对象

Java Web 开发中,MyBatis Mapper、Spring AOP、Spring 事务都大量使用代理思想。MyBatis 通过动态代理让没有实现类的 Mapper 接口可以执行 SQL;Spring 通过代理对象在不修改业务方法的情况下实现事务管理和 AOP 增强。

4.基础操作

1.增删改查

查询所有数据:

// 查询 tb_emp 表中的所有员工
@Select("select * from tb_emp")
List<Emp> list();

根据 ID 查询:

// 根据 id 查询员工信息
@Select("select * from tb_emp where id = #{id}")
Emp getById(Integer id);

条件查询 根据 ID 删除:

// 根据 id 删除员工
@Delete("delete from tb_emp where id = #{id}")
void delete(Integer id);

新增数据:

// 新增员工数据
@Insert("insert into tb_emp(name, age) values(#{name}, #{age})")
void insert(Emp emp);

新增(主键返回)

插入数据时,Mapper 方法可以接收一个对象,SQL 中通过对象属性取值。

@Insert("insert into emp(username, name, gender) values(#{username}, #{name}, #{gender})")
void insert(Emp emp);

如果数据库主键是自增 ID,新增后有时需要拿到生成的主键,可以使用 useGeneratedKeyskeyProperty

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into emp(username, name, gender) values(#{username}, #{name}, #{gender})")
void insert(Emp emp);

执行插入后,数据库生成的主键会被回填到 emp.id 中。

Emp emp = new Emp();
emp.setUsername("tom");
emp.setName("Tom");

empMapper.insert(emp);

System.out.println(emp.getId()); // 可以拿到新增后的自增主键

修改数据:

// 根据 id 修改员工信息
@Update("update tb_emp set name = #{name}, age = #{age} where id = #{id}")
void update(Emp emp);

说明:

#{id}:从方法参数或对象属性中取值
#{name}:从 Emp 对象的 name 属性中取值
#{age}:从 Emp 对象的 age 属性中取值

2.参数传递

Mapper 方法中的参数会传给 SQL。MyBatis 使用 #{} 获取参数值,它会自动进行参数设置,并且底层使用 PreparedStatement,可以减少 SQL 注入风险。

@Select("select * from emp where id = #{id}")
Emp getById(Integer id);

调用:

Emp emp = empMapper.getById(1);

执行时,#{id} 会被参数 1 替换,最终执行类似:

select * from emp where id = ?

如果方法参数是一个对象,MyBatis 会根据对象属性名取值:

@Insert("insert into emp(username, name, gender) values(#{username}, #{name}, #{gender})")
void insert(Emp emp);

这里的 #{username}#{name}#{gender} 对应的是 Emp 对象中的属性。 如果方法有多个简单参数,建议使用 @Param 明确参数名:

@Select("select * from emp where username = #{username} and gender = #{gender}")
List<Emp> listByCondition(@Param("username") String username,
                          @Param("gender") Integer gender);

这样 SQL 中的参数名就能和方法参数明确对应,代码更清晰,也能避免参数名识别问题。

3.#{}${} 的区别

MyBatis 中常见两种取值方式:#{}${}

#{} 是预编译参数占位,底层使用 PreparedStatement,会把参数当成普通值处理,安全性更高,常用于普通条件查询、插入、修改、删除。

@Select("select * from emp where id = #{id}")
Emp getById(Integer id);

${} 是字符串拼接,会把参数内容直接拼接进 SQL,存在 SQL 注入风险,一般不用于接收用户输入。它通常只在必须拼接 SQL 结构时使用,比如动态表名、字段名、排序字段等。

@Select("select * from emp order by ${field}")
List<Emp> listOrderBy(@Param("field") String field);

简单记:

#{}:安全,占位符传参,常用
${}:字符串拼接,有 SQL 注入风险,慎用

绝大多数情况下优先使用 #{}

4.查询结果封装

数据库查询结果是一行一行的数据,MyBatis 会根据返回值类型,把结果封装成 Java 对象。

@Select("select id, username, name, gender from emp")
List<Emp> list();

如果数据库字段名和 Java 属性名一致,MyBatis 可以自动封装:

数据库字段:id       username       name       gender
Java 属性:id       username       name       gender

如果字段名和属性名不一致,比如数据库字段是 create_time,Java 属性是 createTime,可能需要开启驼峰命名映射,或者在 SQL 中起别名。

方式一:SQL 起别名:

@Select("select create_time createTime, update_time updateTime from emp")
List<Emp> list();

方式二:开启驼峰命名自动映射:

mybatis:
  configuration:
    map-underscore-to-camel-case: true

开启后,MyBatis 会自动把数据库中的下划线命名映射为 Java 的驼峰命名:

create_time  →  createTime
update_time  →  updateTime

5.sql 和 include

<sql> 可以抽取公共 SQL 片段,<include> 可以复用这些 SQL 片段,减少重复代码。

<!-- 抽取公共字段列表 -->
<sql id="commonSelect">
    id, name, age, gender, entrydate
</sql>

<!-- 使用 include 引入公共 SQL 片段 -->
<select id="list" resultType="com.jinbai.demo.pojo.Emp">
    select
        <include refid="commonSelect"/>
    from tb_emp
</select>

说明:

sql:定义公共 SQL 片段
include:引入公共 SQL 片段
refid:引用 sql 片段的 id

5. XML 映射文件

注解适合简单 SQL,XML 更适合复杂 SQL,尤其是动态条件查询、批量操作、多表查询等。XML 的核心作用是把 SQL 从 Java 代码中抽出来,让复杂 SQL 更容易维护。 Mapper 接口:

@Mapper
public interface EmpMapper {
    List<Emp> list(String name, Integer gender);
}

XML 映射文件:

<mapper namespace="com.example.mapper.EmpMapper">

    <select id="list" resultType="com.example.pojo.Emp">
        select *
        from emp
        where name like concat('%', #{name}, '%')
          and gender = #{gender}
    </select>

</mapper>

这里有两个关键对应关系:

namespace:对应 Mapper 接口的全类名
id:对应 Mapper 接口中的方法名

也就是说:

com.example.mapper.EmpMapper.list()
        ↓
XML 中 namespace + id 对应的 SQL

使用 XML 时,接口方法上不需要写 @Select,MyBatis 会根据接口方法去 XML 中找对应 SQL。

6. 动态 SQL

动态 SQL 是 MyBatis 很重要的功能,适合根据不同条件拼接不同 SQL。比如查询员工时,可能传了姓名就按姓名查,传了性别就按性别查,没传就不加对应条件。 常用动态 SQL 标签有:

<if>       条件判断
<where>    自动生成 where,并处理多余的 and / or
<set>      用于 update,自动处理多余逗号
<foreach>  遍历集合,常用于批量删除、批量插入

示例:条件查询。

test<if> 标签的条件判断属性,用来决定是否拼接这段 SQL。

<select id="list" resultType="com.example.pojo.Emp">  
select *  
from emp  
<where>  
<if test="name != null and name != ''">  
name like concat('%', #{name}, '%')  
</if>  
<if test="gender != null">  
and gender = #{gender}  
</if>  
</where>  
</select>


<select id="list" resultType="com.example.pojo.Emp">
    select *
    from emp
    <where>
        <if test="name != null and name != ''">
            name like concat('%', #{name}, '%')
        </if>
        <if test="gender != null">
            and gender = #{gender}
        </if>
    </where>
</select>

<where> 的作用是:如果内部有条件成立,就自动加 where;如果第一个条件前面多了 andor,它会自动去掉。 示例:动态更新。

<update id="update">
    update emp
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="name != null">name = #{name},</if>
        <if test="gender != null">gender = #{gender},</if>
    </set>
    where id = #{id}
</update>

<set> 的作用是:自动加 set,并去掉最后多余的逗号。 示例:批量删除。

<delete id="deleteByIds">
    delete from emp
    where id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

<foreach> 用来遍历集合,最终拼出类似:

delete from emp where id in (1, 2, 3)

7. Mapper 接口

Mapper 接口就是 MyBatis 中专门负责操作数据库的接口。以前写 Dao 层时,通常要写接口和实现类;使用 MyBatis 后,一般只需要写 Mapper 接口,不需要手写实现类。

@Mapper
public interface EmpMapper {
    @Select("select * from emp")
    List<Emp> list();
}

这里的 EmpMapper 是接口,list() 方法没有方法体,按普通 Java 语法它自己不能执行。但是 MyBatis 会在程序启动时扫描 @Mapper 接口,读取方法上的 SQL 注解,并在运行时生成代理对象。调用 empMapper.list() 时,实际调用的是 MyBatis 生成的代理对象,由代理对象负责执行 SQL。

调用流程可以理解为:

调用 empMapper.list()
        ↓
进入 MyBatis 生成的 Mapper 代理对象
        ↓
根据 EmpMapper.list 找到对应 SQL
        ↓
执行 select * from emp
        ↓
把查询结果封装成 List<Emp>
        ↓
返回给 Service 层

所以 Mapper 接口的本质作用是:用 Java 方法来表示一条数据库操作。方法名代表一个操作,参数代表 SQL 参数,返回值代表 SQL 执行结果。

8.XML 映射文件

简单 SQL 可以写在注解中,复杂 SQL 通常写在 XML 映射文件中。XML 文件一般放在:

src/main/resources/mapper/EmpMapper.xml

Mapper 接口:

package com.jinbai.demo.mapper;

import com.jinbai.demo.pojo.Emp;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

// Mapper 接口,只定义方法,不直接写复杂 SQL
@Mapper
public interface EmpMapper {

    // 查询所有员工,SQL 写在 XML 映射文件中
    List<Emp> list();
}

XML 映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace 必须对应 Mapper 接口的全限定类名 -->
<mapper namespace="com.jinbai.demo.mapper.EmpMapper">

    <!-- id 必须对应 Mapper 接口中的方法名 -->
    <!-- resultType 表示查询结果封装成 Emp 对象 -->
    <select id="list" resultType="com.jinbai.demo.pojo.Emp">
        select * from tb_emp
    </select>

</mapper>

如果 XML 文件放在 resources/mapper 下,通常还需要在配置文件中写:

# 配置 MyBatis XML 映射文件位置
mybatis.mapper-locations=classpath:mapper/*.xml

说明:

namespace:绑定 Mapper 接口
id:绑定 Mapper 方法名
resultType:指定返回结果封装类型
mapper-locations:告诉 MyBatis 去哪里找 XML 文件

9.动态 SQL

动态 SQL 用于根据参数是否存在,动态拼接 SQL 语句。常见场景是条件查询、动态更新、批量删除等。

常用标签:

标签 作用
<if> 条件判断
<where> 自动生成 WHERE,并处理多余的 AND / OR
<set> 动态更新时自动处理多余逗号
<foreach> 遍历集合,常用于 IN 查询
<sql> 抽取公共 SQL 片段
<include> 引入公共 SQL 片段

开发规范:RESTful 与统一响应结果

1 RESTful 是什么

RESTful 是一种接口设计风格,不是强制语法。核心思想是:URL 表示资源,HTTP 请求方式表示对资源的操作。也就是说,不建议把动作写进 URL,而是通过 GET / POST / PUT / DELETE 区分查询、新增、修改、删除。 常见写法:

操作 HTTP 方法 推荐 URL 含义
查询全部 GET /emps 查询员工列表
根据 id 查询 GET /emps/{id} 查询某个员工
新增 POST /emps 新增员工
修改 PUT /emps/emps/{id} 修改员工
删除 DELETE /emps/{id} 删除某个员工

记忆:URL 尽量写名词,不写动词;动作交给 HTTP 方法表达。例如 /deleteEmp?id=1 不如 DELETE /emps/1 规范。

2 Spring Boot 中的 RESTful 注解

Controller 中常用这些注解接收不同类型请求:

@RestController
@RequestMapping("/emps")
public class EmpController {

    @GetMapping
    public Result list() {
        return Result.success();
    }

    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id) {
        return Result.success();
    }

    @PostMapping
    public Result save(@RequestBody Emp emp) {
        return Result.success();
    }

    @PutMapping
    public Result update(@RequestBody Emp emp) {
        return Result.success();
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        return Result.success();
    }
}

3 统一响应结果 Result

前后端交互时,不建议每个接口随便返回不同格式的数据。实际项目通常统一成:

{
  "code": 1,
  "msg": "success",
  "data": {}
}

常见字段:

字段 类型 含义
code Integer 状态码,例如 1 成功,0 失败
msg String 提示信息,例如 success用户名或密码错误
data Object 真正返回的数据,可以是对象、数组、分页结果,也可以为 null

示例类:

项目结构与三层架构

1 Nginx、Tomcat、Spring Boot 的位置

实际 Web 项目中,请求大致流程是:

浏览器 / 前端页面
        ↓
      Nginx
        ↓
Spring Boot / Java 后端服务
        ↓
      数据库

Nginx 是 Web 服务器,也常用作反向代理。它可以负责统一入口、前端静态资源、接口转发、HTTPS、负载均衡。Tomcat 是 Java Web 运行容器,Spring Boot 内置 Tomcat,所以 Spring Boot 项目启动后,本身就能接收 HTTP 请求。线上常把 Nginx 放在 Spring Boot 前面,但学习阶段可以先直接访问 Spring Boot。

2.2 Controller、Service、Mapper 三层职责

Spring Boot + MyBatis 项目通常分三层:

常见包名 主要职责 不应该做什么
Controller controller 接收请求参数,调用 Service,返回 Result 不直接写 SQL,不堆复杂业务逻辑
Service service / service.impl 组织业务流程,处理业务规则 不直接接收 HTTP 请求
Mapper mapper 操作数据库,执行 SQL 不处理业务规则,不返回前端响应格式

完整调用流程:

前端请求
  ↓
Controller 接收请求参数
  ↓
Service 处理业务逻辑
  ↓
Mapper 执行 SQL
  ↓
数据库返回结果
  ↓
Service 封装业务结果
  ↓
Controller 包装 Result 返回前端

2.3 为什么 Service 经常写接口 + 实现类

教程里常写:

EmpService        接口
EmpServiceImpl    实现类

原因是 Controller 面向接口编程,后面如果业务实现要替换,只需要换实现类,不一定改 Controller。简单项目也可以只写一个 Service 类,但教程这样写是为了训练企业开发规范。

示例:

public interface EmpService {
    PageBean page(Integer page, Integer pageSize);
}

@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageBean page(Integer page, Integer pageSize) {
        Long total = empMapper.count();
        Integer start = (page - 1) * pageSize;
        List<Emp> rows = empMapper.list(start, pageSize);
        return new PageBean(total, rows);
    }
}

2.4 Mapper 为什么只写接口也能执行 SQL

普通 Java 接口不能直接创建对象,但 MyBatis 的 Mapper 接口可以被 Spring / MyBatis 管理。你写:

@Mapper
public interface EmpMapper {
    @Select("select count(*) from emp")
    Long count();

    @Select("select * from emp limit #{start}, #{pageSize}")
    List<Emp> list(Integer start, Integer pageSize);
}

MyBatis 会在运行时为这个接口生成代理对象。调用 empMapper.list() 时,并不是接口自己执行代码,而是代理对象根据注解或 XML 找到 SQL,完成参数绑定、SQL 执行、结果封装。

记忆:Mapper 代理对象主要就是为了让 Mapper 接口能执行 SQL。你只写接口和 SQL,不写实现类。

Controller、Service、Mapper 三层结构

在 Spring Boot + MyBatis 项目里,后端代码通常分成 Controller、Service、Mapper 三层。它们不是为了故意复杂,而是为了让代码职责清楚:Controller 负责接收前端请求,Service 负责业务逻辑,Mapper 负责操作数据库。完整流程是:前端发送请求 → Controller 接收请求 → Controller 调用 Service → Service 调用 Mapper → Mapper 执行 SQL 查询数据库 → 数据返回给 Service → Service 封装结果 → Controller 返回给前端。

Controller:请求控制层

Controller 是前端访问后端的入口,主要负责接收请求参数、调用 Service、返回响应结果。比如前端访问 /emps?page=1&pageSize=10,这个请求会先进入 Controller。Controller 不应该直接写 SQL,也不应该写太多业务逻辑,它更像是“接口入口”,负责把前端请求转交给业务层处理。

@RestController
public class EmpController {
    @Autowired
    private EmpService empService;

    @GetMapping("/emps")
    public Result page(Integer page, Integer pageSize) {
        PageBean pageBean = empService.page(page, pageSize);
        return Result.success(pageBean);
    }
}

Service:业务逻辑层

Service 负责具体业务流程,也就是“这个功能应该怎么做”。例如分页查询员工时,Service 要计算分页起始位置,调用 Mapper 查询总记录数和当前页数据,然后把结果封装成 PageBean 返回。Service 一般不直接接收浏览器请求,也不直接写 SQL,它主要负责组织业务逻辑。

public interface EmpService {
    PageBean page(Integer page, Integer pageSize);
}
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageBean page(Integer page, Integer pageSize) {
        Long total = empMapper.count();
        Integer start = (page - 1) * pageSize;
        List<Emp> rows = empMapper.list(start, pageSize);
        return new PageBean(total, rows);
    }
}

Mapper:数据库访问层

Mapper 专门负责操作数据库,里面通常写 SQL。MyBatis 项目里的 Mapper 一般是接口,不需要自己写实现类,因为 MyBatis 会在运行时根据 @Mapper@Select 或 XML 配置自动生成代理对象。也就是说,你调用 empMapper.list(),底层实际执行的是对应的 SQL。

@Mapper
public interface EmpMapper {
    @Select("select count(*) from emp")
    Long count();

    @Select("select * from emp limit #{start}, #{pageSize}")
    List<Emp> list(Integer start, Integer pageSize);
}

Mapper 是接口还能用,是因为 MyBatis 会自动创建它的实现对象。普通 Java 接口不能直接调用,但带有 @Mapper 的接口会被 MyBatis 管理,MyBatis 根据接口方法上的 SQL 注解或 XML 文件,自动完成 SQL 执行、参数传递和结果封装。

分页查询的完整流程

当前端请求 /emps?page=1&pageSize=10 时,Controller 先接收 pagepageSize 参数,然后调用 empService.page(page, pageSize)。Service 计算分页起始位置 start = (page - 1) * pageSize,再调用 empMapper.count() 查询总记录数,调用 empMapper.list(start, pageSize) 查询当前页员工列表。Mapper 执行 SQL,从数据库拿到结果后返回给 Service,Service 把 totalrows 封装成 PageBean,最后 Controller 把结果包装成统一格式返回给前端。

最核心记忆

Controller 管请求和响应,Service 管业务流程,Mapper 管数据库 SQL。Controller 不直接查数据库,Mapper 不处理业务逻辑,Service 负责把数据库操作组织成一个完整功能。这样分层后,代码更清楚,后期修改接口、业务规则或 SQL 时,不容易互相影响。## 最精炼总结

Mapper 代理对象主要就是为了让 Mapper 接口能执行 SQL。你只写 Mapper 接口和 SQL,不用写实现类,MyBatis 会自动生成代理对象。Controller 负责接请求,Service 负责业务流程,Mapper 负责数据库操作。调用流程是:前端请求进入 Controller,Controller 调 Service,Service 调 Mapper,Mapper 代理对象执行 SQL,最后结果一层层返回给前端。

文件上传

3.1 前端上传文件的本质

文件上传使用 HTTP 请求,常见提交方式是:

<form action="/upload" method="post" enctype="multipart/form-data">
    姓名: <input type="text" name="username"><br>
    年龄: <input type="text" name="age"><br>
    头像: <input type="file" name="image"><br>
    <input type="submit" value="提交">
</form>

关键点是 enctype="multipart/form-data"。普通表单参数可以直接用文本传输,但文件是二进制内容,需要用 multipart 格式把多个表单项分块提交。每一块都有自己的 Content-Disposition,文件块还会带 filenameContent-Type

3.2 后端接收文件:MultipartFile

Spring Boot 用 MultipartFile 接收上传的文件:

@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String username,
                         Integer age,
                         @RequestParam("image") MultipartFile file) throws IOException {
        System.out.println(username);
        System.out.println(age);
        System.out.println(file.getOriginalFilename());
        return Result.success();
    }
}

注意:@RequestParam("image") 要和前端 <input type="file" name="image">name 对上。变量名可以叫 file,但表单字段名必须匹配。

3.3 MultipartFile 常用方法

方法 作用
getOriginalFilename() 获取上传文件的原始文件名
transferTo(File dest) 把上传文件保存到服务器本地磁盘
getSize() 获取文件大小,单位字节
getBytes() 获取文件的字节数组
getInputStream() 获取文件输入流,适合上传到 OSS 等外部存储
isEmpty() 判断文件是否为空
getContentType() 获取文件 MIME 类型,如 image/png

3.4 本地存储与云存储区别

本地存储就是把文件保存到后端服务器硬盘,例如 /upload/xxx.jpg。缺点是多台服务器之间文件不同步,服务器扩容、迁移、重启时管理麻烦。云存储是把文件传到第三方对象存储服务,例如阿里云 OSS,后端只保存文件访问 URL。真实项目更常用云存储。

推荐流程:

前端选择文件
  ↓
后端接收 MultipartFile
  ↓
后端把文件上传到 OSS
  ↓
OSS 返回文件访问 URL
  ↓
后端把 URL 返回给前端 / 保存到数据库

阿里云 OSS 集成

1 OSS、Bucket、SDK 是什么

名词 说明
OSS Object Storage Service,对象存储服务,用来存图片、视频、文档等文件
Bucket 存储空间,类似一个文件容器,文件必须放在某个 Bucket 里
Object 存储在 Bucket 中的具体文件
AccessKey ID / Secret 访问 OSS 的身份凭证,类似账号和密码,不应写死在代码里
SDK 官方提供的工具包,Java 程序通过 SDK 调用 OSS 上传、下载、删除文件

2 OSS 使用步骤

教程中的大流程可以理解为:注册阿里云账号 → 充值 → 开通 OSS → 创建 Bucket → 获取 AccessKey → 引入 OSS SDK → 编写上传工具类 → 接口调用上传方法。

实际项目中最重要的是:AccessKey 不能提交到 Git,也不要硬编码在 Java 类里,应该放到配置文件或环境变量中

3 配置文件写法

aliyun:
  oss:
    endpoint: https://oss-cn-hangzhou.aliyuncs.com
    access-key-id: yourAccessKeyId
    access-key-secret: yourAccessKeySecret
    bucket-name: yourBucketName

4 使用 @Value 注入配置

@Component
public class AliOSSUtils {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;

    @Value("${aliyun.oss.access-key-id}")
    private String accessKeyId;

    @Value("${aliyun.oss.access-key-secret}")
    private String accessKeySecret;

    @Value("${aliyun.oss.bucket-name}")
    private String bucketName;
}

@Value 适合少量配置。缺点是字段多时比较散,每个字段都要单独写 ${...}

5 使用 @ConfigurationProperties 批量绑定配置

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

需要注意字段名和配置名的对应关系:access-key-id 可以绑定到 accessKeyId。这种写法更适合一组配置统一管理。

如果 IDEA 提示 Spring Boot Configuration Annotation Processor not configured,可以添加配置处理器依赖,让配置文件有自动提示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

登录校验:Cookie、Session、Token、JWT

1 登录校验要解决什么问题

HTTP 请求默认是无状态的。用户第一次登录成功后,后面再请求 /emps/depts,服务器需要知道“这个请求是不是已经登录的用户发来的”。登录校验的核心就是:登录成功后给用户一个凭证,后续请求带上凭证,服务器验证凭证后决定是否放行

2 Cookie、Session、Token 对比

方案 凭证保存在哪里 后端是否保存登录状态 优点 缺点
Cookie 浏览器 可以保存,也可以只保存信息 浏览器自动携带 跨域、移动端支持不方便,安全控制麻烦
Session 服务器保存 Session,浏览器保存 SessionId 保存 服务端控制强 集群部署要共享 Session,服务器压力更大
Token / JWT 客户端保存 token 通常不保存 适合前后端分离、移动端、集群 token 泄露后有风险,需要过期时间和签名校验
Cookie
浏览器帮你存着一个小纸条,每次访问网站时自动带上。后端可以选择只存信息(比如用户名),也可以同时保存你的登录状态。优点是浏览器全自动处理,不用你操心;缺点是跨网站时容易出问题,手机App用起来也不方便,而且这个小纸条容易被伪造或窃取,安全性需要额外加固。

Session 服务器自己记小本本,把你的登录信息存在服务器上,然后只给浏览器发一个"门牌号"(SessionId)。优点是服务器完全掌控一切,可以随时让你强制下线、查你在干嘛;缺点是如果网站有多台服务器(集群),这些小本本得互相共享,不然你换个服务器就找不到你的登录信息了,而且服务器存的人越多压力越大。

Token / JWT 服务器给你一个"通行证",你自己收好,里面直接写着你是谁、有什么权限,服务器通常不额外存你的登录状态。优点是特别适合前后端分离、手机App、多台服务器集群的场景,服务器不用记小本本,压力小;缺点是如果这个通行证被别人偷了,对方就能冒充你,所以必须设置过期时间数字签名来防伪造。

前后端分离项目常用 Token / JWT。登录成功后,后端生成 token 返回给前端;前端保存 token;之后每次请求在请求头里带上 token;后端过滤器或拦截器统一校验。

3 JWT 结构

JWT 全称 JSON Web Token,由三部分组成:

Header.Payload.Signature
部分 含义
Header 头部,说明类型和签名算法,例如 HS256
Payload 载荷,保存用户信息、过期时间等声明,不要放密码等敏感信息
Signature 签名,用秘钥对前两部分计算得到,用来防止内容被篡改

JWT 的 Payload 默认只是 Base64URL 编码,不是加密,所以别人拿到 token 可以解码看到里面的信息。JWT 安全主要靠签名防篡改,而不是靠内容不可见。

4 JWT 生成示例

Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "Tom");

String jwt = Jwts.builder()
        .signWith(SignatureAlgorithm.HS256, "itheima") // 签名秘钥
        .setClaims(claims)                             // 自定义内容
        .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
        .compact();

5 JWT 校验为什么不用手动比较

后端生成 JWT 时,会用签名秘钥对 Header 和 Payload 做签名。后端解析 JWT 时,再用同一个秘钥重新计算签名并比较。如果 token 内容被改过,重新计算出来的签名和 token 里的 Signature 对不上,解析就会抛异常或失败。

Claims claims = Jwts.parser()
        .setSigningKey("itheima")
        .parseClaimsJws(jwt)
        .getBody();

所以不是“不比较”,而是 JWT 工具库在 parseClaimsJws(jwt) 内部已经完成了签名校验、过期时间校验等工作。只要使用了正确秘钥,解析成功就说明 token 没被篡改且未过期;解析失败就说明 token 不合法或已过期。

6 请求头 token 是什么

前端通常把 JWT 放在请求头里,例如:

token: eyJhbGciOiJIUzI1NiJ9.xxx.xxx

或者更标准的写法:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx

后端在 Filter / Interceptor 中从请求头取出 token,校验通过就放行,校验失败就返回未登录。

过滤器 Filter

1 Filter 是什么

Filter 是 Java Web 三大组件之一,作用是在请求进入目标资源之前、响应返回浏览器之前做统一处理。它属于 Servlet 规范,不是 Spring 独有。常见用途:登录校验、统一编码、日志记录、敏感词处理等。 请求流程:

浏览器请求
  ↓
Filter 放行前逻辑
  ↓
Controller / 其他资源
  ↓
Filter 放行后逻辑
  ↓
浏览器收到响应

2 Filter 快速入门

@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("init 初始化,只执行一次");
    }

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        System.out.println("放行前逻辑");

        chain.doFilter(request, response); // 放行,请求继续往后走

        System.out.println("放行后逻辑");
    }

    @Override
    public void destroy() {
        System.out.println("destroy 销毁,只执行一次");
    }
}

启动类需要开启 Servlet 组件扫描:

@ServletComponentScan
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3 Filter 生命周期

方法 执行时机
init() Filter 创建后执行一次
doFilter() 每次请求被拦截时执行
destroy() 服务器关闭或 Filter 销毁时执行一次

4 Filter 拦截路径

配置 含义
/* 拦截所有请求
/login 只拦截 /login
/emps/* 拦截 /emps/ 下的所有请求

5 登录校验 Filter 流程

1. 获取请求 URL
2. 判断是否是登录接口 /login
   - 是:直接放行
   - 否:继续校验
3. 从请求头获取 token
4. 判断 token 是否为空
   - 为空:返回未登录
   - 不为空:继续校验
5. 解析 JWT
   - 成功:放行
   - 失败:返回未登录

示例:

@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String url = request.getRequestURL().toString();
        if (url.contains("login")) {
            chain.doFilter(request, response);
            return;
        }

        String jwt = request.getHeader("token");
        if (jwt == null || jwt.isEmpty()) {
            response.getWriter().write("NOT_LOGIN");
            return;
        }

        try {
            Jwts.parser().setSigningKey("itheima").parseClaimsJws(jwt);
        } catch (Exception e) {
            response.getWriter().write("NOT_LOGIN");
            return;
        }

        chain.doFilter(request, response);
    }
}

6.拦截路径和过滤器链

拦截器 Interceptor

1 Interceptor 是什么

Interceptor 是 Spring MVC 提供的拦截机制,主要拦截进入 Controller 的请求。它比 Filter 更贴近 Spring,能拿到 Controller 方法相关信息,常用于登录校验、权限控制、操作日志等。

2 HandlerInterceptor 三个方法

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // Controller 方法执行前执行
        // return true 表示放行,return false 表示拦截
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // Controller 方法执行后、视图渲染前执行;前后端分离项目中用得较少
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        // 整个请求完成后执行,适合清理资源、记录日志
    }
}

3 注册 Interceptor

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

4 拦截路径规则

路径 含义 示例
/* 一级路径 能匹配 /depts,不能匹配 /depts/1
/** 任意层级路径 能匹配 /depts/depts/1/depts/1/emps
/depts/* /depts 下一层路径 能匹配 /depts/1,不能匹配 /depts/1/emps
/depts/** /depts 下任意层级 能匹配 /depts/depts/1/depts/1/emps

5 Filter 和 Interceptor 区别

对比点 Filter Interceptor
所属体系 Servlet 规范 Spring MVC
拦截范围 几乎所有 Web 请求,包括静态资源 主要拦截进入 Controller 的请求
执行位置 在 DispatcherServlet 之前 在 DispatcherServlet 之后、Controller 之前
使用场景 通用 Web 层处理,如编码、日志、登录 Spring MVC 业务相关拦截,如权限、Controller 日志

执行顺序可以理解为:

浏览器
  ↓
Filter
  ↓
DispatcherServlet
  ↓
Interceptor preHandle
  ↓
Controller
  ↓
Interceptor postHandle / afterCompletion
  ↓
Filter 放行后逻辑
  ↓
响应浏览器

全局异常处理

1 为什么需要全局异常处理

Controller、Service、Mapper 任意一层都可能抛异常。如果每个接口都写 try-catch,代码会非常重复。全局异常处理可以统一捕获异常,统一返回 Result.error(...)

2 @RestControllerAdvice + @ExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result ex(Exception e) {
        e.printStackTrace();
        return Result.error("对不起,操作失败,请联系管理员");
    }
}

说明:@RestControllerAdvice 表示这是一个全局 Controller 增强类;@ExceptionHandler(Exception.class) 表示捕获所有 Exception 类型异常;方法返回 Result 后会自动转成 JSON。

记忆:全局异常处理不是阻止异常发生,而是把异常变成统一、友好的响应格式。

Spring 事务管理

1 事务解决什么问题

事务用于保证一组数据库操作要么全部成功,要么全部失败。比如删除部门时,既要删除部门表记录,也要删除该部门下的员工记录。如果部门删了,但员工没删,就会造成数据不一致。

2 @Transactional 基本使用

@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;
    @Autowired
    private EmpMapper empMapper;

    @Transactional
    public void delete(Integer id) {
        deptMapper.deleteById(id);
        empMapper.deleteByDeptId(id);
    }
}

@Transactional 可以加在方法、类、接口上。最常见是加在 Service 层方法上,因为 Service 代表一个完整业务流程。

3 默认回滚规则与 rollbackFor

Spring 事务默认只对 RuntimeExceptionError 回滚。对于普通编译时异常 Exception,默认不一定回滚。为了更稳,可以写:

@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception {
    deptMapper.deleteById(id);
    if (true) {
        throw new Exception("出错了");
    }
    empMapper.deleteByDeptId(id);
}

记忆:想让所有异常都回滚,写 rollbackFor = Exception.class

4 事务传播行为

传播行为描述:一个事务方法调用另一个事务方法时,事务如何合并或新建。

传播行为 含义 常见程度
REQUIRED 默认值,有事务就加入,没有事务就新建 最常用
REQUIRES_NEW 每次都新建事务,外部事务会被挂起 常用于日志、审计等希望独立提交的操作
SUPPORTS 有事务就加入,没有就不用事务 较少
NOT_SUPPORTED 不使用事务 较少
MANDATORY 必须在事务中运行,否则报错 较少
NEVER 必须不在事务中运行,否则报错 较少

例子:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
    // 即使外层业务回滚,这里也可以独立提交
}

AOP 面向切面编程

1 AOP 是什么

AOP 全称 Aspect Oriented Programming,面向切面编程。它的作用是:在不修改原业务代码的前提下,给一批方法统一增强功能。常见增强功能包括统计耗时、日志记录、权限校验、事务控制、缓存处理。 例如很多 Service 方法都要统计执行时间,如果每个方法都手写开始时间和结束时间,会重复且难维护。AOP 可以把“统计耗时”抽成一个切面,统一织入目标方法。

2 AOP 核心概念

概念 含义
JoinPoint 连接点 可以被 AOP 控制的方法执行点,通常理解为目标方法
Pointcut 切入点 用表达式筛选哪些连接点需要增强
Advice 通知 具体增强逻辑,例如方法前、方法后、环绕执行
Aspect 切面 通知 + 切入点的组合
Target 目标对象 被增强的原始对象
Proxy 代理对象 Spring 创建的增强对象,Controller 实际注入的往往是代理对象

3 AOP 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4 @Around 环绕通知

@Component
@Aspect
public class TimeAspect {

    @Around("execution(* com.example.service.impl.*.*(..))")//拦截 `com.example.service.impl` 包下 **所有类的所有方法**
    //切面方法,当拦截到匹配的方法时会执行
    //- `ProceedingJoinPoint`:封装了被拦截方法的全部信息(类名、方法名、参数等)
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
		//- **执行原始目标方法**(被拦截的那个业务方法)
        Object result = joinPoint.proceed(); // 调用原始目标方法

        long end = System.currentTimeMillis();
        System.out.println(joinPoint.getSignature() + "耗时:" + (end - begin) + "ms");
        return result;
    }
}

joinPoint.proceed() 表示调用原始方法。如果不调用它,目标方法就不会真正执行。环绕通知必须把目标方法返回值 return 回去,否则 Controller 可能拿不到正确结果。

5 通知类型

注解 执行时机
@Before 目标方法执行前
@After 目标方法执行后,无论是否异常都会执行
@AfterReturning 目标方法正常返回后执行
@AfterThrowing 目标方法抛异常后执行
@Around 环绕目标方法执行,功能最强,能控制是否放行、能拿返回值

实际开发中,@Around 最常用,因为它可以在方法前后都写逻辑,还能拿到参数、返回值、异常和耗时。

6 @Pointcut 抽取公共切入点

如果多个通知都用同一个切入点表达式,可以抽出来:

@Component
@Aspect
public class TimeAspect {

    @Pointcut("execution(* com.example.service.impl.*.*(..))")
    public void pt() {}

    @Around("pt()")// ← 引用上面定义的切点,不用再写长表达式了
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - begin));
        return result;
    }
}

private 的切入点方法只能在当前切面类内部使用;public 的切入点方法可以被其他切面类引用。

7 execution 切入点表达式

基本语法:

execution(访问修饰符? 返回值 包名.类名.方法名(方法参数) throws 异常?)

常用通配符:

符号 含义
* 匹配任意一个元素,可以是返回值、类名、方法名、一个参数等
.. 匹配任意层级包,或任意个参数

示例:

// 匹配 DeptServiceImpl 的 delete(Integer) 方法
@Around("execution(public void com.example.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

// 匹配 service 包及子包下所有类的所有方法
@Around("execution(* com.example.service..*.*(..))")

// 匹配所有 Service 结尾类的所有方法
@Around("execution(* com.example..*Service.*(..))")

实际项目建议优先按包或注解匹配,不要写过于宽泛的 execution(* *(..))

8 @annotation 切入点表达式

@annotation 用来匹配加了某个注解的方法。适合“只增强标记过的方法”。

自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

切面:

@Around("@annotation(com.example.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

使用:

@Log
@PostMapping("/depts")
public Result save(@RequestBody Dept dept) {
    deptService.save(dept);
    return Result.success();
}

这种方式比按包名匹配更精准,适合操作日志:哪个接口需要记录日志,就加 @Log

9 JoinPoint 能拿到什么

在通知中可以获取目标方法信息:

Signature signature = joinPoint.getSignature(); // 方法签名
Object[] args = joinPoint.getArgs();            // 方法参数
Object target = joinPoint.getTarget();          // 目标对象

ProceedingJoinPoint 中还可以调用:

Object result = joinPoint.proceed();            // 执行目标方法并拿返回值

10 多个切面的执行顺序

如果多个切面都匹配同一个方法,可以用 @Order 控制顺序:

@Order(1)
@Aspect
@Component
public class Aspect1 {}

@Order(2)
@Aspect
@Component
public class Aspect2 {}

数字越小,优先级越高。对于 @Around,可以理解为优先级高的切面包在外层:先进入,最后退出。

Spring Boot 配置

1 配置文件类型

Spring Boot 支持三种常见配置文件:

application.properties
application.yml
application.yaml

三者都能写配置,但推荐统一使用一种。教程建议使用 yml,因为层级结构清晰。

示例:

server.port=8081
server:
  port: 8081

2 配置优先级

同一个配置项出现在多个位置时,优先级从低到高大致是:

application.yaml
  < application.yml
  < application.properties
  < Java 系统属性(-Dxxx=xxx)
  < 命令行参数(--xxx=xxx)

例如:

java -Dserver.port=9000 -jar app.jar --server.port=10010

最终端口是 10010,因为命令行参数优先级更高。

3 Java 系统属性和命令行参数

Java 系统属性:

-Dserver.port=9000

命令行参数:

--server.port=10010

它们常用于部署时临时覆盖配置,不需要重新打包项目。

4 @Value 与 @ConfigurationProperties 对比

注解 适合场景 示例
@Value 少量配置、单个值注入 @Value("${server.port}")
@ConfigurationProperties 一组配置批量绑定到对象 OSS、数据库、第三方服务配置

简单记忆:一个两个配置用 @Value,一组配置用 @ConfigurationProperties

配置类、@Configuration@Bean

配置类是专门给 Spring 写启动配置的类。它不负责处理具体业务请求,而是在 Spring Boot 启动时告诉 Spring:哪些对象要创建、哪些组件要注册、哪些框架规则要生效。

配置类通常使用 @Configuration 标记。@Configuration 表示这个类会被 Spring 当作配置类加载。配置类里可以写 @Bean 方法,@Bean 表示把方法返回的对象注册到 Spring IOC 容器中,之后其他类可以通过 @Autowired 注入使用。

配置类常见作用有两个:第一,手动注册 Bean,例如第三方工具类、RedisTemplate、线程池对象等;第二,配置框架规则,例如注册拦截器、配置跨域、配置消息转换器等。

例如登录拦截器本身只负责判断是否登录,但“拦截哪些路径、放行哪些路径”需要写在配置类中:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

Bean 管理与 IOC 容器

1 Bean 是什么

Bean = Spring 容器管理的 Java 对象。

你平时写的类本来只是普通类,加了 @Controller@RestController@Service@Component@Mapper@Configuration,或者通过 @Bean 方法注册之后,Spring 启动时会帮你创建对象并放进 IOC 容器。这个对象就叫 Bean。以后别的类需要它,不用自己 new,直接 @Autowired,Spring 会从容器里找出来注入进去。 Bean 就是被 Spring 创建、保存和管理的对象。平时写的 Controller、Service、Mapper,如果被 Spring 扫描并注册进容器,就都是 Bean。

常见注册 Bean 的方式:

注解 常用位置 含义
@Component 普通组件类 通用 Bean
@Controller / @RestController Controller 类 Web 控制层 Bean
@Service Service 类 业务层 Bean
@Repository / @Mapper DAO / Mapper 数据访问层 Bean
@Bean 配置类方法 把方法返回对象注册成 Bean
维度 @Component @Bean
作用位置 加在我们自己编写的类 加在配置类的方法
注册方式 配合包扫描自动识别、自动创建实例 手动编写对象创建逻辑,方法返回值即为 Bean
适用场景 自己写的业务类、工具类、切面等 第三方库的类(无法修改源码加注解)、需要自定义初始化逻辑的对象
灵活度 低,按默认规则实例化 高,可以完全控制对象的创建、参数配置、初始化流程

2 ApplicationContext 是什么

ApplicationContext 是 Spring 提供的 IOC 容器接口。它不是业务类,也不是所有类的父类。它负责保存和管理 Spring 创建出来的 Bean。

@Autowired
private ApplicationContext applicationContext;

@Test
public void testGetBean() {
    DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
    DeptController bean2 = applicationContext.getBean(DeptController.class);
    DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
}

平时开发一般不手动 getBean(),而是使用依赖注入:

@Autowired
private EmpService empService;

本质上就是 Spring 从 IOC 容器中找到 EmpService 类型的 Bean,然后赋值给这个字段。

3 Bean 的作用域 scope

作用域 含义 常见程度
singleton 容器中同名 Bean 只有一个实例,默认 最常用
prototype 每次获取 Bean 都创建新实例 少用
request 每个 HTTP 请求创建一个实例 Web 环境
session 每个 HTTP Session 创建一个实例 Web 环境
application 每个 Web 应用创建一个实例 Web 环境

示例:

@Scope("prototype")
@RestController
public class DeptController {
}

注意:大部分 Bean 都用默认的 singleton。单例 Bean 不是线程绝对安全的,如果 Bean 中保存可变成员变量,多线程请求可能有问题。实际开发中 Controller / Service 一般不保存请求级状态。

4 第三方 Bean:@Bean

如果对象不是你自己写的类,不能在源码上加 @Component,可以用 @Bean 把它注册进容器。 有些是第三方 jar 包中的类,我们没法修改它的源码加 @Component,因此用 @Bean 方式手动构造、配置并注册到容器中。

@Configuration
public class CommonConfig {

    @Bean
    public SAXReader saxReader() {
        return new SAXReader();
    }
}

@Bean 方法的返回值会放入 IOC 容器。默认 Bean 名称是方法名,例如这里是 saxReader。如果需要指定名称:

@Bean("reader")
public SAXReader saxReader() {
    return new SAXReader();
}

Spring Boot 原理、常用注解与 Maven 高级笔记

一、Spring Boot 的核心原理:起步依赖与自动配置

Spring Boot 最重要的简化点有两个:起步依赖 starter 负责把一类功能需要的 jar 包统一带进来,自动配置 auto-configuration 负责根据这些 jar 包和配置文件自动创建默认 Bean。传统 Spring 项目需要手动引入 Spring MVC、Tomcat、Jackson、日志、校验等依赖,还要写很多 XML 或 Java 配置;Spring Boot 把这些常见组合封装成 starter,并在启动时根据条件自动装配。最典型的例子是引入 spring-boot-starter-web 后,项目会自动拥有 Web 开发能力:内置 Tomcat 用来接收 HTTP 请求,Spring MVC 用来分发请求,Jackson 用来把 Java 对象和 JSON 相互转换,日志和基础配置也会一起准备好。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

starter 本身不一定写很多功能代码,它更像一个“依赖清单”。你引入一个 starter,就等于一次性引入一组经过 Spring Boot 官方或第三方整理好的依赖,并且版本通常由 Spring Boot 父工程统一管理,所以你不用手动关心每个 jar 包的版本是否兼容。记忆方式很简单:starter 解决 jar 包怎么引入的问题,自动配置解决对象怎么创建和配置的问题。如果只引了依赖但没有自动配置,你仍然要自己写很多配置类;如果只有自动配置但缺少依赖,相关类不存在,也无法真正创建 Bean。

1.1 Spring Boot 到底解决了什么问题

Spring Boot 并不是一个脱离 Spring 的新框架,它本质上仍然建立在 Spring Framework 之上。Spring Framework 已经提供了 IOC、依赖注入、AOP、事务、Spring MVC 等核心能力,但传统 Spring 项目存在两个明显问题:一是依赖数量多、版本容易冲突;二是配置工作繁琐,开发者需要手动创建大量基础 Bean。

Spring Boot 在 Spring 的基础上增加了一套约定和自动化机制,核心目标是让开发者快速创建可以独立运行的 Spring 应用。大多数 Spring Boot 项目可以直接打成可执行 JAR,通过 java -jar 启动,而不需要提前把应用部署到外部 Tomcat 中。

Spring Boot 最重要的简化机制可以概括为四点:

机制 解决的问题
起步依赖 Starter 一组相关 jar 包应该怎样引入
依赖版本管理 各个 jar 包使用什么兼容版本
自动配置 Auto-configuration 常见基础 Bean 应该怎样创建
内嵌服务器 Web 项目怎样直接运行,而不需要单独部署 Tomcat

因此,Spring Boot 的核心并不是“少写几个注解”,而是把大量具有通用规律的配置工作封装到了框架内部。

1.2 Starter:解决依赖如何引入的问题

传统 Spring Web 项目通常要手动引入 Spring MVC、Servlet API、Jackson、Tomcat、日志框架和参数校验等依赖,而且不同依赖之间必须保持版本兼容。Spring Boot 将一类功能需要的依赖组合成一个起步依赖,例如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

引入 spring-boot-starter-web 后,Maven 会间接引入 Web 开发常用的组件,包括:

spring-boot-starter-web
├── spring-web
├── spring-webmvc
├── spring-boot-starter-json
│   └── Jackson
├── spring-boot-starter-tomcat
│   └── 内嵌 Tomcat
└── spring-boot-starter
    └── 默认日志体系

Starter 本身通常没有多少业务功能代码,更像一个经过整理的“依赖集合”。它通过 Maven 的传递依赖机制,把开发某类功能需要的 jar 包统一带入项目。

常见 Starter 如下:

Starter 主要用途
spring-boot-starter-web Spring MVC、JSON、Tomcat Web 开发
spring-boot-starter-validation Jakarta Validation 参数校验
spring-boot-starter-jdbc JDBC、数据库连接池
spring-boot-starter-data-jpa Spring Data JPA、Hibernate
spring-boot-starter-data-redis Redis 客户端与 Spring Data Redis
spring-boot-starter-security Spring Security
spring-boot-starter-test JUnit、Spring Test、Mockito、AssertJ 等测试工具
spring-boot-starter-actuator 健康检查、指标监控和运行状态管理

需要注意,Starter 只负责把相关依赖放到类路径中,它不会直接完成 Bean 的创建。真正根据这些依赖创建默认对象的是自动配置机制。 因此可以记成:

Starter:准备原材料,也就是 jar 包
自动配置:根据原材料创建默认 Bean
IOC 容器:保存并管理这些 Bean

1.3 Spring Boot 如何管理依赖版本**

很多 Spring Boot 项目的 pom.xml 中并没有为每个依赖写 <version>,这是因为 Spring Boot 已经统一管理了大量常用依赖的版本。 继承父工程时,通常写成:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.x.x</version>
    <relativePath/>
</parent>

spring-boot-starter-parent 会进一步继承 Spring Boot 的依赖管理配置。它通过 Maven 的 <dependencyManagement> 统一声明 Jackson、Tomcat、Logback、JUnit 等常用库的兼容版本。 这意味着下面的依赖可以省略版本:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

省略版本并不代表“没有版本”,而是 Maven 会从父工程的 dependencyManagement 中找到对应版本。

如果项目不能继承 Spring Boot 父工程,也可以只导入 Spring Boot 的 BOM: BOM 本质上也是一个 POM 文件,只是它不是用来打包代码的,而是专门用来集中管理依赖版本的 POM。 普通 pom.xml:我这个项目要引入哪些依赖、怎么打包、用哪些插件 BOM pom:我只提供一张依赖版本表,告诉别人这些依赖该用什么版本 BOM 也是一个 pom.xml,但是它重点不是写业务依赖,而是写在:

<dependencyManagement>
    <dependencies>
        ...
    </dependencies>
</dependencyManagement>

一个简化版pom:

<!-- 
  这是 Maven 项目的根配置,声明这是一个 XML 文件。
  xmlns 是命名空间,可以暂时理解为“这个文件的格式说明书地址”。
-->
<project>
    <!-- 
        modelVersion:Maven 模型版本。
        对于 Maven 2.x 和 3.x,这个值固定是 4.0.0。
        就像说“我遵循的是 2020 年发布的说明书版本”。
    -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 
        groupId:这个 BOM 项目所属的组织或公司的唯一标识。
        通常用公司域名的倒序,比如 com.example。
        它定义了“谁发布了这份版本清单”。
    -->
    <groupId>com.example</groupId>

    <!-- 
        artifactId:这个 BOM 项目的唯一 ID。
        它定义了“这份版本清单叫什么名字”。
        这里叫 my-bom,意味着其他项目可以通过这个名字来引用它。
    -->
    <artifactId>my-bom</artifactId>

    <!-- 
        version:这个 BOM 项目自身的版本号。
        代表“这份版本清单的第 1.0.0 版”。
        当其他项目引用它时,也需要指定这个版本号。
    -->
    <version>1.0.0</version>

    <!-- 
        packaging:打包方式。
        pom:表示这是一个“纯配置”项目,它不包含任何 Java 代码,
        不会生成 jar 或 war 包,只生成一个供其他项目引用的 pom 文件。
        这是所有 BOM 和父工程的标配。
    -->
    <packaging>pom</packaging>

    <!-- 
        dependencyManagement:依赖版本管理中心。
        这是 BOM 最核心的部分!它的作用是“声明版本”,而不是“引入依赖”。
        它像一本“官方价目表”,告诉其他项目:“如果你用到下面这些依赖,
        请参考我指定的版本,以保证兼容性。”
    -->
    <dependencyManagement>
        <dependencies>
            <!-- 
                这里声明了一个依赖:Spring 的 Web 模块。
                注意:这里只有 groupId、artifactId 和 version。
                当其他项目通过 <scope>import</scope> 导入这个 BOM 后,
                再引入 spring-web 时就可以省略 version 标签了,
                Maven 会自动从这份 BOM 里找到版本号 6.1.6 并填入。
            -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <version>6.1.6</version>
            </dependency>

            <!-- 
                这里声明了 Jackson 的核心数据绑定库。
                同样,其他项目导入此 BOM 后,引入 jackson-databind 时,
                就不需要再写 <version>2.17.0</version> 了。
                这能确保一个组织内所有项目使用相同的 Jackson 版本,
                避免因版本不一致导致的各种奇怪问题。
            -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.17.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

这个 BOM 的意思不是“我要引入 spring-webjackson-databind”。 而是:

以后谁导入了我这个 BOM,
谁再使用 spring-web,就默认用 6.1.6;
谁再使用 jackson-databind,就默认用 2.17.0。

重点区别:**BOM 里的 dependencyManagement 只管版本,不会真的把 jar 包引进来。 BOM 主要体现在这几个标签:

<packaging>pom</packaging>

说明这个项目不是打成 jar,而是一个 POM 类型的管理项目。

<dependencyManagement>
    <dependencies>
        ...
    </dependencies>
</dependencyManagement>

这里面放“依赖版本表”。

如果你要在自己的项目里使用别人的 BOM,需要这样写:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

这里最关键的是两个标签:

<type>pom</type>
<scope>import</scope>

意思是:

type=pom:我要导入的不是 jar,而是一个 pom 文件
scope=import:把这个 pom 里的 dependencyManagement 版本管理规则导入进来

所以 BOM 的典型形式就是:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>xxx</groupId>
            <artifactId>xxx-bom</artifactId>
            <version>xxx</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

因为很多 Spring Boot 项目用了这个:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version>
</parent>

spring-boot-starter-parent 这个父 POM 里面已经帮你导入了 spring-boot-dependencies BOM。 所以你表面上看到的是:

<parent>
    ...
</parent>

实际上背后已经有 BOM 在管版本。 可以理解成:

spring-boot-starter-parent
        ↓
内部使用 spring-boot-dependencies
        ↓
spring-boot-dependencies 就是 Spring Boot 官方 BOM
        ↓
所以你的 starter 可以不写 version

因此,Starter 和依赖版本管理并不是一回事:

  • Starter 决定引入哪些依赖。
  • BOM 或父工程决定这些依赖使用什么版本。

1.4 Spring Boot 启动入口与整体启动流程

Spring Boot 项目的启动入口通常是下面这样:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

main 方法本身仍然是普通 Java 程序的入口。真正负责启动 Spring Boot 的是:SpringApplication.run(NetworkDiskApplication.class, args); SpringApplication 是 Spring Boot 提供的启动器,用于从 main() 方法引导并启动整个 Spring 应用。 Spring Boot 启动可以先按一条主线理解:

启动类 main 方法
        ↓
SpringApplication.run(...)
        ↓
判断应用类型,准备环境配置
        ↓
创建 ApplicationContext,也就是 IoC 容器
        ↓
加载启动类、组件类、自动配置类,注册 BeanDefinition
        ↓
执行 refresh(),创建非懒加载单例 Bean,完成依赖注入和初始化
        ↓
如果是 Web 项目,启动内嵌 Tomcat
        ↓
应用启动完成,可以接收请求

其中,@SpringBootApplication 修饰的类通常就是启动源配置类。Spring Boot 会从这个类开始,扫描当前包及其子包中的组件,并加载自动配置类。

@SpringBootApplication
public class NetworkDiskApplication {
    public static void main(String[] args) {
        SpringApplication.run(NetworkDiskApplication.class, args);
    }
}

这里的 NetworkDiskApplication.class 会作为源配置类传入 Spring Boot。它不是普通意义上的“配置文件”,而是 Spring Boot 启动分析的入口。

1 判断应用类型 Spring Boot 启动时会先根据类路径中的依赖判断当前应用类型:

普通非 Web 应用
Servlet Web 应用
Reactive Web 应用

例如项目引入了:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

类路径中就会出现 Spring MVC、Servlet、Tomcat 等相关类,Spring Boot 通常会判断这是一个 Servlet Web 应用,后续会创建 Web 环境下的 ApplicationContext,并启动内嵌 Tomcat。

2 准备 Environment 在真正创建和刷新容器前,Spring Boot 会先准备 EnvironmentEnvironment 可以理解为 Spring 对“外部配置”的统一抽象,它会把不同来源的配置合并起来。 常见配置来源包括:

application.properties
application.yml
application-{profile}.yml
环境变量
JVM 系统属性
命令行参数
配置中心
其他 PropertySource

例如:

server:
  port: 8081

spring:
  application:
    name: networkdisk-gateway

启动时可以用命令行参数覆盖:

java -jar app.jar --server.port=9090

这些配置进入 Environment 后,后续很多机制都会从里面读取值:

@Value
@ConfigurationProperties
@ConditionalOnProperty
自动配置类
日志配置
Web 服务器端口配置

比如 server.port=8081 最后会影响内嵌 Tomcat 监听哪个端口。

3 创建 ApplicationContext ApplicationContext 就是 Spring 的 IoC 容器。Spring Boot 会根据前面判断出的应用类型创建不同的容器。 例如

普通应用:AnnotationConfigApplicationContext
Servlet Web 应用:ServletWebServerApplicationContext
Reactive Web 应用:ReactiveWebServerApplicationContext

你的 Spring Boot Web 项目一般创建的是:

ServletWebServerApplicationContext

这个容器既有普通 Spring 容器的能力,也有启动 Web 服务器的能力。

4 加载源配置类、自动配置类和 BeanDefinition 容器创建之后,Spring 并不是马上 new 出所有对象,而是先读取各种配置,把每个 Bean 的“定义信息”注册到容器中。 这个定义信息叫:

BeanDefinition

它可以理解成 Bean 的“创建说明书”。

里面记录:

Bean 的 class 类型
Bean 的名称
作用域 singleton / prototype
是否懒加载
构造方法参数
依赖关系
初始化方法
销毁方法

BeanDefinition 的来源主要有:

@ComponentScan 扫描到的 @Controller、@Service、@Repository、@Component
@Configuration 配置类中的 @Bean 方法
@Import 导入的配置类
@EnableAutoConfiguration 加载的自动配置类
XML 配置
程序化注册的 BeanDefinition

比如你写:

@Service
public class UserServiceImpl implements UserService {
}

Spring 启动时会先把它解析成一个 BeanDefinition,而不是一开始就直接创建 UserServiceImpl 对象。

5 refresh:实例化 Bean,完成依赖注入和初始化 refresh() 是 Spring 容器启动的核心方法。大部分关键动作都发生在这里。 可以简化成:

准备 BeanFactory
        ↓
执行 BeanFactoryPostProcessor
        ↓
注册 BeanPostProcessor
        ↓
初始化消息源、事件广播器
        ↓
创建非懒加载单例 Bean
        ↓
完成依赖注入
        ↓
执行初始化回调
        ↓
发布容器启动完成事件

一个普通 Bean 的创建过程可以简化为:

读取 BeanDefinition
        ↓
选择构造方法并实例化对象
        ↓
填充属性,完成依赖注入
        ↓
执行 Aware 回调
        ↓
执行 BeanPostProcessor 前置处理
        ↓
执行 @PostConstruct、InitializingBean、initMethod
        ↓
执行 BeanPostProcessor 后置处理
        ↓
Bean 创建完成,可以被业务代码使用

这里要注意:AOP 代理通常也是通过 BeanPostProcessor 创建的。

所以你注入的对象不一定是原始对象。例如:

@Autowired
private UserService userService;

如果 UserService 上有事务、权限、日志切面,那么注入进来的可能是代理对象。

6 启动内嵌 Web 服务器

如果当前应用是 Web 应用,Spring Boot 会在容器刷新过程中启动内嵌服务器,例如 Tomcat。

例如你引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

它默认会带入:

Spring MVC
内嵌 Tomcat
Jackson
日志
参数校验等 Web 相关依赖

启动时,Spring Boot 会根据配置创建并启动 Tomcat:

读取 server.port
        ↓
创建 TomcatServletWebServerFactory
        ↓
创建 TomcatWebServer
        ↓
启动 Tomcat
        ↓
注册 DispatcherServlet
        ↓
开始接收 HTTP 请求

1 创建 ApplicationContext ApplicationContext 就是 Spring 的 IOC 容器。不同类型的应用会创建不同类型的容器,例如 Servlet Web 应用会创建支持 Web 环境的ApplicationContext。 容器在启动过程中主要完成:

读取配置元数据
注册 BeanDefinition
执行 BeanFactoryPostProcessor
执行 BeanPostProcessor
实例化非懒加载单例 Bean
完成依赖注入
执行初始化方法
启动 Web 服务器

2 准备 Environment Spring Boot 会读取并合并各种外部配置,包括:

application.properties
application.yml
application-{profile}.yml
环境变量
JVM 系统属性
命令行参数
配置中心或其他 PropertySource

这些配置最终会统一放入 Spring 的 Environment 中,后续自动配置、@Value@ConfigurationProperties 和条件注解都可以从中读取属性。 Spring Boot 支持将配置放在 YAML、Properties、环境变量和命令行参数等不同来源中,使同一份应用代码可以运行在不同环境。 例如:

server:
  port: 8081

spring:
  application:
    name: networkdisk-gateway

可以通过下面的命令行参数覆盖:

java -jar app.jar --server.port=9090

3 加载配置类和 BeanDefinition Spring 不会一开始就直接把所有对象实例化出来,而是先把对象的定义信息保存为 BeanDefinitionBeanDefinition 可以理解为 Bean 的“创建说明书”,其中记录:

Bean 对应的类
Bean 的名称
作用域 singleton 或 prototype
是否懒加载
构造参数
依赖关系
初始化方法
销毁方法

Spring IOC 容器正是根据这些定义信息创建和管理 Bean。BeanDefinition 的来源主要包括:

@ComponentScan 扫描到的组件
@Configuration 中的 @Bean 方法
@Import 导入的配置
自动配置类提供的 Bean
XML 配置中声明的 Bean
程序化注册的 Bean

5 刷新 IOC 容器 容器执行 refresh() 时,会完成大部分核心工作,包括解析配置、注册处理器、创建单例 Bean、进行依赖注入和执行生命周期回调。 一个普通 Bean 的创建过程可以简化为:

读取 BeanDefinition
        ↓
选择构造方法并实例化
        ↓
填充属性和注入依赖
        ↓
执行 Aware 回调
        ↓
执行 BeanPostProcessor 前置处理
        ↓
执行 @PostConstruct、InitializingBean、initMethod
        ↓
执行 BeanPostProcessor 后置处理
        ↓
Bean 可以被业务代码使用

AOP 代理通常也是通过 BeanPostProcessor 在 Bean 初始化阶段创建的。因此,注入到 Controller 中的 Service 有时并不是原始对象,而是被事务、权限、日志或其他切面增强后的代理对象。

1.5 @SpringBootApplication 为什么能启动整个项目

@SpringBootApplication 是一个组合注解,核心包含:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
}

官方文档将它概括为同时启用自动配置、组件扫描和额外配置能力。

注解 作用
@SpringBootConfiguration 声明当前类是 Spring Boot 的主配置类
@ComponentScan 扫描当前包及子包中的业务组件
@EnableAutoConfiguration 开启 Spring Boot 自动配置

1 @SpringBootConfiguration @SpringBootConfiguration 本质上是一个特殊的 @Configuration。它告诉 Spring Boot:

这个类是当前应用的主要配置入口。

配置类中可以继续定义 @Bean

@SpringBootApplication
public class Application {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

不过实际项目中通常会把较复杂的配置拆分到独立的 @Configuration 类中,而不是全部堆积在启动类里。

2 @ComponentScan @ComponentScan 负责扫描自己项目中的组件类。假设启动类位于:

com.disk.NetworkDiskApplication

默认扫描范围是:

com.disk
com.disk.controller
com.disk.service
com.disk.mapper
com.disk.infrastructure
以及 com.disk 下的其他子包

因此推荐把启动类放在项目根包下:

com.disk
├── NetworkDiskApplication.java
├── controller
├── service
├── mapper
├── domain
└── infrastructure

如果某个类放在 org.example 中,它不属于 com.disk 的子包,默认不会被扫描。 可以显式扩大扫描范围:

@SpringBootApplication(scanBasePackages = {
        "com.disk",
        "org.example"
})
public class Application {
}

或者单独使用:

@ComponentScan({
        "com.disk",
        "org.example"
})

Spring Framework 官方文档也说明,@ComponentScan 用于启用基于包路径的组件扫描。

@SpringBootApplication 是一个组合注解,核心包含 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan@SpringBootConfiguration 表示当前类是 Spring Boot 配置类,本质上也是一种配置类;@ComponentScan 负责扫描启动类所在包及其子包里的组件,例如 @Controller@Service@Component 等;@EnableAutoConfiguration 负责开启自动配置,它会通过 @Import 导入 AutoConfigurationImportSelector,再去读取 Spring Boot 和第三方 starter 中声明的自动配置类,最后根据条件判断把符合要求的配置类加载进 IOC 容器。

组成部分 主要作用 速记
@SpringBootConfiguration 声明当前启动类也是配置类 当前类可以参与 Spring 配置
@ComponentScan 扫描启动类所在包及子包中的组件 扫描你自己写的 Controller、Service 等
@EnableAutoConfiguration 开启 Spring Boot 自动配置机制 加载框架和 starter 提供的默认配置

1.6 自动配置的完整原理

自动配置的目标是:

根据当前项目已经引入的依赖、已有 Bean、配置文件和应用类型,自动提供一套合理的默认 Bean。

Spring Boot 官方对自动配置的描述是:它会根据项目中已经添加的 jar 依赖尝试配置 Spring 应用。 例如项目中引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

并配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/network_disk
    username: root
    password: 123456

Spring Boot 会尝试自动创建:

DataSource
JdbcTemplate
事务管理器
数据库初始化相关组件

但它必须同时满足若干条件,例如:

类路径存在 JDBC 和 DataSource
配置文件存在数据库连接信息
容器中没有用户自己定义的 DataSource
当前环境符合该自动配置的要求

1 自动配置类从哪里来 在较新的 Spring Boot 中,自动配置类通常声明在:

META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容是自动配置类的全限定名:

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration

Spring Boot 启动时会读取这些候选自动配置类,然后再通过条件注解逐个筛选。 因此不是:

扫描所有 jar 包 → 找到全部配置类

而更接近:

读取自动配置候选清单
        ↓
导入候选自动配置类
        ↓
检查每个配置类的条件
        ↓
只让满足条件的配置生效

2 @Conditional 为什么重要 如果没有条件判断,Spring Boot 一旦发现几百个自动配置类,就会尝试创建数据库、Redis、消息队列、Web、Security 等所有组件,这显然不可行。 条件注解让自动配置能够做到“按需启用”。

条件注解 含义
@ConditionalOnClass 类路径存在指定类时生效
@ConditionalOnMissingClass 类路径不存在指定类时生效
@ConditionalOnBean IOC 容器存在指定 Bean 时生效
@ConditionalOnMissingBean IOC 容器不存在指定 Bean 时生效
@ConditionalOnProperty 配置属性满足要求时生效
@ConditionalOnWebApplication 当前是 Web 应用时生效
@ConditionalOnNotWebApplication 当前不是 Web 应用时生效
@ConditionalOnResource 类路径存在指定资源时生效
@ConditionalOnExpression SpEL 表达式计算为真时生效

例如你的 pgvector 配置:

@Configuration
@ConditionalOnProperty(
        name = "com.disk.ai.pgvector.enabled",
        havingValue = "true"
)
public class PgVectorConfiguration {
}

意味着只有配置文件中存在:

com:
  disk:
    ai:
      pgvector:
        enabled: true

该配置类才会生效。

如果没有配置或者值不是 true,其中的 DataSourceJdbcTemplate 就不会被注册。还可以设置默认匹配:

@ConditionalOnProperty(
        name = "com.disk.ai.pgvector.enabled",
        havingValue = "true",
        matchIfMissing = true
)

matchIfMissing = true 表示即使没有配置该属性,也视为满足条件。

3 用户配置为什么能够覆盖默认配置

自动配置中经常出现:

@Bean
@ConditionalOnMissingBean
public ObjectMapper objectMapper() {
    return new ObjectMapper();
}

它的含义不是“强制创建一个 ObjectMapper”,而是:

容器里没有 ObjectMapper
    → 创建默认 ObjectMapper

容器里已经有用户定义的 ObjectMapper
    → 自动配置退让,不再创建

这就是 Spring Boot 常说的“约定优于配置,但允许用户覆盖”。框架先提供合理默认值,让普通项目开箱即用;业务有特殊需求时,再由开发者自定义 Bean 替代默认配置。

1.7 为什么引入 Web Starter 后 Tomcat 能直接启动

引入 spring-boot-starter-web 后,项目中会出现 Servlet、Spring MVC 和内嵌 Tomcat 相关类。Web 自动配置检测到这些类后,会创建 Web 应用需要的基础组件。

大致包括:

ServletWebServerFactory
TomcatServletWebServerFactory
DispatcherServlet
DispatcherServletRegistrationBean
HandlerMapping
HandlerAdapter
HttpMessageConverter
Jackson JSON 转换器
异常处理相关组件
静态资源处理器

Spring Boot 启动过程中会通过 ServletWebServerFactory 创建并启动内嵌 Tomcat。 因此项目运行时不是:

Spring Boot 运行在一个事先启动的 Tomcat 中

而是:

main 方法启动 Spring Boot
        ↓
Spring Boot 创建 IOC 容器
        ↓
IOC 容器创建 TomcatServletWebServerFactory
        ↓
Spring Boot 在当前 JVM 中启动 Tomcat
        ↓
Tomcat 开始监听端口

一次 HTTP 请求到达后,典型流程为:

浏览器或前端发送 HTTP 请求
        ↓
Tomcat 接收网络连接
        ↓
请求进入 DispatcherServlet
        ↓
HandlerMapping 查找 Controller 方法
        ↓
HandlerAdapter 调用 Controller
        ↓
Controller 调用 Service
        ↓
Service 调用 Mapper 或其他服务
        ↓
返回 Java 对象
        ↓
HttpMessageConverter 使用 Jackson 转成 JSON
        ↓
Tomcat 返回 HTTP 响应

Tomcat 负责底层 HTTP 连接、Servlet 容器和请求响应;Spring MVC 负责请求路由、参数绑定、控制器调用、异常处理和响应转换。

自动配置的大致流程可以记成一条链:启动类运行 → 解析 @SpringBootApplication → 开启组件扫描和自动配置 → 通过 @Import 找到自动配置导入选择器 → 读取自动配置类清单 → 根据 @Conditional 条件筛选 → 创建符合条件的 Bean → 放入 IOC 容器。这也是为什么你没有手写 Tomcat、DispatcherServlet、JSON 转换器,它们却能正常工作的原因:不是“凭空出现”,而是 Spring Boot 的自动配置类在满足条件时帮你创建好了。

二、@ComponentScan 、@Import 与 @Conditional

2.1 @ComponentScan 、@Import 与 @Conditional

组件扫描决定“你自己写的类能不能被 Spring 找到”。默认情况下,@SpringBootApplication 会扫描启动类所在包及其子包。假设启动类是 com.example.Application,那么 com.example.controllercom.example.servicecom.example.mapper 一般都能扫描到;如果你把类放到 com.other,默认扫描不到,@Autowired 可能注入失败。实际开发中建议把启动类放在项目根包下,例如 com.example,其他代码都放到它的子包中,这样最省事、最稳定。

// 推荐结构
com.example.Application
com.example.controller.EmpController
com.example.service.EmpService
com.example.mapper.EmpMapper

// 如果确实要扫描多个包,可以显式指定
@SpringBootApplication
@ComponentScan({"com.example", "com.other"})
public class Application {}

@ComponentScan 是“按包扫描”,而 @Import 是“手动导入指定类”。扫描适合导入自己项目里带注解的组件,@Import 适合框架或配置类主动把某些类放进容器。

@Import 是 Spring 里的一个注解,作用很直接:把某些类“导入”到 Spring 容器中,让它们成为 Bean 或参与配置解析。 你可以把它理解成:

@ComponentScan:自动扫描当前包及子包里的组件
@Import:手动指定我要把哪些类交给 Spring 管理

1.最基础用法:直接导入一个普通类

假设有一个类没有加 @Component

public class SmsService {

    public void send() {
        System.out.println("发送短信");
    }
}

正常情况下,Spring 不会管理它,因为它没有:

@Component
@Service
@Configuration

但是可以用 @Import 手动导入:

@Configuration
@Import(SmsService.class)
public class AppConfig {
}

这样 SmsService 就会被注册进 Spring 容器,你就可以注入它:

@RestController
public class TestController {

    private final SmsService smsService;

    public TestController(SmsService smsService) {
        this.smsService = smsService;
    }

    @GetMapping("/test")
    public String test() {
        smsService.send();
        return "ok";
    }
}

这时 SmsService 虽然没有 @Component,但仍然是 Spring Bean。

2.导入配置类:最常见、最推荐的用法 更多时候,@Import 不是导入普通类,而是导入一个配置类。 例如:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        return template;
    }
}

然后在主配置类里导入它:

@Configuration
@Import(RedisConfig.class)
public class AppConfig {
}

效果相当于告诉 Spring:

除了 AppConfig 这个配置类,
RedisConfig 这个配置类也要一起解析。
RedisConfig 里面的 @Bean 方法也要生效。

所以 redisTemplate() 方法返回的对象会被注册为 Bean。

这类用法在框架和中间件整合里很常见,比如:

@Import(MyBatisConfig.class)
@Import(RedisConfig.class)
@Import(SecurityConfig.class)

3.@Import 可以导入哪些东西?

@Import 主要有三种用法:

1. 导入普通类
2. 导入配置类
3. 导入 ImportSelector / ImportBeanDefinitionRegistrar

前两个你日常开发能看懂就够了。第三个是 Spring Boot 自动配置底层常用的高级玩法。

4.高级用法一:导入 ImportSelector ImportSelector 的作用是:根据条件返回一批需要导入的类名。

例如:

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {
                "com.example.service.SmsService",
                "com.example.service.EmailService"
        };
    }
}

然后使用:

@Configuration
@Import(MyImportSelector.class)
public class AppConfig {
}

效果是:Spring 会执行 selectImports(),然后把返回的类注册进容器。

也就是说:

@Import(MyImportSelector.class)
        ↓
执行 selectImports()
        ↓
返回 SmsService、EmailService 的全限定类名
        ↓
Spring 把这些类导入容器

这种方式比直接 @Import(SmsService.class) 更灵活,因为它可以根据条件动态决定导入谁。

5.高级用法二:导入 ImportBeanDefinitionRegistrar ImportBeanDefinitionRegistrar 更底层。它的作用是:直接向 Spring 容器注册 BeanDefinition。 例如:

public class MyRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata,
            BeanDefinitionRegistry registry) {

        RootBeanDefinition beanDefinition =
                new RootBeanDefinition(SmsService.class);

        registry.registerBeanDefinition("smsService", beanDefinition);
    }
}

使用:

@Configuration
@Import(MyRegistrar.class)
public class AppConfig {
}

这个过程比 ImportSelector 更底层:

ImportSelector:返回类名,让 Spring 自己注册
ImportBeanDefinitionRegistrar:我自己手动注册 BeanDefinition

很多框架会用这种方式做复杂注册,比如根据接口生成代理对象。 例如你熟悉的这类注解底层经常会用到类似机制:

@MapperScan
@EnableFeignClients
@EnableDubbo
@EnableScheduling

它们不是简单扫描一个类,而是要动态注册很多 Bean 或代理对象。

6.@Import 和 @Bean 的区别 @Bean 是在配置类方法上写的:

@Configuration
public class AppConfig {

    @Bean
    public SmsService smsService() {
        return new SmsService();
    }
}

它的意思是:调用这个方法,把返回值注册成 Bean。

@Import 是在类上写的:

@Configuration
@Import(SmsService.class)
public class AppConfig {
}

它的意思是:把指定的类导入到 Spring 容器。

区别可以这样记:

@Bean:我自己 new 一个对象,交给 Spring
@Import:我告诉 Spring,你去把这个类处理成 Bean 或配置类

一般来说: 如果你要控制对象怎么创建,用 @Bean

@Bean
public SmsService smsService() {
    return new SmsService("aliyun", "accessKey");
}

如果只是想把某个配置类或组件类引进来,用 @Import

@Import(SmsConfig.class)

@Import 是 Spring 里的一个注解,作用很直接:

把某些类“导入”到 Spring 容器中,让它们成为 Bean 或参与配置解析。

你可以把它理解成:

@ComponentScan:自动扫描当前包及子包里的组件
@Import:手动指定我要把哪些类交给 Spring 管理

7.Spring Boot 自动配置和 @Import 的关系

你前面学 Spring Boot 启动流程时,看到:

加载源配置类
加载并处理所有配置类
自动配置

这里面 @Import 很关键。

@SpringBootApplication 里面有一个:

@EnableAutoConfiguration

@EnableAutoConfiguration 底层就依赖类似 @Import 的机制,把大量自动配置类导入进来。

大概可以理解成:

@SpringBootApplication
        ↓
@EnableAutoConfiguration
        ↓
@Import(...)
        ↓
导入 Spring Boot 自动配置类
        ↓
根据条件创建默认 Bean

比如你引入 spring-boot-starter-web 后,Spring Boot 会导入 Web 相关自动配置类,然后根据条件创建:

DispatcherServlet
RequestMappingHandlerMapping
RequestMappingHandlerAdapter
TomcatServletWebServerFactory
Jackson 的 ObjectMapper
错误处理相关 Bean

这些并不是你自己写的,而是 Spring Boot 通过自动配置机制导入并创建的。

@Conditional 是自动配置能做到“按需生效”的关键。Spring Boot 不会把所有自动配置类都无脑注册进 IOC 容器,而是先判断条件是否满足。常见条件包括:类路径中有没有某个类、容器中有没有某个 Bean、配置文件中有没有某个属性、当前是不是 Web 环境等。比如引入了 Web starter,类路径里有 Spring MVC 和 Tomcat 相关类,Web 自动配置才会生效;如果某个 starter 没引入,对应类不存在,相关配置就不会创建。

条件注解 判断含义 常见用途
@ConditionalOnClass 类路径中存在指定类才生效 引了某个 jar 包才启用配置
@ConditionalOnMissingBean 容器中没有某个 Bean 才生效 用户没自定义时才使用默认 Bean
@ConditionalOnBean 容器中存在某个 Bean 才生效 依赖某个 Bean 后再配置另一个 Bean
@ConditionalOnProperty 配置文件中某个属性满足条件才生效 通过配置开关控制功能是否启用
@ConditionalOnWebApplication 当前是 Web 应用才生效 Web 相关自动配置
自动配置不是“所有自动配置类都会变成 Bean”。更准确地说,Spring Boot 会先找到很多候选自动配置类,然后通过 @Conditional 判断是否满足环境条件,只有条件成立的配置类才会真正创建 Bean。所以排查自动配置问题时要看三件事:依赖有没有引入、配置项有没有写对、条件注解是否满足

2.2@Component@Bean@Import 的区别

这三个机制最终都可以把对象交给 Spring 管理,但适用场景不同。 1 @Component:让 Spring 扫描当前类

@Service
public class UserService {
}

@Service@Controller@Repository 都属于 @Component 的派生注解。组件扫描发现这些类后,会注册对应 BeanDefinition,再由 IOC 容器创建对象。它适合管理自己项目中能够修改源码的类。 2 @Bean:通过方法创建对象

@Configuration
public class JsonConfiguration {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

@Bean 方法的返回值会注册到 ApplicationContext 中,默认 Bean 名称就是方法名。

@Bean 适合以下场景:

第三方类不能添加 @Component
对象创建过程需要传入复杂参数
需要根据配置动态构造对象
需要对第三方对象进行统一初始化

例如 HikariDataSource 来自第三方依赖,不能修改源码增加 @Component,因此可以使用:

@Bean
public DataSource dataSource() {
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    return dataSource;
}

3 @Import:主动导入指定配置

@Configuration
@Import({
        RedisConfiguration.class,
        SecurityConfiguration.class
})
public class ApplicationConfiguration {
}

@Import 的作用类似 XML 配置中的 <import/>,用于组合和加载其他配置。

它可以导入:

普通类
@Configuration 配置类
ImportSelector
DeferredImportSelector
ImportBeanDefinitionRegistrar

其中:

  • 普通类会作为 Bean 注册。
  • 配置类中的 @Bean 会被继续解析。
  • ImportSelector 可以动态返回需要导入的配置类名。
  • ImportBeanDefinitionRegistrar 可以直接向容器注册 BeanDefinition。

Spring Boot 自动配置并不是扫描所有第三方 jar 包,而是通过导入选择机制加载自动配置候选类。 三者可以这样区分:

方式 适用对象 核心特点
@Component 自己编写的业务类 按包自动扫描
@Bean 第三方对象或复杂对象 通过方法主动创建
@Import 配置类或框架扩展 主动导入一组定义

三、自定义 starter:把公共功能做成开箱即用组件

自定义 starter 的作用是把某个公共功能封装起来,让其他项目只要引入一个 starter 依赖,再写少量配置,就能直接使用。例如多个项目都要上传文件到阿里云 OSS,如果每个项目都复制一遍 AliOSSUtils、配置类和属性读取代码,会很乱;更好的方式是把 OSS 上传功能封装成 aliyun-oss-spring-boot-starter,以后项目只需要引入 starter,并在 application.yml 中配置 endpointaccessKeyIdaccessKeySecretbucketName,就能直接注入工具类使用。

自定义 starter 通常拆成两个模块:xxx-spring-boot-autoconfigurexxx-spring-boot-starterautoconfigure 模块负责写真正的自动配置逻辑,例如属性绑定类、配置类、@Bean 方法、条件注解、自动配置导入文件;starter 模块负责聚合依赖,通常依赖 autoconfigure 模块和第三方 SDK。业务项目最终只引入 starter,不直接关心内部有哪些依赖和配置类。 典型结构如下:

aliyun-oss-spring-boot-starter
└── pom.xml                // 只负责依赖聚合,依赖 autoconfigure 和 OSS SDK

aliyun-oss-spring-boot-autoconfigure
├── AliOssProperties.java  // 读取 aliyun.oss.* 配置
├── AliOssAutoConfiguration.java // 自动创建 AliOSSUtils Bean
└── META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

属性绑定类一般使用 @ConfigurationProperties,它适合把一组有共同前缀的配置批量绑定到 Java 对象中,比多个 @Value 更清晰。下面的类会读取 aliyun.oss.endpointaliyun.oss.access-key-idaliyun.oss.access-key-secretaliyun.oss.bucket-name 等配置。

@Data
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOssProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

自动配置类负责创建工具 Bean。这里常配合 @EnableConfigurationProperties 开启属性绑定,再通过 @Bean 把工具对象放进 IOC 容器。更规范的写法还会加 @ConditionalOnMissingBean,意思是:如果用户自己没有定义 AliOSSUtils,就使用 starter 提供的默认对象;如果用户自己定义了,就优先用用户自己的,避免框架默认配置覆盖业务自定义配置。

@AutoConfiguration
@EnableConfigurationProperties(AliOssProperties.class)
@ConditionalOnClass(AliOSSUtils.class)
@ConditionalOnProperty(
        prefix = "aliyun.oss",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true
)
public class AliOssAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliOSSUtils aliOSSUtils(AliOssProperties properties) {
        AliOSSUtils utils = new AliOSSUtils();

        utils.setEndpoint(properties.getEndpoint());
        utils.setAccessKeyId(properties.getAccessKeyId());
        utils.setAccessKeySecret(properties.getAccessKeySecret());
        utils.setBucketName(properties.getBucketName());

        return utils;
    }
}

这里每个注解各有作用:

@AutoConfiguration
    声明这是一个自动配置类

@EnableConfigurationProperties
    注册并绑定 AliOssProperties

@ConditionalOnClass
    类路径中存在 AliOSSUtils 才配置

@ConditionalOnProperty
    配置开关满足要求才启用

@ConditionalOnMissingBean
    用户没有自定义时才创建默认 Bean

声明自动配置入口

文件路径:

src/main/resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容:

com.example.oss.autoconfigure.AliOssAutoConfiguration

Spring Boot 2.7 以后通常使用这一文件声明自动配置类。旧项目可能使用:

META-INF/spring.factories

阅读旧教程或维护旧项目时要注意版本差异。

Starter 模块聚合依赖

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
        <version>${project.version}</version>
    </dependency>

    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
    </dependency>
</dependencies>

业务项目使用时只需要引入 starter,然后写配置并注入工具类:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>aliyun-oss-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
aliyun:
  oss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: xxx
    access-key-secret: xxx
    bucket-name: xxx
@RestController
public class UploadController {
    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public Result upload(MultipartFile image) throws IOException {
        String url = aliOSSUtils.upload(image);
        return Result.success(url);
    }
}

自定义 starter 的本质可以一句话记住:把“依赖 + 配置读取 + Bean 创建 + 条件判断”封装到 starter 里,让业务项目只负责引入依赖和写配置。这也是 Spring Boot 生态强大的原因,MyBatis、PageHelper、Redis、OSS 等功能都可以通过 starter 的方式接入。

四、常用注解速查:按开发场景理解

注解不是“给字节码随便加一段代码”,而是给类、方法、字段、参数打上元数据标记。Spring、MyBatis 等框架在运行时会读取这些注解,然后决定是否创建对象、是否映射请求、是否执行 SQL、是否开启事务、是否做 AOP 增强。理解注解时不要只背名字,要看它放在哪里、被哪个框架读取、产生什么效果。

注解 常用位置 作用 典型场景
@RestController Controller 类 声明 REST 接口控制器,方法返回值直接作为响应体,通常转 JSON 写后端接口
@RequestMapping 类 / 方法 配置请求路径,可作为类上的公共前缀 /emps/depts
@GetMapping 方法 接收 GET 请求,通常用于查询 查询列表、详情
@PostMapping 方法 接收 POST 请求,通常用于新增或提交 新增员工、登录
@PutMapping 方法 接收 PUT 请求,通常用于整体修改 修改员工信息
@DeleteMapping 方法 接收 DELETE 请求,通常用于删除 删除员工
@PathVariable 方法参数 接收 URL 路径中的变量 /emps/{id} 中的 id
@RequestParam 方法参数 接收 query/form 参数,可指定参数名、默认值、是否必填 ?page=1&pageSize=10
@RequestBody 方法参数 接收 JSON 请求体,并转换为 Java 对象 前端提交 JSON 表单
@Autowired 字段 / 构造器 / 方法 从 IOC 容器中自动注入 Bean Controller 注入 Service
@Service Service 实现类 把业务层对象注册为 Bean EmpServiceImpl
@Mapper Mapper 接口 让 MyBatis 为接口生成代理对象 EmpMapper 执行 SQL
@Select / @Insert / @Update / @Delete Mapper 方法 直接在注解中写 SQL 简单 SQL 查询
@Transactional Service 类 / 方法 开启事务,方法成功提交,异常回滚 删除部门同时删除员工
@RestControllerAdvice 异常处理类 全局处理 Controller 层异常,并返回 JSON 统一错误响应
@ExceptionHandler 异常处理方法 指定当前方法处理哪类异常 处理 Exception.class
@Aspect 切面类 声明 AOP 切面 统一日志、耗时统计
@Around 通知方法 环绕目标方法执行,可在前后增强 方法执行前后记录时间
@Pointcut 方法 抽取可复用的切入点表达式 多个通知共用切点
@Configuration 配置类 声明 Java 配置类 自定义 Bean 配置
@Bean 配置类方法 把方法返回值注册进 IOC 容器 注册第三方对象
@Value 字段 注入单个配置值 ${aliyun.oss.endpoint}
@ConfigurationProperties 批量绑定配置文件中的一组属性 aliyun.oss.*

Web 接口最常见的参数接收方式有三种。普通 query 参数或表单参数用 @RequestParam,例如 /emps?page=1&pageSize=10;路径参数用 @PathVariable,例如 /emps/10;JSON 请求体用 @RequestBody,例如前端提交一个员工对象。返回值通常封装成统一结果 Result,这样前端永远按固定格式读取 codemsgdata,不会因为每个接口返回结构不同而难以处理。

@RestController
@RequestMapping("/emps")
public class EmpController {

    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page,
                       @RequestParam(defaultValue = "10") Integer pageSize) {
        return Result.success();
    }

    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id) {
        return Result.success();
    }

    @PostMapping
    public Result save(@RequestBody Emp emp) {
        return Result.success();
    }
}

五、Web 后端整体开发链路

一个 Spring Boot Web 项目可以按请求流向理解:浏览器或前端页面发送 HTTP 请求,请求先经过 Filter,再进入 Spring MVC 的 DispatcherServlet,经过 Interceptor,匹配到 Controller 方法,Controller 调用 Service,Service 调用 Mapper/Dao,Mapper 通过 MyBatis 执行 SQL 访问 MySQL,结果再按相反方向返回给前端。Filter 属于 Java Web 规范,位置更靠前,能拦截进入 Web 容器的请求;Interceptor 属于 Spring MVC,位置在 DispatcherServlet 之后、Controller 之前,更适合处理登录校验、权限判断、接口耗时统计等和业务接口相关的拦截逻辑。 从框架分工上看,Spring MVC 负责 Web 层,也就是接收请求、参数绑定、调用 Controller、返回响应;Spring Framework 提供 IOC、DI、AOP、事务管理等底层能力;MyBatis 负责持久层,帮 Mapper 接口生成代理对象并执行 SQL;Spring Boot 把这些能力整合起来,用 starter 和自动配置降低配置成本。你写业务代码时最常接触的是 Controller、Service、Mapper 三层,但它们能运行起来,背后依赖的是 Spring Boot 的启动、扫描、自动配置、IOC 容器、MyBatis 代理对象和内置服务器。

实际开发时可以这样定位问题:接口 404,多半先看 Controller 路径、请求方式、启动类扫描范围;参数为 null,先看前端传参位置、后端注解是 @RequestParam@PathVariable 还是 @RequestBody;Service 注入失败,先看实现类是否加了 @Service、包是否被扫描;Mapper 注入失败,先看是否加了 @Mapper 或是否配置了 @MapperScan;SQL 报错,进入 Mapper 和数据库字段排查;返回格式不统一,就检查是否都用了 Result.success() / Result.error()

六、Maven 高级:分模块、继承、版本锁定与聚合

Maven 高级部分主要解决一个问题:项目变大以后,不能把所有代码和依赖都塞在一个工程里。常见做法是按功能拆成多个模块,例如商品模块、搜索模块、购物车模块、订单模块,或者按层拆成 pojoutilsweb-management。拆模块以后,各模块可以独立维护,也可以复用公共代码,比如 tlias-web-management 依赖 tlias-pojo 获取实体类,依赖 tlias-utils 获取通用工具类。

在没有 Maven 的年代,开发者需要手动下载 MySQL 驱动、Spring、MyBatis、Jackson 等 jar 包,再把这些 jar 包复制到项目目录中。这样很容易出现版本冲突、依赖缺失和团队环境不一致的问题。Maven 将这些工作统一放进一个名为 pom.xml 的配置文件中:开发者只需要声明“我要使用什么依赖”,Maven 就会自动下载对应 jar 包以及这些 jar 包依赖的其他 jar 包。 可以把 Maven 理解成 Java 项目的“项目管家”:

pom.xml
   ↓
Maven 读取项目配置
   ↓
下载依赖
编译源码
运行测试
打包 jar
安装到本地仓库
发布到远程仓库

1.前置概念

[!note] 1.pom.xml

pom.xml 是 Maven 项目的核心配置文件。POM 是 Project Object Model,项目对象模型 的缩写。 Maven 不直接分析“这个项目是什么”,而是通过 pom.xml 了解项目的基本信息:

这个项目叫什么
属于哪个组织
当前版本是什么
需要哪些依赖
使用哪个 Java 版本
如何编译和打包
有哪些子模块
父工程是谁

一个最小的 Maven 项目通常如下:

project                     # 项目根目录
|-- src                     # 源代码目录
|   |-- main                # 主目录
|   |   |-- java            # Java源代码目录
|   |   |-- resources       # 资源文件目录,配置文件、Mapper XML、静态资源
|   |   |-- webapp          # Web应用目录
|   |-- test                # 测试目录
|       |-- java            # 测试源代码目录
|       |-- resources       # 测试资源文件目录
|-- target                  #  Maven 编译和打包后生成的目录
|-- pom.xml                 # Maven 项目配置文件

最基础的 pom.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>

<!-- Maven 项目的根标签 -->
<project xmlns = "http://maven.apache.org/POM/4.0.0"
    xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
    http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <!-- 模型版本 -->
  <modelVersion>4.0.0</modelVersion>
  <!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成,
        如:com.companyname.project-group,
        maven会将该项目打成的jar包放本地路径:
        /com/companyname/project-group -->
  <groupId>com.companyname.project-group</groupId>

  <!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
  <artifactId>project</artifactId>

  <!-- 版本号 -->
  <version>1.0</version>

  <!-- 属性变量 -->
  properties:定义一些可在整个POM文件中重复使用的变量。
        这里定义了Java编译的源代码版本和目标字节码版本都是 1.8。
        在其他地方可以通过 ${maven.compiler.source} 来引用这个值,方便统一修改。
  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <!-- 依赖 -->
    dependencies:这是项目最常用的部分,用来声明项目所依赖的第三方库。
        在这个区域里列出的每个 dependency,Maven 都会去仓库(本地或中央)中下载并引入到你的项目中。
        一个依赖通常由 groupId, artifactId, version 三个坐标唯一定位。
        这里声明依赖了 Spring Framework 的核心模块 spring-core,版本是 5.3.9
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.3.9</version>
    </dependency>
  </dependencies>

  <!-- 依赖管理 -->
  dependencyManagement:这是一个“依赖版本管理”区域,看起来和 dependencies 很像。
        但是,这里的声明只会影响版本号,并不会真正地引入依赖。
        它的作用是:在子模块的 pom.xml 中声明依赖时,可以不指定 version,从而统一所有子模块的依赖版本。
        可以理解为“爸爸定好规矩,孩子们都按这个规矩来”。
  <dependencyManagement>
    <dependencies>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-core</artifactId>
          <version>5.3.9</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <!-- 仓库管理 -->
    repositories:配置 Maven 去哪些远程仓库下载依赖。
        这里配置了 Maven 中央仓库(Central Repository),这是默认的公共仓库。
        如果你的项目需要用到公司内部的私有仓库,也需要在这里添加配置。
  <repositories>
    <repository>
        <id>central</id>
        <url>https://repo.maven.apache.org/maven2</url>
    </repository>
  </repositories>

  <!-- 构建 -->
  build:配置项目构建相关的所有内容。
        包括最终打包的文件名、使用的插件等。
  <build>
    <!-- 插件管理 -->
    plugins:配置构建过程中使用的 Maven 插件。
            插件可以扩展 Maven 的功能,比如编译代码、打包、运行测试等。
            这里配置了 maven-compiler-plugin,它负责编译 Java 代码。
            通过 <configuration> 标签,我们告诉它使用 1.8 版本的编译器。
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>
  1. 坐标三要素<groupId><artifactId><version> 共同构成了 Maven 世界中任何构件(项目、依赖)的唯一“身份证号”。
  2. dependencies vs dependencyManagement:前者是实际引入依赖(购物车),后者是统一管理版本(价目表)。项目模块多时,在父工程中用后者统一定版本是很好的实践。
  3. properties:是定义变量的好地方,可以用来集中管理版本号、JDK版本等,实现一处修改,处处生效。
  4. buildplugins:定义了项目从代码到可运行软件包的“流水线”工序。

[!note] 2.Maven仓库

Maven中有一个仓库的概念,仓库简单来说就是指存放jar包的地方,按照作用范围的不同可以分为本地仓库、远程仓库和中央仓库。

  • 本地仓库就是我们自己电脑上的一个目录,一般默认是在用户家目录($HOME)下的.m2这个目录里面,这个位置可以在Maven的配置文件中修改
    <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
    <!--
     | The path to the local repository maven will use to store artifacts.
     | Default: ${user.home}/.m2/repository
    -->
    <localRepository>/path/to/local/repo</localRepository>
    
    
  • 远程仓库也叫做私服仓库,一般是公司内部搭建的一个仓库,用来给公司内部的项目提供统一的依赖管理,这样就可以避免jar包的重复下载,而且也可以把一些公司内部发布的私有的jar包放到这个仓库里面,供其他项目来使用,一般由公司内部专门的运维人员来维护,最常用的搭建私服仓库的工具是 Nexus,远程仓库并不是必须的,如果没有配置的话,Maven会直接去中央仓库中下载依赖。
  • 中央仓库是Maven官方提供的一个仓库,里面包含了大量的开源项目,地址是 https://repo.maven.apache.org/maven2

Maven 查找依赖时一般按照下面的顺序:

项目声明依赖
   ↓
先检查本地仓库
   ↓
本地没有则检查公司私服
   ↓
私服没有则访问中央仓库
   ↓
下载后保存到本地仓库

[!note] 3.Maven生命周期

Maven提供了三种主要的生命周期:CleanDefault和`Site

  1. 默认生命周期(Default Lifecycle):主要用于项目的构建,包含编译、测试、打包等。
  2. 清理生命周期(Clean Lifecycle):用于项目的清理工作,如删除先前的构建结果。
  3. 站点生命周期(Site Lifecycle):用于创建和发布项目站点文档。

Clean:用于项目清理(mvn clean,执行clean生命周期,会删除target目录下的所有文件,包括编译后的字节码文件、打包后的jar包、生成的站点等等。

  • mvn clean:清理项目

Default :用于项目部署

validate => compile => test => package => verify => install => deploy
阶段 处理 描述
mvn validate 验证项目 验证项目是否正确且所有必须信息是可用的
mvn compile 编译项目 源代码编译在此阶段完成
mvn test 测试 使用适当的单元测试框架(例如JUnit)运行测试。
mvn package 打包 将编译后的代码打包成可分发的格式,例如 JAR 或 WAR
mvn verify 检查 对集成测试的结果进行检查,以保证质量达标
mvn install 安装 安装打包的项目到本地仓库,以供其他项目使用
mvn deploy 部署 拷贝最终的工程包到远程仓库中,以共享给其他开发人员和工程

Site:用于生成项目站点,包括项目的文档、报告、API文档等等。

# 生成站点文档
mvn site
# 部署站点文档
mvn site:deploy

插件命令 Maven插件扩展了Maven的功能,可以用来完成一些特定的任务,

  1. mvn archetype:generate:创建一个新的Maven项目,并生成项目骨架
  2. mvn dependency:tree:查看项目依赖树
  3. mvn dependency:analyze:分析项目依赖
  4. mvn dependency:resolve:解析项目依赖
  5. mvn dependency:copy-dependencies:复制项目依赖
  6. mvn versions:display-dependency-updates:显示项目依赖的更新

生命周期具有顺序性。执行:mvn package,并不是只执行打包,而是会依次执行前面的阶段:

validate
→ compile
→ test
→ package

执行:mvn install,则会执行:

validate
→ compile
→ test
→ package
→ verify
→ install

因此,企业项目中常见命令:mvn clean package,含义是:先删除旧构建结果,再重新编译、测试并打包。如果暂时不想运行测试:mvn clean package -DskipTests,这通常用于快速构建,但正式发布前不应长期跳过测试。

[!note] 4.依赖

  • “依赖”:指你的项目需要用到某个外部模块(jar包)。
  • “范围(scope)”:指定这个依赖在编译、测试、运行这三个不同阶段,哪个阶段需要它,哪个阶段可以“隐身”或“甩掉”。

1 依赖的范围 依赖不一定在编译、测试和运行的所有阶段都需要,因此 Maven 使用 scope 控制依赖生效范围。 compile是默认值,不写 scope 就是 compile<scope>compile</scope>。编译、测试和运行都可以使用。

runtime编译代码时不直接使用,但运行时需要。<scope>runtime</scope>典型例子是数据库驱动:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

业务代码通常通过 JDBC 接口编程,并不直接调用 MySQL 驱动实现类,但程序运行时必须存在该驱动。

test只在测试代码编译和运行时有效:<scope>test</scope>例如:

JUnit
Mockito
Spring Boot Test

这些依赖不会进入正式运行包。

provided编译时需要,但运行环境会提供:<scope>provided</scope>传统 Servlet 容器项目中,Servlet API 可能由 Tomcat 提供,因此项目不需要重复打包。

import只用于 dependencyManagement 中导入 BOM:

<type>pom</type>
<scope>import</scope>
  • compile:默认范围,编译、测试、运行时都有效
  • provided:编译、测试有效,运行时无效,比如servlet-api
  • runtime:测试、运行时有效,编译时无效
  • test:测试时有效,编译、运行时无效
  • system:类似provided,但是需要指定jar包的路径
  • import:导入依赖的范围

2 依赖的传递性 Maven会自动的解决依赖的传递性,比如说A依赖B,B依赖C,那么Maven会自动的将C也导入到A中,这样就不需要我们手动的去导入C了。 只有当依赖的范围是compile或者runtime的时候,依赖才会被传递,如果依赖的范围是provided或者test的时候,依赖是不会被传递的。

3 依赖的排除 有时候我们引入的依赖包中可能会包含一些我们不需要的依赖,这个时候我们可以使用<exclusions>标签来排除这些依赖。

我声明依赖 spring-core,但明确告诉 Maven,请不要把 spring-beans 这个依赖传递进来

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>6.1.11</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </exclusion>
    </exclusions>
</dependency>

也可以通过<optional>标签来指定依赖是否可选,如果依赖是可选的,那么在引入这个依赖的时候,可以不用引入这个依赖的依赖。

4 依赖的版本冲突 当通过依赖传递导入的两个依赖包版本不一致时,Maven会根据一定的规则来解决这个冲突,一个是最短路径优先,另一个是先声明优先。

一个项目可能通过不同路径引入同一个依赖的不同版本:

项目
├── A
│   └── jackson 2.15
└── B
    └── jackson 2.17

Maven 最终只能选择一个 Jackson 版本。

常见选择原则是:

路径更近的依赖优先
同一层级时,先声明的依赖优先
dependencyManagement 指定的版本优先

例如:

项目 → A → Jackson 2.15
项目 → B → C → Jackson 2.17

因为 2.15 距离项目更近,Maven 通常选择 2.15。

企业项目中更可靠的做法是使用父 POM 的 dependencyManagement 明确锁定版本,而不是依赖 Maven 自动裁决。 查看完整依赖树:

mvn dependency:tree

只查某个依赖:

mvn dependency:tree -Dincludes=com.fasterxml.jackson.core

输出可能类似:

user-service
+- spring-boot-starter-web
|  \- jackson-databind:2.17.1
\- other-library
   \- jackson-databind:2.15.4

这样可以判断某个 jar 是被谁间接引入的。

[!note] 5.继承和聚合

1.父工程 父工程通常不写业务代码,主要负责统一管理:

Java 版本
Spring Boot 版本
Spring Cloud 版本
第三方依赖版本
Maven 插件版本
子模块列表
公共构建配置

父工程的 packaging 通常为:

<packaging>pom</packaging>

这里的 pom 表示这个工程本身不打成业务 jar,而是作为其他模块的配置父节点和构建入口。

根父工程示例:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
         http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mall</groupId>
    <artifactId>mall-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <!-- 父工程主要保存配置,不生成业务 jar -->
    <packaging>pom</packaging>

    <!-- 统一定义属性 -->
    <properties>
        <java.version>17</java.version>
        <spring-boot.version>3.3.1</spring-boot.version>
        <spring-cloud.version>2023.0.3</spring-cloud.version>
        <mysql.version>8.4.0</mysql.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <lombok.version>1.18.32</lombok.version>

        <!-- 统一项目编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>${java.version}</maven.compiler.release>
    </properties>

    <!-- 统一管理依赖版本,不主动引入依赖 -->
    <dependencyManagement>
        <dependencies>

            <!-- Spring Boot 的 BOM -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- Spring Cloud 的 BOM -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.mysql</groupId>
                <artifactId>mysql-connector-j</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>

            <!-- 管理项目内部模块版本 -->
            <dependency>
                <groupId>com.mall</groupId>
                <artifactId>mall-common</artifactId>
                <version>${project.version}</version>
            </dependency>

            <dependency>
                <groupId>com.mall</groupId>
                <artifactId>mall-api</artifactId>
                <version>${project.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <!-- 声明本次聚合构建包含哪些模块 -->
    <modules>
        <module>mall-common</module>
        <module>mall-api</module>
        <module>user-service</module>
        <module>product-service</module>
        <module>order-service</module>
        <module>gateway-service</module>
    </modules>

</project>

这个父 POM 做了三件主要事情:

properties
    定义公共变量,例如 Java 版本和依赖版本

dependencyManagement
    统一规定各依赖应使用什么版本

modules
    声明执行构建时需要一起构建哪些模块

2.继承

继承指的是:子模块通过 <parent> 使用父 POM 中的配置。 例如 user-service/pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
         http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <!-- 当前模块继承 mall-parent -->
    <parent>
        <groupId>com.mall</groupId>
        <artifactId>mall-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>

        <!-- 父 POM 位于当前模块的上一级目录 -->
        <relativePath>../pom.xml</relativePath>
    </parent>

    <!-- groupId 和 version 可以继承父工程,因此通常只写 artifactId -->
    <artifactId>user-service</artifactId>

</project>

子模块继承父 POM 后,可以获得父工程中的:

groupId
version
properties
dependencyManagement
pluginManagement
部分 build 配置
普通 dependencies 中声明的依赖

例如父工程定义:

<properties>
    <java.version>17</java.version>
</properties>

子模块就可以使用:

${java.version}

父工程在 dependencyManagement 中规定了 MySQL 版本,子模块使用 MySQL 驱动时可以不写 version

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

Maven 会沿着父子 POM 关系查找版本,最终使用父 POM 中定义的版本。

继承解决的是:

多个模块如何复用同一套 Maven 配置

可以把它理解成 Java 类继承,但继承的是项目配置,而不是 Java 方法。

3.聚合到底是什么

聚合指的是:一个 Maven 工程通过 <modules> 一次性构建多个子模块。 父 POM 中:

<modules>
    <module>mall-common</module>
    <module>mall-api</module>
    <module>user-service</module>
    <module>product-service</module>
    <module>order-service</module>
</modules>

在根目录执行:

mvn clean package

Maven 会一次构建所有模块,而不需要进入每个目录分别执行。

假设依赖关系如下:

mall-common
      ↑
mall-api
      ↑
user-service
      ↑
order-service

其中 order-service 依赖 user-service 提供的接口契约,几个服务又依赖 mall-common

Maven Reactor 会分析模块间的依赖关系,并自动决定构建顺序,例如:

1. mall-common
2. mall-api
3. user-service
4. product-service
5. order-service

这里的顺序不完全由 <modules> 中的书写顺序决定,而是主要由模块依赖关系决定。

继承:子模块从谁那里获得配置 聚合:父工程一次构建哪些模块

最常见的企业项目会让根 POM 同时承担父工程和聚合工程的职责,但二者在概念上仍然独立。

2.高级

多模块项目常见结构如下:

tlias-parent              // 父工程,主要管理版本和模块,packaging=pom
├── tlias-pojo            // 实体类模块
├── tlias-utils           // 工具类模块
└── tlias-web-management  // Web 管理模块,依赖 pojo 和 utils

父工程通常不是用来写业务代码的,而是用来统一管理依赖版本、插件版本和子模块。父工程的 pom.xml 里一般会设置 <packaging>pom</packaging>,子工程通过 <parent> 继承父工程。这样做的好处是:如果多个子模块都用同一个依赖版本,例如 Lombok、JWT、MyBatis,只需要在父工程统一定义,子模块不需要到处写版本号,后期升级也只改父工程一处。

很多初学者会把 dependencyManagement 和普通的 <dependencies> 搞混,我用一个比喻帮你分清:

对比 <dependencies>(直接依赖) <dependencyManagement>(版本管理)
效果 真正下载 jar 包,子模块自动继承 不下载任何东西,只是定了个版本号
子模块 子模块自动拥有该依赖,不用再写 子模块需要主动再写一次,但可以不写 version
类比 老板直接给你发了台电脑 公司公告说“电脑标准配置是 MacBook Pro”,你需要自己去领
<!-- 父工程 pom.xml -->

<!-- 
  项目的唯一标识(坐标),在 Maven 仓库中定位这个项目:
  - groupId: 组织/公司域名倒写,通常是 com.公司名
  - artifactId: 项目名称,这里叫 tlias-parent
  - version: 版本号
-->
<groupId>com.example</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 
  packaging: 打包方式
  - pom: 表示这是一个"父工程"(聚合工程),本身不写代码、不打 jar/war 包
  - 它的作用只有两个:① 管理依赖版本 ② 聚合子模块
-->
<packaging>pom</packaging>

<!-- 
  properties: 自定义属性标签,类似 Java 的变量定义
  作用:把版本号抽出来统一管理,避免在下面多处重复写版本号
  使用时通过 ${变量名} 引用,比如 ${lombok.version}
-->
<properties>
    <!-- Lombok 版本:编译时自动生成 getter/setter/构造器等 -->
    <lombok.version>1.18.24</lombok.version>
    <!-- JJWT 版本:用于生成和解析 JWT 令牌(常用于登录认证) -->
    <jjwt.version>0.9.1</jjwt.version>
</properties>

<!-- 
  dependencyManagement: 依赖版本管理(注意 Management 这个词)
  作用:声明"这个项目可能会用到的依赖及其版本",但注意 ——
  ❌ 不会真正引入依赖(子模块不会自动继承)
  ✅ 只是定了一个"版本参考表",子模块引用时可以不写 version
-->
<dependencyManagement>
    <dependencies>
        <!-- Lombok 依赖声明 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <!-- 用 ${} 引用上面 properties 里定义的版本号 -->
            <version>${lombok.version}</version>
        </dependency>

        <!-- JJWT 依赖声明 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

子工程通过 <parent> 继承父工程后,如果要使用某个依赖,仍然要在自己的 <dependencies> 中声明依赖,只是可以省略版本号,因为版本已经由父工程的 dependencyManagement 锁定了。

<!-- 子工程 pom.xml -->

<!-- 
  parent: 声明当前模块的"父工程"是谁
  作用:继承父工程的配置(依赖版本管理、插件管理、公共属性等)
-->
<parent>
    <!-- 父工程的坐标,必须和父工程 pom.xml 中的 groupId/artifactId/version 完全一致 -->
    <groupId>com.example</groupId>
    <artifactId>tlias-parent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 
      relativePath: 父工程 pom.xml 的相对路径
      作用:告诉 Maven 去哪里找父工程的 pom 文件
      - 默认值是 ../pom.xml(父工程在上一级目录)
      - 这里写成 ../tlias-parent/pom.xml,说明父工程在"上一级目录的 tlias-parent 文件夹下"
      - 如果父工程在上一级目录,可以省略不写,Maven 会自动去 ../pom.xml 找
    -->
    <relativePath>../tlias-parent/pom.xml</relativePath>
</parent>

<!-- 
  dependencies: 当前子模块真正的依赖
  注意:这里引用了 jjwt,但 没有写 version!
  因为版本号由父工程的 <dependencyManagement> 统一管理
-->
<dependencies>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <!-- version 省略,从父工程继承,实际使用 ${jjwt.version}=0.9.1 -->
    </dependency>
</dependencies>

<dependencies><dependencyManagement> 的区别非常重要。<dependencies> 是直接依赖,写了就会真正引入 jar 包;<dependencyManagement> 只是统一管理依赖版本,本身不会真正引入依赖,子工程还需要在 <dependencies> 中声明要用哪个依赖。记忆:dependencies 是“我要用”,dependencyManagement 是“如果你要用,版本按我这里来”

聚合用于一次性构建多个模块。父工程通过 <modules> 声明包含哪些子模块,执行 mvn clean package 时,Maven 会根据模块之间的依赖关系自动决定构建顺序。例如 tlias-web-management 依赖 tlias-pojotlias-utils,那么聚合构建时会先构建被依赖的模块,再构建 Web 模块。

<modules>
    <module>../tlias-pojo</module>
    <module>../tlias-utils</module>
    <module>../tlias-web-management</module>
</modules>

继承和聚合经常写在同一个父工程里,但它们不是一回事。继承是子工程使用父工程的配置,重点是统一依赖版本、插件和公共配置;聚合是父工程管理多个子模块的构建,重点是一键打包和构建顺序。一个工程可以只有继承没有聚合,也可以只有聚合没有继承,只是实际项目里经常把两者合在一个 parent 工程里。

概念 解决的问题 典型标签 记忆
继承 子模块复用父工程配置 子工程写 <parent> 配置从父工程来
聚合 父工程一次构建多个模块 父工程写 <modules> 构建由父工程统一调度
dependencies 真正引入依赖 <dependencies> 写了就会导入 jar
dependencyManagement 统一锁定依赖版本 <dependencyManagement> 只管版本,不主动导入
properties 抽取版本变量 <properties> 版本集中管理,方便升级

七、Maven 私服:公司内部 jar 包怎么共享

私服是一种部署在局域网或公司网络中的 Maven 仓库,用来代理外部中央仓库,也用来保存公司内部自己的 jar 包。Maven 默认会先查本地仓库,本地没有再去远程仓库下载。实际企业中通常会搭建私服,开发者从私服下载依赖,上传公司内部公共组件到私服。这样做有几个好处:提高下载速度,统一依赖来源,避免每个人都直接访问中央仓库,也方便共享公司内部不能发布到中央仓库的 jar 包。

依赖查找顺序可以理解为:本地仓库 → 私服 → 中央仓库。如果你项目依赖了 tlias-utils,Maven 会先看本地仓库有没有;没有就去配置的私服找;私服如果也没有,才可能去中央仓库找。公司内部 jar 包通常不会出现在中央仓库,所以需要有人把它发布到私服,其他项目才能通过 Maven 坐标下载使用。

资源上传和下载对应两个常见命令:install 是把当前项目打包结果安装到本地仓库,主要给自己本机其他项目使用;deploy 是把当前项目发布到远程私服,给团队其他人使用。简单记忆:install 到本地,deploy 到远程私服

版本后缀也有含义。RELEASE 表示发布版本,通常比较稳定,适合线上或稳定环境使用;SNAPSHOT 表示快照版本,内容可能随时变化,适合开发阶段频繁更新。依赖别人正在开发的模块时,经常会看到 1.0-SNAPSHOT;正式发布后,一般会改成 1.0.0 这类稳定版本。

执行一个mvn deploy命令,就可以将jar包上传到私服仓库中,上传之后在Nexus的管理界面中就可以看到对应的jar包。

本地开发调试:mvn install
团队共享组件:mvn deploy
稳定版本:RELEASE / 1.0.0
开发版本:SNAPSHOT / 1.0-SNAPSHOT

八、Spring Boot 日志原理与实际使用

日志不是简单的 System.out.println() 替代品,而是应用运行状态、故障定位、性能分析和审计追踪的重要基础设施。 Spring Boot 内部使用 Commons Logging 作为日志抽象,同时允许底层使用 Logback、Log4j2 或 Java Util Logging;默认 Starter 通常会提供 Logback 配置。 可以把日志体系理解为两层:

日志门面或抽象:SLF4J、Commons Logging
日志实现:Logback、Log4j2、JUL

业务代码一般通过 SLF4J API 写日志:

@Slf4j
@Service
public class FileService {

    public void upload(String userId, String fileName) {
        log.info(
                "开始上传文件, userId={}, fileName={}",
                userId,
                fileName
        );
    }
}```

`@Slf4j` 是 Lombok 提供的注解,编译后相当于生成:
```java
private static final Logger log =
        LoggerFactory.getLogger(FileService.class);

1 日志级别

常见日志级别从详细到严重依次为:

TRACE < DEBUG < INFO < WARN < ERROR
级别 适用场景
TRACE 极细粒度执行轨迹,通常只在深入排查时启用
DEBUG 调试信息、关键变量和分支判断
INFO 正常业务流程中的关键节点
WARN 存在异常情况,但系统仍能继续运行
ERROR 请求失败、数据错误或系统异常

例如日志级别设置为 INFO 时:

TRACE 不输出
DEBUG 不输出
INFO 输出
WARN 输出
ERROR 输出

2 正确记录日志

推荐使用参数占位符:

log.info(
        "用户登录成功, userId={}, clientIp={}",
        userId,
        clientIp
);

不要直接字符串拼接:

// 不推荐
log.debug("查询用户信息: " + user);

因为即使 DEBUG 级别没有启用,字符串拼接也可能提前执行。占位符写法只有真正需要输出时才会进行格式化。

记录异常时,要把异常对象作为最后一个参数传入:

try {
    fileService.upload(file);
} catch (IOException e) {
    log.error(
            "文件上传失败, fileName={}",
            file.getOriginalFilename(),
            e
    );
}

不要只记录:

log.error("文件上传失败: {}", e.getMessage());

这种写法只有异常消息,没有完整堆栈,很难定位异常发生位置。

3 日志内容应该记录什么

适合记录:

请求或任务的关键标识
用户 ID、订单 ID、文件 ID
业务执行的关键节点
耗时信息
第三方调用结果
异常堆栈
重要配置加载结果
状态变化

不应该直接记录:

用户密码
AccessKeySecret
Token 完整内容
银行卡号
身份证号
敏感个人信息
超大的请求体或文件内容

例如:

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;

    public UserDTO findById(Long userId) {
        long start = System.currentTimeMillis();

        log.debug("开始查询用户, userId={}", userId);

        User user = userMapper.selectById(userId);

        if (user == null) {
            log.warn("用户不存在, userId={}", userId);
            throw new UserNotFoundException(userId);
        }

        log.info(
                "查询用户完成, userId={}, cost={}ms",
                userId,
                System.currentTimeMillis() - start
        );

        return convert(user);
    }
}

4 在配置文件中设置日志级别

logging:
  level:
    root: info
    com.disk: debug
    org.springframework.web: info
    org.springframework.jdbc.core: debug

含义是:

整个项目默认 INFO
自己项目 com.disk 包输出 DEBUG
Spring MVC 输出 INFO
JdbcTemplate 输出 DEBUG

开发环境和生产环境通常采用不同配置:

# application-dev.yml
logging:
  level:
    com.disk: debug
# application-prod.yml
logging:
  level:
    com.disk: info

5 输出日志文件

Spring Boot 默认通常输出到控制台,也可以配置日志文件:

logging:
  file:
    name: logs/network-disk.log

复杂生产环境一般使用 logback-spring.xml 配置:

控制台输出
按日期滚动
按文件大小切分
保留天数
不同级别写入不同文件
异步日志
自定义格式

一个简化示例:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <include resource=
        "org/springframework/boot/logging/logback/defaults.xml"/>

    <property name="LOG_PATH" value="logs"/>

    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS}
                [%thread] %-5level
                %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <appender name="FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">

        <file>${LOG_PATH}/application.log</file>

        <rollingPolicy class=
            "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">

            <fileNamePattern>
                ${LOG_PATH}/application-%d{yyyy-MM-dd}.%i.log.gz
            </fileNamePattern>

            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>

        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS}
                [%thread] %-5level
                %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

文件名使用 logback-spring.xml 比单纯的 logback.xml 更适合 Spring Boot,因为可以使用 Spring Boot 提供的扩展能力。

6 请求链路标识 Trace ID

微服务中一个请求可能经过 Gateway、认证服务、用户服务和文件服务。只有普通日志时,很难从大量日志中找出同一次请求。

通常会为每次请求生成 Trace ID,并放入 MDC:

String traceId = UUID.randomUUID().toString()
        .replace("-", "");

MDC.put("traceId", traceId);

try {
    filterChain.doFilter(request, response);
} finally {
    MDC.remove("traceId");
}

日志格式中加入:

%X{traceId}

这样同一次请求产生的日志都能通过 Trace ID 检索。实际微服务项目还可以接入 OpenTelemetry、Micrometer Tracing 或集中日志系统。 Spring Boot Actuator 还提供运行时查看和调整日志级别的能力。


九、Spring Boot 测试体系

测试的目的不是单纯提高覆盖率,而是验证代码行为,防止重构或需求修改破坏已有功能。 Spring Boot 为测试提供了核心测试模块、自动配置测试支持以及多种针对特定功能的测试切片。

通常引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-starter-test 通常包含 JUnit、Spring Test、Spring Boot Test、AssertJ、Hamcrest 和 Mockito 等测试工具。 测试代码一般放在:

src/test/java

企业项目可以把测试分成三层:

单元测试
测试切片
集成测试

1 单元测试:只测试一个类

单元测试不启动 Spring 容器,只创建当前待测试对象,并使用 Mockito 模拟它依赖的组件。 假设业务代码为:

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;

    public UserDTO findById(Long userId) {
        User user = userMapper.selectById(userId);

        if (user == null) {
            throw new UserNotFoundException(userId);
        }

        return new UserDTO(
                user.getId(),
                user.getUsername()
        );
    }
}

单元测试:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldReturnUserWhenUserExists() {
        // Arrange:准备测试数据
        User user = new User();
        user.setId(1L);
        user.setUsername("jinbai");

        when(userMapper.selectById(1L))
                .thenReturn(user);

        // Act:执行待测试方法
        UserDTO result = userService.findById(1L);

        // Assert:验证结果
        assertThat(result.id()).isEqualTo(1L);
        assertThat(result.username()).isEqualTo("jinbai");

        // 验证依赖是否按预期调用
        verify(userMapper).selectById(1L);
    }

    @Test
    void shouldThrowExceptionWhenUserNotFound() {
        when(userMapper.selectById(99L))
                .thenReturn(null);

        assertThatThrownBy(
                () -> userService.findById(99L)
        )
                .isInstanceOf(UserNotFoundException.class);

        verify(userMapper).selectById(99L);
    }
}

这里:

@Mock
    创建假的 UserMapper

@InjectMocks
    创建 UserService,并注入假的 UserMapper

when(...).thenReturn(...)
    定义模拟对象被调用时返回什么

verify(...)
    验证某个方法是否被调用

单元测试的优点是运行快、定位准确、不依赖数据库和 Spring 容器。Service 中的大部分业务分支都应该优先使用单元测试覆盖。

2 Controller 测试:@WebMvcTest

只测试 Web 层时,没有必要启动整个项目,可以使用:

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private UserService userService;

    @Test
    void shouldReturnUser() throws Exception {
        UserDTO user = new UserDTO(1L, "jinbai");

        when(userService.findById(1L))
                .thenReturn(user);

        mockMvc.perform(
                get("/api/users/{id}", 1L)
        )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.id").value(1))
                .andExpect(
                        jsonPath("$.data.username")
                                .value("jinbai")
                );
    }
}

@WebMvcTest 主要加载:

Controller
Spring MVC 基础组件
参数绑定
JSON 转换
校验
ControllerAdvice
MockMvc

它不会完整加载 Service、Mapper 和数据库,因此 Controller 依赖通常需要模拟。

适合验证:

URL 是否正确
HTTP 方法是否正确
参数绑定是否正确
参数校验是否生效
返回状态码是否正确
JSON 结构是否符合预期
全局异常处理是否正确

3 数据层测试:@DataJpaTest@JdbcTest

数据层测试可以只加载数据库相关组件。

JPA 项目:

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldFindUserByUsername() {
        User user = new User();
        user.setUsername("jinbai");
        userRepository.save(user);

        Optional<User> result =
                userRepository.findByUsername("jinbai");

        assertThat(result).isPresent();
    }
}

使用 JDBC 时可以使用 @JdbcTest。Spring Boot 也提供了多种针对数据访问技术的测试切片。

不过 MyBatis 项目通常需要结合 MyBatis 自己的测试支持,或者使用 @SpringBootTest 加测试数据库验证 Mapper SQL。

4 集成测试:@SpringBootTest

@SpringBootTest 会加载完整的 Spring Boot ApplicationContext。Spring Boot 通常会自动向上查找项目中的主配置类,也就是带有 @SpringBootApplication 的启动类。

@SpringBootTest
class UserServiceIntegrationTest {

    @Autowired
    private UserService userService;

    @Test
    void shouldLoadUserFromDatabase() {
        UserDTO user = userService.findById(1L);

        assertThat(user).isNotNull();
    }
}

它适合验证:

Bean 能否正常创建
自动配置是否生效
依赖注入是否正确
事务是否生效
多个业务组件能否协作
数据库、Redis 等基础设施能否正常交互

但它启动慢,不适合所有测试都使用。

5 Web 集成测试的两种模式

默认模拟 Web 环境:

@SpringBootTest
@AutoConfigureMockMvc
class UserApiTest {

    @Autowired
    private MockMvc mockMvc;
}

这种方式加载完整 Spring 容器,但不一定真正开启监听端口。

如果需要启动真实服务器:

@SpringBootTest(
        webEnvironment =
                SpringBootTest.WebEnvironment.RANDOM_PORT
)
class UserApiRealServerTest {

    @LocalServerPort
    private int port;

    @Test
    void shouldStartServer() {
        assertThat(port).isPositive();
    }
}

RANDOM_PORT 会启动真实内嵌服务器并使用随机端口,适合更接近真实环境的接口测试。

6 测试事务与数据回滚

Spring 测试中可以使用:

@SpringBootTest
@Transactional
class UserServiceIntegrationTest {
}

测试方法完成后,事务通常会回滚,避免测试数据污染数据库。

但要注意:如果测试启动了真实服务器,请求在另一个线程中执行,请求事务与测试方法事务可能不是同一个事务,此时不能简单依赖测试方法回滚。

7 Testcontainers

如果项目依赖 MySQL、PostgreSQL、Redis 或 Kafka,直接依赖开发机已经安装的服务容易导致测试结果不稳定。

Testcontainers 可以在测试期间通过 Docker 启动临时服务,并与 JUnit 集成。

例如 PostgreSQL:

@Testcontainers
@SpringBootTest
class UserRepositoryIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
            new PostgreSQLContainer<>(
                    "postgres:16"
            );

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveUser() {
        User user = new User();
        user.setUsername("jinbai");

        User saved = userRepository.save(user);

        assertThat(saved.getId()).isNotNull();
    }
}

测试启动时会自动创建临时 PostgreSQL 容器,测试结束后再销毁。相比使用 H2 模拟数据库,它与生产数据库行为更加一致,适合集成测试和持续集成环境。

8 测试应该怎样分配

实际项目可以采用以下策略:

测试类型 是否启动 Spring 数量建议 主要目标
单元测试 最多 验证业务逻辑和异常分支
Controller 测试 部分加载 较多 验证接口、参数和 JSON
数据层测试 部分加载 适量 验证 SQL 和数据映射
集成测试 完整加载 少量关键场景 验证组件协作和配置
端到端测试 完整系统 最少 验证核心业务链路

不要把所有测试都写成 @SpringBootTest。合理做法是:

能用普通 JUnit 测试,就不启动 Spring
能用测试切片,就不加载整个项目
只有跨组件协作时,才使用 @SpringBootTest