黑马程序员JavaWeb学习笔记 - 2(后端基础部分)

文章目录

前言🚀

本人是一个大三的计算机科学与技术专业的学生,目前还处于学习阶段,已经跟随bilibili黑马的课程学完了很多java后端的内容(javase、javaweb、git、linux、mysql、redis、springcloud等等),平时学习会做一些笔记,笔记毫不废话,开门见山,这也是我做笔记的个人习惯吧,这里分享一下我做的JavaWeb部分的学习笔记,笔记内容跟随黑马程序员的javaweb教程,课程地址如下:

https://www.bilibili.com/video/BV1m84y1w7Tb?t=1.6

笔记内容有点多,但是可以根据目录自动导航到特定章节进行复习或学习。

说明:这篇笔记只是一个初步的第一版笔记哦,相应的配套资料可以下载黑马的资料,我也准备了一个飞书版本的文章,平时博主复习也是使用的飞书知识库进行复习,所以另一个版本的文章会更加完善,而且,另一个版本也有一些资料的提供(不是全部哦),如果你想要,点击链接https://mcnerzykwkel.feishu.cn/wiki/KIfZw5wKAi0rXjkKRDkcPc2onyf就可以了,如果你有更好的建议,直接在飞书内评论说明就可以咯,但是博主还是很自信不会有几个建议的😊。


九、Maven

1.Maven概述

Maven是Apache旗下的一个开源项目,是一款用于管理和构建java项目的工具。

1.1 Maven的作用

  1. 方便的依赖管理:方便快捷的管理项目依赖的资源(jar包),避免版本冲突问题

在maven项目的pom.xml文件中,添加一段如下图所示的配置即可实现

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.2.13.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.4</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
<dependency>
    <groupId>com.github.oshi</groupId>
    <artifactId>oshi-core</artifactId>
    <version>5.6.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. 统一的项目结构:提供标准、统一的项目结构,解决不同开发工具的项目结构不一致问题。

image-20250404220939157

  1. 标准的项目构建流程:标准跨平台(Linux、Windows、MacOS)的自动化项目构建方式。我们开发一套系统需要进行编译、测试、打包、发布,这些操作如果需要反复进行就显得特别麻烦,Maven提供了一套简单的命令来完成项目构建。

1.2 Maven模型

  • 项目对象模型 (Project Object Model):将我们自己的项目抽象成一个对象模型,有自己专属的坐标,通过坐标可以定位到所需资源(jar包)位置

    image-20250404222143336

  • 依赖管理模型(Dependency):使用坐标来描述当前项目依赖哪些第三方jar包,通过在pom.xml文件中自定义的坐标自动从本地仓库下载导入相关的jar包

  • 构建生命周期/阶段(Build lifecycle & phases):当我们需要编译,Maven提供了一个编译插件供我们使用;当我们需要打包,Maven就提供了一个打包插件供我们使用等

1.3 Maven仓库

仓库:用于存储资源,管理各种jar包。

Maven仓库分为:

  • 本地仓库:自己计算机上的一个目录(用来存储jar包)
  • 中央仓库:由Maven团队维护的全球唯一的。仓库地址:https://repo1.maven.org/maven2/
  • 远程仓库(私服):一般由公司团队搭建的私有仓库

当项目中使用坐标引入对应依赖jar包后,首先会查找本地仓库中是否有对应的jar包

  • 如果有,则在项目直接引用

  • 如果没有,则去中央仓库中下载对应的jar包到本地仓库

如果还可以搭建远程仓库(私服),将来jar包的查找顺序则变为: 本地仓库 --> 远程仓库–> 中央仓库

1.4 Maven安装

参考资料中的安装文档安装即可

2.IDEA集成Maven

2.1 配置Maven环境

参考资料中的安装文档安装即可,创建maven项目和导入maven项目也参考资料中的安装文档安装。

2.2 POM配置详解

POM (Project Object Model) :指的是项目对象模型,用来描述当前的maven项目。

  • 使用pom.xml文件来实现

pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- POM模型版本 -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 当前项目坐标 -->
    <groupId>com.itheima</groupId>
    <artifactId>maven_project1</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <!-- 打包方式 -->
    <packaging>jar</packaging>
 
</project>

pom文件详解:

  • <project> :pom文件的根标签,表示当前maven项目
  • <modelVersion> :声明项目描述遵循哪一个POM模型版本
    • 虽然模型本身的版本很少改变,但它仍然是必不可少的。目前POM模型版本是4.0.0
  • 坐标 :<groupId><artifactId><version>
    • 定位项目在本地仓库中的位置,由以上三个标签组成一个坐标
  • <packaging> :maven项目的打包方式,通常设置为jar或war(默认值:jar)

2.3 Maven坐标详解

什么是坐标?

  • Maven中的坐标是资源的唯一标识 , 通过该坐标可以唯一定位资源位置
  • 使用坐标来定义项目或引入项目中需要的依赖

Maven坐标主要组成

  • groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)
  • artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)
  • version:定义当前项目版本号

如下图就是使用坐标表示一个项目:

image-20250405020719137

注意:

  • 上面所说的资源可以是插件、依赖、当前项目。
  • 我们的项目如果被其他的项目依赖时,也是需要坐标来引入的。

3.依赖管理

3.1 依赖配置

依赖指当前项目运行所需要的jar包,例如,在当前工程中,我们需要用到logback来记录日志,此时就可以在maven工程的pom.xml文件中,引入logback的依赖:

  1. 在pom.xml中编写<dependencies>标签

  2. <dependencies>标签中使用<dependency>引入坐标

  3. 定义坐标的 groupId、artifactId、version

<dependencies>
    <!-- 第1个依赖 : logback -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
    </dependency>
    <!-- 第2个依赖 : junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
  • 注:如果不知道依赖的坐标信息,可以到mvn的中央仓库(https://mvnrepository.com/)中搜索
  1. 点击刷新按钮,引入最新加入的坐标

3.2 依赖传递

由于logback-classic依赖logback-core和slf4j,在添加logback-classic依赖时,会自动把所依赖的其他jar包logback-core和slf4j也一起导,故只需要在pom.xml配置文件中,添加logback-classic的依赖坐标即可。

依赖传递可以分为:

  1. 直接依赖:在当前项目中通过依赖配置建立的依赖关系

  2. 间接依赖:被依赖的资源如果依赖其他资源,当前项目间接依赖其他资源

例如对于projectA 来说,projectB 就是直接依赖,projectC就是间接依赖:

image-20250405160500284

排除依赖

主动断开依赖的资源(被排除的资源无需指定版本)。

<dependency>
    <groupId>com.itheima</groupId>
    <artifactId>maven-projectB</artifactId>
    <version>1.0-SNAPSHOT</version>
   
    <!--排除依赖, 主动断开依赖的资源-->
    <exclusions>
    	<exclusion>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </exclusion>
    </exclusions>
</dependency>

3.3 依赖范围

限制依赖的使用范围,可以通过<scope>标签设置其作用范围。

作用范围:

  1. 主程序范围有效(main文件夹范围内)

  2. 测试程序范围有效(test文件夹范围内)

  3. 是否参与打包运行(package指令范围内)

scope标签的取值范围:

scope主程序测试程序打包(运行)范例
compile(默认)YYYlog4j
test-Y-junit
providedYY-servlet-api
runtime-YYjdbc驱动
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.13.1</version>
	<scope>test</scope>
</dependency>

3.4 生命周期

Maven的生命周期描述了一次项目构建经历哪些阶段,在Maven出现之前,项目构建的生命周期就已经存在

Maven对项目构建的生命周期划分为3套(相互独立):

  • clean:清理工作。

  • default:核心工作。如:编译、测试、打包、安装、部署等。

  • site:生成报告、发布站点等。

image-20250405162544212

常使用的5个阶段含义:

• clean:移除上一次构建生成的文件

• compile:编译项目源代码

• test:使用合适的单元测试框架运行测试(junit)

• package:将编译后的文件打包,如:jar、war等

• install:安装项目到本地仓库

在同一套生命周期中,我们在执行后面的生命周期时,前面的生命周期都会执行,例如执行package阶段,compile和test都会执行,但是clean不会执行,因为package和clean不在同一套生命周期。

执行某一阶段生命周期

执行指定的生命周期时,有两种执行方式:

  1. 在idea工具右侧的maven工具栏中,选择对应的生命周期,双击执行
  2. 在DOS命令行中,通过maven命令执行
    • 进入到maven项目的命令行中
    • 运行命令mvn 阶段名

image-20250405163456747

3.5 清理maven仓库

从私服下载jar包时,可能由于网络的原因,jar包下载不完全,这些不完整的jar包都是以lastUpdated结尾,maven不会再重新下载,需要手动删除这些以lastUpdated结尾的文件,然后maven才会再次自动下载这些jar包。

可以定义一个批处理文件,在其中编写如下脚本来删除:

set REPOSITORY_PATH=E:\develop\apache-maven-3.6.1\mvn_repo
rem 正在搜索...

del /s /q %REPOSITORY_PATH%\*.lastUpdated

rem 搜索完毕
pause

1). 定义批处理文件del_lastUpdated.bat (直接创建一个文本文件,命名为del_lastUpdated,后缀名直接改为bat即可 )

image-20250405163857547

2). 在上面的bat文件上右键—》编辑 。修改文件:

image-20250405163918403

修改完毕后,运行即可删除maven仓库中的残留文件

十、SpringBootWeb

通过springboot可以快速的帮我们构建应用程序,简化开发、提高效率。

springboot最大的特点有两个:简化配置和快速开发

1.SpringBootWeb快速入门

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

image-20250406212721298

1.1 创建SpringBoot工程(需要联网)

基于Spring官方骨架,创建SpringBoot工程。

image-20250406212845729

之后选上Spring Web即可。

1.2 定义请求处理类

在Demo1Application类所在的包下创建java类HelloController:

package com.itheima.controller;
import org.springframework.web.bind.annotation.*;

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("Hello World ~");
        return "Hello World ~";
    }
} 

1.3 运行测试

  1. 运行SpringBoot自动生成的引导类HelloController
  2. 打开浏览器,输入 http://localhost:8080/hello,出现Hello World~即表示成功。

1.4 Web分析

image-20250406230211897

浏览器:

  • 输入网址:http://192.168.100.11:8080/hello

    • 通过IP地址192.168.100.11定位到网络上的一台计算机

      我们之前在浏览器中输入的localhost,就是127.0.0.1(本机)

    • 通过端口号8080找到计算机上运行的程序

      localhost:8080 , 意思是在本地计算机中找到正在运行的8080端口的程序

    • /hello是请求资源位置

      • 资源:对计算机而言资源就是数据
        • web资源:通过网络可以访问到的资源(通常是指存放在服务器上的数据)

      localhost:8080/hello ,意思是向本地计算机中的8080端口程序,获取资源位置是/hello的数据

      • 8080端口程序,在服务器找/hello位置的资源数据,发给浏览器

服务器:(可以理解为ServerSocket)

  • 接收到浏览器发送的信息(如:/hello)
  • 在服务器上找到/hello的资源
  • 把资源发送给浏览器

2.HTTP协议

HTTP协议(超文本传输协议),规定了浏览器与服务器之间数据传输的规则,即浏览器在向服务器发送请求数据时,或是服务器在向浏览器发送响应数据时,都必须按照固定的格式进行数据传输。

特点

  1. 基于TCP协议:面向连接,安全
  2. 基于请求-响应模型:一次请求对应一次响应(先请求后响应,没有请求就没有响应)
  3. 无状态协议:对于数据没有记忆能力,每次请求-响应都是独立的。无状态指客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息

2.1 HTTP-请求协议

HTTP协议分为请求协议和响应协议。

  • 请求协议:浏览器将数据以请求格式发送到服务器
    • 包括:请求行请求头请求体
  • 响应协议:服务器将数据以响应格式返回给浏览器
    • 包括:响应行响应头响应体

在HTTP1.1版本中,浏览器访问服务器的几种方式:

请求方式请求说明
GET获取资源。
向特定的资源发出请求。例:http://www.baidu.com/s?wd=itheima
POST传输实体主体。
向指定资源提交数据进行处理请求(例:上传文件),数据被包含在请求体中。
OPTIONS返回服务器针对特定资源所支持的HTTP请求方式。
因为并不是所有的服务器都支持规定的方法,为了安全有些服务器可能会禁止掉一些方法,例如:DELETE、PUT等。那么OPTIONS就是用来询问服务器支持的方法。
HEAD获得报文首部。
HEAD方法类似GET方法,但是不同的是HEAD方法不要求返回数据。通常用于确认URI的有效性及资源更新时间等。
PUT传输文件。
PUT方法用来传输文件。类似FTP协议,文件内容包含在请求报文的实体中,然后请求保存到URL指定的服务器位置。
DELETE删除文件。
请求服务器删除Request-URI所标识的资源
TRACE追踪路径。
回显服务器收到的请求,主要用于测试或诊断
CONNECT要求用隧道协议连接代理。
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器

在我们实际应用中常用的也就是 :GET、POST

2.1.1 GET方式的请求协议

image-20250409182542300

  • 请求行 :HTTP请求中的第一行数据。由:请求方式资源路径协议/版本组成(之间使用空格分隔)

    • 请求方式:GET
    • 资源路径:/brand/findAll?name=OPPO&status=1
      • 请求路径:/brand/findAll
      • 请求参数:name=OPPO&status=1
        • 请求参数是以key=value形式出现
        • 多个请求参数之间使用&连接
      • 请求路径和请求参数之间使用?连接
    • 协议/版本:HTTP/1.1
  • 请求头 :第二行开始,上图黄色部分内容就是请求头。格式为key: value形式

    • http是个无状态的协议,所以在请求头设置浏览器的一些自身信息和想要响应的形式。这样服务器在收到信息后,就可以知道是谁,想干什么了

    常见的HTTP请求头有:

    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标签的结果会有不一致,所以就会导致相同的代码在不同的浏览器会出现不同的效果
  • 服务端根据客户端请求头中的数据获取到客户端的浏览器类型,就可以根据不同的浏览器设置不同的代码来达到一致的效果(这就是我们常说的浏览器兼容问题)
  • 请求体 :存储请求参数
    • GET请求的请求参数在请求行中,故不需要设置请求体
2.1.2 POST方式的请求协议

image-20250409183118536

  • 请求行(以上图中红色部分):包含请求方式、资源路径、协议/版本
    • 请求方式:POST
    • 资源路径:/brand
    • 协议/版本:HTTP/1.1
  • 请求头(以上图中黄色部分)
  • 请求体(以上图中绿色部分) :存储请求参数
    • 请求体和请求头之间是有一个空行隔开(作用:用于标记请求头结束)

GET请求和POST请求的区别:

区别方式GET请求POST请求
请求参数请求参数在请求行中。
例:/brand/findAll?name=OPPO&status=1
请求参数在请求体中
请求参数长度请求参数长度有限制(浏览器不同限制也不同)请求参数长度没有限制
安全性安全性低。原因:请求参数暴露在浏览器地址栏中。安全性相对高

2.2 HTTP-响应协议

与HTTP的请求一样,HTTP响应的数据也分为3部分:响应行响应头响应体

image-20250409192119891

  • 响应行(以上图中红色部分):响应数据的第一行。响应行由协议及版本响应状态码状态码描述组成

    • 协议/版本:HTTP/1.1
    • 响应状态码:200
    • 状态码描述:OK
  • 响应头(以上图中黄色部分):响应数据的第二行开始。格式为key:value形式

    • http是个无状态的协议,所以可以在请求头和响应头中设置一些信息和想要执行的动作,这样,对方在收到信息后,就可以知道你是谁,你想干什么

    常见的HTTP响应头有:

    Content-Type:表示该响应内容的类型,例如text/html,image/jpeg ;
    
    Content-Length:表示该响应内容的长度(字节数);
    
    Content-Encoding:表示该响应压缩算法,例如gzip ;
    
    Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 ;
    
    Set-Cookie: 告诉浏览器为当前页面所在的域设置cookie ;
    
  • 响应体(以上图中绿色部分): 响应数据的最后一部分。存储响应的数据
    • 响应体和响应头之间有一个空行隔开(作用:用于标记响应头结束)
响应状态码
状态码分类说明
1xx响应中 — 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略
2xx成功 — 表示请求已经被成功接收,处理已完成
3xx重定向 — 重定向到其它地方,让客户端再发起一个请求以完成整个处理
4xx客户端错误 — 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等
5xx服务器端错误 — 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等
状态码英文描述解释
200OK客户端请求成功,即处理成功,这是我们最想看到的状态码
302Found指示所请求的资源已移动到由Location响应头给定的 URL,浏览器会自动重新访问到这个页面
304Not Modified告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向
400Bad Request客户端请求有语法错误,不能被服务器所理解
403Forbidden服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源
404Not Found请求资源不存在,一般是URL输入有误,或者网站资源被删除了
405Method Not Allowed请求方式有误,比如应该用GET请求方式的资源,用了POST
428Precondition Required服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头
429Too Many Requests指示用户在给定时间内发送了太多请求(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用
431 Request Header Fields Too Large请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。
500Internal Server Error服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧
503Service Unavailable服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好

状态码大全:https://cloud.tencent.com/developer/chapter/13553

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

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

2.3 HTTP-协议解析

以下是一个自定义的服务器代码,主要使用到的是ServerSocketSocket

package com.itheima;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/*
 * 自定义web服务器
 */
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8080); // 监听指定端口
        System.out.println("server is running...");

        while (true){
            Socket sock = ss.accept();
            System.out.println("connected from " + sock.getRemoteSocketAddress());
            Thread t = new Handler(sock);
            t.start();
        }
    }
}

class Handler extends Thread {
    Socket sock;

    public Handler(Socket sock) {
        this.sock = sock;
    }

    public void run() {
        try (InputStream input = this.sock.getInputStream();
             OutputStream output = this.sock.getOutputStream()) {
                handle(input, output);
        } catch (Exception e) {
            try {
                this.sock.close();
            } catch (IOException ioe) {
            }
            System.out.println("client disconnected.");
        }
    }

    private void handle(InputStream input, OutputStream output) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        // 读取HTTP请求:
        boolean requestOk = false;
        String first = reader.readLine();
        if (first.startsWith("GET / HTTP/1.")) {
            requestOk = true;
        }
        for (;;) {
            String header = reader.readLine();
            if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
                break;
            }
            System.out.println(header);
        }
        System.out.println(requestOk ? "Response OK" : "Response Error");

        if (!requestOk) {// 发送错误响应:
            writer.write("HTTP/1.0 404 Not Found\r\n");
            writer.write("Content-Length: 0\r\n");
            writer.write("\r\n");
            writer.flush();
        } else {// 发送成功响应:
            //读取html文件,转换为字符串
            InputStream is = Server.class.getClassLoader().getResourceAsStream("html/a.html");
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            StringBuilder data = new StringBuilder();
            String line = null;
            while ((line = br.readLine()) != null){
                data.append(line);
            }
            br.close();
            int length = data.toString().getBytes(StandardCharsets.UTF_8).length;

            writer.write("HTTP/1.1 200 OK\r\n");
            writer.write("Connection: keep-alive\r\n");
            writer.write("Content-Type: text/html\r\n");
            writer.write("Content-Length: " + length + "\r\n");
            writer.write("\r\n"); // 空行标识Header和Body的分隔
            writer.write(data.toString());
            writer.flush();
        }
    }
}

启动ServerSocket程序:

image-20250410003638131

浏览器输入:http://localhost:8080 就会访问到ServerSocket程序

  • ServerSocket程序,会读取服务器上html/a.html文件,并把文件数据发送给浏览器
  • 浏览器接收到a.html文件中的数据后进行解析,显示一个表格

在开发中真正用到的Web服务器,我们不会自己写的,都是使用目前比较流行的web服务器。如:Tomcat

3.WEB服务器-Tomcat

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

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

Web服务器软件使用步骤

  • 准备静态资源:直接找到资料中的 部署项目 文件夹即可
  • 下载安装Web服务器软件:解压即安装
  • 将静态资源部署到Web服务器上:将 部署项目 下的demo直接拷贝到Tomcat安装目录下的webapps即可
  • 启动Web服务器使用浏览器访问对应的资源:双击启动bin目录下的startup.bat即可

浏览器输入:http://localhost:8080/demo/index.html看到表格就表示成功了

3.1 Tomcat基本使用

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

image-20250409232331142

Tomcat软件类型说明:

  • tar.gz文件,是linux和mac操作系统下的压缩版本
  • zip文件,是window操作系统下压缩版本

直接解压到不含中文和空格的目录下即安装,卸载直接删除这个文件夹即可。

3.1.1 目录结构

image-20250409232646892

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

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

3.1.2 启动与关闭

启动Tomcat

双击tomcat解压目录/bin/startup.bat文件即可启动tomcat。

Tomcat的默认端口为8080,所以在浏览器的地址栏输入:http://127.0.0.1:8080 即可访问tomcat服务器

注意:Tomcat启动的过程中,遇到控制台有中文乱码时,可以通常修改conf/logging.pro perties文件解决

image-20250409233148757

关闭

方式一:强制关闭 -> 直接x掉Tomcat窗口(不建议)

方式二:正常关闭 -> bin\shutdown.bat

方式三:正常关闭 -> 在Tomcat启动窗口中按下 Ctrl+C

3.1.3 常见问题

问题1:Tomcat启动时,窗口一闪而过

检查JAVA_HOME环境变量是否正确配置:…\JDKXxx

问题2:端口号冲突

修改Tomcat启动的端口号,需要修改 conf/server.xml文件

image-20250409234102765

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

3.2 入门程序解析

3.2.1 Spring官方骨架

Spring官方骨架,可以理解为Spring官方为程序员提供一个搭建项目的模板。之前创建项目就是使用的官方骨架:

image-20250409234921222

可以通过访问:https://start.spring.io/ ,进入到官方骨架页面。

image-20250409235010062

  • SpringBoot项目需要依赖Spring Web

image-20250409235129254

  • SpringBoot项目创建成功后,会下载到本地,解压缩后就可以得到一个Spring Boot项目文件夹

  • 不论使用IDEA创建SpringBoot项目,还是直接在官方网站利用骨架生成SpringBoot项目,项目的结构和pom.xml文件中内容是相似的。

3.2.2 起步依赖

spring-boot-starter-web和spring-boot-starter-test,在SpringBoot中又称为起步依赖,每一个起步依赖,都用于开发一个特定的功能。

起步依赖共同的特征就是以spring-boot-starter-作为开头。

  • spring-boot-starter-web:包含了web应用开发所需要的常见依赖。内部把关于Web开发所有的依赖都已经导入并且指定了版本,只需引入 spring-boot-starter-web 依赖就可以实现Web开发的需要的功能
  • spring-boot-starter-test:包含了单元测试所需要的常见依赖

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

3.2.3 SpringBoot父工程

每一个SpringBoot工程,都有一个父工程。依赖的版本号,在父工程中统一管理,所以不用指定依赖的版本号:

image-20250410002004483

3.2.4 内嵌Tomcat

spring-boot-starter-web起步依赖内部已经集成了内置的Tomcat服务器,所以不用部署springboot项目也能运行。

当我们运行SpringBoot的引导类时(运行main方法),就会看到命令行输出的日志,其中占用8080端口的就是Tomcat。

十一、SpringBootWeb请求响应

1.前言

浏览器发送请求请求web服务器 (也就是内置的Tomcat),被部署在Tomcat中的控制器类Controller接收,Controller再给浏览器一个响应,整个过程遵守http协议。但是Tomcat不识别自定义的Controller,可以识别 Servlet程序。所以Tomcat内置了一个核心的Servlet程序 DispatcherServlet(核心控制器),负责接收页面发送的请求,然后根据执行规则将请求再转发给请求处理器Controller,请求处理器处理完请求后再由DispatcherServlet给浏览器响应数据

image-20250410005342457

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

Tomcat接收到浏览器发送的数据后,会先解析这些请求数据,然后将解析后的请求数据传递给Servlet程序的HttpServletRequest对象,Tomcat还会给Servlet程序传递一个参数 HttpServletResponse用以给浏览器设置响应数据。

image-20250410005236807

2.请求

2.1 Postman

Postman工具是后端开发员用来测试自己所开发的程序的,可以在没有前端页面的情况下测试后端程序的正确性,即模拟浏览器向后端服务器发起任何形式(如:get、post)的HTTP请求。

安装:双击资料中提供的Postman-win64-8.3.1-Setup.exe即可自动安装。

基本使用

登录完成之后,可以创建工作空间:

image-20250410172905634

image-20250410172942955

创建请求:

image-20250410173017680

点击"Save",保存当前请求

image-20250410173119091

image-20250410173154245

image-20250410173215657

image-20250410173243045

image-20250410173331071

2.2 简单参数

image-20250410173844003

后端程序接收浏览器传递过来的普通参数数据有两种方式:

2.2.1 原始方式(不建议)

通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息,即在方法的形参中声明 HttpServletRequest 对象,通过该对象来获取请求信息。

//根据指定的参数名获取请求参数的数据值
String  request.getParameter("参数名")
@RestController
public class RequestController {
    //原始方式
    @RequestMapping("/simpleParam")
    public String simpleParam(HttpServletRequest request){
        // http://localhost:8080/simpleParam?name=Tom&age=10
        // 请求参数: name=Tom&age=10   (有2个请求参数)

        String name = request.getParameter("name");//name就是请求参数名
        String ageStr = request.getParameter("age");//age就是请求参数名

        int age = Integer.parseInt(ageStr);//需要手动进行类型转换
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}
2.2.2 SpringBoot方式

参数名与形参变量名相同,定义同名的形参即可接收参数。

@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=10
    // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
    // 第2个请求参数: age=10     参数名:age , 参数值:10
    
    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}
  • 不论是GET请求还是POST请求,对于简单参数来讲,只要保证请求参数名和Controller方法中的形参名保持一致,就可以获取到请求参数中的数据值。
2.2.3 参数名不一致

对于简单参数来讲,请求参数名和controller方法中的形参名不一致时,无法接收到请求数据。

	@RequestMapping("/simpleParam")
    public String simpleParam(String username , Integer age ){//请求参数名和形参名不相同
        // http://localhost:8080/simpleParam?name=Tom&age=20
        
        System.out.println(username+"  :  "+age); //username=null,age=20
        return "OK";
    }

解决方案:可以使用Spring提供的@RequestParam注解完成映射:在方法形参前面加上 @RequestParam 然后通过name属性指定请求参数名,从而完成映射。

	//springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(@RequestParam("name") String username , Integer age ){
        // http://localhost:8080/simpleParam?name=Tom&age=20
        
        System.out.println(username+"  :  "+age);  //username=Tom,age=20
        return "OK";
    }

注意事项:@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错,例如username和age缺少任意一个都会响应状态码400,可以将required属性设置为false代表这个参数可选:

@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){
	System.out.println(username+ ":" + age);
	return "OK";
}

2.3 实体参数

接受请求参数可以封装到一个实体类对象中,这样形参只要一个对象就可以接受所有请求参数,要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同

2.3.1 简单实体对象

定义pojo实体类:

public class User {
    private String name;
    private Integer age;
    ...
    @Override
    public String toString() {
        return ...;
    }
}

Controller方法:

@RestController
public class RequestController {
    //实体参数:简单实体对象
    @RequestMapping("/simplePojo")
    public String simplePojo(User user){
        System.out.println(user);
        return "OK";
    }
}
2.3.2 复杂实体对象

复杂实体对象即在实体类中有一个或多个属性,也是实体对象类型的。

复杂实体对象的封装,需要遵守如下规则:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。

http://localhost:8080/complexPojo?name=Tom&age=10&address.province=beijing&address.city=beijing为例。

定义POJO实体类:

  • Address实体类
public class Address {
    private String province;
    private String city;
	...
    @Override
    public String toString() {
        return ...;
    }
}
  • User实体类
public class User {
    private String name;
    private Integer age;
    private Address address; //地址对象
    ...
    @Override
    public String toString() {
        return ...;
    }
}
  • Controller方法
@RestController
public class RequestController {
    //实体参数:复杂实体对象
    @RequestMapping("/complexPojo")
    public String complexPojo(User user){
        System.out.println(user);
        return "OK";
    }
}

2.4 数组集合参数

在HTML的表单中,复选框可以提交选择的多个值,接受复选框的参数有两种方式(以http://localhost:8080/arrayParam?hobby=game&hobby=javahttp://localhost:8080/arrayParam?hobby=game,java为例):

2.4.1 数组

数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数

Controller方法:

@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/arrayParam")
    public String arrayParam(String[] hobby){
        System.out.println(Arrays.toString(hobby));
        return "OK";
    }
}
2.4.2 集合

集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系

默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系

Controller方法:

@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/listParam")
    public String listParam(@RequestParam List<String> hobby){
        System.out.println(hobby);
        return "OK";
    }
}

2.5 日期参数

对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。

后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。

http://localhost:8080/dataParam?updateTime=2022-12-12 10:05:45为例:

Controller方法:

@RestController
public class RequestController {
    //日期时间参数
   @RequestMapping("/dateParam")
    public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
        System.out.println(updateTime);
        return "OK";
    }
}

2.6 JSON参数

Postman发送JSON格式数据:

image-20250410205906677

服务端Controller方法接收JSON格式数据:

  • 传递json格式的参数,在Controller中会使用实体类进行封装。

  • 封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。

  • @RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)

实体类:Address

public class Address {
    private String province;
    private String city;
    ...
}

实体类:User

public class User {
    private String name;
    private Integer age;
    private Address address;
    ...
} 

Controller方法:

@RestController
public class RequestController {
    //JSON参数
    @RequestMapping("/jsonParam")
    public String jsonParam(@RequestBody User user){
        System.out.println(user);
        return "OK";
    }
}

2.7 路径参数

路径参数:

  • 前端:通过请求URL直接传递参数
  • 后端:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数

image-20250410213357066

传递单个路径参数:

Controller方法:

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}")
    public String pathParam(@PathVariable Integer id){
        System.out.println(id);
        return "OK";
    }
}

Postman测试:

访问http://localhost:8080/path/1,控制台输出1,浏览器显示OK。

传递多个路径参数:

Controller方法:

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}/{name}")
    public String pathParam2(@PathVariable Integer id, @PathVariable String name){
        System.out.println(id+ " : " +name);
        return "OK";
    }
}

Postman测试:

访问http://localhost:8080/path/1/itcast,控制台输出1 : itcast,浏览器显示OK。

3.响应

3.1 @ResponseBody

@ResponseBody注解:

  • 类型:方法注解、类注解
  • 位置:书写在Controller方法上或类上
  • 作用:将方法返回值直接响应给浏览器
    • 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

:@RestController = @Controller + @ResponseBody

@RestController源码:

@Target({ElementType.TYPE})   //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME)  //元注解
@Documented    //元注解
@Controller   
@ResponseBody 
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

3.2 统一响应结果

统一的返回结果使用类来描述,在这个结果中包含:

  • 响应状态码:当前请求是成功,还是失败

  • 状态码信息:给页面的提示信息

  • 返回的数据:给前端响应的数据(字符串、对象、集合)

定义在一个实体类Result来包含以上信息:

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);
    }
}

4.案例

4.1 需求说明

加载并解析xml文件中的数据,完成数据处理,并在页面展示

image-20250410220714494

4.2 准备工作

  1. XML文件

    • 已经准备好(emp.xml),直接导入进来,放在 src/main/resources目录下
  2. 工具类

    • 已经准备好解析XML文件的工具类,无需自己实现
    • 直接在创建一个包 com.itheima.utils ,然后将工具类拷贝进来
  3. 前端页面资源

    • 已经准备好,直接拷贝进来,放在src/main/resources下的static目录下

Springboot项目的静态资源(html,css,js等前端资源)默认存放目录为:classpath:/static 、 classpath:/public、 classpath:/resources

在SpringBoot项目中,静态资源默认可以存放的目录:

  • classpath:/static/
  • classpath:/public/
  • classpath:/resources/
  • classpath:/META-INF/resources/

classpath:

  • 代表的是类路径,在maven的项目中,其实指的就是 src/main/resources 或者 src/main/java,但是java目录是存放java代码的,所以相关的配置文件及静态资源文档,就放在 src/main/resources下。

4.3 实现步骤

  1. 在pom.xml文件中引入dom4j的依赖,用于解析XML文件

    <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>2.1.3</version>
    </dependency>
    
  2. 引入资料中提供的:解析XML的工具类XMLParserUtils、实体类Emp、XML文件emp.xml

image-20250410221809379

  1. 引入资料中提供的静态页面文件,放在resources下的static目录下

image-20250410223225702

  1. 创建EmpController类,编写Controller程序,处理请求,响应数据

image-20250410223306048

4.4 代码实现

Contriller代码:

@RestController
public class EmpController {
    @RequestMapping("/listEmp")
    public Result list(){
        //1. 加载并解析emp.xml
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        //System.out.println(file);
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);

        //2. 对数据进行转换处理 - gender, job
        empList.stream().forEach(emp -> {
            //处理 gender 1: 男, 2: 女
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            }else if("2".equals(gender)){
                emp.setGender("女");
            }

            //处理job - 1: 讲师, 2: 班主任 , 3: 就业指导
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("讲师");
            }else if("2".equals(job)){
                emp.setJob("班主任");
            }else if("3".equals(job)){
                emp.setJob("就业指导");
            }
        });
        //3. 响应数据
        return Result.success(empList);
    }
}

4.5 测试

打开浏览器,在浏览器地址栏输入: http://localhost:8080/emp.html

image-20250410231314535

5.分层解耦

5.1 三层架构

5.1.1 介绍

在进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。

image-20250410231631151

案例中的Contriller代码,从组成上看可以分为三个部分:

  • 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
  • 逻辑处理:负责业务逻辑处理的代码。
  • 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。

三层架构就是把这三个部分分离出来,使各层相互独立,互不影响

  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

三层架构的程序执行流程:

image-20250410232322576

  • 前端发起的请求,由Controller层接收(Controller响应数据给前端)
  • Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
  • Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
  • Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
5.1.2 代码拆分
  • 控制层包名:xxxx.controller
  • 业务逻辑层包名:xxxx.service
  • 数据访问层包名:xxxx.dao

image-20250410232651708

**控制层:**接收前端发送的请求,对请求进行处理,并响应数据

@RestController
public class EmpController {
    //业务层对象
    private EmpService empService = new EmpServiceA();

    @RequestMapping("/listEmp")
    public Result list(){
        //1. 调用service层, 获取数据
        List<Emp> empList = empService.listEmp();

        //3. 响应数据
        return Result.success(empList);
    }
}

**业务逻辑层:**处理具体的业务逻辑

  • 业务接口
//业务逻辑接口(制定业务标准)
public interface EmpService {
    //获取员工列表
    public List<Emp> listEmp();
}
  • 业务实现类
//业务逻辑实现类(按照业务标准实现)
public class EmpServiceA implements EmpService {
    //dao层对象
    private EmpDao empDao = new EmpDaoA();

    @Override
    public List<Emp> listEmp() {
        //1. 调用dao, 获取数据
        List<Emp> empList = empDao.listEmp();

        //2. 对数据进行转换处理 - gender, job
        empList.stream().forEach(emp -> {
            ...  //和之前一样,赋值粘贴即可
        });
        return empList;
    }
}

**数据访问层:**负责数据的访问操作,包含数据的增、删、改、查

  • 数据访问接口
//数据访问层接口(制定标准)
public interface EmpDao {
    //获取员工列表数据
    public List<Emp> listEmp();
}
  • 数据访问实现类
//数据访问实现类
public class EmpDaoA implements EmpDao {
    @Override
    public List<Emp> listEmp() {
        //1. 加载并解析emp.xml
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        System.out.println(file);
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

image-20250410233550041

5.2 分层解耦

5.2.1 耦合问题
  • 内聚:软件中各个功能模块内部的功能联系。

  • 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。

软件设计原则:高内聚低耦合。

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

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

高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。

5.2.2 解耦思路

之前对象都是用new创建的,但是这样就使两层耦合了,例如:当service层的实现变了就需要修改controller层的代码。解决思路如下:

首先不能在EmpController中使用new对象,然后提供一个容器,容器中存储一些对象(例:EmpService对象),controller程序从容器中获取EmpService类型的对象。

  • **控制反转:**简称IOC。对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器
  • 依赖注入: 简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

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

5.3 IOC&DI

5.3.1 IOC&DI入门

任务:完成Controller层、Service层、Dao层的代码解耦

第1步:删除Controller层、Service层中new对象的代码

第2步:Service层及Dao层的实现类,交给IOC容器管理

  • 使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理

第3步:为Controller及Service注入运行时依赖的对象

  • 使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象

image-20250411162100772

5.3.2 IOC详解
5.3.2.1 bean的声明

Spring框架提供了@Component的衍生注解用来标识bean对象具体归属于哪一层:

  • @Controller (标注在控制层类上)
  • @Service (标注在业务层类上)
  • @Repository (标注在数据访问层类上)
注解说明位置
@Controller@Component的衍生注解标注在控制器类上
@Service@Component的衍生注解标注在业务类上
@Repository@Component的衍生注解标注在数据访问类上(由于与mybatis整合,用的少)
@Component声明bean的基础注解不属于以上三类时,用此注解

@RestController = @Controller + @ResponseBody

在IOC容器中,每一个Bean类都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。

@Repository(value = "empRepositoryA")  //如果没有指定,默认empDaoA
public class EmpDaoA implements EmpDao{...}

注意:在springboot集成web开发中,声明控制器bean只能用@Controller。

5.3.2.2 组件扫描

bean想要生效,需要被组件扫描。扫描注解@ComponentScan用来扫描组件,@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,默认扫描的范围是SpringBoot启动类所在包及其子包

要想扫描到SpringBoot启动类所在包及其子包之外的组件,有两种解决方案:

  1. 为SpringBoot启动类手动添加@ComponentScan注解,指定要扫描的包,例如@ComponentScan({"com.itheima","dao"}).
  2. 将所有需要扫描的包都放在引导类所在包com.itheima的子包下(推荐做法)
5.3.3 DI详解

@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)

如果在IOC容器中存在多个相同类型的bean对象,会出现报错,解决方案如下:

方式一:使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

image-20250411170600054

方式二:使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。

image-20250411170707373

方式三:使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

image-20250411170807370

面试题 : @Autowird 与 @Resource的区别

  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注入,而@Resource是按照名称注入

十二、数据库开发-MySQL

数据库:英文为 DataBase,简称DB,它是存储和管理数据的仓库。

数据库管理系统:简称DBMS,是操作和管理数据库的大型软件,通过这个软件可以操纵和管理数据库。

SQL:简称SQL,结构化查询语言,是操作关系型数据库的编程语言,定义了一套操作关系型数据库的统一标准。

三层架构中的数据连接层,就是用来从数据库中获取数据的。

1.MySQL概述

分为商业版本(收费,可以免费试用30天,提供技术支持)和社区版本(免费,但是不提供技术支持),本节使用社区版本(8.0.31)。

1.1 安装

参考资料中的mysql安装文档。

1.2 连接

命令行使用mysql -u用户名 -p[密码] [-h数据库服务器的IP地址 -P端口号]命令就可以连接到MySQL服务器。

  • -h 参数不加,默认连接的是本地 127.0.0.1 的MySQL服务器
  • -P 参数不加,默认连接的端口号是 3306

1.3 数据模型

关系型数据库:简称RDBMS,建立在关系模型基础上,由多张相互连接的二维表组成的数据库,如MySQL、Oracle、SQLServer等。

非关系型数据库:不是由二维表组成的数据库,如Redis。

MySQL是关系型数据库,是基于二维表进行数据存储的,所有数据都存放在二维表中:

  • 通过MySQL客户端连接数据库管理系统DBMS,然后通过DBMS操作数据库
  • 使用MySQL客户端,向数据库管理系统发送一条SQL语句,由数据库管理系统根据SQL语句指令去操作数据库中的表结构及数据
  • 一个数据库服务器中可以创建多个数据库,一个数据库中也可以包含多张表,而一张表中又可以包含多行记录。

1.4 SQL简介

1.4.1 SQL通用语法

1、SQL语句可以单行或多行书写,以分号结尾。

2、SQL语句可以使用空格/缩进来增强语句的可读性。

3、不区分大小写。

4、注释:

  • 单行注释:-- 注释内容 或 # 注释内容(MySQL特有)
  • 多行注释: /* 注释内容 */
1.4.2 分类

SQL语句根据其功能被分为四大类:DDL、DML、DQL、DCL

分类全称说明
DDLData Definition Language数据定义语言,用来定义数据库对象(数据库,表,字段)
DMLData Manipulation Language数据操作语言,用来对数据库表中的数据进行增删改
DQLData Query Language数据查询语言,用来查询数据库中表的记录
DCLData Control Language数据控制语言,用来创建数据库用户、控制数据库的访问权限

2.数据库设计-DDL

2.1 项目开发流程

image-20250411175226399

  1. 数据库设计阶段
    • 参照产品经理提供的页面原型和需求文档设计数据库表结构
  2. 数据库操作阶段
    • 根据业务功能的实现,编写SQL语句对数据表中的数据进行增删改查操作
  3. 数据库优化阶段
    • 通过数据库的优化来提高数据库的访问性能。优化手段:索引、SQL优化、分库分表等

2.2 数据库操作

DDL中数据库的常见操作:查询、创建、使用、删除。

2.2.1 查询数据库

查询所有数据库:

show databases;

查询当前数据库:

select database();
2.2.2 创建数据库

语法:

create database [ if not exists ] 数据库名;

在同一个数据库服务器中,不能创建两个名称相同的数据库,否则将会报错,可以使用if not exists来避免这个问题。

2.2.3 使用数据库

语法:

use 数据库名 ;
2.2.4 删除数据库

语法:

drop database [ if exists ] 数据库名 ;

如果删除一个不存在的数据库,将会报错,可以使用if exists来避免这个问题。

:上述所有语法中的database,也可以替换成 schema

2.3 图形化工具

DataGrip是JetBrains旗下的一款数据库管理工具,是管理和开发MySQL、Oracle、PostgreSQL的理想解决方案。

2.3.1 安装

参考资料中的DataGrip安装手册。

2.3.2 使用

1、打开IDEA自带的Database

image-20250411181829963

2、配置MySQL

image-20250411182004194

3、输入相关信息并下载MySQL连接驱动

image-20250411182057324

4、测试数据库连接

点击Text Connection即可。

5、点击OK创建连接成功

2.4 表操作

关于表结构的操作也是包含四个部分:创建表、查询表、修改表、删除表。

2.4.1 创建
2.4.1.1 语法
create table  表名(
	字段1  字段1类型 [约束]  [comment  字段1注释 ],
	字段2  字段2类型 [约束]  [comment  字段2注释 ],
	......
	字段n  字段n类型 [约束]  [comment  字段n注释 ] 
) [ comment  表注释 ] ;
2.4.1.2 约束

约束就是作用在表中字段上的规则,用于限制存储在表中的数据,从而保证数据库当中数据的正确性、有效性和完整性。

约束描述关键字
非空约束限制该字段值不能为nullnot null
唯一约束保证字段的所有数据都是唯一、不重复的unique
主键约束主键是一行数据的唯一标识,要求非空且唯一primary key
默认约束保存数据时,如果未指定该字段值,则采用默认值default
外键约束让两张表的数据建立连接,保证数据的一致性和完整性foreign key

注意:约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束。

create table tb_user (
    id int primary key auto_increment comment 'ID,唯一标识', #主键自动增长
    username varchar(20) not null unique comment '用户名',
    name varchar(10) not null comment '姓名',
    age int comment '年龄',
    gender char(1) default '男' comment '性别'
) comment '用户表';

主键自增:auto_increment

  • 每次插入新的行记录时,数据库自动生成id字段(主键)下的值
  • 具有auto_increment的数据列是一个正数序列开始增长(从1开始自增)
2.4.1.3 数据类型

MySQL中的数据类型有很多,主要分为三类:数值类型、字符串类型、日期时间类型。

数值类型

类型大小有符号(SIGNED)范围无符号(UNSIGNED)范围描述
TINYINT1byte(-128,127)(0,255)小整数值
SMALLINT2bytes(-32768,32767)(0,65535)大整数值
MEDIUMINT3bytes(-8388608,8388607)(0,16777215)大整数值
INT/INTEGER4bytes(-2147483648,2147483647)(0,4294967295)大整数值
BIGINT8bytes(-263,263-1)(0,2^64-1)极大整数值
FLOAT4bytes(-3.402823466 E+38,3.402823466351 E+38)0 和 (1.175494351 E-38,3.402823466 E+38)单精度浮点数值
DOUBLE8bytes(-1.7976931348623157 E+308,1.7976931348623157 E+308)0 和 (2.2250738585072014 E-308,1.7976931348623157 E+308)双精度浮点数值
DECIMAL依赖于M(精度)和D(标度)的值依赖于M(精度)和D(标度)的值小数值(精确定点数)

字符串类型

类型大小描述
CHAR0-255 bytes定长字符串(需要指定长度)
VARCHAR0-65535 bytes变长字符串(需要指定长度)
TINYBLOB0-255 bytes不超过255个字符的二进制数据
TINYTEXT0-255 bytes短文本字符串
BLOB0-65 535 bytes二进制形式的长文本数据
TEXT0-65 535 bytes长文本数据
MEDIUMBLOB0-16 777 215 bytes二进制形式的中等长度文本数据
MEDIUMTEXT0-16 777 215 bytes中等长度文本数据
LONGBLOB0-4 294 967 295 bytes二进制形式的极大文本数据
LONGTEXT0-4 294 967 295 bytes极大文本数据

char是定长字符串,指定长度多长,就占用多少个字符。而varchar是变长字符串,指定的长度为最大占用长度 。char的性能更高。

日期时间类型

类型大小范围格式描述
DATE31000-01-01 至 9999-12-31YYYY-MM-DD日期值
TIME3-838:59:59 至 838:59:59HH:MM:SS时间值或持续时间
YEAR11901 至 2155YYYY年份值
DATETIME81000-01-01 00:00:00 至 9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS混合日期和时间值
TIMESTAMP41970-01-01 00:00:01 至 2038-01-19 03:14:07YYYY-MM-DD HH:MM:SS混合日期和时间值,时间戳
2.4.2 查询

查询当前数据库所有表:

show tables;

查看指定表结构:

desc 表名 ;  #可以查看指定表的字段、字段的类型、是否可以为NULL、是否存在默认值等信息

查询指定表的建表语句:

show create table 表名 ;
2.4.3 修改

添加字段:

alter table 表名 add  字段名  类型(长度)  [comment 注释]  [约束];

修改数据类型:

alter table 表名 modify  字段名  新数据类型(长度);

alter table 表名 change  旧字段名  新字段名  类型(长度)  [comment 注释]  [约束];

删除字段:

alter table 表名 drop 字段名;

修改表名:

rename table 表名 to  新表名;
2.4.4 删除

删除表语法:

drop  table [ if exists ]  表名;

3.数据库操作-DML

3.1 增加(insert)

向指定字段添加数据:

insert into 表名 (字段名1, 字段名2) values (值1, 值2);

全部字段添加数据:

insert into 表名 values (值1, 值2, ...);

批量添加数据(指定字段):

insert into 表名 (字段名1, 字段名2) values (值1, 值2), (值1, 值2);

批量添加数据(全部字段):

insert into 表名 values (值1, 值2, ...), (值1, 值2, ...);

Insert操作的注意事项:

  1. 插入数据时,指定的字段顺序需要与值的顺序是一一对应的。

  2. 字符串和日期型数据应该包含在引号中。

  3. 插入的数据大小,应该在字段的规定范围内。

3.2 修改(update)

update语法:

update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , .... [where 条件] ;

注意事项:

  1. 修改语句的条件可以有,也可以没有,如果没有条件,则会修改整张表的所有数据。

  2. 在修改数据时,一般需要同时修改公共字段update_time,将其修改为当前操作时间。

3.3 删除(delete)

delete语法:

delete from 表名  [where  条件] ;

注意事项:

​ • DELETE 语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据。

​ • DELETE 语句不能删除某一个字段的值(可以使用UPDATE,将该字段值置为NULL即可)。

​ • 当进行删除全部数据操作时,会提示询问是否确认删除所有数据,直接点击Execute即可。

4.数据库操作-DQL

查询操作是所有SQL语句当中最为常见、最为重要的操作。在一个正常的业务系统中,查询操作的使用频次远高于增删改操作。

4.1 语法

SELECT
	字段列表
FROM
	表名列表
WHERE
	条件列表
GROUP  BY
	分组字段列表
HAVING
	分组后条件列表
ORDER BY
	排序字段列表
LIMIT
	分页参数

4.2 基本查询

在基本查询的DQL语句中,不带任何的查询条件。

查询多个字段:

select 字段1, 字段2, 字段3 from  表名;

查询所有字段(通配符):

select *  from  表名;

设置别名:

select 字段1 [ as 别名1 ] , 字段2 [ as 别名2 ]  from  表名;

去除重复记录:

select distinct 字段列表 from  表名;

4.3 条件查询

select  字段列表  from   表名   where   条件列表 ; -- 条件列表:意味着可以有多个条件

在SQL语句当中构造条件的运算符分为两类:

  • 比较运算符
  • 逻辑运算符

比较运算符:

比较运算符功能
>大于
>=大于等于
<小于
<=小于等于
=等于
<> 或 !=不等于
between … and …在某个范围之内(含最小、最大值)
in(…)在in之后的列表中的值,多选一
like 占位符模糊匹配(_匹配单个字符, %匹配任意个字符)
is null是null

逻辑运算符:

逻辑运算符功能
and 或 &&并且 (多个条件同时成立)
or 或 ||或者 (多个条件任意一个成立)
not 或 !非 , 不是

4.4 聚合函数

语法:

select  聚合函数(字段列表)  from  表名 ;

聚合函数会忽略空值,对NULL值不作为统计。

常用聚合函数:

函数功能
count统计数量
max最大值
min最小值
avg平均值
sum求和

count :按照列去统计有多少行数据。

  • 在根据指定的列统计的时候,如果这一列中有null的行,该行不会被统计在其中。

sum :计算指定列的数值和,如果不是数值类型,那么计算结果为0

max :计算指定列的最大值

min :计算指定列的最小值

avg :计算指定列的平均值

4.5 分组查询

分组: 按照某一列或者某几列,把相同的数据进行合并输出。

分组其实就是按列进行分类(指定列下相同的数据归为一类),然后可以对分类完的数据进行合并计算。

分组查询通常会使用聚合函数进行计算。

语法:

select  字段列表  from  表名  [where 条件]  group by 分组字段名  [having 分组后过滤条件];

注意事项:

​ • 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义

​ • 执行顺序:where > 聚合函数 > having

where与having区别(面试题)

  • 执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;而having是分组之后对结果进行过滤。
  • 判断条件不同:where不能对聚合函数进行判断,而having可以。

4.6 排序查询

语法:

select  字段列表  
from   表名   
[where  条件列表] 
[group by  分组字段 ] 
order  by  字段1  排序方式1 , 字段2  排序方式2 … ;
  • 排序方式:

    • ASC :升序(默认值)

    • DESC:降序

如果是升序, 可以不指定排序方式ASC

如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序

4.7 分页查询

分页查询语法:

select  字段列表  from   表名  limit  起始索引, 查询记录数 ;

起始索引从0开始。 计算公式 : 起始索引 = (查询页码 - 1)* 每页显示记录数

分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT

如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 条数

5.多表设计

实际项目开发中,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:

  • 一对多(多对一)

  • 多对多

  • 一对一

5.1 一对多

实现:在数据库表中多的一方,添加字段,来关联属于一这方的主键。

外键约束:让两张表的数据建立连接,保证数据的一致性和完整性。

对应的关键字:foreign key

外键约束的语法:

-- 创建表时指定
create table 表名(
	字段名    数据类型,
	...
	[constraint]   [外键名称]  foreign  key (外键字段名)   references   主表 (主表列名)	
);


-- 建完表后,添加外键
alter table  表名  add constraint  外键名称  foreign key(外键字段名) references 主表(主表列名);

当我们添加外键约束时,需要保证当前数据库表中的数据是完整的。

  • 物理外键

    • 概念:使用foreign key定义外键关联另外一张表。
    • 缺点:
      • 影响增、删、改的效率(需要检查外键关系)。
      • 仅用于单节点数据库,不适用与分布式、集群场景。
      • 容易引发数据库的死锁问题,消耗性能。
  • 逻辑外键

    • 概念:在业务层逻辑中,解决外键关联。
    • 实现:通过应用程序逻辑代码层面的设计来维护表之间的关联关系,从而模拟外键的关联性,不会依赖数据库的物理约束。
    • 通过逻辑外键,就可以很方便的解决上述问题。

**在现在的企业开发中,很少会使用物理外键,都是使用逻辑外键。 甚至在一些数据库开发规范中,会明确指出禁止使用物理外键 foreign key **

5.2 一对一

一对一关系通常是用来做单表的拆分,也就是将一张大表拆分成两张小表,将大表中的一些基础字段放在一张表当中,将其他的字段放在另外一张表当中,以此来提高数据的操作效率。

实现:在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)

5.3 多对多

多对多的关系在开发中比较常见。比如:学生和老师的关系,一个学生可以有多个授课老师,一个授课老师也可以有多个学生。

实现:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键。

6.多表查询

6.1 概述

多表查询:查询时从多张表中获取所需数据

单表查询的SQL语句:select 字段列表 from 表名;

那么要执行多表查询,只需要使用逗号分隔多张表即可,如: select 字段列表 from 表1, 表2;

例如,查询用户表和部门表中的数据:

select * from  tb_emp , tb_dept;

笛卡尔积:笛卡尔乘积是指在数学中,两个集合(A集合和B集合)的所有组合情况。

image-20250411222807778

在多表查询时,需要消除无效的笛卡尔积,只保留表关联部分的数据,只需要给多表查询加上连接查询的条件即可:

select * from tb_emp , tb_dept where tb_emp.dept_id = tb_dept.id ;

分类

多表查询可以分为:

  1. 连接查询

    • 内连接:相当于查询A、B交集部分数据

    image-20250411223023104

  2. 外连接

    • 左外连接:查询左表所有数据(包括两张表交集部分数据)

    • 右外连接:查询右表所有数据(包括两张表交集部分数据)

  3. 子查询

6.2 内连接

内连接查询:查询两表或多表中交集部分数据。

内连接从语法上可以分为:

  • 隐式内连接

  • 显式内连接

隐式内连接语法:

select  字段列表   from   表1 , 表2   where  条件 ... ;

显式内连接语法:

select  字段列表   from   表1  [ inner ]  join 表2  on  连接条件 ... ;

一旦为表起了别名,就不能再使用表名来指定对应的字段了,此时只能够使用别名来指定字段。

6.3 外连接

外连接分为两种:左外连接 和 右外连接。

左外连接语法结构:

select  字段列表   from   表1  left  [ outer ]  join 表2  on  连接条件 ... ;

左外连接相当于查询表1(左表)的所有数据,当然也包含表1和表2交集部分的数据。

右外连接语法结构:

select  字段列表   from   表1  right  [ outer ]  join 表2  on  连接条件 ... ;

右外连接相当于查询表2(右表)的所有数据,当然也包含表1和表2交集部分的数据。

左外连接和右外连接可以相互替换,只需要调整连接查询SQL语句中表的先后顺序就形了。在日常开发使用时,更偏向于左外连接。

6.4 子查询

SQL语句中嵌套select语句,称为嵌套查询,又称子查询。

SELECT  *  FROM   t1   WHERE  column1 =  ( SELECT  column1  FROM  t2 ... );

子查询外部的语句可以是insert / update / delete / select 的任何一个,最常见的是 select。

根据子查询结果的不同分为:

  1. 标量子查询(子查询结果为单个值[一行一列])

  2. 列子查询(子查询结果为一列,但可以是多行)

  3. 行子查询(子查询结果为一行,但可以是多列)

  4. 表子查询(子查询结果为多行多列[相当于子查询结果是一张表])

子查询可以书写的位置:

  1. where之后
  2. from之后
  3. select之后
6.4.1 标量子查询

常用的操作符: = <> > >= < <=

6.4.2 列子查询

常用的操作符:

操作符描述
IN在指定的集合范围之内,多选一
NOT IN不在指定的集合范围之内
6.4.3 行子查询

常用的操作符:= 、<> 、IN 、NOT IN

6.4.4 表子查询

子查询返回的结果是多行多列,常作为临时表,这种子查询称为表子查询。

7.事务

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

事务作用:保证在一个事务中多次操作数据库表中数据时,要么全都成功,要么全都失败。

7.1 操作

MYSQL中有两种方式进行事务的操作:

  1. 自动提交事务:即执行一条sql语句提交一次事务。(默认MySQL的事务是自动提交)
  2. 手动提交事务:先开启,再提交

事务操作有关的SQL语句:

SQL语句描述
start transaction; / begin ;开启手动控制事务
commit;提交事务
rollback;回滚事务

手动提交事务使用步骤:

  • 第1种情况:开启事务 => 执行SQL语句 => 成功 => 提交事务
  • 第2种情况:开启事务 => 执行SQL语句 => 失败 => 回滚事务

7.2 四大特性

  • 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

事务的四大特性简称为:ACID

8.索引

索引(index):是帮助数据库高效获取数据的数据结构,使用索引可以提高查询的效率。

优点:

  1. 提高数据查询的效率,降低数据库的IO成本。
  2. 通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗。

缺点:

  1. 索引会占用存储空间。
  2. 索引大大提高了查询效率,同时却也降低了insert、update、delete的效率。

语法

创建索引

create  [ unique ]  index 索引名 on  表名 (字段名,... ) ;

查看索引

show  index  from  表名;

删除索引

drop  index  索引名  on  表名;

注意事项:

  • 主键字段,在建表时,会自动创建主键索引

  • 添加唯一约束时,数据库实际上会添加唯一索引

十三、Mybatis

MyBatis是一款优秀的 持久层 框架,用于简化JDBC的开发。

  • 持久层:指的是就是数据访问层(dao),是用来操作数据库的。
  • 框架:是一个半成品软件,是一套可重用的、通用的、软件基础代码模型。

1.快速入门

1.1 准备工作

创建springboot工程:创建springboot工程,并导入 mybatis的起步依赖、mysql的驱动包。

image-20250411231416968

image-20250411231518111

项目工程创建完成后,会自动在pom.xml文件中,导入Mybatis依赖和MySQL驱动依赖

		<!-- mybatis起步依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- mysql驱动包依赖 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

数据准备:创建用户表user,并创建对应的实体类com.itheima.pojo.User。

用户表

-- 用户表
create table user(
    id int unsigned primary key auto_increment comment 'ID',
    name varchar(100) comment '姓名',
    age tinyint unsigned comment '年龄',
    gender tinyint unsigned comment '性别, 1:男, 2:女',
    phone varchar(11) comment '手机号'
) comment '用户表';
-- 插入测试数据省略

实体类

public class User {
    private Integer id;   //id(主键)
    private String name;  //姓名
    private Short age;    //年龄
    private Short gender; //性别
    private String phone; //手机号
    
    //省略GET, SET方法
}

属性名与表中的字段名一一对应。

1.2 配置Mybatis

image-20250411235052025

从上图可以看出连接数据库的四大参数:

  • MySQL驱动类
  • 登录名
  • 密码
  • 数据库连接字符串

在springboot项目中,编写application.properties文件,配置数据库连接信息driver-class-name、url 、username和password:

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=123456

1.3 编写SQL语句

在创建出来的springboot工程中,在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper ,这是一个持久层接口(Mybatis的持久层接口规范一般都叫 XxxMapper)。

image-20250412001449528

UserMapper:

import com.itheima.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;

@Mapper
public interface UserMapper {
    
    //查询所有用户数据
    @Select("select id, name, age, gender, phone from user")
    public List<User> list();
    
}

@Mapper注解:表示是mybatis中的Mapper接口

  • 程序运行时:框架会自动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理

@Select注解:代表的就是select查询,用于书写select查询语句

1.4 单元测试

在创建出来的SpringBoot工程中,在src下的test目录下,已经自动帮我们创建好了测试类 ,并且在测试类上已经添加了注解 @SpringBootTest,代表该测试类已经与SpringBoot整合。

该测试类在运行时,会自动通过引导类加载Spring的环境(IOC容器)。我们要测试那个bean对象,就可以直接通过@Autowired注解直接将其注入进行,然后就可以测试了。

@SpringBootTest
public class MybatisQuickstartApplicationTests {
	
    @Autowired
    private UserMapper userMapper;
	
    @Test
    public void testList(){
        List<User> userList = userMapper.list();
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

1.5 解决SQL警告与提示

如果想让idea给我们提示对应的SQL语句,我们需要在IDEA中配置与MySQL数据库的链接。

image-20250412004929660

如果idea不识别表名,就需要建立连接。

2.JDBC介绍(了解)

java语言操作数据库只能通过sun公司提供的 JDBC 规范。Mybatis框架,就是对原始的JDBC程序的封装。

image-20250412155015062

本质:

  • sun公司官方定义的一套操作所有关系型数据库的规范,即接口。

  • 各个数据库厂商去实现这套接口,提供数据库驱动jar包。

  • 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。

2.1 代码

...  //导包省略
public class JdbcTest {
    @Test
    public void testJdbc() throws Exception {
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2. 获取数据库连接
        String url="jdbc:mysql://127.0.0.1:3306/mybatis";
        String username = "root";
        String password = "1234";
        Connection connection = DriverManager.getConnection(url, username, password);

        //3. 执行SQL
        Statement statement = connection.createStatement(); //操作SQL的对象
        String sql="select id,name,age,gender,phone from user";
        ResultSet rs = statement.executeQuery(sql);//SQL查询结果会封装在ResultSet对象中

        List<User> userList = new ArrayList<>();//集合对象(用于存储User对象)
        //4. 处理SQL执行结果
        while (rs.next()){
            //取出一行记录中id、name、age、gender、phone下的数据
            int id = rs.getInt("id");
            String name = rs.getString("name");
            short age = rs.getShort("age");
            short gender = rs.getShort("gender");
            String phone = rs.getString("phone");
            //把一行记录中的数据,封装到User对象中
            User user = new User(id,name,age,gender,phone);
            userList.add(user);//User对象添加到集合
        }
        //5. 释放资源
        statement.close();
        connection.close();
        rs.close();

        //遍历集合
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

DriverManager(类):数据库驱动管理类。

  • 作用:

    1. 注册驱动

    2. 创建java代码和数据库之间的连接,即获取Connection对象

Connection(接口):建立数据库连接的对象

  • 作用:用于建立java程序和数据库之间的连接

Statement(接口): 数据库操作对象(执行SQL语句的对象)。

  • 作用:用于向数据库发送sql语句

ResultSet(接口):结果集对象(一张虚拟表)

  • 作用:sql查询语句的执行结果会封装在ResultSet中

2.2 问题分析和对比

JDBC操作数据库把四要素(驱动、链接、用户名、密码)硬性写在java程序中,查询解析非常繁琐,每次都要重新建立和释放资源,导致资源浪费,性能降低。

而JDBC把四要素(驱动、链接、用户名、密码)配置在配置文件 application.properties中,便于修改,查询结果的解析和封装自动映射,不必关注具体的实现,通过数据库连接池技术,避免了频繁创建销毁连接而带来的资源浪费。

对于Mybatis,在操作数据库时,重点关注两个方面:配置文件application.properties和Mapper接口,大大节省开发压力。

3.数据库连接池

数据库连接池是一个容器,负责分配、管理数据库连接,程序启动时,会自动创建一些Connection连接对象放在连接池中。

用户使用SQL时,只需要从连接池中获取一个Connection对象,用完归还。

如果Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动收回这个连接对象

产品

  • 官方(sun)提供了数据库连接池标准(javax.sql.DataSource接口)

    • 功能:获取连接

      public Connection getConnection() throws SQLException;
      
    • 第三方组织必须按照DataSource接口实现

常见的数据库连接池:

  • C3P0
  • DBCP
  • Druid(德鲁伊)
  • Hikari (追光者,springboot默认,间接依赖于mybatis-spring-boot-starter)

Druid(德鲁伊):阿里巴巴开源的数据库连接池项目,功能强大,性能优秀,是Java语言最好的数据库连接池之一。

把默认的数据库连接池Hikari 切换为Druid数据库连接池的步骤:

  1. 在pom.xml文件中引入依赖
<dependency>
    <!-- Druid连接池依赖 -->
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
  1. 在application.properties中引入数据库连接配置

方式1:

spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=1234

方式2:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=1234

4.lombok

Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码。

注解作用
@Getter/@Setter为所有的属性提供get/set方法
@ToString会给类自动生成易阅读的 toString 方法
@EqualsAndHashCode根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)
@NoArgsConstructor为实体类生成无参的构造器方法
@AllArgsConstructor为实体类生成除了static修饰的字段之外带有各参数的构造器方法。

使用

第1步:在pom.xml文件中引入依赖

<!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

第2步:在实体类上添加注解

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

在实体类上添加了@Data注解,那么这个类在编译时期,就会生成getter/setter、equals、hashcode、toString等方法。

Lombok的注意事项:

  • Lombok会在编译时,会自动生成对应的java代码
  • 在使用lombok时,还需要安装一个lombok的插件(新版本的IDEA中自带)

5.Mybatis基础操作

5.1 准备

准备数据库表:

-- 部门管理
create table dept
(
    id          int unsigned primary key auto_increment comment '主键ID',
    name        varchar(10) not null unique comment '部门名称',
    create_time datetime    not null comment '创建时间',
    update_time datetime    not null comment '修改时间'
) comment '部门表';

-- 部门表测试数据
...
-- 员工管理
create table emp
(
    id          int unsigned primary key auto_increment comment 'ID',
    username    varchar(20)      not null unique comment '用户名',
    password    varchar(32) default '123456' comment '密码',
    name        varchar(10)      not null comment '姓名',
    gender      tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
    image       varchar(300) comment '图像',
    job         tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
    entrydate   date comment '入职时间',
    dept_id     int unsigned comment '部门ID',
    create_time datetime         not null comment '创建时间',
    update_time datetime         not null comment '修改时间'
) comment '员工表';
-- 员工表测试数据
...

创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)

application.properties中引入数据库连接信息

创建对应的实体类Emp(实体类属性采用驼峰命名)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private Short gender;
    private String image;
    private Short job;
    private LocalDate entrydate;     //LocalDate类型对应数据表中的date类型
    private Integer deptId;
    private LocalDateTime createTime;//LocalDateTime类型对应数据表中的datetime类型
    private LocalDateTime updateTime;
}

准备Mapper接口:EmpMapper

@Mapper
public interface EmpMapper {
}

image-20250412211325780

5.2 删除

根据主键删除数据:

@Mapper
public interface EmpMapper {
    //使用#{key}方式获取方法中的参数值,将来形参id会替换参数占位符#{id}
    @Delete("delete from emp where id = #{id}")
    public void delete(Integer id);	//可以指定返回值为int型,表示delete删除的行数
    
}

@Delete注解:用于编写delete操作的SQL语句

如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写,如:#{id}、#{value}。但是建议保持名字一致。

5.3 日志输入

在Mybatis中可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果:

  1. 在application.properties文件中开启mybatis的日志,并指定输出到控制台
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

5.4 预编译SQL

5.4.1 优势

预编译SQL有两个优势:

  1. 性能更高:将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)
  2. 更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。
5.4.2 SQL注入

通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

例如登录页面(用户名和密码),本质是执行查询语句select count(*) from emp where username = '输入的用户名' and password = '输入的密码';,不法分子可以修改密码为‘ or '1' = '1从而进入系统,原理是‘ or '1' = '1替换输入的密码可以得到

select count(*) from emp where username = '输入的用户名' and password = '' or '1' = '1';,由于'1' = '1'始终成立,所以可以登陆成功。而通过预编译就可以避免SQL注入。

5.4.3 参数占位符

在Mybatis中提供的参数占位符有两种:${…} 、#{…}

  • #{…}

    • 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值
    • 使用时机:参数传递,都使用#{…}
  • ${…}

    • 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
    • 使用时机:如果对表名、列表进行动态设置时使用

注意事项:在项目开发中,建议使用#{…},生成预编译SQL,防止SQL注入安全。

5.5 插入

@Mapper
public interface EmpMapper {

    @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
    public void insert(Emp emp);

}

说明:#{…} 里面写的名称是对象的属性名

在数据添加成功后,如果想要拿到主键值,需要在Mapper接口中的方法上添加一个Options注解,并在注解中指定属性useGeneratedKeys=true和keyProperty=“实体类属性名”:

@Mapper
public interface EmpMapper {
    //会自动将生成的主键值,赋值给emp对象的id属性
    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
    public void insert(Emp emp);
}

5.6 更新

@Mapper
public interface EmpMapper {
    //根据id修改员工信息
    @Update("update emp set username=#{username}, name=#{name}, gender=#{gender}, image=#{image}, job=#{job}, entrydate=#{entrydate}, dept_id=#{deptId}, update_time=#{updateTime} where id=#{id}")
    public void update(Emp emp);
}

可以设置返回值类型为int,表示更新操作影响的行数。

5.7 查询

5.7.1 根据ID查询
@Mapper
public interface EmpMapper {
    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
    public Emp getById(Integer id);
}

在测试类测试后,发现deptId、createTime、updateTime这三个字段没有值,这是因为实体类的属性名和数据库的字段名一致会映射成功,而deptId、createTime、updateTime属性在数据库中对应dept_id、create_time、update_time,映射不匹配。解决方案:

方案一:起别名,在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样

@Select("select id, username, password, name, gender, image, job, entrydate, " +
        "dept_id AS deptId, create_time AS createTime, update_time AS updateTime " +
        "from emp " +
        "where id=#{id}")
public Emp getById(Integer id);

方案二:手动结果映射,通过 @Results及@Result 进行手动结果映射

@Results({@Result(column = "dept_id", property = "deptId"),
          @Result(column = "create_time", property = "createTime"),
          @Result(column = "update_time", property = "updateTime")})
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);

方案三:开启驼峰命名(推荐),如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射

# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。如字段dept_id自动映射到deptId属性。

5.7.2 条件查询
@Mapper
public interface EmpMapper {
    @Select("select * from emp " +
            "where name like '%${name}%' " +	//这里不能使用#{name}占位符,因为在''中会被认为是字符串
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
}

方法中的形参名和SQL语句中的参数占位符名保持一致

解决SQL注入风险:使用MySQL提供的字符串拼接函数:concat(‘%’ , ‘关键字’ , ‘%’)

@Mapper
public interface EmpMapper {

    @Select("select * from emp " +
            "where name like concat('%',#{name},'%') " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

}
5.7.3 参数名说明

在springBoot的2.x版本:在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}可以直接通过形参名获取对应的值。

在springBoot的1.x版本:编译时生成的字节码文件不再保留原方法形参名,默认是var1、var2 …,可以通过@Param注解保留形参名:

image-20250413192952225

6.Mybatis的XML配置文件

如果需要实现复杂的SQL功能,注解将会非常繁琐,可以通过XML文件存放SQL语句。

6.1 XML配置文件规范

  1. XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)

  2. XML映射文件的namespace属性与Mapper接口全限定名一致

  3. XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。

image-20250413193321614

<select>标签:就是用于编写select查询语句的。

  • id属性:指定执行SQL语句的方法
  • resultType属性,指的是查询返回的单条记录所封装的类型。

6.2 XML配置文件实现

第1步:创建XML映射文件

第2步:编写XML映射文件

  • dtd约束,直接从mybatis官网复制即可
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
 
</mapper>
  • sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致
<mapper namespace="com.itheima.mapper.EmpMapper">
    <!--查询操作-->
    <select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where name like concat('%',#{name},'%')
              and gender = #{gender}
              and entrydate between #{begin} and #{end}
        order by update_time desc
    </select>
</mapper>

注解和XML配置文件的选择

使用Mybatis的注解,主要是来完成一些简单的增删改查功能。

如果需要实现复杂的SQL功能,建议使用XML来配置映射语句。

6.3 MybatisX

MybatisX是一款基于IDEA的快速开发Mybatis的插件,可以通过MybatisX快速定位,直接搜索插件安装即可。

7.动态SQL

动态SQL就是方法参数可以只传递一部分,其他的传递null,实现部分条件的SQL语句执行。例如empMapper.list("张", null, null, null)表示只根据name查询。

7.1 动态SQL-if

<if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。

<if test="条件表达式">
   要拼接的sql语句 
</if>
7.1.1 条件查询

改造5.7.2中的XML配置文件为动态SQL:

<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        <where>
             <!-- if做为where标签的子元素 -->
             <if test="name != null">
                 and name like concat('%',#{name},'%')
             </if>
             <if test="gender != null">
                 and gender = #{gender}
             </if>
             <if test="begin != null and end != null">
                 and entrydate between #{begin} and #{end}
             </if>
        </where>
        order by update_time desc
</select>

<where>标签只会在子元素有内容的情况下才插入where子句,而且在合适时会自动去除子句的开头的AND或OR

7.1.2 条件更新

改造5.6中的XML配置文件为动态SQL:

	<update id="update">
        update emp
        <!-- 使用set标签,代替update语句中的set关键字 -->
        <set>
            <if test="username != null">
                username=#{usern ame},
            </if>
            <if test="name != null">
                name=#{name},
            </if>
            <if test="gender != null">
                gender=#{gender},
            </if>
            <if test="image != null">
                image=#{image},
            </if>
            <if test="job != null">
                job=#{job},
            </if>
            <if test="entrydate != null">
                entrydate=#{entrydate},
            </if>
            <if test="deptId != null">
                dept_id=#{deptId},
            </if>
            <if test="updateTime != null">
                update_time=#{updateTime}
            </if>
        </set>
        where id=#{id}
    </update>

<set>:动态的在SQL语句中插入set关键字,并会在合适时删掉额外的逗号。(用于update语句中)

7.2 动态SQL-foreach

Mapper接口:

@Mapper
public interface EmpMapper {
    //批量删除
    public void deleteByIds(List<Integer> ids);
}

XML映射文件:

  • 使用<foreach>遍历deleteByIds方法中传递的参数ids集合
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符" 
         open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>
	<delete id="deleteByIds">
        <!-- delete from emp where id in (1,2,3,...); -->
        delete from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>

7.3 动态SQL-sql&include

在xml映射文件中配置的SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码

image-20250413200846270

  • <sql>:定义可重用的SQL片段

  • <include>:通过属性refid,指定包含的SQL片段

<!-- SQL片段: 抽取重复的代码 -->
<sql id="commonSelect">
 	select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>
<!-- 通过<include>标签在原来抽取的地方进行引用 -->
<select id="list" resultType="com.itheima.pojo.Emp">
    <include refid="commonSelect"/>
    <where>
        <if test="name != null">
            name like concat('%',#{name},'%')
        </if>
        <if test="gender != null">
            and gender = #{gender}
        </if>
        <if test="begin != null and end != null">
            and entrydate between #{begin} and #{end}
        </if>
    </where>
    order by update_time desc
</select>

十四、SpringBootWeb案例

具体操作见资料中的SpringBootWeb综合案例。这里只写出现的新知识点。

1.开发规范

1.1 开发规范-REST

在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。

REST(Representational State Transfer):表述性状态转换,它是一种软件架构风格。

传统URL风格:

http://localhost:8080/user/getById?id=1     GET:查询id为1的用户
http://localhost:8080/user/saveUser         POST:新增用户
http://localhost:8080/user/updateUser       POST:修改用户
http://localhost:8080/user/deleteUser?id=1  GET:删除id为1的用户

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

基于REST风格URL:

http://localhost:8080/users/1  GET:查询id为1的用户
http://localhost:8080/users    POST:新增用户
http://localhost:8080/users    PUT:修改用户
http://localhost:8080/users/1  DELETE:删除id为1的用户

通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。

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

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

描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books…

1.2 开发规范-统一响应结果

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

@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);
    }
}

1.3 开发流程

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

image-20250413233015242

接口文档一般由后端程序员书写。

2.日志对象

若要使用日志功能,需要在每个类里手动声明一个 Logger 对象:

private static final Logger log = LoggerFactory.getLogger(WithoutSlf4jExample.class);
log.info("Doing something...");  //日志记录

在类上添加 @Slf4j 注解后,Lombok 会在编译阶段自动为该类生成一个 org.slf4j.Logger 类型的日志对象。这个日志对象的名称通常为 log,可以直接使用它进行日志记录操作:

@Slf4j
public class WithSlf4jExample {
    public void doSomething() {
        log.info("Doing something...");
    }
}

3.Controller层请求方式

@RequestMapping注解可以接受任何形式的请求方式,如果想要指定请求方式的限制,可以通过method属性指定:

@RequestMapping(value = "/depts" , method = RequestMethod.GET)  //只允许GET请求
@RequestMapping(value = "/depts" , method = RequestMethod.POST)  //只允许POST请求
@RequestMapping(value = "/depts" , method = RequestMethod.PUT)  //只允许PUT请求
@RequestMapping(value = "/depts" , method = RequestMethod.DELETE)  //只允许DELETE请求

springboot还提供了简便方式指定请求方式:

@GetMapping("/depts")  //只接受get请求
@POSTMapping("/depts") //只接受post请求
@PUTMapping("/depts") //只接受put请求
@DELETEMapping("/depts") //只接受delete请求

4.请求路径优化

Controller层如果重复的请求路径过多,可以把重复的请求路径抽取到注解@RequestMapping中:

//原注解
@RestController
public class DeptController {
    @Autowired
    private DeptService deptService;
    @GetMapping("/depts")
    ...
    @DeleteMapping("/depts/{id}")
    ...
    @PostMapping("/depts")
    ...
}
    
//优化后的注解
@RestController
@RequestMapping("/depts")
public class DeptController {
    @Autowired
    private DeptService deptService;
    @GetMapping  ///depts
	...
    @DeleteMapping("/{id}")  ///depts/{id}
	...
    @PostMapping  ///depts
    ...
}

一个完整的请求路径,应该是类上@RequestMapping的value属性 + 方法上的 @RequestMapping的value属性

5.分页插件

PageHelper是Mybatis的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。

5.1 实现

1、在pom.xml引入依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>

2、代码改造

image-20250416180307970

分页插件执行过程:

  1. 先获取到要执行的SQL语句:select * from emp
  2. 把SQL语句中的字段列表,变为:count(*)
  3. 执行SQL语句:select count(*) from emp //获取到总记录数
  4. 再对要执行的SQL语句:select * from emp 进行改造,在末尾添加 limit ? , ?
  5. 执行改造后的SQL语句:select * from emp limit ? , ?

5.2 测试

重启项目工程,打开postman,发起GET请求,访问 :http://localhost:8080/emps?page=1&pageSize=5,得到JSON数据。

6.文件上传

文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。

6.1 简介

想要完成文件上传这个功能需要涉及到两个部分:

  1. 前端程序
  2. 服务端程序
6.1.1 前端部分
<form action="/upload" method="post" enctype="multipart/form-data">
	姓名: <input type="text" name="username"><br>
    年龄: <input type="text" name="age"><br>
    头像: <input type="file" name="image"><br>
    <input type="submit" value="提交">
</form>

上传文件页面三要素:

  • 表单必须有file域,用于选择要上传的文件

    <input type="file" name="image"/>
    
  • 表单提交方式必须为POST

    通常上传的文件会比较大,所以需要使用 POST 提交方式

  • 表单的编码类型enctype必须要设置为:multipart/form-data

    普通默认的编码格式不适合传输大型的二进制数据,所以在文件上传时,表单的编码格式必须设置为multipart/form-data

实现

  1. 将资料里的"upload.html"文件,复制到springboot项目工程下的static目录,在火狐浏览器打开。
  2. 设置form表单标签中enctype属性值为multipart/form-data,在控制台查看文件传输情况:

image-20250417213715984

如果使用enctype的默认属性值或不指定enctype属性,会看不到文件中的数据,只能看到文件名(带后缀)。

6.1.2 后端部分
  • 在服务端定义一个controller层的类用来进行文件上传,然后在controller当中定义一个方法来处理/upload 请求

  • 在定义的方法中接收提交过来的数据(形参名和传输的名字相同):

    • 用户名:String name

    • 年龄: Integer age

    • 文件: MultipartFile image

      Spring中提供了一个API:MultipartFile,使用这个API就可以来接收到上传的文件

image-20250417214401566

如果表单项的名字和方法中形参名不一致,可以使用@RequestParam注解解决。

6.1.3 测试
  • 启动服务端程序
  • 打开浏览器输入:http://localhost:8080/upload.html , 录入数据并提交

上传的文件放在了一个临时文件(.tmp)中,通过后端控制台可以得到临时文件的路径,当controller代码正在运行时,临时文件存在,当返回一个结果后,这个临时目录就被释放了。

6.2 本地存储

如果想要保留浏览器传输的文件当程序结束时不被自动释放,就需要把文件保存到本地磁盘中:

  1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
  2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下

MultipartFile 常见方法:

  • String getOriginalFilename(); //获取原始文件名
  • void transferTo(File dest); //将接收的文件转存到磁盘文件中
  • long getSize(); //获取文件的大小,单位:字节
  • byte[] getBytes(); //获取文件内容的字节数组
  • InputStream getInputStream(); //获取接收到的文件内容的输入流
@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        log.info("文件上传:{},{},{}",username,age,image);

        //获取原始文件名
        String originalFilename = image.getOriginalFilename();

        //将文件存储在服务器的磁盘目录
        image.transferTo(new File("E:/images/"+originalFilename));

        return Result.success();
    }

}

利用postman测试:

image-20250417215738760

由于上传的文件名可能重名,可以使用UUID获取唯一文件名进行本地存储:

@PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        log.info("文件上传:{},{},{}",username,age,image);

        //获取原始文件名
        String originalFilename = image.getOriginalFilename();

        //构建新的文件名
        String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
        String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名

        //将文件存储在服务器的磁盘目录
        image.transferTo(new File("E:/images/"+newFileName));

        return Result.success();
    }

在SpringBoot中,文件上传时默认单个文件最大大小为1M,修改application.properties进行如下配置:

#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

6.3 阿里云OSS

阿里云对象存储OSS,是一款安全可靠的云 存储服务。可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

6.3.1 准备

SDK:软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。简单说,SDK中包含了使用第三方云服务时所需要的依赖,以及一些示例代码。

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

使用步骤

image-20250417222829275

注册登录阿里云后,点击右上角的控制台,点击对象存储OSS:

image-20250418011131010

点击左侧的 “Bucket列表”,创建一个Bucket:

image-20250418011340998

6.3.2 入门

首先需要来打开阿里云OSS的官方文档,在官方文档中找到 SDK 的示例代码:

image-20250418011512928

image-20250418011607329

在实际开发当中,我们是需要从前往后仔细的去阅读这一份文档,这里只说重点。

image-20250418011710167

public class AliOssTest {
    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
        String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
        
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "web-framework01";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "1.jpg";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "C:\\Users\\Administrator\\Pictures\\1.jpg";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 设置该属性可以返回response。如果不设置,则返回的response为空。
            putObjectRequest.setProcess("true");
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
            // 如果上传成功,则返回200。
            System.out.println(result.getResponse().getStatusCode());
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}
  • accessKeyId:阿里云账号AccessKey
  • accessKeySecret:阿里云账号AccessKey对应的秘钥
  • bucketName:Bucket名称
  • objectName:对象名称,在Bucket中存储的对象的名称
  • filePath:文件路径

AccessKey获取 :

image-20250418011940631

运行以上程序后,会把本地的文件上传到阿里云OSS服务器上,点击文件列表就可以查看了。

注意:在新版本中,抛弃了在代码中硬性使用秘钥,而采用了从系统环境变量中获取,所以需要配置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。

7.配置文件

对于代码中重复的且固定的信息,可以配置在配置文件properties中,当需要使用时,通过springboot提供的Value注解注入。

7.1 参数配置化

旧版本的OSS中,需要手动在代码引入OSS地址、秘钥和bucket容器名字,可以把这些信息配置到配置文件增加安全性和代码简洁性:

properties文件

#自定义的阿里云OSS配置信息
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias

程序代码

@Component
public class AliOSSUtils {
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;
    
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;
    
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
 	...
 } 

7.2 yml配置文件

传统的配置文件比较臃肿,变量的层级关系不清晰,使用yml配置文件可以很清晰的显示出层级关系,在开发中也更偏向于yml配置文件。

  • application.properties

    server.port=8080
    server.address=127.0.0.1
    
  • application.yml

    server:
      port: 8080
      address: 127.0.0.1
    
  • application.yaml

    server:
      port: 8080
      address: 127.0.0.1
    

yml 格式的配置文件,后缀名有两种:

  • yml (推荐)
  • yaml

yml配置文件的基本语法:

  • 大小写敏感
  • 数值前边必须有空格,作为分隔符
  • 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • #表示注释,从这个字符一直到行尾,都会被解析器忽略

yml文件中常见的数据格式:

  1. 对象/Map集合
user:
  name: zhangsan  #:后必须要有一个空格
  age: 18
  password: 123456
  1. 数组/List/Set集合
hobby: 
  - java  #-后必须要有一个空格
  - game
  - sport

7.3 @ConfigurationProperties

使用@Value注解给变量赋值在变量很多时会非常繁琐,Spring提供了@ConfigurationProperties注解实现自动注入:

  1. 创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须一致,实体类当中的属性还需要提供 getter / setter方法
  2. 将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象(@Component)
  3. 在实体类上添加@ConfigurationProperties注解,并通过perfect属性来指定配置参数项的前缀

image-20250420212501145

如果出现警告,表明需要添加一个依赖自动识别被@ConfigurationProperties注解标识的bean对象(可选项):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

@ConfigurationProperties注解和@Value注解的区别和选择:

相同点:都是用来注入外部配置的属性的。

不同点:

  • @Value注解只能一个一个的进行外部属性的注入。

  • @ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。

如果需要注入的属性比较多,而且需要复用,就考虑用@ConfigurationProperties,如果仅仅有少量属性,就可以考虑@Value

8.登录认证

8.1 登录校验

登录校验就是服务器接收到浏览器的请求后,先判断是否已经登录,如果已经登录,就执行对应的业务需求,否则就给前端返回一个错误信息。

登录校验的实现思路:在服务端设置统一拦截,拦截到浏览器的请求后根据请求头获取之前的登录信息,再进行相应的登录校验。

登录校验需要两个技术:

  1. 会话技术
  2. 统一拦截技术

统一拦截技术有两种:

  1. Servlet规范中的Filter过滤器
  2. Spring提供的interceptor拦截器
8.1.1 会话技术
8.1.1.1 概述

会话:指的是浏览器与服务器之间的一次连接,我们就称为一次会话,如下图有三个浏览器,就有三个会话:

  • 一次会话可以包含多次请求和响应
  • 会话的一方连接断开,整个会话就结束

image-20250421171018373

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据,例如上图的1和3是否属于同一会话(是),3和5是否属于同一会话(否)。

共享数据:HTTP协议是无状态协议,需要共享数据记录上一次请求的内容,进行登录校验。

8.1.1.2 会话跟踪方案

方案一 :Cookie

cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的。被HTTP协议支持自带。

当浏览器第一次请求了登录接口,登录接口执行完成之后就可以设置一个cookie,在 cookie 当中我们存储用户相关的一些数据信息。

三个自动:

  • 服务器会 自动 的将 cookie 响应给浏览器。

  • 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。

  • 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。

代码测试:

@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
        return Result.success();
    }
	
    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }
} 

打开浏览器,访问c1接口,http://localhost:8080/c1:

image-20250421174727123

访问c2接口 http://localhost:8080/c2,此时浏览器会自动将Cookie携带到服务端,是通过请求头Cookie携带的:

image-20250421174832205

  • 优点:HTTP协议中支持的技术
  • 缺点:
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 不安全,用户可以自己禁用Cookie
    • Cookie不能跨域

跨域介绍:

前后端分离开发中,前端部署在一台服务器上(假设是192.168.150.200),后端部署在另一台服务器上(假设是192.168.150.100)上,打开浏览器直接访问前端工程http://192.168.150.200/login.html,在该页面发起请求到服务端http://192.168.150.100:8080/login 接口,此时就会出现跨域:

image-20250603011917430

区分跨域的维度:

  • 协议
  • IP/协议
  • 端口

只要上述的三个维度有任何一个维度不同,那就是跨域操作

方案二 :Session

Session:服务器端会话跟踪技术,存储在服务器端,底层是通过Cookie实现。

浏览器第一次请求服务器,服务器会创建一个Session对象,每个Session对象都有一个ID,响应数据时,服务器将Session 的 ID 通过 Cookie 响应给浏览器,浏览器自动识别这个ID并存储在浏览器本地,之后每一次请求都会将Cookie 的数据携带到服务端,服务端拿到这个ID就会从众多JSESSIONID中找到当前请求对应的JSESSIONID,从而实现数据共享。

代码测试:

@Slf4j
@RestController
public class SessionController {

    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1: {}", session.hashCode());

        session.setAttribute("loginUser", "tom"); //往session中存储数据
        return Result.success();
    }

    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info("HttpSession-s2: {}", session.hashCode());

        Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
        log.info("loginUser: {}", loginUser);
        return Result.success(loginUser);
    }
}

访问 s1 接口,http://localhost:8080/s1,就可以通过Set-Cookie看到JSESSIONID:

image-20250421211524535

访问 s2 接口,http://localhost:8080/s2,就可以通过Cookie看到Session数据:

image-20250421211658221

  • 优点:Session是存储在服务端的,安全
  • 缺点:
    • 服务器集群环境下无法直接使用Session
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 用户可以自己禁用Cookie
    • Cookie不能跨域

Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案也就失效了

集群环境为何无法使用Session?

在企业开发中,最终部署时会采用集群部署,即同一个项目部署在多个服务器中,用户访问时,会先访问到负载均衡服务器(将前端发起的请求均匀的分发给后面的这三台服务器)。假如通过 session 进行会话跟踪,若第一次分发到第一台服务器,第二次分发到第二台服务器,这时第二台服务器中没有对应Session对象,就会重新构建一个会话对象,这样两次请求就不是同一个会话。

image-20250421212830608

方案三:令牌技术(最常用)

令牌:用户的一个身份凭证,在请求登录接口时,如果登录成功,就会生成一个令牌,将这个令牌响应给前端。

前端接收到令牌后会将令牌存储在cookie 中(也可以存储在其他空间如 localStorage)。后续每一次请求都会将令牌携带到服务端,服务端校验令牌的有效性。如果令牌有效就说明用户已经登录,否则就说明用户未登录。

共享数据可以存放在令牌中

  • 优点:
    • 支持PC端、移动端
    • 解决集群环境下的认证问题
    • 减轻服务器的存储压力(无需在服务器端存储)
  • 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
8.1.2 JWT令牌

定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

自包含:指的是jwt令牌,看似是一个随机的字符串,但是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

8.1.2.1 JWT的组成

三个部分之间使用英文的点来分割:

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,这正是签名保证的。

image-20250421214956814

JWT是如何将原始的JSON格式数据,转变为字符串的呢?

通过base64编码方式进行编码。

Base64:一种基于64个可打印的字符来表示二进制数据的编码方式。64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号。

8.1.2.2 生成和校验

引入JWT的依赖(提供工具类Jwts进行JWT的生成和校验):

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
  1. 生成JWT代码实现:
@Test
public void genJwt(){
    Map<String,Object> claims = new HashMap<>();
    claims.put("id",1);
    claims.put("username","Tom");
    
    String jwt = Jwts.builder()
        .setClaims(claims) //自定义内容(载荷),需要一个Map集合      
        .signWith(SignatureAlgorithm.HS256, "itheima") //签名算法        
        .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期,需要一个date对象 
        .compact();
    
    System.out.println(jwt);
}

运行后打开JWT的官网https://jwt.io/,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来

image-20250421220245740

第三部分由于是有签名算法得出来的,所以不会解码。

  1. 解析生成的令牌代码实现:
@Test
public void parseJwt(){
    Claims claims = Jwts.parser()
        .setSigningKey("itheima")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
  .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
        .getBody();

    System.out.println(claims);
}

运行测试方法,得到{id=1, exp=1672729730}

注意事项

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。

  • 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。

8.1.3 过滤器Filter

Filter:过滤器, JavaWeb三大组件(Servlet、Filter、Listener)之一,可以把资源的请求拦截下来,从而实现一些特殊的功能,如登录校验、统一编码处理、敏感字符处理等。

8.1.3.1 快速入门
  • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
  • 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。

定义过滤器:

//定义一个类,实现一个标准的Filter过滤器的接口
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {  //jakarta.servlet包下
    @Override //初始化方法, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法执行了");
    }

    @Override //拦截到请求之后调用, 调用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Demo 拦截到了请求...放行前逻辑");
        //放行
        chain.doFilter(request,response);
    }

    @Override //销毁方法, 只调用一次
    public void destroy() {
        System.out.println("destroy 销毁方法执行了");
    }
}
  • init方法:初始化方法。web服务器启动时自动创建Filter过滤器对象,创建过滤器对象时自动调用init初始化方法,只会被调用一次。

  • doFilter方法:每一次拦截到请求之后都会被调用,所以会被调用多次,每拦截一次就调用一次。

  • destroy方法: 销毁方法。关闭服务器时会自动调用destroy,只会被调用一次。

在启动类上添加注解@ServletComponentScan开启SpringBoot项目对Servlet组件的支持:

@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {

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

}

在浏览器请求一个路径,控制台可以看到相关信息就表示成功。

在过滤器Filter中,如果不执行放行操作,将无法访问后面的资源。 放行操作:chain.doFilter(request, response);

8.1.3.2 Filter详解

执行流程

image-20250422171250102

当拦截到一个请求后,要有FilterChain对象当中的doFilter()方法放行,放行后执行相应的逻辑,逻辑执行完毕后会到doFilter方法中执行放行后的逻辑,如果放行后没有逻辑,就结束方法响应。

拦截路径

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

过滤器链

在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链:

image-20250422172412665

接收到请求后,先执行Filter1的放行前逻辑和放行,放行后进入Fileter2拦截器,执行相应逻辑后放行,执行完路径的逻辑后先返回到Filter2逻辑中,Filter2中剩余逻辑执行完毕后再执行Filter1中的逻辑。

以注解方式配置的Filter过滤器执行优先级是按过滤器的类名自动排序确定的,类名排名越靠前,优先级越高,例如AFilter和BFilter会先执行AFilter。

8.1.4 拦截器Interceptor

拦截器:Spring框架中提供的一种动态拦截方法调用的机制,类似于过滤器。

拦截器会拦截前端的请求,判断用户是否有JWT令牌且令牌是否合法,再决定是放行还是执行其他操作。

8.1.4.1 快速入门

自定义拦截器

实现HandlerInterceptor接口,并重写其所有方法

//自定义拦截器
@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 true; //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 .... ");
    }
}

preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行

postHandle方法:目标资源方法执行后执行,请求资源执行完毕后执行

afterCompletion方法:视图渲染完毕后执行,最后执行

注册配置拦截器

实现WebMvcConfigurer接口,并重写addInterceptors方法

@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
    }
}
8.1.4.2 Interceptor详解

拦截路径

addPathPatterns("要拦截路径")指定要拦截哪些路径;excludePathPatterns("不拦截路径")指定哪些路径不需要拦截

拦截路径含义举例
/*一级路径能匹配/depts,/emps,/login,不能匹配 /depts/1
/**任意级路径能匹配/depts,/depts/1,/depts/1/2
/depts/*/depts下的一级路径能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**/depts下的任意级路径能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

执行流程

image-20250422224341172

  1. 浏览器发送一个请求后,先被Filter过滤器拦截,Filter过滤器执行放行前逻辑并放行,此时进入Spring环境要访问controller

  2. 由于Tomcat识别Servlet而不识别controller,所以请求会先到DispatcherServlet(前端控制器),再将请求转给Controller

  3. 拦截器此时会拦截请求执行preHandle()方法,如果返回true,就放行执行相关业务逻辑,执行后执行postHandle()方法和afterCompletion() 方法,如果返回false,就不放行

返回false,postHandle()方法不执行,但是afterCompletion()方法不受影响。

  1. 然后返回给DispatcherServlet,执行Filter中剩余内容,最后响应数据

过滤器和拦截器之间的区别

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

8.2 异常处理

当没有做任何异常处理时,三层架构处理异常的方案:

  • Mapper接口出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。
  • service 中也存在异常了,会抛给controller。
  • 而在controller当中,没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合开发规范。

image-20250422232709971

解决方案

  • 方案一:在所有Controller的所有方法中进行try…catch处理
    • 缺点:代码臃肿(不推荐)
  • 方案二:全局异常处理器
    • 好处:简单、优雅(推荐)
全局异常处理器

定义一个类,在类上加上@RestControllerAdvice注解表示定义一个全局异常处理器,然后在这个类中定义一个方法处理异常,方法加上@ExceptionHandler注解,并通过value属性指定捕获异常的类型:

@RestControllerAdvice
public class GlobalExceptionHandler {

    //处理异常
    @ExceptionHandler(Exception.class) //指定能够处理的异常类型
    public Result ex(Exception e){
        e.printStackTrace();//打印堆栈中的异常信息

        //捕获到异常之后,响应一个标准的Result
        return Result.error("对不起,操作失败,请联系管理员");
    }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody,处理异常的方法返回值会转换为json后再响应给前端

十五、事务&AOP

以后得案例均基于SpringBootWeb案例进行讲解,参考资料中的SpringBootWeb综合案例文件夹。

1.事务管理

1.1 Spring事务管理

事务:一组操作的集合,是一个不可分割的工作单位。所有操作要么全部成功,要么一个也不执行。

事务的操作主要有三步:

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
1.1.1 案例

解散部门:不仅删除部门,还要删除部门下的员工。

此时DeptServiceImpl的代码如下:

@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;


    //根据部门id,删除部门信息及部门下的所有员工
    @Override
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        int i = 1/0;

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }
}

如果没有异常发生,结果正确,但是出现了异常ArithmeticException,所以删除员工的逻辑不会执行,出现不一致现象。

1.1.2 Transactional注解

@Transactional作用:在当前这个方法执行之前开启事务,方法执行完毕后提交事务。如果执行过程出现异常就回滚事务。

@Transactional注解书写位置:

  • 方法:当前方法交给spring进行事务管理
  • 类:当前类中所有的方法都交由spring进行事务管理
  • 接口:接口下所有的实现类当中所有的方法都交给spring 进行事务管理

在使用@Transactional注解之前需要在配置文件中开启事务管理日志:

#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

在案例中可以在delete方法上加上@Transactional注解解决数据不一致的现象。

1.2 事务进阶

@Transactional注解当中有两个常见的属性:

  1. 异常回滚的属性:rollbackFor
  2. 事务传播行为:propagation
1.2.1 rollbackFor

在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚,如果是手动抛出的异常(throw new Exception),事务不会回滚,而是直接提交。

如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。

	@Override
    @Transactional(rollbackFor=Exception.class)
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        if(true){
            throw new Exception("出现异常了~~~");
        }

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }

此时不论是RuntimeException还是手动抛出异常,都会进行回滚。

1.2.2 propagation

当一个事务方法被另一个事务方法调用,此时会出现事务的传播。例如,两个事务方法,A方法和B方法,在A方法当中又调用了B方法。

image-20250423233235612

此时是事务B加入到事务A中还是新建一个事务B,就涉及到事务的传播行为,事务的传播行为由propagation属性决定:

属性值含义
REQUIRED【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY必须有事务,否则抛异常
NEVER必须没事务,否则抛异常

示例

删除部门时,不论是否删除成功,都要记录日志到deptLog表中。

@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    
    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private DeptLogService deptLogService;


    //根据部门id,删除部门信息及部门下的所有员工
    @Override
    @Log
    @Transactional(rollbackFor = Exception.class) 
    public void delete(Integer id) throws Exception {
        try {
            //根据部门id删除部门信息
            deptMapper.deleteById(id);
            //模拟:异常
            if(true){
                throw new Exception("出现异常了~~~");
            }
            //删除部门下的所有员工信息
            empMapper.deleteByDeptId(id);
        }finally {
            //不论是否有异常,最终都要执行的代码:记录日志
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
            //调用其他业务类中的方法
            deptLogService.insert(deptLog);
        }
    }
    
    //省略其他代码...
}

当程序执行后,会有两个操作,即deleteById(删除部门)和insert(记录日志),deleteByDeptId(删除员工)永远执行不到,此时事务insert默认直接加入到事务delete中,所以遇到异常会直接回滚deleteById和insert,插入失败却没有记录日志。

可以在insert方法上添加@Transactional(propagation = Propagation.REQUIRES_NEW)控制事务的传递行为:

@Service
public class DeptLogServiceImpl implements DeptLogService {

    @Autowired
    private DeptLogMapper deptLogMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)  //事务传播行为:不论是否有事务,都新建事务
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

REQUIRES_NEW表示会新建一个事务insert,即使delete中遇到异常,事务insert只要不出错,就能提交从而记录日志。

2.AOP基础

2.1 AOP概述

AOP:面向切面编程、面向方面编程,即面向指定的一个或多个方法的编程。

现在需要统计业务层所有方法的执行时间进行优化,如果在每个方法前后都记录时间,相减得到运行时间会非常繁琐,此时就可以使用AOP设计一个模版方法,方法运行前记录开始时间,方法运行后记录结束时间,中间运行原始业务方法:

image-20250424000413267

例如当需要运行list方法时,不会立即执行list,而是跳转到模版方法中执行:

  • 记录方法运行开始时间
  • 运行原始的业务方法(那此时原始的业务方法,就是 list 方法)
  • 记录方法运行结束时间,计算方法执行耗时

AOP是通过动态代理方式实现的

2.2 AOP快速入门

统计各个业务层方法执行耗时。

pom.xml

<!-- AOP依赖 -->  
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOP程序:TimeAspect

@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

    @Around("execution(* com.itheima.service.*.*(..))") 
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();

        //执行原始方法
        Object result = pjp.proceed();

        //记录方法执行结束时间
        long end = System.currentTimeMillis();

        //计算方法执行耗时
        log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

        return result;  //返回值为原有方法返回值
    }
}

AOP常见运用场景:

  • 记录系统的操作日志
  • 权限控制
  • 事务管理:Spring事务管理底层是通过AOP实现的,只要添加@Transactional注解,AOP程序会自动在原始方法运行前开启事务,在原始方法运行后提交或回滚事务。

2.3 AOP核心概念

1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息),入门程序当中所有业务方法都是连接点

image-20250424003601197

2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

image-20250424003628408

3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

image-20250424003658214

4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)

image-20250424003721864

  • 切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)

5. 目标对象:Target,通知所应用的对象

image-20250424003749125

通知是如何与目标对象结合在一起,对目标对象当中的方法进行功能增强的?

image-20250424003937696

答:Spring的AOP底层是基于动态代理技术来实现的,即在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

3.AOP进阶

3.1 通知类型

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
@Slf4j
@Component
@Aspect
public class MyAspect1 {

    //切入点方法(公共的切入点表达式)
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    private void pt(){

    }

    //前置通知(引用切入点)
    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("before ...");

    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        //原始方法在执行时:发生异常
        //后续代码不在执行

        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("pt()")
    public void after(JoinPoint joinPoint){
        log.info("after ...");
    }

    //返回后通知(程序在正常执行的情况下,会执行的后置通知)
    @AfterReturning("pt()")
    public void afterReturning(JoinPoint joinPoint){
        log.info("afterReturning ...");
    }

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("pt()")
    public void afterThrowing(JoinPoint joinPoint){
        log.info("afterThrowing ...");
    }
}

切入点方法:将重复的切入点表达式抽取出来,放在自定义方法的注解@Pointcut上,需要使用时可以通过方法名调用,例如:

@Before("pt()")相当于@Before("execution(* com.itheima.service.*.*(..))")

如果要在其他类中使用pt()这个切入点表达式,需要使用全类名.方法名()(权限修饰符要支持),例如:

@Before("com.itheima.aspect.MyAspect1.pt()")

程序发生异常的情况下:

  • @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法会执行

  • @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会执行(因为原始方法调用已经出异常了)

使用通知时的注意事项:

  • @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。

3.2 通知顺序

同一个切面类中,通知的执行顺序:

  • 目标方法前的通知方法:@Around、@Before
  • 目标方法后的通知方法:@AfterReturning | @AfterThrowing、@After、@Around

在不同切面类中,同类型通知默认按照切面类的类名字母排序:

  • 目标方法前的通知方法:字母排名靠前的先执行
  • 目标方法后的通知方法:字母排名靠前的后执行

如果我们想控制通知的执行顺序有两种方式:

  1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)
  2. 使用Spring提供的@Order注解:例如@Order(2)
    • 前置通知:数字越小先执行
    • 后置通知:数字越小越后执行

3.3 切入点表达式

切入点表达式:描述切入点方法的一种表达式,主要用来决定项目中的哪些方法需要加入通知。

常见形式:

  1. execution(……):根据方法的签名来匹配
  2. @annotation(……) :根据注解匹配
3.3.1 execution

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

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

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

  • 访问修饰符:可省略(比如: public、protected)

  • 包名.类名: 可省略

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

示例:

@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

可以使用通配符描述切入点:

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式示例:

//使用*代替包名(一层包使用一个*)
execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))
//使用..省略包名
execution(* com..DeptServiceImpl.delete(java.lang.Integer))
//使用*代替方法名
execution(* com..*.*(java.lang.Integer))
//使用..省略参数
execution(* com..*.*(..))
//匹配DeptServiceImpl类中以find开头的方法
execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))

根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式:

execution(* com.itheima.service.DeptService.list(..)) || 
execution(* com.itheima.service.DeptService.delete(..))

切入点表达式的书写建议:

  1. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
  2. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  3. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 …,使用 * 匹配单个包
3.3.2 @annotation

execution切入点表达式用来匹配多个无规则的方法,通过自定义注解给目标方法添加上自定义注解,就可以通过注解方便的匹配。

实现步骤

  1. 自定义注解MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
  1. 给需要匹配的方法加上注解@MyLog
  2. 切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
    //前置通知
    @Before("@annotation(com.itheima.anno.MyLog)") //自定义注解的全类名
    public void before(){
        log.info("MyAspect6 -> before ...");
    }
}

3.4 连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

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

@Slf4j
@Aspect
@Component
public class MyAspect8 {

    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("MyAspect8 ... before ...");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("MyAspect8 around before ...");

        //1. 获取 目标对象的类名 .
        String className = joinPoint.getTarget().getClass().getName();
        log.info("目标对象的类名:{}", className);

        //2. 获取 目标方法的方法名 .
        String methodName = joinPoint.getSignature().getName();
        log.info("目标方法的方法名: {}",methodName);

        //3. 获取 目标方法运行时传入的参数 .
        Object[] args = joinPoint.getArgs();
        log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

        //4. 放行 目标方法执行 .
        Object result = joinPoint.proceed();

        //5. 获取 目标方法运行的返回值 .
        log.info("目标方法运行的返回值: {}",result);

        log.info("MyAspect8 around after ...");
        return result;
    }
}
功能实现返回值类型
获取 目标对象的类名joinPoint.getTarget().getClass().getName()String
获取 目标方法的方法名joinPoint.getSignature().getName()String
获取 目标方法运行时传入的参数joinPoint.getArgs()Object[]
放行 目标方法执行joinPoint.proceed()Object
获取 目标方法运行的返回值joinPoint.proceed()Object

3.5 AOP案例

具体操作见资料中的SpringBootWeb综合案例。


说明:这篇笔记只是一个初步的第一版笔记哦,相应的配套资料可以下载黑马的资料,我也准备了一个飞书版本的文章,平时博主复习也是使用的飞书知识库进行复习,所以另一个版本的文章会更加完善,而且,另一个版本也有一些资料的提供(不是全部哦),如果你想要,点击链接https://mcnerzykwkel.feishu.cn/wiki/KIfZw5wKAi0rXjkKRDkcPc2onyf就可以了,如果你有更好的建议,直接在飞书内评论说明就可以咯,但是博主还是很自信不会有几个建议的😊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值