Spring 到底在做什么?从零开始理解 Java 企业开发的核心框架

这篇文章在讲什么

这篇文章会带你从零开始理解 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。"

你需要:

  1. 写一个新的 PostgresOrderRepository 实现
  2. 修改 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 之上的。它们不是独立的,而是一个生态系统的不同层级。

相关推荐
每天进步一点_JL2 小时前
Spring 【多实现切换 & 事务代理机制】深度解析
后端
彩票管理中心秘书长2 小时前
MySQL 数据库高级与网络管理操作命令大全
后端
Gopher_HBo2 小时前
CompletableFuture函数原理
后端
香山上的麻雀10082 小时前
由 Rust 开发的能大幅降低LLM token消耗的高性能 CLI 代理工具 rtk
开发语言·后端·rust
神奇小汤圆2 小时前
Java vs Go:哈希冲突解决之道,为什么一个用红黑树,一个用桶?
后端
神奇小汤圆2 小时前
得物二面:Redis 中某个 Key 访问量特别大怎么办?我:Redis 能顶得住... 生瓜蛋子 生瓜蛋子
后端
掘金者阿豪2 小时前
Spring Data JPA 接入金仓数据库:少写代码,多干活
前端·后端
Moment2 小时前
AI 时代,为什么全栈项目越来越离不开 Monorepo 和 TypeScript
前端·javascript·后端
Rick19932 小时前
LangChain(含 LangChain4j)和 Spring AI的区别
人工智能·spring·langchain