从零认识 SpringBoot Web 后端:HTTP 协议 + 三层架构 + IOC/DI 知识点全梳理(附全套代码)

从零认识 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 虽然功能强大,但使用起来存在两大痛点:

  1. 配置繁琐:虽然 IOC、AOP 大大简化了开发,但使用 Spring Framework 开发一个程序,需要写大量的配置(比如 xml 配置、bean 注册、依赖引入等),哪怕只是做一个简单的小功能,也要配置一大坨;
  2. 依赖繁琐 :引入一个功能,就需要引入一大堆依赖(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 文件中的数据渲染成用户列表,效果如下:

需求拆解一下:

  1. 前端:在 static 下准备 user.html 静态页面,页面加载时用 Ajax 请求 /list;
  2. 后端:准备 user.txt 数据文件(一行一条用户数据,字段以逗号分隔);
  3. 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 里的代码,改一处、重新编译、重新部署,牵一发动全身:

这就涉及两个核心概念------内聚 与耦合:

高内聚:指的是一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即"高内聚"。

低耦合:指的是软件中各个层、模块之间的依赖关联程度越低越好。

我们的目标,就是让程序做到高内聚、低耦合 ,甚至把层与层之间的耦合解除掉:

解耦思路

解除耦合的核心思路分三步:

  1. 不使用 new 关键字------Controller 里不再直接 new 出 Service 实现类:
  2. 把要用的对象交给一个容器(IOC 容器)管理,由容器统一创建、统一持有;
  3. 需要用到时,从容器中获取------应用程序运行时从容器里取依赖对象,而不是自己创建。

第一步:去掉 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 后端的基础知识就梳理完啦。回顾一下这篇的核心脉络:

  1. 静态资源 vs 动态资源:动态资源由服务器动态生成,现在主流用 Spring 体系开发,部署在 Tomcat 上;
  2. SpringBootWeb 入门:Spring Boot 通过起步依赖 + 自动配置 + 内嵌 Tomcat,一个 main 方法就能跑起 Web 应用;
  3. HTTP 协议:请求 = 请求行 + 请求头 +(请求体),响应 = 响应行 + 响应头 + 响应体,状态码 1xx~5xx 各有含义;
  4. SpringBootWeb 案例:从读文件到 JSON 响应,写出了一个完整的用户列表接口;
  5. 分层解耦 :三层架构(Controller/Service/Dao)让职责单一,IOC+DI 让层间不再耦合,@Component 声明 bean、@Autowired 注入依赖,这就是 Spring 最核心的思想。

参考内容 heuqqdmbyk.feishu.cn/wiki/MQ95wD... 黑马磊哥

相关推荐
海豚调度1 小时前
Apache DolphinScheduler 2026 年 9 月社区贡献总结报告
后端
全栈弄潮儿1 小时前
Python实战第10期:异常处理
后端·python·编程语言
专业程序开发源1 小时前
springboot校企合作双选会一站式预约与面试系统16010-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·面试·php·课程设计
郑州光合科技余经理2 小时前
同城商城源码:商品库和订单库怎么随包交付
android·java·开发语言·后端·uni-app·php·ai编程
邱杰4703 小时前
数智人事系统(十七):加班核算与薪资构成
后端·github
captain3763 小时前
Spring IoC和DI(2)
java·后端·spring
梦想很大很大3 小时前
重新思考 Workrun:AI 时代,我们还需要手动编排 Workflow 吗?
前端·人工智能·后端
2501_933923253 小时前
SpringCloud组件:Nacos服务注册快速入门
后端·spring·spring cloud·nacos