从零认识 SpringBoot Web 后端:HTTP 协议 + 三层架构 + IOC/DI 知识点全梳理(附全套代码)
大家好!前面几篇我们把前端三大件(HTML、CSS、JS+Vue+Ajax)和 Maven 都梳理完了,这次终于轮到真正的后端动态资源开发了。
本文按照「静态资源与动态资源 → SpringBootWeb 入门 → HTTP 协议 → SpringBootWeb 案例 → 分层解耦 → 附录 」的顺序整理,覆盖了 Web 后端基础的全部知识点:Spring 是什么、怎么用 IDEA 快速搭建一个能跑的 Web 工程、HTTP 请求/响应协议长什么样、如何从零写一个用户列表接口,以及最关键的三层架构 + IOC/DI 解耦思想。
一、开篇:静态资源与动态资源
那在前面讲解 Web 前端开发的时候,我们学习了前端网页开发的三剑客 HTML、CSS、JS,通过这三项技术,我们就可以制作前端页面了。最终这些页面资料可以部署在服务器上,打开浏览器就能直接访问服务器上部署的前端页面:

像 HTML、CSS、JS 以及图片、音频、视频等这些资源,我们都称为静态资源。所谓静态资源,就是指在服务器上存储的、不会改变的数据,通常不会根据用户的请求而变化。
与静态资源对应的还有一类资源,就是动态资源。所谓动态资源,就是指存储在服务器端、会根据用户请求和其他数据动态生成的,内容可能会在每次请求时都发生变化。比如:Servlet、JSP(负责逻辑处理):

而 Servlet、JSP 这些技术现在早都被企业淘汰了,现在企业项目开发中,都是直接基于 Spring 框架来构建动态资源。对于 Java 程序开发的动态资源,我们通常会将其部署在 Tomcat 这样的 Web 服务器中运行,而浏览器与服务器通信时,基本都是基于 HTTP 协议的:

上述所描述的浏览器/服务器的架构模式,我们称之为 BS 架构。它和 CS 架构是两种主流的软件架构:
- BS 架构 :Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。
- 优点:维护方便
- 缺点:体验一般
- CS 架构 :Client/Server,客户端/服务器架构模式。需要单独开发维护客户端。
- 优点:体验不错
- 缺点:开发维护麻烦
既然静态资源开发技术(HTML、CSS、JS、Vue、Axios)前面都学过了,接下来我们就要学习动态资源开发技术。而动态资源开发中,像早期的 Servlet、JSP 早被企业淘汰,现在企业开发主流就是基于 Spring 体系中的框架。所以,本次课程内容分为四个部分:
- SpringBootWeb 入门
- HTTP 协议
- SpringBootWeb 案例
- 分层解耦
二、SpringBootWeb 入门
2.1 概述
① Spring 是什么
打开 Spring 官网(spring.io),映入眼帘的就是一句话:Spring makes Java simple(让 Java 变得更简单):

Spring 是一款轻量级的、开源的 Java 框架,它就是为了让 Java 开发变得更简单、更高效而生的。Spring 从 2002 年诞生至今,已经发展为一个庞大的家族体系,下面是 Spring 家族的部分开源项目(按流行度排序):

这么多子项目组合在一起,就构成了所谓的 Spring 全家桶------一个完整的开发生态圈:

② Spring Framework
Spring 家族里最核心的,就是 Spring Framework(Spring 框架),它是整个 Spring 体系的基石。Spring Framework 提供了两个核心功能:
- IOC(Inversion of Control):控制反转
- AOP(Aspect Oriented Programming):面向切面编程

不过 Spring Framework 虽然功能强大,但使用起来存在两大痛点:
- 配置繁琐:虽然 IOC、AOP 大大简化了开发,但使用 Spring Framework 开发一个程序,需要写大量的配置(比如 xml 配置、bean 注册、依赖引入等),哪怕只是做一个简单的小功能,也要配置一大坨;
- 依赖繁琐 :引入一个功能,就需要引入一大堆依赖(jar 包),而且这些依赖的版本还需要手动管理,容易冲突,特别痛苦。
正是因为 Spring Framework 存在这两大痛点,Spring Boot 应运而生。Spring Boot 是 Spring 家族中一个全新的框架,它用来简化 Spring 应用的初始搭建以及开发过程。简单来说,Spring Boot 让开发者不用再配置一大堆东西,拿来就能用,开箱即食。
2.2 入门程序
需求
我们先做一个最经典的入门案例:浏览器发起请求 http://localhost:8080/hello?name=itheima,页面输出 Hello itheima~:

开发步骤
第 1 步:创建 SpringBoot 工程
打开 IDEA,通过 File → New → Project 创建新工程,选择 Spring Initializr(Spring 官方骨架),点击 Next:

第 2 步:勾选 Web 开发相关依赖
在创建工程时,勾选 Spring Web 依赖,IDEA 会自动帮我们引入 Web 开发所需的一系列依赖:

第 3 步:定义 HelloController
创建完成后的工程目录结构如下:

在 com.itheima 包下新建一个 HelloController 类,编写如下代码:
java
package com.itheima;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController //标识当前类是一个请求处理类
public class HelloController {
@RequestMapping("/hello") //标识请求路径
public String hello(String name){
System.out.println("HelloController ... hello: " + name);
return "Hello " + name;
}
}

第 4 步:运行测试
运行带 @SpringBootApplication 注解的引导类(即 XxxApplication),项目启动后在浏览器访问:


页面输出 Hello itheima,入门案例搞定。
常见问题
如果创建工程时连不上 Spring 的脚手架网站(start.spring.io),会出现下面的报错:

解决办法:改用阿里云脚手架 https://start.aliyun.com(国内服务器,速度快、稳定):

2.3 入门解析
① 为什么一个 main 方法就能启动 Web 应用?
我们明明没有配置 Tomcat,为什么一个 main 方法运行起来,Web 应用就能在 8080 端口被访问?秘密就在起步依赖 和内嵌 Tomcat 上:

起步依赖(starter) 是 Spring Boot 的核心特性之一。我们在 pom.xml 中引入的 spring-boot-starter-web,本身并不直接提供功能,但它会自动传递依赖------把我们开发 Web 应用所需的一整套依赖(Spring 核心、Web MVC、Jackson 等)全部拉进来:

而 Web 开发需要运行环境,spring-boot-starter-web 也会传递引入 Tomcat。Spring Boot 内置了 Tomcat 服务器,启动时自动把应用部署到内嵌的 Tomcat 上,监听 8080 端口,所以一个 main 方法就能跑起一个完整的 Web 应用:

② Spring Boot 的特点
到这里,Spring Boot 的核心特点也就呼之欲出了:
- 起步依赖:简化依赖配置,自动完成依赖传递;
- 自动配置:根据依赖自动完成大量的默认配置,无需手动编写;
- 内嵌服务器:内置 Tomcat,无需单独部署,一个 main 方法即可启动。
三、HTTP 协议
3.1 HTTP 概述
介绍
HTTP (Hyper Text Transfer Protocol,超文本传输协议),是一个基于 TCP 的应用层协议,用于客户端与服务器之间的通信:

在浏览器中按 F12 打开开发者工具,切到 Network(网络) 面板,就能直观地看到每一次请求和响应:

特点
HTTP 协议主要有三个特点:
① 基于 TCP 协议
TCP 是一种面向连接的(建立连接之前需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全。
② 请求-响应模型
浏览器发请求,服务器给响应,一来一回、一一对应:
请求和响应是一一对应关系,没有请求,就没有响应。


③ 无状态
无状态指的是客户端发送 HTTP 请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。
- 缺点:多次请求间不能共享数据
- 优点:速度快
请求之间无法共享数据会引发一个问题:比如京东购物,加入购物车和去购物车结算是两次请求。由于 HTTP 的无状态特性,加入购物车请求响应结束后,并未记录加入了什么商品;发起结算请求后,因为无法获取哪些商品加入了购物车,会导致数据展示错误。
实际使用中京东能正常展示数据,是因为 Java 早已考虑到这个问题,使用**会话技术(Cookie、Session)**来解决。具体怎么做,后面的课程会讲到。
3.2 HTTP 请求协议
介绍
HTTP 请求分为 GET 和 POST 两种常见方式。
GET 方式请求协议:请求行 + 请求头(请求行标红、请求头标黄):

POST 方式请求协议:请求行 + 请求头 + 请求体(多了一个绿色请求体):

请求协议一共由三部分组成:
- 请求行:请求方式、请求路径、协议版本;
- 请求头:客户端向服务器发送的一些附加信息,键值对形式;
- 请求体:POST 方式携带的请求数据(GET 方式没有请求体)。
常用请求头(7 个):
| 请求头 | 含义 |
|---|---|
| Host | 表示请求的主机名 |
| User-Agent | 浏览器版本。例如:Chrome 浏览器的标识类似 Mozilla/5.0 ...Chrome/79,IE 浏览器的标识类似 Mozilla/5.0 (Windows NT ...)like Gecko |
| Accept | 表示浏览器能接收的资源类型,如 text/,image/ 或者 / 表示所有 |
| Accept-Language | 表示浏览器偏好的语言,服务器可以据此返回不同语言的网页 |
| Accept-Encoding | 表示浏览器可以支持的压缩类型,例如 gzip、deflate 等 |
| Content-Type | 请求主体的数据类型 |
| Content-Length | 数据主体的大小(单位:字节) |
举例说明:服务端可以根据请求头中的内容来获取客户端的相关信息,有了这些信息服务端就可以处理不同的业务需求。
比如:不同浏览器解析 HTML 和 CSS 标签的结果会有不一致,导致相同代码在不同浏览器出现不同效果。服务端根据请求头中的 User-Agent 获取到浏览器类型,就可以针对不同浏览器返回不同的代码来达到一致的效果(这就是我们常说的浏览器兼容问题)。
GET 与 POST 的区别:
| 区别方式 | GET 请求 | POST 请求 |
|---|---|---|
| 请求参数 | 请求参数在请求行中。例:/brand/findAll?name=OPPO&status=1 | 请求参数在请求体中 |
| 请求参数长度 | 请求参数长度有限制(浏览器不同限制也不同) | 请求参数长度没有限制 |
| 安全性 | 安全性低。原因:请求参数暴露在浏览器地址栏中 | 安全性相对高 |
获取请求数据
浏览器发来的请求数据(请求行、请求头、请求体),最终由 Tomcat 解析,封装成 HttpServletRequest 对象,并传给我们的 Controller 方法:

在方法参数中声明 HttpServletRequest request,就能拿到原始请求数据:
java
@RestController
public class RequestController {
/**
* 请求路径 http://localhost:8080/request?name=Tom&age=18
* @param request
* @return
*/
@RequestMapping("/request")
public String request(HttpServletRequest request){
//1.获取请求参数 name, age
String name = request.getParameter("name");
String age = request.getParameter("age");
System.out.println("name = " + name + ", age = " + age);
//2.获取请求路径
String uri = request.getRequestURI();
String url = request.getRequestURL().toString();
System.out.println("uri = " + uri);
System.out.println("url = " + url);
//3.获取请求方式
String method = request.getMethod();
System.out.println("method = " + method);
//4.获取请求头
String header = request.getHeader("User-Agent");
System.out.println("header = " + header);
return "request success";
}
}
运行后在浏览器访问 http://localhost:8080/request?name=Tom&age=18,控制台输出如下:

3.3 HTTP 响应协议
格式介绍
HTTP 响应同样由三部分组成:
- 响应行:协议版本、状态码、状态描述;
- 响应头:服务器返回给客户端的一些附加信息;
- 响应体:服务器返回给客户端的数据(页面内容、JSON 数据等)。

常用响应头:
| 响应头 | 含义 |
|---|---|
| Content-Type | 表示该响应内容的类型,例如 text/html、image/jpeg |
| Content-Length | 表示该响应内容的长度(字节数) |
| Content-Encoding | 表示该响应压缩算法,例如 gzip |
| Cache-Control | 指示客户端应如何缓存,例如 max-age=300 表示可以最多缓存 300 秒 |
| Set-Cookie | 告诉浏览器为当前页面所在的域设置 cookie |
响应状态码
状态码用于标识服务器对请求的处理结果,是响应协议里最核心的部分。常见状态码一共分为五类(1xx~5xx):
| 状态码分类 | 说明 |
|---|---|
| 1xx | 响应中 ------ 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 |
| 2xx | 成功 ------ 表示请求已经被成功接收,处理已完成 |
| 3xx | 重定向 ------ 重定向到其它地方,让客户端再发起一个请求以完成整个处理 |
| 4xx | 客户端错误 ------ 处理发生错误,责任在客户端。如:客户端请求一个不存在的资源、客户端未被授权、禁止访问等 |
| 5xx | 服务器端错误 ------ 处理发生错误,责任在服务端。如:服务端抛出异常、路由出错、HTTP 版本不支持等 |
设置响应数据
服务器端如何设置响应数据?Tomcat 会把响应数据封装成 HttpServletResponse 对象传给我们:

方式一:使用原生的 HttpServletResponse 对象设置:
java
package com.itheima;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class ResponseController {
@RequestMapping("/response")
public void response(HttpServletResponse response) throws IOException {
//1.设置响应状态码
response.setStatus(401);
//2.设置响应头
response.setHeader("name","itcast");
//3.设置响应体
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");
response.getWriter().write("<h1>hello response</h1>");
}
@RequestMapping("/response2")
public ResponseEntity<String> response2(HttpServletResponse response) throws IOException {
return ResponseEntity
.status(401)
.header("name","itcast")
.body("<h1>hello response</h1>");
}
}
方式二:直接返回 ResponseEntity 对象(推荐,链式调用更简洁)。上面代码里的 /response2 就是这种方式:.status() 设置状态码、.header() 设置响应头、.body() 设置响应体。
运行后在浏览器分别访问 /response 和 /response2,F12 里可以看到状态码 401、响应头 name:itcast、响应体 <h1>hello response</h1> 都生效了:

四、SpringBootWeb 案例
4.1 需求说明
现在做一个稍微完整的案例:页面加载时,通过 Ajax 请求 /list 接口,把 user.txt 文件中的数据渲染成用户列表,效果如下:

需求拆解一下:
- 前端:在
static下准备user.html静态页面,页面加载时用 Ajax 请求/list; - 后端:准备
user.txt数据文件(一行一条用户数据,字段以逗号分隔); - Controller 接收
/list请求,读取文件、解析数据,以 JSON 格式响应。
4.2 代码实现
第 1 步:创建工程(勾选 web + lombok)
新建 SpringBoot 工程,这次除了 Web 依赖,还要勾选 Lombok 依赖:


第 2 步:准备数据与页面
在 resources 下创建 user.txt 数据文件,在 static 下放 user.html 静态页面:

第 3 步:定义 User 实体类
利用 Lombok 的注解,实体类代码非常简洁:
java
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 封装用户信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String name;
private Integer age;
private LocalDateTime updateTime;
}
@Data:自动生成 getter/setter、toString 等方法;@NoArgsConstructor:生成无参构造器;@AllArgsConstructor:生成全参构造器。
第 4 步:引入 hutool 工具类
在 pom.xml 中加入 hutool 依赖(一个非常好用的 Java 工具库,后面读取文件、转换 JSON 都要用到它):
xml
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.27</version>
</dependency>
第 5 步:编写 UserController
java
import cn.hutool.core.io.IoUtil;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONUtil;
import com.itheima.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
public class UserController {
@RequestMapping("/list")
public String list(){
//1.加载并读取文件
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
//2.解析数据,封装成对象 --> 集合
List<User> userList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime);
}).collect(Collectors.toList());
//3.响应数据
//return JSONUtil.toJsonStr(userList, JSONConfig.create().setDateFormat("yyyy-MM-dd HH:mm:ss"));
return userList;
}
}
第 6 步:启动测试
启动服务,浏览器访问 http://localhost:8080/user.html,页面正常展示用户列表:

4.3 @ResponseBody 注解
细心的同学会发现,案例的 /list 接口返回的是一个 List<User> 集合,但前端拿到的是 JSON 数据 。这是因为 Spring MVC 默认的 MessageConverter 会自动把方法返回值序列化成 JSON。
那么 @ResponseBody 和 @RestController 是什么关系?其实很简单:
@ResponseBody:标注在方法上,表示方法的返回值直接作为 HTTP 响应的内容体返回(对象会自动转成 JSON),而不是返回一个视图页面;@RestController=@Controller+@ResponseBody,类上加了它,就等于所有方法都自动带上了@ResponseBody。
提示:前后端分离的项目中,一般直接在请求处理类上加
@RestController注解,就无需在方法上加@ResponseBody注解了。
4.4 问题分析
案例虽然跑通了,但代码存在一个明显的坏味道------数据加载、逻辑处理、响应输出全部堆在 Controller 一个类里:

这样的代码违反了我们常说的单一职责原则:
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。
这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利于后期的维护。

而且,这个程序一旦规模变大,还会引发一连串问题:Controller 越来越臃肿、难以维护;代码复用性差;模块之间耦合度高,改一处牵全身......要解决这些问题,就需要引入分层架构的思想。
五、分层解耦
5.1 三层架构
介绍
三层架构是一种软件分层设计思想:把程序按照职责划分为三个层次:
- Controller 控制层:接收请求,响应数据;
- Service 业务层:处理业务逻辑(对数据的加工处理);
- Dao 数据访问层(Data Access Object):负责数据访问操作(增删改查),比如读文件、操作数据库。

三个层次各司其职、单一职责,调用关系是:Controller → Service → Dao,每一层只和相邻的层打交道。
基于三层架构的程序执行流程如下:

代码拆分
把刚才的案例按三层架构拆分,完整代码分为四个文件:
① UserController(控制层)------只负责接收请求、调用 Service、响应数据:
java
package com.itheima.controller;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import com.itheima.service.impl.UserServiceImpl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
private UserService userService = new UserServiceImpl();
@RequestMapping("/list")
public List<User> list(){
//1.调用Service
List<User> userList = userService.findAll();
//2.响应数据
return userList;
}
}
② UserService 接口 + 实现类(业务层)------只负责业务逻辑,把"解析文件行 → 封装对象"的加工逻辑放进来:
java
package com.itheima.service;
import com.itheima.pojo.User;
import java.util.List;
public interface UserService {
public List<User> findAll();
}
java
package com.itheima.service.impl;
import com.itheima.dao.UserDao;
import com.itheima.dao.impl.UserDaoImpl;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
@Override
public List<User> findAll() {
List<String> lines = userDao.findAll();
List<User> userList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime);
}).collect(Collectors.toList());
return userList;
}
}
③ UserDao 接口 + 实现类(数据访问层)------只负责最底层的文件读取:
java
package com.itheima.dao;
import java.util.List;
public interface UserDao {
public List<String> findAll();
}
java
package com.itheima.dao.impl;
import cn.hutool.core.io.IoUtil;
import com.itheima.dao.UserDao;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class UserDaoImpl implements UserDao {
@Override
public List<String> findAll() {
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
return lines;
}
}
分层改造后,具体的请求调用流程变成:浏览器 → Controller → Service → Dao → 文件,然后再原路返回:

思考:按照三层架构的思想,如果要对业务逻辑(Service 层)进行变更,会影响到 Controller 层和 Dao 层吗?
答案:不会影响。程序的扩展性、维护性变得更好了。
5.2 分层解耦
问题分析
三层架构虽然把代码按职责拆开了,但还有一个隐患------层与层之间耦合 。看上面的代码,Controller 里直接 new UserServiceImpl(),ServiceImpl 里直接 new UserDaoImpl():

一旦我们需要更换实现类(比如把 UserServiceImpl 换成 UserServiceImpl2),就必须修改 Controller 里的代码,改一处、重新编译、重新部署,牵一发动全身:

这就涉及两个核心概念------内聚 与耦合:
高内聚:指的是一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即"高内聚"。
低耦合:指的是软件中各个层、模块之间的依赖关联程度越低越好。
我们的目标,就是让程序做到高内聚、低耦合 ,甚至把层与层之间的耦合解除掉:

解耦思路
解除耦合的核心思路分三步:
- 不使用 new 关键字------Controller 里不再直接 new 出 Service 实现类:
- 把要用的对象交给一个容器(IOC 容器)管理,由容器统一创建、统一持有;
- 需要用到时,从容器中获取------应用程序运行时从容器里取依赖对象,而不是自己创建。
第一步:去掉 new,只声明接口引用:

第二步:把所有将要使用的对象,统一交给一个容器来管理:

第三步:程序运行时,从容器中获取依赖对象并使用:

这样,更换实现类时只需要在容器里"换一个对象",Controller 代码一行都不用改。这个思想就是 IOC 控制反转 + DI 依赖注入,也正是 Spring 框架最核心的两大特性。
5.3 IOC & DI 入门
先记住两个概念:
- IOC(Inversion Of Control)控制反转 :对象的创建控制权由程序自身反转 到外部(容器)。以前是程序主动 new 对象,现在是容器负责创建并管理对象;
- DI(Dependency Injection)依赖注入 :容器为程序注入运行时所依赖的对象(把依赖"喂"给需要它的地方)。
IOC 和 DI 是一体两面的:IOC 是思想、是目标,DI 是实现手段。用 IOC 容器管理对象,再用 DI 把依赖注入进来,耦合就解除了。
入门用法只需两个注解:
@Component:加在类上,表示把这个类交给 IOC 容器管理(声明 bean);@Autowired:加在需要依赖的成员变量上,表示让容器把依赖对象注入进来。
改造我们的三层代码,给 UserDaoImpl 和 UserServiceImpl 加 @Component:
java
@Component
public class UserDaoImpl implements UserDao {
@Override
public List<String> findAll() {
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
return lines;
}
}
java
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public List<User> findAll() {
List<String> lines = userDao.findAll();
List<User> userList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime);
}).collect(Collectors.toList());
return userList;
}
}
给 Controller 里的 userService 加 @Autowired,去掉 new:
java
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/list")
public List<User> list(){
//1.调用Service
List<User> userList = userService.findAll();
//2.响应数据
return userList;
}
}
再次启动服务,访问 user.html,功能完全正常------而且这次 Controller 里没有任何 new,依赖全部由容器注入:

入门程序举例:在 Controller 运行的时候,就要到 IOC 容器当中去查找 UserService 这个类型的对象,而 IOC 容器中刚好有一个 UserService 类型的对象,所以就找到了这个对象完成注入操作。
5.4 IOC 详解
① Bean 的声明
@Component 是声明 bean 的基础注解 。但在实际开发中,我们更习惯用它的衍生注解,让代码语义更清晰:
| 注解 | 说明 | 位置 |
|---|---|---|
| @Component | 声明 bean 的基础注解 | 不属于以下三类时,用此注解 |
| @Controller | @Component 的衍生注解 | 标注在控制层类上 |
| @Service | @Component 的衍生注解 | 标注在业务层类上 |
| @Repository | @Component 的衍生注解 | 标注在数据访问层类上(由于与 mybatis 整合,用的少) |
所以规范一点,我们应该把三层代码改写成:Controller 类上 @RestController、Service 实现类上 @Service、Dao 实现类上 @Repository:
java
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Override
public List<User> findAll() {
// ... 业务逻辑
}
}
java
@Repository
public class UserDaoImpl implements UserDao {
@Override
public List<String> findAll() {
// ... 数据访问逻辑
}
}
这四个注解本质上都做了同一件事:把类交给 IOC 容器管理 。@RestController/@Controller/@Service/@Repository 内部都"包含"了 @Component 的语义。
② 组件扫描
那 IOC 容器到底是怎么知道哪些类需要被管理的?靠的是组件扫描。
Spring 启动时,会扫描启动类所在包及其子包 下的所有类,凡是标注了 @Component 及其衍生注解的类,都会被注册成容器中的 bean:

用 @ComponentScan 注解可以显式指定扫描范围。默认情况下 ,Spring Boot 会以启动类(带 @SpringBootApplication 的类)所在的包作为扫描根,扫描该包及其子包。所以约定:启动类要放在包的根位置,Controller、Service、Dao 等类都放在它的子包下,这样才能被扫到。
5.5 DI 详解
@Autowired 用法
@Autowired 的注入方式有三种:属性注入、构造器注入、setter 注入。
方式一:属性注入(最常用)
java
@RestController
public class UserController {
//方式一: 属性注入
@Autowired
private UserService userService;
}
方式二:构造器注入(推荐,final 不可变)
java
@RestController
public class UserController {
//方式二: 构造器注入
private final UserService userService;
@Autowired //如果当前类中只存在一个构造函数, @Autowired可以省略
public UserController(UserService userService) {
this.userService = userService;
}
}
方式三:setter 注入
java
/**
* 用户信息Controller
*/
@RestController
public class UserController {
//方式三: setter注入
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
@Autowired 默认按类型装配 :容器中只有一个 UserService 类型的 bean 时,直接注入;构造器注入时,如果类中只有一个构造器,@Autowired 甚至可以省略。
注意事项
当容器中出现多个同类型 bean 时(比如有两个 UserService 的实现类都被容器管理),按类型装配就会失败:


这时有三种解决方案:
方案一:@Primary(优先选择) ------给想优先注入的 bean 加上 @Primary 注解:
java
@Primary
@Service
public class UserServiceImpl implements UserService {
}
方案二:@Qualifier(按名称指定) ------配合 @Autowired,指定注入哪一个 bean 的名称(默认名称 = 类名首字母小写):
java
@RestController
public class UserController {
@Qualifier("userServiceImpl")
@Autowired
private UserService userService;
}
方案三:@Resource(按名称装配)------JDK 提供的注解,可以直接按名称装配:
java
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
}
面试小考点:@Autowired 与 @Resource 的区别
| 对比项 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架提供的注解 | JDK(javax.annotation)提供的注解 |
| 装配方式 | 默认按类型装配 | 默认按名称装配(也可指定 name) |
| 同类型多 bean | 需配合 @Primary / @Qualifier | 用 name 属性直接指定 |
| 使用范围 | 属性、构造器、setter | 属性、setter |
六、附录:常见状态码
最后附上一张常用的 HTTP 状态码速查表,后端调试时非常有用(面试也爱考):
| 状态码 | 英文描述 | 解释 |
|---|---|---|
| 200 | OK | 客户端请求成功,即处理成功,这是我们最想看到的状态码 |
| 302 | Found | 指示所请求的资源已移动到由 Location 响应头给定的 URL,浏览器会自动重新访问到这个页面 |
| 304 | Not Modified | 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 |
| 400 | Bad Request | 客户端请求有语法错误,不能被服务器所理解 |
| 403 | Forbidden | 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 |
| 404 | Not Found | 请求资源不存在,一般是 URL 输入有误,或者网站资源被删除了 |
| 405 | Method Not Allowed | 请求方式有误,比如应该用 GET 请求方式的资源,用了 POST |
| 428 | Precondition Required | 服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头 |
| 429 | Too Many Requests | 指示用户在给定时间内发送了太多请求("限速"),配合 Retry-After(多长时间后可以请求)响应头一起使用 |
| 431 | Request Header Fields Too Large | 请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交 |
| 500 | Internal Server Error | 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 |
| 503 | Service Unavailable | 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 |
结语
到这里,Web 后端的基础知识就梳理完啦。回顾一下这篇的核心脉络:
- 静态资源 vs 动态资源:动态资源由服务器动态生成,现在主流用 Spring 体系开发,部署在 Tomcat 上;
- SpringBootWeb 入门:Spring Boot 通过起步依赖 + 自动配置 + 内嵌 Tomcat,一个 main 方法就能跑起 Web 应用;
- HTTP 协议:请求 = 请求行 + 请求头 +(请求体),响应 = 响应行 + 响应头 + 响应体,状态码 1xx~5xx 各有含义;
- SpringBootWeb 案例:从读文件到 JSON 响应,写出了一个完整的用户列表接口;
- 分层解耦 :三层架构(Controller/Service/Dao)让职责单一,IOC+DI 让层间不再耦合,
@Component声明 bean、@Autowired注入依赖,这就是 Spring 最核心的思想。
参考内容 heuqqdmbyk.feishu.cn/wiki/MQ95wD... 黑马磊哥