SpringBoot基础总结

文章目录

    • [一、 SpringBootWeb快速入门](#一、 SpringBootWeb快速入门)
      • [1 需求](#1 需求)
      • 1.创建空工程
      • 2.创建SpringBoot模块
      • 3.SpringBoot启动内嵌tomcat分析
      • [4. HTTP协议](#4. HTTP协议)
        • [4.1 HTTP-概述](#4.1 HTTP-概述)
          • [4.1.1 介绍](#4.1.1 介绍)
          • [4.2.2 特点](#4.2.2 特点)
        • [4.2 HTTP-请求协议](#4.2 HTTP-请求协议)
        • [4.3 HTTP-响应协议](#4.3 HTTP-响应协议)
          • [4.3.1 格式介绍](#4.3.1 格式介绍)
          • [4.3.2 响应状态码](#4.3.2 响应状态码)
      • [5. WEB服务器-Tomcat](#5. WEB服务器-Tomcat)
        • [5.1 简介](#5.1 简介)
          • [5.1.1 Web服务器](#5.1.1 Web服务器)
          • [5.1.2 Tomcat](#5.1.2 Tomcat)
        • [5.2 基本使用](#5.2 基本使用)
          • [5.2.1 下载](#5.2.1 下载)
          • [5.2.2 安装与卸载](#5.2.2 安装与卸载)
          • [5.2.3 启动与关闭](#5.2.3 启动与关闭)
          • [5.2.4 配置](#5.2.4 配置)
        • [5.3 入门程序解析](#5.3 入门程序解析)
      • [6. Springboot内部创建并启动tomcat容器的源码分析](#6. Springboot内部创建并启动tomcat容器的源码分析)
        • [6.1. 核心入口:`SpringApplication.run()`](#6.1. 核心入口:SpringApplication.run())
        • [6.2. 自动配置触发:`ServletWebServerFactoryAutoConfiguration`](#6.2. 自动配置触发:ServletWebServerFactoryAutoConfiguration)
        • [6.3. Tomcat 容器工厂的创建:`EmbeddedTomcat`](#6.3. Tomcat 容器工厂的创建:EmbeddedTomcat)
        • [6.4. Tomcat 容器的初始化与启动](#6.4. Tomcat 容器的初始化与启动)
        • [6.5. 核心组件与流程总结](#6.5. 核心组件与流程总结)
        • [6.6. 自定义扩展点](#6.6. 自定义扩展点)
        • [6.7. 源码位置参考](#6.7. 源码位置参考)
  • 二、SpringBoot请求响应笔记
  • 三、SpringBoot的配置文件
    • [1. YAML配置文件](#1. YAML配置文件)
    • [2. yml支持数据格式](#2. yml支持数据格式)
    • [3. 配置文件属性注入Bean](#3. 配置文件属性注入Bean)
      • [3.1 @Value](#3.1 @Value)
      • [3.2 @ConfigurationProperties](#3.2 @ConfigurationProperties)
    • [4. Profile(多配置文件)](#4. Profile(多配置文件))
      • [4.1. Profile概述](#4.1. Profile概述)
      • [4.2 配置多Profile案例](#4.2 配置多Profile案例)
        • [1. 多profile文件方式:](#1. 多profile文件方式:)
        • [2. yml多文档方式:](#2. yml多文档方式:)
  • 四、SpringBoot整合其他框架
    • [1. 集成MyBatis](#1. 集成MyBatis)
    • [2. 集成Junit](#2. 集成Junit)
    • [3. 集成 RestTemplate](#3. 集成 RestTemplate)
    • [4. 扩展了解:除此之外还可以整合什么?](#4. 扩展了解:除此之外还可以整合什么?)
    • [5. 附:常见问题](#5. 附:常见问题)
      • [1. @Autowired注入Bean报错](#1. @Autowired注入Bean报错)
      • [2. 找不到@RunWith](#2. 找不到@RunWith)
  • 五、开发规范
    • [5.1. 开发规范](#5.1. 开发规范)
      • [5.1.1 REST风格](#5.1.1 REST风格)
      • [5.1.2 统一响应结果](#5.1.2 统一响应结果)
      • [5.1.3 开发流程](#5.1.3 开发流程)
  • 六、异常处理
    • [6.1. 现象](#6.1. 现象)
    • [6.2. 思考](#6.2. 思考)
    • [6.3. 全局异常处理器](#6.3. 全局异常处理器)
    • [6.4. 测试](#6.4. 测试)
  • 七、过滤器Filter
    • [7.1. 介绍](#7.1. 介绍)
    • [7.2. 快速入门](#7.2. 快速入门)
    • [7.3. 执行流程](#7.3. 执行流程)
    • [7.4. Filter 拦截路径](#7.4. Filter 拦截路径)
    • [7.5. 登录校验Filter](#7.5. 登录校验Filter)
    • [7.6. 测试](#7.6. 测试)
  • 八、拦截器Interceptor
    • [8.1. 介绍](#8.1. 介绍)
    • [8.2. 快速入门](#8.2. 快速入门)
    • [8.3. 执行流程](#8.3. 执行流程)
    • [8.4. 登录校验Interceptor](#8.4. 登录校验Interceptor)
    • [8.5. 测试](#8.5. 测试)
    • [8.6 Filter 与 Interceptor 区别](#8.6 Filter 与 Interceptor 区别)
  • 九、SpringBoot-事务&AOP
    • [1. 事务管理](#1. 事务管理)
      • [1.1 事务回顾](#1.1 事务回顾)
      • [1.2 案例](#1.2 案例)
      • [1.3 Spring事务管理](#1.3 Spring事务管理)
      • [1.4 事务进阶](#1.4 事务进阶)
        • [1.4.1 rollbackFor](#1.4.1 rollbackFor)
        • [1.4.2 propagation](#1.4.2 propagation)
    • [2. AOP基础](#2. AOP基础)
      • [2.1 记录方法执行耗时](#2.1 记录方法执行耗时)
      • [2.2 AOP快速入门](#2.2 AOP快速入门)
      • [2.3 执行流程](#2.3 执行流程)
      • [2.4 AOP核心概念](#2.4 AOP核心概念)
    • [3. AOP进阶](#3. AOP进阶)
      • [3.1 通知类型](#3.1 通知类型)
      • [3.2 通知顺序](#3.2 通知顺序)
      • [3.3 切点表达式](#3.3 切点表达式)
        • [3.3.1 execution](#3.3.1 execution)
        • [3.3.2 annotation](#3.3.2 annotation)
        • [3.3.3 @PointCut](#3.3.3 @PointCut)
      • [3.4 连接点](#3.4 连接点)
    • [4. AOP案例](#4. AOP案例)
      • [4.1 需求](#4.1 需求)
      • [4.2 分析](#4.2 分析)
      • [4.3 步骤:](#4.3 步骤:)
  • 十、相关注解&自动装配原理&自定义starter
    • [1. 自动配置原理](#1. 自动配置原理)
      • [1.1 起步依赖](#1.1 起步依赖)
      • [1.2 自动配置](#1.2 自动配置)
        • [1.2.1 介绍](#1.2.1 介绍)
        • [1.2.2 @Conditional](#1.2.2 @Conditional)
        • [1.2.3 @Import](#1.2.3 @Import)
        • [1.2.4 自动配置原理](#1.2.4 自动配置原理)
        • [1.2.5 自定义starter](#1.2.5 自定义starter)
    • [2. 实现步骤分析:](#2. 实现步骤分析:)

相关介绍

我们可以打开Spring的官网(https://spring.io),去看一下Spring的简介:Spring makes Java simple。

spring的官方提供很多开源的项目,我们可以点击上面的projects,看到spring家族旗下的项目,按照流行程度排序为:

我们发现Spring发展到今天已经形成了一种开发生态圈,Spring提供了若干个子项目,每个项目用于完成特定的功能。而我们在项目开发时,一般会偏向于选择这一套spring家族的技术,来解决对应领域的问题,那我们称这一套技术为spring全家桶

而Spring家族旗下,这么多的技术,最基础、最核心的是 SpringFramework。其他的spring家族的技术,都是基于SpringFramework的,SpringFramework中提供很多实用功能,如:依赖注入、事务管理、web开发支持、数据访问、消息服务等等。

而如果我们在项目中,直接基于SpringFramework进行开发,存在两点问题:配置繁琐、入门难度大。 所以基于此呢,spring官方推荐我们从另外一个项目开始学习,那就是目前最火爆的SpringBoot。 通过springboot就可以快速的帮我们构建应用程序,所以springboot呢,最大的特点有两个: 简化配置、快速开发。

Spring Boot 可以帮助我们非常快速的构建应用程序、简化开发、提高效率 。


一、 SpringBootWeb快速入门

1 需求

需求:基于SpringBoot的方式开发一个web应用,浏览器发起请求 /hello 后 ,给浏览器返回字符串 "Hello World ~"。


1.创建空工程

1.1.创建工程
2.设置项目编码
3.jdk设置

2.创建SpringBoot模块

2.1.创建模块
http 复制代码
默认:https://start.spring.io
修改成:https://start.aliyun.com
2.2.自动创建了哪些东西
2.3.pom.xml文件
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>
    <groupId>com.itxg</groupId>
    <artifactId>day01-springboot-01-quickstart</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>day01-springboot-01-quickstart</name>
    <description>day01-springboot-01-quickstart</description>
    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>3.0.2</spring-boot.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.itxg.day01springboot01quickstart.Day01Springboot01QuickstartApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
2.4.SpringbootDemo03Application启动类
java 复制代码
@SpringBootApplication
public class Day01Springboot01QuickstartApplication {

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

}
2.5.HelloController控制器类
java 复制代码
package com.itxg.day01springboot01quickstart.web.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello World ~ ");
        return "Hello World ~ ";
    }
}
2.6.启动项目
2.7.浏览器访问
http 复制代码
地址:http://localhost:8080/hello

3.SpringBoot启动内嵌tomcat分析

3.1.启动类入口
3.2.创建并启动Tomcat

4. HTTP协议

4.1 HTTP-概述
4.1.1 介绍

HTTP:Hyper Text Transfer Protocol,超文本传输协议,规定了浏览器与服务器之间数据传输的规则。

  • 数据传输的规则指的是请求数据和响应数据需要按照指定的格式进行传输。
  • 如果想知道具体的格式,可以打开浏览器,点击F12打开开发者工具,点击Network来查看某一次请求的请求数据和响应数据具体的格式内容,如下图所示:

注意: 在浏览器中如果看不到上述内容,需要清除浏览器的浏览数据。chrome浏览器可以使用ctrl+shift+Del进行清除。

所以,我们学习HTTP主要就是学习请求和响应数据的具体格式内容。

4.2.2 特点

HTTP协议有它自己的一些特点,分别是:

  • 基于TCP协议: 面向连接,安全

    TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全。

  • 基于请求-响应模型: 一次请求对应一次响应

    请求和响应是一一对应关系,没有请求,就没有响应。

  • HTTP协议是无状态协议: 对于事物处理没有记忆能力。每次请求-响应都是独立的

    无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。这种特性有优点也有缺点,

    • 缺点: 多次请求间不能共享数据
    • 优点: 速度快

    请求之间无法共享数据会引发的问题,如:

    • 京东购物,加入购物车去购物车结算是两次请求,
    • HTTP协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
    • 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据

    具体使用的时候,我们发现京东是可以正常展示数据的,原因是Java早已考虑到这个问题,并提出了使用会话技术(Cookie、Session)来解决这个问题。具体如何来做,我们后面会详细讲到。刚才提到HTTP协议是规定了请求和响应数据的格式,那具体的格式是什么呢?

4.2 HTTP-请求协议

请求数据分为三个部分:请求行、请求头、请求体。

  • 请求行: HTTP请求中的第一行数据,请求行包含三块内容,分别是:

    • GET [请求方式]

    • /brand/findAll [请求URL路径]

    • ?name=OPPO&status=1 [请求数据]

    • HTTP/1.1 [HTTP协议及版本]

    请求方式有七种,最常用的是GET和POST

  • 请求头: 第二行开始,格式为key: value形式

    请求头中会包含若干个属性,常见的HTTP请求头有:

json 复制代码
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标签的结果会有不一致,所以就会导致相同的代码在不同的浏览器会出现不同的效果

​ - 服务端根据客户端请求头中的数据获取到客户端的浏览器类型,就可以根据不同的浏览器设置不同的代码来达到一致的效果

​ - 这就是我们常说的浏览器兼容问题

  • 请求体:POST请求的最后一部分,存储请求参数

如上图绿色部分的内容就是请求体的内容,请求体和请求头之间是有一个空行隔开。此时浏览器发送的是POST请求,为什么不能使用GET呢?这时就需要回顾GET和POST两个请求之间的区别了:

  • GET请求请求参数在请求行中,没有请求体,如:/brand/findAll?name=OPPO&status=1。GET请求请求参数大小有限制。
  • POST请求请求参数在请求体中,POST请求大小是没有限制的。
4.3 HTTP-响应协议
4.3.1 格式介绍

响应数据总共分为三部分内容,分别是:响应行、响应头、响应体

  • 响应行:响应数据的第一行,响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]

  • 响应头:第二行开始,格式为key:value形式

    响应头中会包含若干个属性,常见的HTTP响应头有:

json 复制代码
Content-Type:表示该响应内容的类型,例如text/html,image/jpeg ;

Content-Length:表示该响应内容的长度(字节数);

Content-Encoding:表示该响应压缩算法,例如gzip ;

Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 ;

Set-Cookie: 告诉浏览器为当前页面所在的域设置cookie ;
  • 响应体: 最后一部分。存放响应数据

​ 上图中 [{id:1, brandName:"阿里巴巴"}] 这部分内容就是响应体,它和响应头之间有一个空行隔开。

4.3.2 响应状态码
状态码分类 说明
1xx 响应中------临时状态码,表示请求已经接受,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx 成功------表示请求已经被成功接收,处理已完成
3xx 重定向------重定向到其它地方:它让客户端再发起一个请求以完成整个处理。
4xx 客户端错误------处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等
5xx 服务器端错误------处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等

参考: 资料/SpringbootWeb/响应状态码.md

关于响应状态码,我们先主要认识三个状态码,其余的等后期用到了再去掌握:

  • 200 ok 客户端请求成功
  • 404 Not Found 请求资源不存在
  • 500 Internal Server Error 服务端发生不可预期的错误

小结

  1. 响应数据中包含三部分内容,分别是响应行、响应头和响应体

  2. 掌握200,404,500这三个响应状态码所代表含义,分布是成功、所访问资源不存在和服务的错误

5. WEB服务器-Tomcat

5.1 简介
5.1.1 Web服务器

Web服务器是一个应用程序(软件),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是"提供网上信息浏览服务"。

Web服务器是安装在服务器端的一款软件,将来我们把自己写的Web项目部署到Web Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了。

Web服务器软件使用步骤

  • 准备静态资源
  • 下载安装Web服务器软件
  • 将静态资源部署到Web服务器上
  • 启动Web服务器使用浏览器访问对应的资源

上述内容在演示的时候,使用的是Apache下的Tomcat软件,至于Tomcat软件如何使用,后面会详细的讲到。而对于Web服务器来说,实现的方案有很多,Tomcat只是其中的一种,而除了Tomcat以外,还有很多优秀的Web服务器,比如:

Tomcat就是一款软件,我们主要是以学习如何去使用为主。具体我们会从以下这些方向去学习:

  1. 简介: 初步认识下Tomcat

  2. 基本使用: 安装、卸载、启动、关闭、配置和项目部署,这些都是对Tomcat的基本操作

  3. IDEA中如何创建Maven Web项目

  4. IDEA中如何使用Tomcat,后面这两个都是我们以后开发经常会用到的方式

首选我们来认识下Tomcat。

5.1.2 Tomcat
  • Tomcat是Apache软件基金会一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范。

  • 概念中提到了JavaEE规范,那什么又是JavaEE规范呢?

    JavaEE: Java Enterprise Edition,Java企业版。指Java企业级开发的技术规范总和。包含13项技术规范:JDBC、JNDI、EJB、RMI、JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF。

  • 因为Tomcat支持Servlet/JSP规范,所以Tomcat也被称为Web容器、Servlet容器。JavaWeb程序需要依赖Tomcat才能运行。

  • Tomcat的官网: https://tomcat.apache.org/ 从官网上可以下载对应的版本进行使用。

小结

通过这一节的学习,我们需要掌握以下内容:

  1. Web服务器的作用

封装HTTP协议操作,简化开发

可以将Web项目部署到服务器中,对外提供网上浏览服务

  1. Tomcat是一个轻量级的Web服务器,支持Servlet/JSP少量JavaEE规范,也称为Web容器,Servlet容器。
5.2 基本使用
5.2.1 下载

直接从官方网站下载:https://tomcat.apache.org/download-90.cgi

大家可以自行下载

5.2.2 安装与卸载

安装:

Tomcat是绿色版,直接解压即可

  • 在E盘的develop目录下,将apache-tomcat-9.0.27-windows-x64.zip进行解压缩,会得到一个apache-tomcat-9.0.27的目录,Tomcat就已经安装成功。

    注意,Tomcat在解压缩的时候,解压所在的目录可以任意,但最好解压到一个不包含中文和空格的目录,因为后期在部署项目的时候,如果路径有中文或者空格可能会导致程序部署失败。

  • 打开apache-tomcat-9.0.27目录就能看到如下目录结构,每个目录中包含的内容需要认识下,

bin:目录下有两类文件,一种是以.bat结尾的,是Windows系统的可执行文件,一种是以.sh结尾的,是Linux系统的可执行文件。

webapps:就是以后项目部署的目录

到此,Tomcat的安装就已经完成。

卸载:

卸载比较简单,可以直接删除目录即可

5.2.3 启动与关闭

启动:

双击: bin\startup.bat

启动后,通过浏览器访问 http://localhost:8080能看到Apache Tomcat的内容就说明Tomcat已经启动成功。

注意: 启动的过程中,控制台有中文乱码,需要修改 conf/logging.prooperties

关闭:

关闭有三种方式

  • 直接x掉运行窗口:强制关闭[不建议]
  • bin\shutdown.bat:正常关闭
  • ctrl+c: 正常关闭
5.2.4 配置

修改端口:

Tomcat默认的端口是8080,要想修改Tomcat启动的端口号,需要修改 conf/server.xml

注: HTTP协议默认端口号为80,如果将Tomcat端口号改为80,则将来访问Tomcat时,将不用输入端口号。

启动时可能出现的错误:

  • Tomcat的端口号取值范围是0-65535之间任意未被占用的端口,如果设置的端口号被占用,启动的时候就会包如下的错误.

​ 解决方案:换一个端口号

  • Tomcat启动的时候,启动窗口一闪而过: 需要检查JAVA_HOME环境变量是否正确配置
5.3 入门程序解析

在入门程序中,我们所编写的程序并没有部署到外部的tomcat中,因为在SpringBoot中,一旦我们引入了web的运行环境(也就是引入spring-boot-starter-web的依赖),其实内部已经集成了内置的tomcat服务器。

我们可以通过IDEA开发工具右侧的maven面板中,就可以看到当前工程引入的依赖。其中已经将Tomcat的相关依赖传递下来了,也就是说, SpringBoot中已经内置了Tomcat服务器。当我们运行引导类时,就会看到命令行输出的日志,占用8080端口的Tomcat。

  • 起步依赖:

    而在SpringBoot的项目中,我们称 spring-boot-starter-xxx 这类的依赖为起步依赖。比如,入门程序中,我们引入了:

    • spring-boot-starter-web:包含了web应用开发所需要的常见依赖。
    • spring-boot-starter-test:包含了单元测试所需要的常见依赖。

    Spring的官方提供了很多现成的starter,我们在开发相关应用时,只需要引入对应的starter即可。

    官方地址:https://docs.spring.io/spring-boot/docs/2.7.2/reference/htmlsingle/#using.build-systems.starters

前面我们开发了springbootweb的入门程序。 基于SpringBoot的方式开发一个web应用,浏览器发起请求 /hello 后 ,给浏览器返回字符串 "Hello World ~"。

其实呢,是我们在浏览器发起请求,请求了我们的后端web服务器,也就是内置的Tomcat。而我们在开发web程序时呢,定义了一个控制器类Controller,请求会被部署在Tomcat中的Controller接收,然后Controller再给浏览器一个响应,响应一个字符串 "Hello World"。 而在请求响应的过程中是遵循HTTP协议的。

但是呢,这里要告诉大家的时,其实在Tomcat这类Web服务器中,是不识别我们自己定义的Controller的。但是我们前面讲到过Tomcat是一个Servlet容器,是支持Serlvet规范的,所以呢,在tomcat中是可以识别 Servlet程序的。 那我们所编写的XxxController 是如何处理请求的,又与Servlet之间有什么联系呢?

其实呢,在SpringBoot进行web程序开发时,它内置了一个核心的Servlet程序 DispatcherServlet,称之为 核心控制器。 DispatcherServlet 负责接收页面发送的请求,然后根据执行的规则,将请求再转发给后面的请求处理器Controller,请求处理器处理完请求之后,最终再由DispatcherServlet给浏览器响应数据。

那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达tomcat之后,tomcat会负责解析这些请求数据,然后呢将解析后的请求数据会传递给Servlet程序的HttpServletRequest对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而Tomcat,还给Servlet程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。

那上述所描述的这种浏览器/服务器的架构模式呢,我们称之为:BS架构。

• BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。


6. Springboot内部创建并启动tomcat容器的源码分析

6.1. 核心入口:SpringApplication.run()

当你启动 Spring Boot 应用时,会调用SpringApplication.run()方法:

java 复制代码
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args); // 入口方法
    }
}

这个方法会触发一系列自动配置,其中就包括嵌入式 Servlet 容器的创建。

6.2. 自动配置触发:ServletWebServerFactoryAutoConfiguration

Spring Boot 通过@EnableAutoConfiguration注解加载所有自动配置类,其中ServletWebServerFactoryAutoConfiguration是 Servlet 容器的核心配置类:

  1. 配置类定义 (位于spring-boot-autoconfigure模块):

    java 复制代码
    @Configuration(proxyBeanMethods = false)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @ConditionalOnClass(ServletRequest.class)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @EnableConfigurationProperties(ServerProperties.class)
    public class ServletWebServerFactoryAutoConfiguration {
        // ...
    }
    • 关键条件:
      • @ConditionalOnClass(ServletRequest.class):确保当前类路径下存在 Servlet API。
      • @ConditionalOnWebApplication(type = Type.SERVLET):确保应用是 Servlet Web 应用。
  2. 导入 Tomcat 配置

    java 复制代码
    @Import({
        ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
        EmbeddedTomcat.class, // 导入Tomcat配置
        EmbeddedJetty.class,
        EmbeddedUndertow.class
    })
    public class ServletWebServerFactoryAutoConfiguration {
        // ...
    }
6.3. Tomcat 容器工厂的创建:EmbeddedTomcat

EmbeddedTomcat类会在满足条件时(类路径存在 Tomcat 依赖)创建TomcatServletWebServerFactory

java 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {

    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(
            ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
            ObjectProvider<TomcatContextCustomizer> contextCustomizers,
            ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
        
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.getTomcatConnectorCustomizers()
                .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
        factory.getTomcatContextCustomizers()
                .addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
        factory.getProtocolHandlerCustomizers()
                .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
        return factory;
    }
}
  • 关键依赖:
    • Tomcat.class:来自org.apache.tomcat.embed:tomcat-embed-core依赖。
  • 工厂作用:创建并配置 Tomcat 实例,设置端口、上下文路径等。
6.4. Tomcat 容器的初始化与启动

当 Spring 容器刷新时,ServletWebServerApplicationContext会触发容器的创建:

  1. 上下文刷新时启动容器

    java 复制代码
    public class ServletWebServerApplicationContext extends GenericWebApplicationContext {
        @Override
        protected void onRefresh() {
            super.onRefresh();
            try {
                createWebServer(); // 创建Web服务器
            }
            catch (Throwable ex) {
                throw new ApplicationContextException("Unable to start web server", ex);
            }
        }
    }
  2. createWebServer()方法实现

    java 复制代码
    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = getServletContext();
        if (webServer == null && servletContext == null) {
            // 获取ServletWebServerFactory(即TomcatServletWebServerFactory)
            ServletWebServerFactory factory = getWebServerFactory();
            // 创建WebServer(即TomcatWebServer)
            this.webServer = factory.getWebServer(getSelfInitializer());
        }
        // ...
    }
  3. TomcatServletWebServerFactory.getWebServer()方法

    java 复制代码
    @Override
    public WebServer getWebServer(ServletContextInitializer... initializers) {
        // 创建Tomcat实例
        Tomcat tomcat = new Tomcat();
        File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        
        // 创建连接器(默认HTTP/1.1)
        Connector connector = new Connector(this.protocol);
        tomcat.getService().addConnector(connector);
        customizeConnector(connector);
        tomcat.setConnector(connector);
        
        // 配置Engine、Host、Context等组件
        tomcat.getHost().setAutoDeploy(false);
        configureEngine(tomcat.getEngine());
        
        // 创建Servlet上下文并注册Spring MVC的DispatcherServlet
        prepareContext(tomcat.getHost(), initializers);
        
        // 返回封装后的TomcatWebServer
        return getTomcatWebServer(tomcat);
    }
  4. TomcatWebServer启动容器

    java 复制代码
    public class TomcatWebServer implements WebServer {
        @Override
        public void start() throws WebServerException {
            synchronized (this.monitor) {
                if (this.started) {
                    return;
                }
                prepareContext();
                try {
                    // 启动Tomcat
                    this.tomcat.start();
                    // 注册关闭钩子
                    startDaemonAwaitThread();
                    this.started = true;
                }
                catch (Exception ex) {
                    // ...
                }
            }
        }
    }
6.5. 核心组件与流程总结
  1. 自动配置链

    plaintext 复制代码
    SpringApplication.run() → 
    ServletWebServerFactoryAutoConfiguration → 
    EmbeddedTomcat → 
    TomcatServletWebServerFactory → 
    TomcatWebServer
  2. 关键组件

    • TomcatServletWebServerFactory:创建和配置 Tomcat 实例。
    • TomcatWebServer:封装 Tomcat 生命周期管理(启动、停止)。
    • Connector:处理 HTTP 请求的连接器(默认端口 8080)。
    • Context:Servlet 上下文,用于注册 Servlet、Filter 等。
  3. 配置来源

    • ServerProperties:读取application.properties中的server.port等配置。
    • TomcatConnectorCustomizer/TomcatContextCustomizer:自定义 Tomcat 组件。
6.6. 自定义扩展点

如果你想自定义 Tomcat 容器,可以通过以下方式:

  1. 配置文件

    properties 复制代码
    server.port=8081
    server.tomcat.threads.max=200
  2. Java Bean

    java 复制代码
    @Bean
    public TomcatConnectorCustomizer connectorCustomizer() {
        return connector -> {
            Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
            protocol.setMaxThreads(500);
            protocol.setConnectionTimeout(20000);
        };
    }
6.7. 源码位置参考
  • ServletWebServerFactoryAutoConfigurationspring-boot-autoconfigure-3.0.2.jar!/org/springframework/boot/autoconfigure/web/servlet
  • TomcatServletWebServerFactoryspring-boot-3.0.2.jar!/org/springframework/boot/web/embedded/tomcat
  • TomcatWebServer:同上

通过以上流程,Spring Boot 3.0.2 实现了 Tomcat 容器的自动化创建和启动,无需手动编写复杂的 XML 配置。


二、SpringBoot请求响应笔记

1. 请求参数的绑定

1.请求方式(request)

控制器方法

java 复制代码
@RestController
public class UserController {
    //手动封装简单参数
    @RequestMapping("/manualParam")
    public String manualParam(HttpServletRequest request) {
        String name = request.getParameter("name");
        String age = request.getParameter("age");
        System.out.println(name + "::::" + age);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/manualParam?name=zhangsan&age=18

2.请求路径拼接参数

控制器方法

java 复制代码
@RestController
public class UserController {
   /**
     * RequestMapping注解要求:
     * 1.get或者post方式提交都可以,get或post都可以在请求路径后面拼接参数
     * 2.路径中的请求参数名和方法参数名必须一致,否则获取不到(默认情况)
     * 3.如果是post请求,选择请求体方式提交,在apipost中可以选择form-data或者urlencoded
     * 4.如果表单中含有文件上传表单项,则必须选择form-data
     * @param name
     * @param age
     * @return
     */
    //自动封装简单参数
    @RequestMapping("/autoParam")
    public String autoParam(String name,Integer age) {
        System.out.println(name + "::::" + age);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/autoParam?name=zhangsan&age=18

3.请求路径拼接参数(请求参数和方法参数名称不一致)

控制器方法

java 复制代码
@RestController
public class UserController {
   /**
     * 提交请求参数和方法参数名称不一致,默认情况下获取不到,获取的结果为null
     * 解决方案:使用RequestParam注解,value属性值指定为请求参数的名称
     * 注意:
     * 1.如果RequestParam指定的参数名称和请求参数名称不一致,直接报错
     * 2.如果RequestParam指定了参数,而请求中没有传递对应的参数,直接报错
     * 3.可以给RequestParam注解添加required属性值为false,
     *		表示RequestParam注解指定的名称和请求参数名称不一致(包含不写),直接获取结果为null
     * @param name
     * @param age
     * @return
     */
    //自动封装简单参数
    @RequestMapping("/diffParam")
    public String diffParam(@RequestParam("username") String name, Integer age) {
        System.out.println(name + "::::" + age);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/diffParam?username=zhangsan&age=18

4.封装简单POJO

实体类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person {
    private String name;//性别
    private Integer age;//年龄
}
java 复制代码
//地址
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Address {
    private String province;//省份
    private String city;//市
}
java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    private String name;//性别
    private Integer age;//年龄
    private Address address; //地址
}

控制器方法

java 复制代码
@RestController
public class UserController {
    /**
     * 注意: 请求参数名称必须和POJO类的成员变量名称对应
     *
     * @param person
     * @return
     */
    //封装简单POJO
    @RequestMapping("/simplePojo")
    public String simplePojo(Person person)  {
        System.out.println("person = " + person);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/simplePojo?name=zhangsan&age=18

5.封装复杂POJO

控制器方法

java 复制代码
@RestController
public class UserController {
    /**
     * 注意:
     * 必须使用属性名.成员变量名的形式给复杂类型的复杂属性赋值
     *
     * @param user
     * @return
     */
    //封装复杂POJO
    @RequestMapping("/complexPojo")
    public String complexPojo(User user)  {
        System.out.println("user = " + user);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/complexPojo?name=zhangsan&age=18&address.province=zhejiang&address.city=hangzhou

6.请求路径参数(占位符)

控制器方法

java 复制代码
@RestController
public class UserController {
    /**
     * 请求路径参数
     * PathVariable注解:
     * 作用: 获取指定名称的请求路径中的占位符数据赋值给方法参数
     * 注意:
     * 1.请求路径占位符名称和方法参数名称相同,不需要给PathVariable注解的value属性赋值
     * 2.请求路径占位符名称和方法参数名称不相同,就需要给PathVariable注解的value属性赋值(方法参数名称)
     *
     * @param id
     * @return
     */
    @RequestMapping("/pathOneParam/{id}")
    public String pathOneParam(@PathVariable("id") Integer id) {
        System.out.println("id = " + id);
        return "OK";
    }

    @RequestMapping("/pathManyParam/{name}/{age}")
    public String pathManyParam(@PathVariable("name") String username, @PathVariable("age") Integer nianling) {
        System.out.println(username + "::::" + nianling);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/pathOneParam/10

7.封装数组

控制器方法

java 复制代码
@RestController
public class UserController {
   /**
     * 注意:
     * 请求路径?后面拼接的多个参数要使用相同的键名(和方法参数数组名保持一致),
     * 如果不一致使用RequestParam注解指定请求路径中的参数名称
     *
     * @param hobbies
     * @return
     */
    //封装数组
    @RequestMapping("/arrayParam")
    public String arrayParam(String[] hobbies)  {
        System.out.println(Arrays.toString(hobbies));
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/arrayParam?hobbies=code&hobbies=majiang&hobbies=tangtou

8.封装List集合

控制器方法

java 复制代码
@RestController
public class UserController {
    /**
     * 注意:
     *     默认情况下SpringBoot只会把客户端同名参数的多个值封装到数组中
     *     如果需要封装到集合中必须使用@RequestParam注解
     *     如果客户端参数名称和方法参数集合名称不一致,需要在@RequestParam注解中通过value属性指定名称
     * @param hobbies
     * @return
     */
    //封装List集合
    @RequestMapping("/listParam")
    public String listParam(@RequestParam List<String> hobbies)  {
        System.out.println(hobbies);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/listParam?hobbies=tangtou&hobbies=majiang

9.封装日期格式参数

控制器方法

java 复制代码
@RestController
public class UserController {
    /**
     * SpringBoot中默认按照2025/12/12这种格式的日期时间参数进行数据的封装
     * 如果客户端需要使用特有的日期格式,则需要在控制器方法参数前面添加@DateTimeFormat注解指定日期时间格式
     * @param updateTime
     * @return
     */
    //封装Date日期类型参数
    @RequestMapping("/dateParam")
    public String dateParam(
            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date updateTime)  {
        System.out.println(updateTime);
        return "OK";
    }
    //封装Date日期类型参数
    @RequestMapping("/dateParam2")
    public String dateParam2(
            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime)  {
        System.out.println(updateTime);
        return "OK";
    }
}

请求路径

http 复制代码
http://localhost:8080/dateParam?updateTime=2025-07-1 08:20:30

10.json参数封装实体对象

控制器方法

java 复制代码
@RestController
public class UserController {
    /**
     * 把请求json参数解析成方法的pojo参数
     * 1.添加jackson依赖(负责json到pojo对象之间的转换): 
     *      引入spring-boot-starter-web起步依赖内部引入了jackson
     * 2.在方法pojo参数前面添加RequestBody注解
     *
     * @param user
     * @return
     */
    //json格式参数-->封装实体
    @RequestMapping("/jsonParam")
    public String jsonParam(@RequestBody User user)  {
        System.out.println(user);
        return "OK";
    }
}

请求路径

http 复制代码
必须是POST请求:
	http://localhost:8080/jsonParam
json参数:
	{
        "name":"Tom",
        "age":10,
        "address":{
            "province":"zhejiang",
            "city":"hangzhou"
        }
    }

2. 响应结果

2.1. 响应实体类

注意

txt 复制代码
必须添加ResponseBody注解

控制器方法

java 复制代码
//RestController注解 ==> Controller注解 + ResponseBody注解
//@RestController = @Controller + @ResponseBody
/**
 * Controller注解作用:
 *      1.标识当前的类是一个控制器类,可以被客户端浏览器访问
 *      2.SpringBoot扫描到该注解后,会创建控制器类的对象存储IOC容器中
 * ResponseBody注解作用:
 *      1.把方法返回字符串直接写出给浏览器
 *      2.把方法返回的对象解析成json字符串写出给浏览器
 *		(在spring-boot-starter-web中已经引入了jackson的依赖)
 */
//@Controller
@RestController
public class UserController {
    //响应实体类
    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser() {
        User user = User.builder()
                .name("tom")
                .age(38)
                .build();
        return user;
    }
}

请求路径

http 复制代码
http://localhost:8080/getUser

2.2. 响应集合

java 复制代码
@RequestMapping("/getList")
public List<User> getList() {
    List<User> list = new ArrayList<>();
    list.add(new User("李四",28,new Address("山东","济南")));
    list.add(new User("王五",38,new Address("江苏","徐州")));
    return list;
}

2.3. 统一响应结果

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
//封装统一响应结果
public class Result {
    private Integer code;//响应状态码: 1表示成功,0表示失败
    private String msg;//响应消息: success表示成功,error表示失败
    private Object data;//响应的具体数据

    //针对查询成功返回的结果对象
    public static Result success(Object data) {
        return new Result(1,"success",data);
    }
    //针对增删改成功返回的结果对象(不需要返回具体数据,响应的固定的字符串消息success)
    public static Result success() {
        return new Result(1,"success",null);
    }

    //针对增删改成功返回的结果对象(不需要返回具体数据,响应的自定义的字符串消息)
    public static Result success(String msg) {
        return new Result(1,msg,null);
    }

    //针对增删改查失败返回的结果对象(不需要返回具体数据,响应的固定的字符串消息error)
    public static Result error() {
        return new Result(1,"error",null);
    }

    //针对增删改查失败返回的结果对象(不需要返回具体数据,响应的自定义的字符串消息)
    public static Result error(String msg) {
        return new Result(1,msg,null);
    }
}
java 复制代码
/**
 * 演示封装请求参数
 */
//@ResponseBody + @Controller
@RestController
@RequestMapping("/person")
public class PersonController {
    @RequestMapping("/pathTwoParam/{name}/{no}")
    public Result pathTwoParam(@PathVariable String name, @PathVariable("no") Integer id) {
        System.out.println("name = " + name);
        System.out.println("id = " + id);
        //return new Result(1,"success",null);
        return Result.success();
    }

    /*
       响应POJO和List集合
    */
    @RequestMapping("/getUser")
    public Result getUser() {
        User user = new User("李四", 28, new Address("山东", "济南"));
        //return new Result(1, "success", user);
        return Result.success(user);
    }

    @RequestMapping("/getList")
    public Result getList() {
        List<User> list = new ArrayList<>();
        list.add(new User("李四",28,new Address("山东","济南")));
        list.add(new User("王五",38,new Address("江苏","徐州")));
        //return new Result(1,"success",list);
        return Result.success(list);
    }
}

3. 相关注解

按照视频中的代码来练习

3.1. 核心概念

txt 复制代码
控制反转: 
	Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器创建),
	这种思想称为控制反转。
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

Bean对象:IOC容器中创建、管理的对象,称之为bean。

3.2. 注解介绍

txt 复制代码
Component注解(通用注解,但是不够见名知意):
    1.作用:
        标识当前类交给spring容器管理,被spring扫描后,会创建该类的对象,加入到spring的容器中
    2.特点:
        (1)对象默认的名字,类名第一个字母小写.特殊情况,如果类名的前两个字母是大写,那么对象的名字就是类名
        (2)可以自己指定名称
    3.衍生注解:
        (1)Controller注解: 标注mvc中的控制器类的
        (2)Service注解: 标注service层的业务层实现类的
        (3)Repository注解: 标注dao层的持久层实现类的
txt 复制代码
Autowired注解:
    1.作用: 完成数据的注入
    2.特点:
        (1)默认按照类型注入,如果没有该类型的对象,默认情况下报异常.
        (2)可以通过required属性值为false,如果没有该类型的对象,会被注入一个null值
        (3)默认按照类型注入,如果该类型对象有多个,其中有一个和要注入的属性名一致,直接注入,
        	所有的对象名和属性名都不一致,直接报错,此时可以使用Qualifier指定具体的对象
    3.注意:
    	是spring提供的注解,可以使用在构造方法/成员方法/成员变量/方法参数上
txt 复制代码
Resource注解:
    1.作用: 完成数据的注入
    2.特点:
    	(1)默认按照成员变量名称注入,名称不匹配,就按照类型注入,如果该类型对象唯一,就注入成功,否则抛异常
    	(2)可以通过name属性指定要注入的对象
    3.注意:
    	是JDK提供的,可以使用在构造方法/成员方法/类上
txt 复制代码
Scope注解:
    1.作用: 配置被SpringBoot扫描后的类的对象,在SpringBoot的IOC容器中是单例对象还是多例对象
    2.使用:
    	(1)value属性取值singleton: 
    		表示是单例对象,默认值(不配).
    		SpringBoot启动就会创建单例对象加入到IOC容器中
    	(2)value属性取值prototype: 表示是多例对象.SpringBoot启动不会创建对象,
    		什么时候获取什么时候就创建一个新的对象
PostConstruct注解:
    1.作用:用来指定初始化方法的
    2.执行时机:
    	调用构造方法创建之后,立刻使用对象来调用初始化方法(每次创建对象后都会调用)
PreDestroy注解:
    1.作用:用来指定销毁化方法的
    2.执行时机:
        在单例模式下,销毁容器时,会先使用对象调用销毁方法
        在多例模式下无效
txt 复制代码
 Component注解:
 	作用:是定义业务组件对象,创建该类的普通对象,并不是代理对象
 Configuration注解:
     作用: 是定义属性配置类的.或者它定义的类的内部组装其它对象到IOC容器中
     特点:
         1.默认情况下创建的是当前类的代理对象
         2.可以通过proxyBeanMethods属性值为false,指定该注解创建普通对象(非代理对象)
txt 复制代码
Bean注解:
    1.作用:
        Bean注解所在的类被spring扫描后,spring会自动执行Bean注解标注的方法,
        而且会把方法返回值对象存储到IOC容器当中,默认情况下使用方法名称作为对象名称,
        可以通过value属性自定义该对象在IOC容器中的名称
    2.注意:
        Spring调用@Bean注解标注的方法时,如果方法需要参数,会自动从IOC容器中获取对应的参数对象,
        如果IOC容器中没有对应的对象直接报错
txt 复制代码
Import注解:
    1.作用: 把指定类的对象加入到SpringBoot的IOC容器中
    2.特点:
        (1)对象在SpringBoot的IOC容器中的名字就是所在类的全名称,无法修改
        (2)Import注解要想生效,要求所在的类也必须加入到SpringBoot的IOC容器当中
ComponentScan注解:
    1.作用: 指定要扫描的包
    2.特点:
    	(1)ComponentScan注解要想生效,要求所在的类也必须加入到SpringBoot的IOC容器当中
注解 说明
@Component 使用在类上用于实例化Bean
@Controller 使用在web层类上用于实例化Bean
@Service 使用在service层类上用于实例化Bean
@Repository 使用在dao层类上用于实例化Bean
注解 说明
@Autowired 使用在字段上用于根据类型依赖注入
@Qualifier 结合@Autowired一起使用用于根据名称进行依赖注入
@Resource 相当于@Autowired+@Qualifier,按照名称进行注入
@Value 注入普通属性
注解 说明
@Scope 标注Bean的作用范围
@PostConstruct 使用在方法上标注该方法是Bean的初始化方法
@PreDestroy 使用在方法上标注该方法是Bean的销毁方法
注解 说明
@Configuration 指定当前类是一个配置类,加入容器中
@ComponentScan 用于指定 Spring 在初始化容器时要扫描的包
@Bean 使用在方法上,标注将该方法的返回值存储到容器中
@PropertySource 用于加载.properties 文件中的配置
@Import 用于导入其他配置类

三、SpringBoot的配置文件

SpringBoot是约定大于配置的,所以很多配置都有默认值。如果想修改默认配置,可以用application.properties或application.yml(application.yaml)自定义配置。SpringBoot默认从Resource目录加载自定义配置文件。有下面3个要点:

  1. SpringBoot提供了2种配置文件类型:properties和yml/yaml
  2. 默认配置文件名称:application
  3. (了解)在同一级目录下优先级为:properties>yml > yaml

例如:配置内置Tomcat的端口

application.properties:

properties 复制代码
server.port=8080

application.yml

yaml 复制代码
server: 
	port: 8080

1. YAML配置文件

YML文件格式是YAML(YAML Aint Markup Language)编写的文件格式。可以直观被电脑识别的格式。容易阅读,容易与脚本语言交互。可以支持各种编程语言(C/C++、Ruby、Python、Java、Perl、C#、PHP),扩展名为.yml或.yaml。

  • 大小写敏感
  • 数据值前边必须有空格,作为分隔符
  • 使用缩进表示层级关系
  • 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应的 空格数目可能不同,导致层次混乱)。
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略。
yaml 复制代码
server: 
	port: 8080  
    address: 127.0.0.1

2. yml支持数据格式

普通数据语法key: value

示例代码:

yml 复制代码
# yaml
personName: zhangsan

注意:

  1. Value之前有一个空格
  2. key不要定义为username否则返回的值将是window中的用户名称,username为特定值。

对象(map)数据:

示例代码:

yml 复制代码
person:
	name: haohao
	age: 31
	addr: beijing
#或者(了解即可)
person: {name: haohao,age: 31,addr: beijing}

注意:yml语法中,相同缩进代表同一个级别

集合、数组数据语法:

示例代码:

yml 复制代码
# 数组或集合
city:
  - beijing
  - shanghai
# 行内写法
address: [beijing,shanghai]

#集合中的元素是对象形式
students:
	- name: zhangsan
	  age: 18
	  score: 100
	- name: lisi
	  age: 28
	  score: 88
	- name: wangwu
	  age: 38
	  score: 90

3. 配置文件属性注入Bean

3.1 @Value

@Value注解将配置文件的值映射到Spring管理的Bean属性值

java 复制代码
@Value("${personName}")
private String personName;
// 还可以这样
@Value("${person.name}")
private String persionName;
// 还可以这样
@Value("${students[0].name}")
private String persionName;

3.2 @ConfigurationProperties

通过注解@ConfigurationProperties(prefix=''配置文件中的key的前缀")可以将配置文件中的配置自动与实体进行映射。

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private String age;
    private String addr;
}

使用@ConfigurationProperties方式必须提供Setter方法,使用@Value注解不需要Setter方法。同时可以在pom中添加如下坐标,这样可以在配置文件中配置数据时有更加友好的提示。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

4. Profile(多配置文件)

4.1. Profile概述

1、 profile是用来完成不同环境下,配置动态切换功能的。

2、 profile配置方式

  • 多profile文件方式:提供多个配置文件,每个代表一种环境。
    • application-dev.properties/yml 开发环境
    • application-test.properties/yml 测试环境
    • application-pro.properties/yml 生产环境
  • yml多文档方式:
    • 在yml中使用 --- 分隔不同配置

3、profile激活方式

​ 配置文件: 再配置文件中配置:spring.profiles.active=dev

​ 虚拟机参数(了解):在VM options 指定:-Dspring.profiles.active=dev

​ 命令行参数(了解):java --jar xxx.jar --spring.profiles.active=dev

4.2 配置多Profile案例

1. 多profile文件方式:

提供多个配置文件,每个代表一种环境。

  • application-dev.properties/yml 开发环境

  • application-test.properties/yml 测试环境

  • application-pro.properties/yml 生产环境

properties配置方式

application.properties

properties 复制代码
#通过active指定选用配置环境
spring.profiles.active=test

application-dev.properties

properties 复制代码
#开发环境
server.port=8081

application-test.properties

properties 复制代码
#测试环境
server.port=8082

application-pro.properties

properties 复制代码
#生产环境
server.port=8083

yml配置方式

application.yml

yaml 复制代码
#通过active指定选用配置环境
spring:
  profiles:
    active: pro

application-dev.yml

yaml 复制代码
#开发环境
server:
  port: 8081

application-test.yml

yaml 复制代码
#测试环境
server:
  port: 8082

application-pro.yml

yaml 复制代码
#生产环境
server:
  port: 8083
2. yml多文档方式:

在yml中使用 --- 分隔不同配置

yml 复制代码
---
#开发环境
server:
  port: 8081
spring:
  profiles: dev
---
#测试环境
server:
  port: 8082
spring:
  profiles: test
---
#生产环境
server:
  port: 8083
spring:
  profiles: pro
---
#通过active指定选用配置环境
spring:
  profiles:
    active: dev

注意:

​ 一共三种切换环境方法:

1.虚拟机参数 优先级最高

2.系统环境变量 (系统环境变量)

3.主配置文件中 spring.profiles.active 配置项

1>2>3

四、SpringBoot整合其他框架

1. 集成MyBatis

目标:使用SpringBoot整合MyBatis,完成查询所有用户功能。

实现步骤:

  1. 创建SpringBoot工程
  2. 勾选依赖坐标(web、mybatis、mysql驱动)
  3. 配置数据库连接及mybatis信息
  4. 指定DAO接口所在的包
  5. 创建User表
  6. 编写三层架构:Dao、Service、controller,查询所有用户
  7. 访问测试地址 http://localhost:8080/findAll

实现过程:

  1. 创建SpringBoot工程: spring-boot-mybatis

  2. 勾选依赖坐标

  3. 在application.yml中添加数据库连接信息

    yaml 复制代码
    spring:
      # 数据源相关配置
      datasource:
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        #时区必须配置否则报错,注意数据库名切换为自己的数据库名称
        url:  jdbc:mysql://127.0.0.1/itxg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    #mybatis 相关配置
    mybatis:
      # 指定接口映射文件的位置
      mapper-locations: classpath:mapper/*.xml
      # 为POJO类指定别名
      type-aliases-package: com.itxg.springbootmybatis.pojo
  • 数据库连接地址后加?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC,不然会报错
  1. 指定DAO接口所在的包

  2. 创建User表--->创建实体UserBean

    • 创建表

      sql 复制代码
      CREATE DATABASE itxg;
      USE itxg;
      -- ----------------------------
      -- Table structure for `user`
      -- ----------------------------
      DROP TABLE IF EXISTS `user`;
      CREATE TABLE `user` (
      `id` INT(11) NOT NULL AUTO_INCREMENT,
      `username` VARCHAR(50) DEFAULT NULL,
      `password` VARCHAR(50) DEFAULT NULL,
      `name` VARCHAR(50) DEFAULT NULL,
      PRIMARY KEY (`id`)
      ) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
      -- ----------------------------
      -- Records of user
      -- ----------------------------
      INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '宏伟');
      INSERT INTO `user` VALUES ('2', 'lisi', '123', '雄哥');

SELECT * FROM USER;

​ ```

  • 创建实体

    java 复制代码
    public class User {
        private Integer id;
        private String username;//用户名
        private String password;//密码
        private String name;//姓名
        //getter setter...
        //toString
    }
  1. 编写三层架构:Dao、Service、controller,查询所有用户

    • 项目略图

    • controller层代码

      java 复制代码
      @RestControlle
      @Slf4j
      public class UserController {
      
          @Autowired
          private UserService userService;
          
          /**
           *  查询所有用户
           */
          @GetMapping("/findAllUser")
          public List<User> findAllUser(){
              log.info("[查询所有用户信息,findAllUser....]");
              return userService.findAll();
          }
      }
    • service层代码

      java 复制代码
      public interface UserService {
      
          /**
           * 查询所有用户
           */
          List<User> findAll();
      }
    • serviceImpl

      java 复制代码
      @Service
      public class UserServiceImpl implements UserService {
          @Autowired
          private UserMapper userMapper;
          @Override
          public List<User> findAll() {
              return userMapper.selectAll();
          }
      }
    • UserMapper

      java 复制代码
       public interface UserMapper {
      
              /**
               * 查询所有用户
               */
              List<User> selectAll();
          }
    • 配置Mapper映射文件:在src/main/resources/mapper路径下加入UserMapper.xml配置文件

      xml 复制代码
       <?xml version="1.0" encoding="utf-8" ?>
       <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
              <mapper namespace="com.itxg.mapper.UserMapper">
              <!--查询所有用户-->
              <select id="selectAll" resultType="User">
              select * from user
              </select>
      
      </mapper>
      复制代码
  2. 配置日志输出

    yml 复制代码
    logging:
      level:
        com.itxg.springbootmybatis: debug

    备注:com.itxg.springbootmybatis 这个是启动类所在的包

  3. 访问测试地址: http://localhost:8080/user/findAllUser

2. 集成Junit

目标:SpringBoot集成JUnit测试功能,进行查询用户接口测试。

实现步骤:

  1. 添加Junit起步依赖(默认就有)

    xml 复制代码
    <!--spring boot测试依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
  2. 编写测试类:

    • SpringRunner继承SpringJUnit4ClassRunner,使用哪一个Spring提供的测试引擎都可以。指定运行测试的引擎,2.3.0及更高版本的SpringBoot无需指定该配置

    • SpringBootTest的属性值指的是引导类的字节码对象

      java 复制代码
      import java.util.List;
      @RunWith(SpringRunner.class) // 2.3.0及更高版本的SpringBoot无需指定该配置
      @SpringBootTest
      public class UserServiceImplTest {
      
          @Autowired
          private UserServiceImpl userService;
          @Test
          public void findAll() {
              List<User> users = userService.findAll();
              System.out.println(users);
          }
      }
  3. 控制台打印信息

3. 集成 RestTemplate

目标:创建新的moudle集成RestTemplate,通过远程访问springMVC的处理器对象

实现步骤:

  1. 创建SpringBoot工程,名为spring-boot-restemplate
  2. 编写配置类,创建RestTemplate对象
  3. 编写TestController类,远程访问另一个moudle中的处理器
  4. 修改配置文件,端口改为9008
  5. 访问测试 (先启动spring-boot-mybatis工程,再启动当前工程)

实现步骤:

  1. 创建SpringBoot工程,名为spring-boot-restemplate

  2. 编写配置类,创建RestTemplate对象

    java 复制代码
    @Configuration
    public class BeanConfig {
    
        @Bean
        public RestTemplate createRestTemplate(){
            return new RestTemplate();
        }
    }
  3. 编写TestController类,远程访问另一个moudle中的处理器

    java 复制代码
    @RestController
    @RequestMapping("/test")
    public class TestController {
    
        @Autowired
        RestTemplate restTemplate;
    	
    	@GetMapping("/callFindAll")
    	public List callFindAll(){
    
    		List returnObj = restTemplate.postForObject("http://localhost:8080/user/findAll",null, List.class);
    		return returnObj;
    	}
        
        
    }
  4. 修改配置文件,端口改为9008

  5. 测试

    1. 启动 spring-boot-mybatis工程,端口是8080

    2. 启动 当前工程,端口是9008

    3. 在地址栏 http://localhost:9008/test/callFindAll

      通过9008的接口,访问8080提供的服务。

方法介绍

方法名 方法含义 参数列表 返回值
put 访问put请求 参1:url地址(绝对路径) 参2:请求体 参3:参数(可变参数类型) void
delete 访问delete请求 参1:url地址(绝对路径) 参2:请求体 参3:参数(可变参数类型) void
getForObject 访问get请求 参1:url地址(绝对路径) 参2:参数(可变参数类型)
postForObject 访问post请求 参1:url地址(绝对路径) 参2:参数(可变参数类型)

备注:getForObject()getForEntity()多包含了将HTTP转成POJO的功能,但是getForObject没有处理response的能力。因为它拿到手的就是成型的pojo。省略了很多response的信息。

4. 扩展了解:除此之外还可以整合什么?

  • 集成 MongoDB
  • 基础 Redis
  • 集成 ElasticSearch
  • 集成RabbitMQ消息中间件
  • 集成 Memcached
  • 集成邮件服务:普通邮件、模板邮件、验证码、带Html的邮件
  • 集成Freemarker或者Thymeleaf
  • 集成dubbo

5. 附:常见问题

1. @Autowired注入Bean报错

这是idea检查错误,注解关闭相关检查即可

2. 找不到@RunWith

五、开发规范

5.1. 开发规范

5.1.1 REST风格

当前案例,我们基于当前最为主流的前后端分离模式进行开发。

在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发,而在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。

那么什么是REST风格呢?

  • REST(Representational State Transfer),表现形式状态转换,它是一种软件架构风格。

原来我们定义URL风格如下:

原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。

如果是基于REST风格:

在REST风格的URL中,我们通过四种请求方式,来操作数据的增删改查。

  • GET : 查询
  • POST :新增
  • PUT :修改
  • DELETE :删除

我们看到如果是基于REST风格,定义URL,URL将会更加简洁、更加规范、更加优雅。

注意事项:

  • 上述行为是风格,是约定方式,约定不是规范,可以打破,所以称为 REST风格,而不是REST规范。
  • 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books...

5.1.2 统一响应结果

前后端工程在进行交互时,使用统一响应结果 Result。

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Integer code;//响应码,1 代表成功; 0 代表失败
    private String msg;  //响应码 描述字符串
    private Object data; //返回的数据

    //增删改 成功响应
    public static Result success(){
        return new Result(1,"success",null);
    }
    //查询 成功响应
    public static Result success(Object data){
        return new Result(1,"success",data);
    }
    //失败响应
    public static Result error(String msg){
        return new Result(0,msg,null);
    }
}

5.1.3 开发流程

我们在进行功能开发时,都是根据如下流程进行:

  • 查询页面原型明确需求

  • 阅读接口文档(已提供)

  • 思路分析

  • 接口开发:就是开发后台的业务功能,一个业务功能,我们称为一个接口。

  • 接口测试:功能开发完毕后,先通过Postman进行接口测试,测试通过后,和前端进行联调测试。

  • 前后端联调测试:和前端开发人员开发好的前端工程一起测试。

六、异常处理

6.1. 现象

程序开发过程中不可避免的会遇到异常现象。比如,当我们进行修改员工数据时,如果员工的用户名重复,将会返回如下错误信息:

我们看法,返回的结果数据,默认框架返回的结果数据,并不是我们项目规范中定义的Result。从返回的信息中,我们可以看到状态码为 500,代表服务端错误,服务端出现了异常。此时,我们可以打开服务端控制台,查看错误信息。

在我们编写的程序中,可能由于传入参数问题,系统问题导致各种各样异常,那再SpringBoot 项目中异常该如何处理呢?

回忆一下以前学习的异常处理原则

  • dao、service 层向上抛即可
  • 但 controller 层不同,它如果再向上抛,此异常必然暴露给最终用户,这是不允许的。

6.2. 思考

现在各层代码出现的异常,我们是如何处理的? 答案:未做处理

如果未做处理,也就意味着,Mapper层出现的异常,会自动往上抛,抛给service层。 service层出现的异常,也会自动往上抛,抛给controller。 而controller中我们也并未处理,此时再往上抛,就抛给框架了,框架会就会将错误信息响应给用户。

那么异常,我们该如何来处理呢?

  • 方案一:在Controller的方法中进行try...catch处理 (代码过于臃肿
  • 方案二:全局异常处理器

6.3. 全局异常处理器

由于将来项目中会有很多Controller,那么每个Controller都需要一个异常处理器,则比较麻烦。而且这些异常处理器的处理逻辑又比较相似,所以SpringMVC中提供了全局异常处理器接收所有Controller中产生的异常。一般定义在exception包下:

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * Exception异常分类
     *  - 运行时异常 : RuntimeException , 编译时无需处理 .
     *  - 编译时异常 : 非 RuntimeException , 编译时处理 .
     */
    @ExceptionHandler(Exception.class)
    public Result ex(Exception ex){
        ex.printStackTrace();
        return Result.error("系统繁忙, 请稍后重试 ... ");
    }

}

流程:

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

6.4. 测试


七、过滤器Filter

7.1. 介绍

  • 概念:Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。

  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。

  • 过滤器一般完成一些通用的操作,比如:登陆鉴权、统一编码处理、敏感字符处理等等...

7.2. 快速入门

1). 定义类,实现 Filter接口,并重写doFilter方法

java 复制代码
public class DemoFilter implements Filter {
	@Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

    }
}

2). 配置Filter拦截资源的路径:在类上定义 @WebFilter 注解

java 复制代码
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
	@Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

    }
}

3). 在doFilter方法中输出一句话,并放行

java 复制代码
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
	@Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截方法执行, 拦截到了请求 ...");
        System.out.println("执行放行前逻辑 ...");

        chain.doFilter(request, response);

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

4). 在引导类上使用@ServletComponentScan 开启 Servlet 组件扫描

java 复制代码
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
	
    public static void main(String[] args) {
        SpringApplication.run(TliasWebManagementApplication.class, args);
    }
	
}

7.3. 执行流程

疑问:

  • 放行后访问对应资源,资源访问完成后,还会回到Filter中吗? 答案:会
  • 如果回到Filter中,是重新执行还是执行放行后的逻辑呢?答案:执行放行后逻辑

7.4. Filter 拦截路径

拦截路径 urlPattern值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

7.5. 登录校验Filter

登录完成后,会把JWT令牌返回给前端,前端浏览器会将其存入本地存储。 在后面的请求中,前端会自动在请求头中将令牌token携带到服务端,接下来呢,我们就需要在服务端中通过过滤器来进行统一拦截校验。 过滤器中具体的校验流程如下:

  • 获取请求url。

  • 判断请求url中是否包含login,如果包含,说明是登录操作,放行。

  • 获取请求头中的令牌(token)。

  • 判断令牌是否存在,如果不存在,返回错误结果(未登录)。

  • 解析token,如果解析失败,返回错误结果(未登录)。

  • 放行。

代码实现:

1). pom.xml

引入json数据处理的工具 .

java 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.39</version>
</dependency>

2). 登录校验过滤器

java 复制代码
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
		
        String url = request.getRequestURL().toString();
        //如果是login, 直接放行
        if(url.contains("login")){
            System.out.println("登录操作, 直接放行...");
            filterChain.doFilter(req, res);
            return;
        }
		
        //如果不是 login ,需要校验 token
        String token = request.getHeader("token");
        if(!StringUtils.hasLength(token)){ //如果没有JWT令牌
            System.out.println("获取到token为空 , 返回错误信息...");
            //返回 未登录 提示信息
            String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(result);
            return ;
        }
		
        //解析jwt令牌, 如果解析失败, 则说明令牌无效 , 返回 未登录 提示信息
        try {
            JwtUtils.parseJWT(token);
            System.out.println("令牌解析成功, 直接放行 ...");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("令牌解析失败 , 返回错误信息...");
			
            //返回 未登录 提示信息
            String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(result);
            return ;
        }
		
        //如果校验通过放行
        filterChain.doFilter(req, res);
    }

}

7.6. 测试

登录校验Filter功能开发完毕后,我们就可以启动服务,打开postman来测试。

我们会看到未登录情况下,服务端响应回来了错误信息 NOT_LOGIN。

接下来,我们再来调用登录接口,获取到JWT令牌。

然后,再请求其他接口时,需要在请求头 Header 中,添加token,将JWT的值放在请求头中,点击请求。

通过Web组件Filter可以完成请求的统一校验,我们也可以通过SpringMVC中提供的 Interceptor 来解决。

八、拦截器Interceptor

8.1. 介绍

  • 拦截器:(Interceptor)是一种动态拦截方法调用的机制,类似于过滤器。在SpringMVC中动态拦截控制器方法的执行
  • 作用:在指定的方法调用前后执行预先设定的代码,完成功能增强

8.2. 快速入门

  1. 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法。
java 复制代码
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行 , true : 放行 ; false : 不放行,拦截 ;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		System.out.println("preHandle ....");
        //如果校验通过放行
        return false;
    }


    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ....");
    }
    
     //请求处理完成后调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion ....");
    }

}
  1. 注册拦截器
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

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

8.3. 执行流程

拦截路径 :

拦截路径 urlPattern值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的下一级资源,如: /emps/1 ,但是 不会拦截 /emps/list/1,/emps/list/1/2
目录拦截 /emps/** 访问/emps下的所有资源,都会被拦截
拦截所有 /** 访问所有资源,都会被拦截

8.4. 登录校验Interceptor

  • 获取请求url。

  • 判断请求url中是否包含login,如果包含,说明是登录操作,放行。

  • 获取请求头中的令牌(token)。

  • 判断令牌是否存在,如果不存在,返回错误结果(未登录)。

  • 解析token,如果解析失败,返回错误结果(未登录)。

  • 放行。

代码实现:

java 复制代码
//@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行 , true : 放行 ; false : 不放行,拦截 ;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURL().toString();
        //如果是login, 直接放行
        if(url.contains("login")){
            System.out.println("登录操作, 直接放行...");
            return true;
        }

        //如果不是 login ,需要校验 token
        String token = request.getHeader("token");
        if(!StringUtils.hasLength(token)){ //如果没有JWT令牌
            System.out.println("获取到token为空 , 返回错误信息...");
            //返回 未登录 提示信息
            String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(result);
            return false;
        }

        //解析jwt令牌, 如果解析失败, 则说明令牌无效 , 返回 未登录 提示信息
        try {
            JwtUtils.parseJWT(token);
            System.out.println("令牌解析成功, 直接放行 ...");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("令牌解析失败 , 返回错误信息...");

            //返回 未登录 提示信息
            String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(result);
            return false;
        }

        //如果校验通过放行
        return true;
    }


    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ....");
    }

     //请求处理完成后调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion ....");
    }

}

8.5. 测试

登录校验Filter功能开发完毕后,我们就可以启动服务,打开postman来测试。

我们会看到未登录情况下,服务端响应回来了错误信息 NOT_LOGIN。

接下来,我们再来调用登录接口,获取到JWT令牌。

然后,再请求其他接口时,需要在请求头 Header 中,添加token,将JWT的值放在请求头中,点击请求。

8.6 Filter 与 Interceptor 区别

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。


九、SpringBoot-事务&AOP

1. 事务管理

1.1 事务回顾

在数据库阶段我们学习过事务:事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败

事务操作:

  • 开启事务:begin /start transaction ; 一组操作开始前,开启事务
  • 提交事务: commit; 全部成功后提交事务
  • 回滚事务: rollback; 中间有任何一个子操作出现异常,回滚事务

1.2 案例

需求:解散部门-删除部门、同时删除部门下的员工。

我们需要完善之前删除部门的代码,最终代码实现如下:

1). DeptServiceImpl

java 复制代码
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    @Autowired
    private EmpMapper empMapper;

    @Override
    public void delete(Integer id) {
        //1. 删除部门
        deptMapper.delete(id);
        
        int i = 1/0;//出现异常
        
        //2. 根据部门id, 删除部门下的员工信息
        empMapper.deleteByDeptId(id);
    }
}

2). EmpMapper

java 复制代码
//根据部门ID, 删除该部门下的员工数据
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);

问题:即使程序运行抛出了异常,部门依然删除了,但是部门下的员工却没有删除,造成了数据的不一致。

解散部门,应该是一个事务,这一个事务中的一组操作,要么全部成功,要么全部失败。

1.3 Spring事务管理

  • 注解:@Transactional

  • 位置:业务(service)层的方法上、类上、接口上

  • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务

  • 可以通过如下配置,查看详细的事务管理日志:

    yml 复制代码
    logging:
      level:
        org.springframework.jdbc.support.JdbcTransactionManager: debug

1). 加在方法上

java 复制代码
@Transactional
@Override
public void delete(Integer id) {
    //1. 删除部门
    deptMapper.delete(id);

    int i = 1/0;  //出现异常

    //2. 根据部门id, 删除部门下的员工信息
    empMapper.deleteByDeptId(id);
}

2). 加在接口上

java 复制代码
@Transactional
public interface DeptService {

}

3). 加在类上

java 复制代码
@Transactional
@Service
public class DeptServiceImpl implements DeptService {

}

我们会看到,当进行部门删除时,程序报出异常,而数据库数据呢,也已经回滚了。

1.4 事务进阶

1.4.1 rollbackFor

rollbackFor属性可以控制出现何种异常类型,回滚事务。默认情况下,只有出现 RuntimeException 才回滚异常。而如果出现编译时异常,则不回滚。

可以其出现任意异常都回滚事务

1). 方案一

而这样配置的话,使用比较麻烦,故而在实际开发中一般会进行异常转换

2). 方案二

如果业务代码有编译时异常,则将其转换为运行时异常,再抛出

这样即使用方便,又不至于事务失效。当然,如果全部抛出RuntimeException 不利于调错,故而可以自定义运行时异常,并抛出自定义异常。

java 复制代码
public class CustomerException extends RuntimeException{
    public CustomerException() {
    }
    public CustomerException(String message) {
        super(message);
    }
}
java 复制代码
@Transactional
@Override
public void delete(Integer id) throws Exception {
    //1. 删除部门
    deptMapper.delete(id);
    
    try {
        InputStream in = new FileInputStream("E:/1.txt");
    } catch (Exception e) {
        throw new Exception("出错了");
    }
	
    //2. 根据部门id, 删除部门下的员工信息
    empMapper.deleteByDeptId(id);
}
1.4.2 propagation
  • 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

  • 案例:

    在本例中 DeptServiceImpl中的delete方法,与 EmpServiceImpl中的deleteByDeptId方法, 两个方法都加上@Transactional 注解控制事务。

    由于没有配置事务传播行为,则默认事务传播行为 为 REQUIRED, 则表示 EmpServiceImpl.deleteByDeptId(),使用 DeptServiceImpl.delete() 刚才开启的事务,也就意味着这两个方法公用同一个事务,可以对其进行 统一提交和回滚操作。

1). DeptServiceImpl

java 复制代码
@Transactional
@Override
public void delete(Integer id) throws Exception {
    //1. 删除部门
    deptMapper.delete(id);

    //2. 根据部门id, 删除部门下的员工信息
    empService.deleteByDeptId(id);
    
    //int i = 1/0;
}

2). EmpServiceImpl

java 复制代码
@Transactional
@Override
public void deleteByDeptId(Integer deptId) {
	empMapper.deleteByDeptId(deptId);
}
  • 查看运行日志:

  • 使用 propagation 属性可以配置事务传播行为

属性值 含义 说明
REQUIRE 【默认值】需要事务,有则加入,无则创建新事务 -
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务 -
SUPPORTS 支持事务,有则加入,无则在独立的连接中运行 SQL 结合 Hibernate、JPA 时有用,配在查询方法上
NOT_SUPPORTED 不支持事务,不加入,在独立的连接中运行 SQL -
MANDATORY 必须有事务,否则抛异常 -
NEVER 必须没事务,否则抛异常 -
NESTED 嵌套事务 仅对 DataSourceTransactionManager 有效

我们主需要掌握前两个 : REQUIRED 以及 REQUIRES_NEW ,其他很少用到,无需掌握

接下来,我们再来测试一下 REQUIRES_NEW:

1). DeptServiceImpl

java 复制代码
@Transactional
@Override
public void delete(Integer id) throws Exception {
    //1. 删除部门
    deptMapper.delete(id);

    //2. 根据部门id, 删除部门下的员工信息
    empService.deleteByDeptId(id);
    
    int i = 1/0; //抛出异常
}

2). EmpServiceImpl

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void deleteByDeptId(Integer deptId) {
    empMapper.deleteByDeptId(deptId);
}
  • 查看运行日志:

此时由于 EmpServiceImpl的deleteByDeptId方法 的事务传播行为为 REQUIRES_NEW 开启了一个新事务,则不会因为 DeptServiceImpl的delete方法 出现异常而回滚。

  • 作用:

    • REQUIRED :大部分情况下都是用该传播行为即可。

    • REQUIRES_NEW :当我们不希望事务之间相互影响时可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

2. AOP基础

2.1 记录方法执行耗时

  • 需求:记录业务方法的执行耗时,并输出到控制台。

记录方法执行耗时,其实也非常简单,我们只需要在方法执行之前,获取一个开始时间戳。在方法执行完毕后,获取结束时间的时间戳。然后后者减去前者,就是方法的执行耗时。

  • 具体代码如下所示:
java 复制代码
@Override
public List<Dept> list() {
    long begin = System.currentTimeMillis();
    
    List<Dept> deptList = deptMapper.list();
    
    long end = System.currentTimeMillis();
    log.debug("方法执行耗时 : {} ms", (end-begin));
    return deptList;
}

@Transactional
@Override
public void delete(Integer id) throws Exception {
    long begin = System.currentTimeMillis();
    
    //1. 删除部门
    deptMapper.delete(id);
    //2. 根据部门id, 删除部门下的员工信息
    empService.deleteByDeptId(id);
    //int i = 1/0;
    
    long end = System.currentTimeMillis();
    log.info("方法执行耗时 : {} ms", (end-begin));
}

@Override
public void save(Dept dept) {
    long begin = System.currentTimeMillis();

    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
    deptMapper.save(dept);
	
    long end = System.currentTimeMillis();
    log.info("方法执行耗时 : {} ms", (end-begin));
}

上述功能虽然实现了,但是我们会发现,所有的方法中,代码都是固定的,存在大量的重复代码:

A. 业务方法执行之前,记录开始时间:

java 复制代码
long begin = System.currentTimeMillis();

B. 业务方法执行之后,记录结束时间:

java 复制代码
long end = System.currentTimeMillis();
log.info("方法执行耗时 : {} ms", (end-begin));

2.2 AOP快速入门

  • AOP:Aspect Oriented Programming(面向切面编程),它的核心思想是将重复的逻辑剥离出来,在不修改原始逻辑的基础上对原始功能进行增强。

  • 优势:无侵入、减少重复代码、提高开发效率、维护方便

  • 我们可以通过AOP来完成上述代码的优化:

1). pom.xml 引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2). 定义类抽取公共代码(执行耗时统计操作)

java 复制代码
@Slf4j
public class TimeAspect {
	
    public void recordTime() throws Throwable {
        long begin = System.currentTimeMillis();
        
        //调用原始操作
        
        
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
    }
	
}

3). 标识当前类是一个AOP类,并被Spring容器管理

java 复制代码
@Component
@Aspect
@Slf4j
public class TimeAspect {
	
    public void recordTime() throws Throwable {
        long begin = System.currentTimeMillis();
        
        //调用原始操作
        
        
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
    }
	
}

​ @Aspect:标识当前类是一个AOP类

​ @Component:声明该类是spring的IOC容器中的bean对象

4). 配置公共代码作用于哪些目标方法

java 复制代码
@Component
@Aspect
@Slf4j
public class TimeAspect {
	
    @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void recordTime() throws Throwable {
        long begin = System.currentTimeMillis();
        
        //调用原始操作
        
        
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
    }
	
}

​ @Around: 表示环绕通知,可以在目标方法执行前后执行一些公共代码

​ * 表示通配符,代表任意

​ ... 表示参数通配符,代表任意参数

5). 执行目标方法

java 复制代码
@Component
@Aspect
@Slf4j
public class TimeAspect {
	
    @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        //调用原始操作
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
        return result;
    }
    
}    

6). 测试运行

2.3 执行流程

AOP 的一种实现方式,是通过动态代理技术实现。

  • 当目标对象(此处为DeptServiceImpl)功能需要被增强时,并且我们使用AOP方式定义了增强逻辑(在Aspect类中)
  • Spring会为目标对象自动生成一个代理对象,并在代理对象对应方法中,结合我们定义的AOP增强逻辑完成功能增强

2.4 AOP核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法执行(包含方法信息)

  • 通知:Advice ,重复逻辑代码

  • 切入点:PointCut ,匹配连接点的条件

  • 切面:Aspect,通知+切点

3. AOP进阶

3.1 通知类型

  • @Around:此注解标注的通知方法在目标方法前、后都被执行

  • @Before:此注解标注的通知方法在目标方法前被执行

  • @After :此注解标注的通知方法在目标方法后被执行,无论是否有异常

  • @AfterReturning : 此注解标注的通知方法在目标方法后被执行,有异常不会执行

  • @AfterThrowing : 此注解标注的通知方法发生异常后执行

@Around 需要自己调用 ProceedingJoinPoint.proceed() 来让目标方法执行,其他通知不需要考虑目标方法执行

  • 切面类
java 复制代码
@Component
@Aspect
@Slf4j
public class TimeAspect {

    @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        //调用原始操作
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
        return result;
    }

    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){
        log.info(" T before ....");
    }

    @After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void after(){
        log.info(" T after ....");
    }

    @AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void afterReturning(){
        log.info("afterReturning ....");
    }

    @AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void afterThrowing(){
        log.info("afterThrowing ....");
    }
}
  • 目标类
java 复制代码
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Override
    public List<Dept> list() {
        List<Dept> deptList = deptMapper.list();
        return deptList;
    }

    @Override
    public void delete(Integer id) {
        deptMapper.delete(id);
    }

    @Override
    public void save(Dept dept) {
        dept.setCreateTime(LocalDateTime.now());
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.save(dept);
    }
}
  • 测试1

​ 程序正常运行的情况下,通知类型 @AfterThrowing 是不会运行的,但是@AfterReturning 是会运行的。

  • 测试2

​ 程序运行出现异常的情况下,通知类型 @AfterReturning 是会运行的,但是@AfterThrowing 是不会运行的。

3.2 通知顺序

当有多个切面的切点都匹配目标时,多个通知方法都会被执行。之前介绍的 pjp.proceed() 在有多个通知方法匹配时,更准确的描述应该是这样的:

  • 如果还有下一个通知,则调用下一个通知
  • 如果没有下一个通知,则调用目标

那么它们的执行顺序是怎样的呢?

  • 默认按照 bean 的名称字母排序
  • @Order(数字) 加在切面类上来控制顺序
    • 目标前的通知方法:数字小先执行
    • 目标后的通知方法:数字小后执行

1). 默认顺序

java 复制代码
@Component
@Aspect
@Slf4j
public class TimeAspect {

    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){
        log.info(" T before ....");
    }
    
}
java 复制代码
@Component
@Aspect
@Slf4j
public class AimeAspect {

    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){
        log.info("before ...." );
    }

}

定义TimeAspectAimeAspect 切面类,测试执行顺序,默认按照切面类的名称字母排序。此例中AimeAspectTimeAspect 字母顺序排名靠前,故此,AimeAspect 先执行。

所以调用之后执行顺序为:

2). @Order(数字) 排序

  • 目标前的通知方法:数字小先执行
  • 目标后的通知方法:数字小后执行
java 复制代码
@Component
@Aspect
@Slf4j
@Order(1)
public class TimeAspect {

    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){
        log.info(" T before ....");
    }
    
}
java 复制代码
@Component
@Aspect
@Slf4j
@Order(2)
public class AimeAspect {

    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){
        log.info("before ...." );
    }

}

测试结果如下:

3.3 切点表达式

切点表达式用来匹配【哪些】目标方法需要应用通知,常见的切点表达式如下

  • execution(返回值类型 包名.类名.方法名(参数类型))
    • * 可以通配任意返回值类型、包名、类名、方法名、或任意类型的一个参数
    • .. 可以通配任意层级的包、或任意类型、任意个数的参数
  • @annotation() 根据注解匹配
  • args() 根据方法参数匹配
3.3.1 execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

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

其中带 ? 的表示可以省略的部分

• 访问修饰符:可省略(没啥用,仅能匹配 public、protected、包级,private 不能增强)

• 包名.类名: 可省略

• throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

3.3.2 annotation

切点表达式也支持匹配目标方法是否有注解。使用 @annotation

复制代码
@annotation(com.itheima.anno.Log)
3.3.3 @PointCut

通过@PointCut注解,可以抽取一个切入点表达式,然后再其他的地方我们就可以通过类似于 方法调用 的形式来引用该切入点表达式。

java 复制代码
    @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void pt(){}

    @Around("pt()")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        //调用原始操作
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
        return result;
    }

3.4 连接点

连接点简单理解就是 目标方法,在Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如方法名、方法参数类型、方法实际参数等等

  • 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型

那么如何获取这些信息呢?参考下面的代码

java 复制代码
@Slf4j
@Aspect
@Component
public class MyAspect1 {

    @Pointcut("execution(* com.itheima.service.impl.*.*(..)) && @annotation(com.itheima.anno.Log)")
    public void pt(){}

    @Before("pt()")
    public void before(JoinPoint joinPoint){

        log.info("方法名: "+joinPoint.getSignature().getName());
        log.info("类名: "+joinPoint.getTarget().getClass().getName());
        log.info("参数: "+Arrays.asList(joinPoint.getArgs()).toString());

        log.info("before...1");
    }
}    

4. AOP案例

4.1 需求

将对业务类中的 方法操作日志保存到数据库。

  • 操作日志包括:
    • 操作人
    • 操作时间
    • 操作全类名
    • 操作方法名
    • 方法参数
    • 返回值
    • 方法执行耗时

4.2 分析

  • 需要对所有业务类中的 方法添加统一功能,使用AOP技术最为方便
  • 由于 方法名没有规律,可以自定义@Log注解完成目标方法选取

4.3 步骤:

1). 创建操作日志表

java 复制代码
-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
    operate_user int unsigned comment '操作人',
    operate_time datetime comment '操作时间',
    class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值',
    cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

2). 实体类

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private Integer operateUser; //操作人
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}

3). 自定义注解

java 复制代码
/**
 * 自定义Log注解
 */
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

4). 在需要记录日志的方法上加 @Log注解

java 复制代码
    @Log
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) throws Exception {
        deptService.delete(id);
        return Result.success();
    }

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


    @Log
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        Dept dept = deptService.getById(id);
        return Result.success(dept);
    }

5). 定义Mapper接口

java 复制代码
@Mapper
public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    public void insert(OperateLog log);

}

6). 定义切面类

java 复制代码
@Component
@Aspect
public class LogAspect {

    @Autowired
    private OperateLogMapper operateLogMapper;
    @Autowired
    private HttpServletRequest request;

    @Around("@annotation(com.itheima.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis(); //开始时间

        String token = request.getHeader("token");
        Integer operateUser  = (Integer) JwtUtils.parseJWT(token).get("id");

        String className = joinPoint.getTarget().getClass().getName(); //操作类名
        String methodName = joinPoint.getSignature().getName(); //操作方法名

        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args); //操作方法参数

        //放行原始方式
        Object result = joinPoint.proceed();

        String returnValue = JSONObject.toJSONString(result);
        long end = System.currentTimeMillis(); //结束时间
        long costTime = end - begin;

        OperateLog log = new OperateLog(null,operateUser, LocalDateTime.now(),className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(log);
        return result;
    }

}

十、相关注解&自动装配原理&自定义starter

1. 自动配置原理

前面我们提到,Spring家族的其他框架都是基于SpringFramework的。 但是直接使用Spring框架进行集成开发比较繁忙,入门难度很大,所以在现在的企业开发中,都是直接基于Springboot进行开发,简单、快捷、高效,而且spring官方也是建议直接从springboot开始。

那是什么原因,让springboot这么火、这么好用呢?其实呢,原因主要有两点:

  • 起步依赖
  • 自动配置

1.1 起步依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
	
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
	
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

这里看到的spring-boot-starter-xxx 就是 SpringBoot的起步依赖。SpringBoot通过提供众多起步依赖降低项目依赖的复杂度。起步依赖本质上是一个Maven项目对象模型,定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。很多起步依赖的命名都暗示了他们提供的某种或某类功能。

在SpringBoot中还提供了很多的起步依赖,具体可以参考SpringBoot的官网:

1.2 自动配置

1.2.1 介绍
  • SpringBoot的自动配置就是当spring容器启动后,一些配置类就自动装配的IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置等操作。

  • 要想明白SpringBoot 自动配置原理,先需要明白两个前置知识 @Conditional 条件注解 和 @Import 导入配置。

1.2.2 @Conditional

SpringBoot 提供了很多 @ConditionalOnXxx 注解,来判断当达成某一条件后,才加载对应的Bean。

  • 作用:按照一定的条件进行判断,需要声明的Bean,在满足给定条件后才会注册到Spring IOC容器中。

@Conditional 本身还是一个父注解,派生出大量的子注解:

  • @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器。

  • @ConditionalOnMissingBean:判断环境中没有对应的bean ,才注册bean到IOC容器。

  • @ConditionalOnProperty:判断配置文件中是否有对应属性和值,才注册bean到IOC容器。

演示1:

java 复制代码
@Configuration
public class CommonConfig {
    @Bean
    @ConditionalOnClass(name = "io.jsonwebtoken.Jwts")
    public BASE64Encoder base64Encoder(){
        return new BASE64Encoder();
    }
}

io.jsonwebtoken.Jwts 对应Class文件存在时,才加载BASE64Encoder这个Bean。

演示2:

java 复制代码
@Configuration
public class CommonConfig {

    @Bean
    @ConditionalOnProperty(name = "secret",havingValue = "itxg")
    public BASE64Decoder base64Decoder(){
        return new BASE64Decoder();
    }
    
}

当配置文件中存在 secret: itxg,这一个配置项时,才会加载 BASE64Decoder 这个Bean。

演示3:

java 复制代码
@Configuration
public class CommonConfig {

    @Bean
    @ConditionalOnMissingBean(name = "empController")
    public BASE64Decoder base64Decoder(){
        return new BASE64Decoder();
    }
    
}

当IOC容器中,没有名称为 empController 的bean的时候,才会加载 BASE64Decoder 这个Bean。

1.2.3 @Import

@Import注解用于导入一些Bean 和 配置类到IOC容器中。

先思考一个问题,SpringBoot是否能够直接加载第三方Bean?不能。因为第三方Bean很大可能和当前项目包结构不匹配。

故而可以采用@Import 注解导入。@Import注解主要可以导入形式有以下几种:

  1. Bean
  2. 配置类
  3. ImportSelector接口子类

SpringBoot自动配置采用的是第三种。

演示:

java 复制代码
@Import({TokenParser.class,HeaderConfig.class})
@SpringBootApplication
public class SpringbootThirdbeanApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootThirdbeanApplication.class, args);
    }
}
1.2.4 自动配置原理
  1. 当SpringBoot 程序启动时,引导类上 @SpringBootApplication 注解生效,该注解由三个注解组成


  • @SpringBootConfiguration:引导类也是一个配置类
  • @ComponentScan:包扫描
  • @EnableAutoConfiguration:自动配置。底层为:@Import(AutoConfigurationImportSelector.class),通过@Import导入配置类

  1. 查看 AutoConfigurationImportSelector源码

​ SpringBoot程序在启动时会自动加载 META-INF/spring.factories 文件 和 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,并导入其中定义的所有配置类。

  1. 由于这些配置类上都加了Condition条件注解,所有不会将所有Bean 加载到IOC容器中,只有满足条件的Bean才会加载。
1.2.5 自定义starter

在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot 的 starter,再上传到公司的私服中。将来引入对应坐标依赖,即可快速使用这些功能。一般要自定义starter,可以参考其他starter即可,比如 mybatis 的starter

本课程中以自定义 Aliyun的OSS工具类为例。

  1. 创建模块 aliyun-oss-spring-boot-starter,该模块作为整合依赖模块(可以删除src目录)。

  2. 创建aliyun-oss-spring-boot-autoconfigure模块,用于提供自动配置功能,并在 aliyun-oss-spring-boot-starter引入 aliyun-oss-spring-boot-autoconfigure坐标。将来使用阿里云 oss功能,只需要导入 aliyun-oss-spring-boot-starter即可。

    aliyun-oss-spring-boot-starter的依赖配置如下:

    xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    
        <dependency>
            <groupId>com.itxg</groupId>
            <artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    aliyun-oss-spring-boot-autoconfigure的依赖配置如下:

    xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.15.0</version>
        </dependency>
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
    </dependencies>
  3. aliyun-oss-spring-boot-autoconfigure模块中 编写核心逻辑。此例中复制 AliOSSUtils 工具类改造即可。

    创建包 com.itxg.oss

    java 复制代码
    @Data
    @ConfigurationProperties(prefix = "aliyun.oss")
    public class AliOSSProperties {
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    }
    java 复制代码
    /**
     * 阿里云 OSS 工具类
     */
    @Data
    public class AliOSSUtils {
    
        private AliyunOSSProperties aliyunOSSProperties;
    
        /**
         * 实现上传图片到OSS
         */
        public String upload(MultipartFile multipartFile) throws IOException {
            // 获取上传的文件的输入流
            InputStream inputStream = multipartFile.getInputStream();
    
            // 避免文件覆盖
            String fileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) + multipartFile.getOriginalFilename();
    
            //上传文件到 OSS
            OSS ossClient = new OSSClientBuilder().build(aliyunOSSProperties.getEndpoint(), aliyunOSSProperties.getAccessKeyId(), aliyunOSSProperties.getAccessKeySecret());
            ossClient.putObject(aliyunOSSProperties.getBucketName(), fileName, inputStream);
    
            //文件访问路径
            String url = aliyunOSSProperties.getEndpoint().split("//")[0] + "//" + aliyunOSSProperties.getBucketName() + "." + aliyunOSSProperties.getEndpoint().split("//")[1] + "/" + fileName;
            // 关闭ossClient
            ossClient.shutdown();
            return url;// 把上传到oss的路径返回
        }
    
    }
  4. 定义自动配置类

    java 复制代码
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @EnableConfigurationProperties(AliyunOSSProperties.class)
    public class AliyunOSSAutoConfiguration {
    
        @Bean
        public AliOSSUtils aliOSSUtils(AliyunOSSProperties aliyunOSSProperties){
            AliOSSUtils aliOSSUtils = new AliOSSUtils();
            aliOSSUtils.setAliyunOSSProperties(aliyunOSSProperties);
            return aliOSSUtils;
        }
    
    }
  5. 在resources目录下定义 META-INF/spring.factories,SpringBoot程序启动后会自动读取该文件加载键为:org.springframework.boot.autoconfigure.EnableAutoConfiguration的值

    java 复制代码
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.aliyun.oss.AliyunOSSAutoConfiguration
  6. 测试。此例中,在springboot-thirdbean中引入starter依赖

    java 复制代码
    <dependency>
        <groupId>com.alibaba.oss</groupId>
        <artifactId>aliyun-oss-spring-boot-starter</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

    在UploadController中注入 AliOSSUtils,测试使用

    java 复制代码
    @RestController
    public class UploadController {
    
        @Autowired
        private AliOSSUtils aliOSSUtils;
    
        @PostMapping("/upload")
        public R upload(MultipartFile image) throws IOException {
            //1.调用工具类
            String url = aliOSSUtils.upload(image);
            return R.success(url);
        }
    }

2. 实现步骤分析:

javascript 复制代码
1.创建一个普通的maven项目,名称为:mydruid-spring-boot-starter
2.创建一个springboot项目,名称为:mydruid-spring-boot-autoconfigure
3.把自动装配类和属性类,拷贝到mydruid-spring-boot-autoconfigure项目中
4.需要在mydruid-spring-boot-autoconfigure项目中添加必要的依赖
5.在mydruid-spring-boot-autoconfigure项目的META-INF/sping目录下创建固定名称的文件
	org.springframework.boot.autoconfigure.AutoConfiguration.imports
6.在org.springframework.boot.autoconfigure.AutoConfiguration.imports配置文件中添加
	druid连接池的自动装配的类的全名称
7.在mydruid-spring-boot-starter项目的pom文件中引入mydruid-spring-boot-autoconfigure项目
8.对以上两个项目分别执行install命令,加入到本地仓库
9.在其它项目中引入mydruid-spring-boot-starter起步依赖
10.后期处理问题
相关推荐
heartbeat..4 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
5 小时前
java关于内部类
java·开发语言
好好沉淀5 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin5 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder5 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~5 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟5 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日5 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du5 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea