这篇文章在讲什么
这篇文章会带你从零开始理解 Java 生态中最核心的框架------Spring 全家桶。
读完后你会明白三件事:
markdown
1. Spring Framework 解决了什么问题?为什么需要它?
2. Spring MVC 解决了什么问题?为什么需要它?
3. Spring Boot 解决了什么问题?为什么需要它?
核心观点:这三个东西解决的是三个不同层面的问题,后面的建立在前面的基础上。
Spring Framework:解决了代码层面的问题(对象耦合、重复代码、通用逻辑)
Spring MVC: 解决了与前端交互的问题(HTTP 请求处理)
Spring Boot: 解决了项目搭建和配置的问题(自动配置、依赖管理)
在开始之前,先问你几个问题
如果你能回答,说明你已经有了基础,可以直接跳到对应章节。如果不能,没关系,文章会一步步讲清楚。
java
问题1:什么是"依赖注入"?为什么要"注入",直接 new 不行吗?
问题2:Spring MVC 的 DispatcherServlet 是什么?它和 Servlet 是什么关系?
问题3:Spring Boot 的自动配置是怎么知道你用了 Tomcat 而不是 Jetty 的?
问题4:@Autowired 背后到底发生了什么?Spring 是怎么把一个对象塞到另一个对象里的?
问题5:Spring 全家桶这么多模块,它们之间是什么关系?是独立的还是层层依赖的?
带着这些问题往下读,读完后回头看看能不能回答。
第一部分:Spring Framework------解决代码层面的问题
一、一切从一个痛点开始
假设你正在开发一个电商系统,需要实现"下单"功能。你写了两个类:
java
// 数据库访问层
public class OrderRepository {
public void save(Order order) {
// 连接数据库,保存订单
}
}
// 业务逻辑层
public class OrderService {
// 直接在代码里 new 出依赖对象
private OrderRepository repository = new OrderRepository();
public void placeOrder(Order order) {
repository.save(order);
}
}
看起来没问题,对吧?
但请仔细看这一行:
java
private OrderRepository repository = new OrderRepository();
这意味着 OrderService 必须知道 OrderRepository 的存在,必须知道 它的构造方式。如果有一天你要把数据库从 MySQL 换成 PostgreSQL,换了 OrderRepository 的实现,你必须改 OrderService 的代码。
两个类被硬编码绑死了。这就是"耦合"。
二、为什么耦合是个问题?一个实际案例
假设老板说:"我们的订单系统要从 MySQL 迁移到 PostgreSQL。"
你需要:
- 写一个新的
PostgresOrderRepository实现 - 修改
OrderService的代码 ,把new OrderRepository()改成new PostgresOrderRepository()
如果项目里有 50 个 Service 都依赖了 OrderRepository,你就要改 50 个地方。
更糟糕的是,如果 OrderService 里混杂了数据库连接代码:
java
public class OrderService {
public void placeOrder(Order order) {
// 业务逻辑
// 连接数据库的代码也在这里
Connection conn = DriverManager.getConnection("jdbc:mysql://...");
PreparedStatement ps = conn.prepareStatement("INSERT INTO orders...");
// ...
}
}
业务逻辑和基础设施代码混在一起,改数据库意味着改业务代码。
三、怎么解决?一个思想的转变
在没有框架之前,开发者自己想了一个办法:不自己 new,让别人给我。
java
// OrderService 不再自己创建依赖
public class OrderService {
private OrderRepository repository;
// 通过构造器"接受"别人传进来的依赖
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void placeOrder(Order order) {
repository.save(order);
}
}
现在 OrderService 不再关心 repository 具体是什么实现。谁来决定?创建 OrderService 的人来决定:
java
// 用 MySQL 版本
OrderRepository repo = new JdbcOrderRepository(mysqlDataSource);
OrderService service = new OrderService(repo);
// 要换 PostgreSQL?只需要换这一行
OrderRepository repo = new PostgresOrderRepository(postgresDataSource);
OrderService service = new OrderService(repo);
OrderService 的代码完全不用改。 依赖关系从"硬编码"变成了"外部传入"。
这就是 IoC(控制反转) 的核心思想:对象不自己创建依赖,由外部负责创建和组装。
而 DI(依赖注入) 就是实现 IoC 的具体方式------外部通过构造器、setter 或字段把依赖"传进来"。"注入"这个词听起来高级,其实就是"传进来"。
四、但是,谁来负责"组装"?
上面的例子中,你还是需要手动创建对象、手动传入依赖。在真实项目里,对象可能有几百个,依赖关系错综复杂,手动组装不现实。
这就是 Spring 框架要解决的第一个问题:自动管理对象的创建和组装。
五、Spring 怎么做的?先看 XML 配置方式
Spring 早期用 XML 文件来描述对象之间的关系:
xml
<!-- 告诉 Spring:创建一个 DataSource 对象,id 叫 dataSource -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/shop"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- 创建一个 Repository,把 dataSource 传进去 -->
<bean id="orderRepository" class="com.example.JdbcOrderRepository">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 创建一个 Service,把 repository 传进去 -->
<bean id="orderService" class="com.example.OrderService">
<property name="repository" ref="orderRepository"/>
</bean>
逐行解释:
xml
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<!-- 创建一个对象,类型是 HikariDataSource,给它起名叫 dataSource -->
<property name="jdbcUrl" value="jdbc:mysql://..."/>
<!-- 调用 setJdbcUrl() 方法,传入一个具体的字符串值 -->
<property name="username" value="root"/>
<!-- 调用 setUsername(),传入 "root" -->
</bean>
<bean id="orderRepository" class="com.example.JdbcOrderRepository">
<property name="dataSource" ref="dataSource"/>
<!-- 注意这里是 ref 不是 value -->
<!-- ref="dataSource" 表示:引用上面那个 id 叫 dataSource 的对象 -->
<!-- 调用 setDataSource(),把上面创建好的 dataSource 对象传进去 -->
</bean>
Spring 容器启动时,自动执行的操作(伪代码):
java
// Spring 底层帮你做的事
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/shop");
dataSource.setUsername("root");
dataSource.setPassword("123456");
JdbcOrderRepository orderRepository = new JdbcOrderRepository();
orderRepository.setDataSource(dataSource); // 注入
OrderService orderService = new OrderService();
orderService.setRepository(orderRepository); // 注入
// 所有对象组装完毕,可以使用了
你不需要手动 new 任何对象,不需要手动组装依赖关系。Spring 根据 XML 配置自动完成。
六、XML 太啰嗦了------注解方式
XML 配置虽然解决了耦合问题,但写起来太冗长。于是 Spring 引入了注解:
java
@Component // 告诉 Spring:请管理这个类的实例(等价于 XML 里的 <bean>)
public class OrderService {
@Autowired // 告诉 Spring:请自动注入一个 OrderRepository(等价于 <property ref>)
private OrderRepository repository;
public void placeOrder(Order order) {
repository.save(order);
}
}
@Autowired 背后发生了什么?
kotlin
Spring 容器启动
↓
扫描所有带 @Component 注解的类,发现 OrderService
↓
创建 OrderService 实例
↓
扫描 OrderService 的所有字段,发现 repository 上有 @Autowired
↓
在容器里找类型匹配的对象(OrderRepository)
↓
通过反射调用 field.set(orderServiceInstance, repositoryInstance)
把 repository 注入到 orderService 里
注意:@Autowired、@Component 这些注解本身是 Java 原生的注解机制,但这些具体的注解是 Spring 自己定义的。 Spring 利用了 Java 的注解机制,为常见场景定义了这套注解。
七、Spring 解决的第二个问题:常用操作的样板代码
操作数据库、Redis 等基础设施时,有大量的重复代码。以 JDBC 操作数据库为例:
java
// 没有 Spring 封装时------50 行代码,只有 1 行是真正的业务意图
public Order findById(Long id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");
ps.setLong(1, id);
rs = ps.executeQuery();
if (rs.next()) {
Order o = new Order();
o.setId(rs.getLong("id"));
o.setName(rs.getString("name"));
return o;
}
return null;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (ps != null) try { ps.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
}
真正的业务意图只有一行:SELECT * FROM orders WHERE id = ?。其余全是连接管理、资源释放、异常处理的样板代码。
Spring 的 JdbcTemplate 封装了这些:
java
public Order findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE id = ?",
this::mapRow, id
);
}
同样的模式应用到 Redis:
java
// 之前
Jedis jedis = new Jedis("localhost", 6379);
try {
jedis.set("user:1:name", "张三");
} finally {
jedis.close();
}
// Spring Data Redis
@Autowired
private StringRedisTemplate redisTemplate;
redisTemplate.opsForValue().set("user:1:name", "张三");
连接池管理、序列化、异常处理、资源释放------全部由 Spring 自动处理。这种封装模式叫模板类(Template Pattern)。
八、Spring 解决的第三个问题:横切关注点的重复代码
假设你的系统里所有业务方法都需要做权限检查:
java
public class OrderService {
public void placeOrder(Order order) {
checkPermission(); // 重复代码
repository.save(order);
}
public void cancelOrder(Long id) {
checkPermission(); // 又重复一遍
repository.cancel(id);
}
public void refundOrder(Long id) {
checkPermission(); // 又重复一遍
repository.refund(id);
}
}
权限检查、日志记录、事务管理------这些逻辑"横跨"所有业务模块,叫横切关注点。
AOP(面向切面编程) 把这些逻辑抽出来,定义为"切面":
java
@Aspect
@Component
public class SecurityAspect {
// 拦截 OrderService 的所有方法,在执行前做权限检查
@Before("execution(* com.example.OrderService.*(..))")
public void checkPermission() {
if (!currentUser.isAdmin()) {
throw new AccessDeniedException("没有权限");
}
}
}
java
// 业务代码变得干净------只写业务逻辑
@Component
public class OrderService {
public void placeOrder(Order order) {
repository.save(order); // 权限检查由切面自动执行
}
}
调用 placeOrder() 时,Spring 并不会直接执行它,而是:
scss
调用 placeOrder()
↓
Spring 的代理对象拦截到这个调用
↓
先执行 @Before 切面(权限检查)
↓
权限通过 → 执行真正的 placeOrder()
权限不通过 → 抛异常,placeOrder() 不会执行
Spring 通过动态代理实现这个拦截------运行时生成一个代理对象,包装真实对象,所有方法调用先经过代理。
AOP 的典型应用场景:
less
@Before 方法执行前:权限检查、参数校验
@After 方法执行后(无论成败):资源清理
@AfterReturning 成功返回后:记录成功日志
@AfterThrowing 抛异常后:记录异常日志
@Around 包裹整个方法:事务管理、性能监控、缓存
@Transactional 就是用 @Around 实现的------方法执行前开启事务,成功后提交,抛异常后回滚。
九、Spring Framework 的本质
markdown
Spring 解决了三个代码层面的问题:
1. IoC/DI → 管理对象的创建和组装,解决耦合
2. 模板封装 → 封装 Redis/DB 等操作的重复代码,解决重复
3. AOP → 在方法执行前后插入通用逻辑,解决横切关注点
一句话:Spring 把企业开发中与业务无关的基础设施代码抽出来,变成框架自动处理的事情,让开发者只写业务逻辑。
第二部分:Spring MVC------解决与前端交互的问题
一、先问一个问题:为什么不能用 Spring 的模板类来封装 HTTP 请求?
Spring 已经封装了 Redis(RedisTemplate)、数据库(JdbcTemplate)。为什么不能也写一个 HttpTemplate 来封装 HTTP 请求的处理?
因为 HTTP 请求的处理比 Redis/MySQL 复杂得多。
markdown
Redis 的操作模式:
你的代码 → 连接 → 发命令 → 收结果 → 关闭连接
输入固定(命令 + 参数),输出固定(返回值)
HTTP 请求的处理模式:
1. 路由:根据 URL 决定调用谁?
/order/list → OrderController.list()
/user/login → UserController.login()
(Redis 不需要路由,你直接指定调哪个命令)
2. 参数来源:路径、查询参数、请求体、请求头、Cookie
(Redis 参数格式固定)
3. 参数格式:form 表单、JSON、文件上传
(Redis 格式固定)
4. 返回格式:HTML 页面、JSON、文件下载、重定向
(Redis 返回值格式固定)
5. 横切问题:跨域、会话、权限
(Redis 没有这些问题)
HTTP 不是"调一个方法取一个结果",它有路由、多种输入来源、多种输出格式、多种状态码。 一个简单的模板类搞不定,需要一个专门的模块。
二、没有 Spring MVC 之前,怎么处理 HTTP 请求
用最原始的 Servlet:
java
@WebServlet("/order/detail")
public class OrderDetailServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 手动取参数
String idStr = request.getParameter("id");
Long id = Long.parseLong(idStr);
// 手动查数据库
Connection conn = DriverManager.getConnection("jdbc:mysql://...");
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
// 手动转 JSON
Order order = mapRow(rs);
String json = new ObjectMapper().writeValueAsString(order);
// 手动写响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
}
}
一个 URL 一个 Servlet 类。 项目里有 30 个 URL 就要写 30 个 Servlet。
URL 和 Servlet 的映射写在 web.xml 里:
xml
<servlet>
<servlet-name>orderDetail</servlet-name>
<servlet-class>com.example.OrderDetailServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>orderDetail</servlet-name>
<url-pattern>/order/detail</url-pattern>
</servlet-mapping>
三、Spring MVC 做了什么
一句话:在 Servlet 之上加了一层自动化封装。
rust
没有 Spring MVC:你的代码 ←→ Servlet API ←→ Tomcat
有了 Spring MVC:你的代码 ←→ Spring MVC ←→ Servlet API ←→ Tomcat
同样的功能,用 Spring MVC 实现:
java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/detail")
public Order detail(@RequestParam Long id) {
return orderService.findById(id);
}
}
对比:
typescript
之前(Servlet):
一个 URL 一个类
参数手动取、手动转类型
JSON 手动序列化
响应手动写
web.xml 手动配置映射
现在(Spring MVC):
一个 Controller 类处理所有相关 URL
@RequestParam 自动取参数、自动转类型
返回对象自动转 JSON
@GetMapping 代替 web.xml 映射
四、DispatcherServlet------Spring MVC 的心脏
你问的那个"专门的类获取请求再转给不同的类",就是 DispatcherServlet。
bash
Servlet 时代:
每个 URL 对应一个 Servlet 类,Tomcat 根据 web.xml 直接分发
/order/list → OrderListServlet
/order/detail → OrderDetailServlet
Spring MVC 时代:
所有 URL 都先到 DispatcherServlet(只有一个),由它统一分发
/order/* → DispatcherServlet → 根据 URL 匹配 → OrderController 的某个方法
DispatcherServlet 本质上就是一个 Servlet,注册在 Tomcat 里,接收所有请求。
五、参数自动解析------Spring MVC 封装 HTTP 的核心之一
一个 HTTP 请求的参数可能来自很多地方:
java
@GetMapping("/order/{id}")
public Order detail(
@PathVariable Long id, // 从 URL 路径 /order/123 取 123
@RequestParam String sort, // 从查询参数 ?sort=price 取
@RequestHeader String token, // 从请求头取
@CookieValue String sessionId, // 从 Cookie 取
@RequestBody OrderQuery query // 从请求体取 JSON 并转成对象
) {
// Spring MVC 自动从不同地方取值、转类型、传进来
}
自动封装表单参数到对象:
java
// 前端表单提交:name=张三&price=99.9&quantity=2
@PostMapping("/add")
public String add(OrderItem item) {
// Spring MVC 自动把表单参数映射到 OrderItem 的字段上
// item.getName() 已经是 "张三"
// item.getPrice() 已经是 BigDecimal 99.9
// 不需要手动 getParameter(),不需要手动类型转换
}
六、返回值自动处理------Spring MVC 封装 HTTP 的核心之二
java
// 返回 JSON------一个注解搞定
@GetMapping("/api/order/{id}")
@ResponseBody
public Order detail(@PathVariable Long id) {
return orderService.findById(id);
// 自动:对象 → JSON → Content-Type: application/json → 写入响应体
}
// 返回 HTML 页面
@GetMapping("/order/list")
public String list(Model model) {
model.addAttribute("orders", orderService.findAll());
return "order/list";
// 自动:找到 templates/order/list.html → 渲染数据 → 返回 HTML
}
// 重定向
@PostMapping("/order/create")
public String create(Order order) {
orderService.save(order);
return "redirect:/order/list";
// 自动:设置 302 + Location 头
}
七、与前端交互的两种模式
模式一:服务器渲染(传统 MVC)
css
浏览器 → 服务器 → 返回完整 HTML → 浏览器直接显示
V(View)在服务器端(Thymeleaf/JSP 模板)
完整流程:
markdown
1. 浏览器发 GET /order/list
2. DispatcherServlet 路由到 OrderController.list()
3. Controller 查数据库,把数据塞进 Model
4. 返回视图名称 "order/list"
5. 视图解析器找到 templates/order/list.html
6. 模板引擎把数据填入模板,生成 HTML
7. HTML 返回浏览器,浏览器直接渲染
模板示例:
html
<table>
<tr th:each="order : ${orders}">
<td th:text="${order.id}">1001</td>
<td th:text="${order.productName}">iPhone</td>
<td><a th:href="@{/order/detail(id=${order.id})}">查看详情</a></td>
</tr>
</table>
模式二:前后端分离(现代)
javascript
前端(Vue/React)→ 服务器 → 返回 JSON → 前端自己渲染
V(View)在前端
java
@RestController // = @Controller + @ResponseBody,每个方法返回 JSON
@RequestMapping("/api/order")
public class OrderApiController {
@GetMapping("/list")
public List<Order> list() {
return orderService.findAll();
}
}
八、Spring MVC 的本质
typescript
Spring MVC 解决了三个与前端交互的问题:
1. URL 路由 → @RequestMapping / @GetMapping 把 URL 映射到方法
2. 参数解析 → @RequestParam / @PathVariable / @RequestBody 自动取值转类型
3. 返回处理 → 自动转 JSON / 渲染模板 / 重定向
底层还是 Servlet,只是在上面加了一层自动化。
第三部分:Spring Boot------解决配置的问题
一、先看没有 Spring Boot 时搭建项目有多痛苦
用 Spring MVC 做一个 Web 项目,你需要手动做这些:
markdown
1. 手动建项目目录结构
2. 手动管理几十个 JAR 的版本,确保互相兼容
3. 写 web.xml 配置 DispatcherServlet
4. 写 Spring XML 配置(组件扫描、视图解析器、数据源、事务管理器...)
5. 手动安装 Tomcat
6. 打包成 .war 部署到 Tomcat 的 webapps/
7. 启动 Tomcat
8. 祈祷一切配置正确
web.xml:
xml
<web-app>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
spring-mvc.xml:
xml
<context:component-scan base-package="com.example"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/shop"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven/>
pom.xml 每个 JAR 手写版本号:
xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<!-- 还有十几个,每个都要查版本兼容性 -->
一个项目还没写一行业务代码,光配置就要折腾半天。而且这些配置几乎每个项目都差不多。
二、Spring Boot 做了什么
一句话:把"每个项目都要做但几乎一模一样的配置工作"自动化了。
java
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
就这么多。 一个注解,一个 main 方法。你就有了一个完整的 Web 应用。
三、Starter------依赖管理的自动化
xml
<!-- 之前:十几个 JAR,每个写版本号 -->
<!-- 现在:三个 Starter 搞定 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
Starter 本身没有代码,只是一个依赖清单。一个 spring-boot-starter-web 背后自动引入了:
javascript
spring-boot-starter-web
├── spring-boot-starter
│ ├── spring-boot
│ ├── spring-boot-autoconfigure
│ ├── spring-core、spring-context
│ └── slf4j + logback(日志)
├── spring-boot-starter-json
│ └── jackson-databind(JSON 处理)
├── spring-boot-starter-tomcat
│ └── tomcat-embed-core(内嵌 Tomcat)
├── spring-web
└── spring-webmvc
所有版本由 spring-boot-dependencies 这个 BOM 统一锁定。你不用操心任何版本兼容性问题。
四、自动配置------Spring Boot 的灵魂
@SpringBootApplication 等价于三个注解:
java
@SpringBootApplication
// 等价于:
@SpringBootConfiguration // 标记为配置类
@EnableAutoConfiguration // 开启自动配置 ← 核心
@ComponentScan // 扫描当前包及子包
自动配置的工作流程:
markdown
第1步:读取一个名单文件
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
第2步:这个文件列出了所有自动配置类(约 130 个)
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.TomcatServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
...
第3步:逐个评估每个配置类上的条件注解
第4步:条件满足的 → 创建 Bean;条件不满足的 → 跳过
条件注解决定了哪些配置生效:
java
// Tomcat 的自动配置(简化版)
@AutoConfiguration
@ConditionalOnClass({Servlet.class, Tomcat.class}) // classpath 有 Tomcat 才生效
@ConditionalOnMissingBean(WebServerFactory.class) // 你没手动配置才生效
public class TomcatServletWebServerFactoryAutoConfiguration {
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
常见的条件注解:
less
@ConditionalOnClass classpath 里有某个类才生效
@ConditionalOnMissingBean 容器里没有某个 Bean 才生效
@ConditionalOnProperty 配置文件里有某个属性才生效
@ConditionalOnWebApplication 是 Web 应用才生效
这就是"你没配置的它帮你配,你配置了的它不覆盖"的实现原理。
五、内嵌 Tomcat------为什么不需要外部服务器
关键事实:Tomcat 本身就是一个 Java 程序,就是一堆 JAR 文件。 跟你的代码没有任何区别。
传统用法中 Tomcat 是独立运行的,但你完全可以在自己的代码里创建 Tomcat 实例:
java
// 不依赖 Spring Boot,纯手动内嵌 Tomcat
import org.apache.catalina.startup.Tomcat;
public class MyApp {
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.setBaseDir(System.getProperty("java.io.tmpdir"));
tomcat.addWebapp("/", new File("src/main/resources").getAbsolutePath());
tomcat.start();
tomcat.getServer().await();
}
}
前提:pom.xml 引入 tomcat-embed-core(一个普通的 Java 库)。
Spring Boot 的自动化过程:
markdown
1. spring-boot-starter-web 引入了 tomcat-embed-core(Tomcat 代码进入 classpath)
2. 自动配置类检测到 classpath 里有 Tomcat.class
3. 创建 TomcatServletWebServerFactory Bean
4. 上下文刷新时调用 factory.getWebServer()
5. 内部执行 new Tomcat(),设置端口,注册 DispatcherServlet
6. 调用 tomcat.start()
想换 Jetty? Tomcat、Jetty、Undertow 是三个不同的 Servlet 容器产品,来自不同的团队,代码完全不同,但都实现了 Java 的 Servlet 规范。Spring Boot 额外加了一层抽象(WebServerFactory 接口),上层代码不关心具体用哪个。
xml
<!-- 排除 Tomcat,引入 Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
classpath 里的类变了,自动配置类检测到 Jetty 的类存在而 Tomcat 的类不存在,自动切换。
六、application.properties 配置绑定
properties
# application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/shop
spring.datasource.username=root
spring.datasource.password=123456
server.port=8080
Spring Boot 内部通过 @ConfigurationProperties 把配置文件的值映射到 Java 对象:
java
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
private String url;
private String username;
private String password;
// getter/setter
}
ini
启动时:扫描 @ConfigurationProperties → 发现 prefix = "spring.datasource"
→ 去 properties 文件找 spring.datasource.* → 通过反射调用 setter 注入值
七、多环境配置(Profile)
matlab
application.properties ← 通用配置
application-dev.properties ← 开发环境
application-prod.properties ← 生产环境
properties
# application-dev.properties
spring.datasource.url=jdbc:mysql://localhost:3306/shop_dev
server.port=8080
# application-prod.properties
spring.datasource.url=jdbc:mysql://prod-server:3306/shop_prod
server.port=80
bash
# 运行时切换,不需要改代码
java -jar app.jar --spring.profiles.active=prod
八、Spring Boot 的本质
markdown
Spring Boot 解决了四个配置层面的问题:
1. 自动配置 → 条件注解 + 130 个配置类名单,自动检测自动创建
2. Starter → 依赖清单 + BOM 版本管理,不用手动管理 JAR
3. 内嵌服务器 → Tomcat 就是一个 JAR,main 方法直接启动
4. 统一配置 → application.properties 管所有配置,支持多环境
底层还是 Spring Framework + Spring MVC,Spring Boot 只是自动化了组装过程。
第四部分:三者的层次关系
typescript
Spring Framework(2003)
解决:代码层面的问题
├── IoC/DI:管理对象的创建和依赖注入,解耦
├── 模板封装:Redis/DB 等常用操作的封装
└── AOP:事务、权限、日志等横切逻辑
↑ 在此基础上加了一层
Spring MVC
解决:与前端交互的问题
├── URL 路由:@RequestMapping / @GetMapping
├── 参数解析:@RequestParam / @PathVariable / @RequestBody
└── 返回处理:JSON / 模板渲染 / 重定向
↑ 在此基础上加了一层
Spring Boot(2014)
解决:项目搭建和配置的问题
├── 自动配置:条件注解 + 名单文件
├── Starter:依赖清单 + BOM 版本管理
├── 内嵌服务器:Tomcat/Jetty/Undertow
└── 统一配置:application.properties + Profile
Spring Framework:造了很多好用的零件
Spring MVC:在零件之上造了处理 HTTP 的机器
Spring Boot:把所有零件和机器自动组装好,插上电就能用
Spring 全家桶就是 Java 企业开发中不同场景下的解决方案集合:
| 模块 | 解决的问题 |
|---|---|
| Spring Framework | 对象管理、依赖注入、AOP |
| Spring MVC | HTTP 请求处理 |
| Spring Boot | 自动配置、快速搭建 |
| Spring Data | 数据访问统一抽象 |
| Spring Security | 认证授权 |
| Spring Cloud | 微服务架构 |
第五部分:回顾与检验
回顾
markdown
你从这篇文章学到了什么:
1. Spring Framework 的三大核心:IoC/DI、模板封装、AOP
- IoC/DI 解决对象耦合,对象不自己创建依赖
- 模板封装解决重复代码(JdbcTemplate、RedisTemplate)
- AOP 解决横切关注点(事务、权限、日志)
2. Spring MVC 在 Servlet 之上封装了 HTTP 处理
- DispatcherServlet 是唯一入口,负责路由分发
- 自动解析多种来源的参数
- 自动处理多种格式的返回值
3. Spring Boot 自动化了项目搭建
- Starter 管理依赖,BOM 锁定版本
- 条件注解决定哪些自动配置生效
- 内嵌 Tomcat,main 方法直接启动
现在回头看看开头的问题
问题1:什么是"依赖注入"?为什么要"注入",直接 new 不行吗?
依赖注入就是由外部(Spring 容器)把依赖对象"传进来",而不是在代码里硬编码 new。直接 new 的问题是耦合------类与类之间被绑死了,改实现必须改代码,难以测试和替换。
问题2:Spring MVC 的 DispatcherServlet 是什么?它和 Servlet 是什么关系?
DispatcherServlet 就是一个 Servlet,它是 Spring MVC 的唯一入口。所有 HTTP 请求先到它这里,它根据 URL 找到对应的 Controller 方法,然后分发过去。它在 Tomcat 里注册为一个普通的 Servlet。
问题3:Spring Boot 的自动配置是怎么知道你用了 Tomcat 而不是 Jetty 的?
Spring Boot 启动时读取一个名单文件(约 130 个自动配置类),逐个检查条件注解。Tomcat 的配置类上有
@ConditionalOnClass({Tomcat.class}),Jetty 的配置类上有@ConditionalOnClass({Server.class})。你引入了 Tomcat 的 JAR,classpath 里就有 Tomcat.class,Tomcat 的配置生效;没有引入 Jetty 的 JAR,Jetty 的配置跳过。
问题4:@Autowired 背后到底发生了什么?
Spring 容器启动时扫描所有带 @Component 的类,创建实例后扫描字段上的 @Autowired 注解,在容器里找类型匹配的对象,通过反射的
field.set()方法把值注入进去。
问题5:Spring 全家桶这么多模块,它们之间是什么关系?
是层层依赖的。Spring Framework 是基础,Spring MVC 建立在它之上,Spring Boot 建立在两者之上。Spring Data、Spring Security 等也是建立在 Spring Framework 之上的。它们不是独立的,而是一个生态系统的不同层级。