# Maven
# Maven 的作用
使用 Maven 能够做什么呢?
- 依赖管理
- 统一项目结构
- 项目构建
依赖管理:
- 方便快捷的管理项目依赖的资源 (jar 包),避免版本冲突问题
** 统一项目结构 : **
- 提供标准、统一的项目结构
在项目开发中,当你使用不同的开发工具 (如:Eclipse、Idea),创建项目工程时:

若我们创建的是一个 maven 工程,是可以帮我们自动生成统一、标准的项目目录结构:
具体的统一结构如下:
<img src="../../JavaWeb/day04-Maven-SpringBootWeb 入门 / 讲义 / 01. Maven/assets/image-20221130140132209.png" alt="image-20221130140132209" style="zoom:80%;" />
目录说明:
- src/main/java: java 源代码目录
- src/main/resources: 配置文件信息
- src/test/java: 测试代码
- src/test/resources: 测试配置文件信息
项目构建 :
- maven 提供了标准的、跨平台 (Linux、Windows、MacOS) 的自动化项目构建方式
# Maven 介绍
Apache Maven 是一个项目管理和构建工具,它基于项目对象模型 (Project Object Model , 简称: POM) 的概念,通过一小段描述信息来管理项目的构建、报告和文档。
官网:https://maven.apache.org/
Maven 的作用:
- 方便的依赖管理
- 统一的项目结构
- 标准的项目构建流程
# Maven 模型
- 项目对象模型 (Project Object Model)
- 依赖管理模型 (Dependency)
- 构建生命周期 / 阶段 (Build lifecycle & phases)
1). 构建生命周期 / 阶段 (Build lifecycle & phases)

以上图中紫色框起来的部分,就是用来完成标准化构建流程 。当我们需要编译,Maven 提供了一个编译插件供我们使用;当我们需要打包,Maven 就提供了一个打包插件供我们使用等。
2). 项目对象模型 (Project Object Model)

以上图中紫色框起来的部分属于项目对象模型,就是将我们自己的项目抽象成一个对象模型,有自己专属的坐标,如下图所示是一个 Maven 项目:

坐标,就是资源 (jar 包) 的唯一标识,通过坐标可以定位到所需资源 (jar 包) 位置

3). 依赖管理模型 (Dependency)

以上图中紫色框起来的部分属于依赖管理模型,是使用坐标来描述当前项目依赖哪些第三方 jar 包

之前我们项目中需要 jar 包时,直接就把 jar 包复制到项目下的 lib 目录,而现在书写在 pom.xml 文件中的坐标又是怎么能找到所要的 jar 包文件的呢?
答案:Maven 仓库
# Maven 仓库
仓库:用于存储资源,管理各种 jar 包
仓库的本质就是一个目录 (文件夹),这个目录被用来存储开发中所有依赖 (就是 jar 包) 和插件
Maven 仓库分为:
- 本地仓库:自己计算机上的一个目录 (用来存储 jar 包)
- 中央仓库:由 Maven 团队维护的全球唯一的。仓库地址:https://repo1.maven.org/maven2/
- 远程仓库 (私服):一般由公司团队搭建的私有仓库

当项目中使用坐标引入对应依赖 jar 包后,首先会查找本地仓库中是否有对应的 jar 包
如果有,则在项目直接引用
如果没有,则去中央仓库中下载对应的 jar 包到本地仓库
如果还可以搭建远程仓库 (私服),将来 jar 包的查找顺序则变为: 本地仓库 --> 远程仓库 --> 中央仓库
依赖管理
# 依赖配置
依赖:指当前项目运行所需要的 jar 包。一个项目中可以引入多个依赖:
例如:在当前工程中,我们需要用到 logback 来记录日志,此时就可以在 maven 工程的 pom.xml 文件中,引入 logback 的依赖。具体步骤如下:
在 pom.xml 中编写 <dependencies> 标签
在 <dependencies> 标签中使用 < dependency > 引入坐标
定义坐标的 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>
- 点击刷新按钮,引入最新加入的坐标
- 刷新依赖:保证每一次引入新的依赖,或者修改现有的依赖配置,都可以加入最新的坐标
依赖传递可以分为:
直接依赖:在当前项目中通过依赖配置建立的依赖关系
间接依赖:被依赖的资源如果依赖其他资源,当前项目间接依赖其他资源

比如以上图中:
- projectA 依赖了 projectB。对于 projectA 来说,projectB 就是直接依赖。
- 而 projectB 依赖了 projectC 及其他 jar 包。 那么此时,在 projectA 中也会将 projectC 的依赖传递下来。对于 projectA 来说,projectC 就是间接依赖。
# 排除依赖
问题:之前我们讲了依赖具有传递性。那么 A 依赖 B,B 依赖 C,如果 A 不想将 C 依赖进来,是否可以做到?
答案:在 maven 项目中,我们可以通过排除依赖来实现。
什么是排除依赖?
- 排除依赖:指主动断开依赖的资源。(被排除的资源无需指定版本)
<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>
# 依赖范围
在项目中导入依赖的 jar 包后,默认情况下,可以在任何地方使用。

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

作用范围:
主程序范围有效(main 文件夹范围内)
测试程序范围有效(test 文件夹范围内)
是否参与打包运行(package 指令范围内)

如上图所示,给 junit 依赖通过 scope 标签指定依赖的作用范围。 那么这个依赖就只能作用在测试环境,其他环境下不能使用。
scope 标签的取值范围:
scope 值 主程序 测试程序 打包(运行) 范例 compile(默认) Y Y Y log4j test - Y - junit provided Y Y - servlet-api runtime - Y Y jdbc 驱动 # 生命周期
# 介绍
Maven 的生命周期就是为了对所有的构建过程进行抽象和统一。 描述了一次项目构建,经历哪些阶段。
在 Maven 出现之前,项目构建的生命周期就已经存在,软件开发人员每天都在对项目进行清理,编译,测试及部署。虽然大家都在不停地做构建工作,但公司和公司间、项目和项目间,往往使用不同的方式做类似的工作。
Maven 从大量项目和构建工具中学习和反思,然后总结了一套高度完美的,易扩展的项目构建生命周期。这个生命周期包含了项目的清理,初始化,编译,测试,打包,集成测试,验证,部署和站点生成等几乎所有构建步骤。
Maven 对项目构建的生命周期划分为 3 套(相互独立):

clean:清理工作。
default:核心工作。如:编译、测试、打包、安装、部署等。
site:生成报告、发布站点等。
三套生命周期又包含哪些具体的阶段呢,我们来看下面这幅图:

我们看到这三套生命周期,里面有很多很多的阶段,这么多生命周期阶段,其实我们常用的并不多,主要关注以下几个:
・clean:移除上一次构建生成的文件
・compile:编译项目源代码
・test:使用合适的单元测试框架运行测试 (junit)
・package:将编译后的文件打包,如:jar、war 等
・install:安装项目到本地仓库
Maven 的生命周期是抽象的,这意味着生命周期本身不做任何实际工作。在 Maven 的设计中,实际任务(如源代码编译)都交由插件来完成。

IDEA 工具为了方便程序员使用 maven 生命周期,在右侧的 maven 工具栏中,已给出快速访问通道

生命周期的顺序是:clean --> validate --> compile --> test --> package --> verify --> install --> site --> deploy
我们需要关注的就是:clean --> compile --> test --> package --> install
说明:在同一套生命周期中,我们在执行后面的生命周期时,前面的生命周期都会执行。
思考:当运行 package 生命周期时,clean、compile 生命周期会不会运行?
clean 不会运行,compile 会运行。 因为 compile 与 package 属于同一套生命周期,而 clean 与 package 不属于同一套生命周期。
# 执行
在日常开发中,当我们要执行指定的生命周期时,有两种执行方式:
- 在 idea 工具右侧的 maven 工具栏中,选择对应的生命周期,双击执行
- 在 DOS 命令行中,通过 maven 命令执行
# SpringBoot 基础
# SpringBoot 基础
- Spring 的官网 (https://spring.io)
# 1. SpringBootWeb 快速入门
# 1.1 需求
需求:基于 SpringBoot 的方式开发一个 web 应用,浏览器发起请求 /hello 后,给浏览器返回字符串 “Hello World ~”。

# 1.2 开发步骤
第 1 步:创建 SpringBoot 工程项目
第 2 步:定义 HelloController 类,添加方法 hello,并添加注解
第 3 步:测试运行
# 1.2.1 创建 SpringBoot 工程(需要联网)
基于 Spring 官方骨架,创建 SpringBoot 工程。
<img src="../../BaiduNetdiskDownload/day04-Maven-SpringBootWeb 入门 /day04-Maven-SpringBootWeb 入门 / 讲义 / 02. SpringBootWeb 入门 /assets/image-20221201184702136.png" alt="image-20221201184702136" style="zoom:80%;" />
基本信息描述完毕之后,勾选 web 开发相关依赖。
<img src="../../BaiduNetdiskDownload/day04-Maven-SpringBootWeb 入门 /day04-Maven-SpringBootWeb 入门 / 讲义 / 02. SpringBootWeb 入门 /assets/image-20221201184850248.png" alt="image-20221201184850248" style="zoom:80%;" />
点击 Finish 之后,就会联网创建这个 SpringBoot 工程,创建好之后,结构如下:
- 注意:在联网创建过程中,会下载相关资源 (请耐心等待)

# 1.2.2 定义请求处理类
在 com.itheima 这个包下创建一个子包 controller
<img src="../../BaiduNetdiskDownload/day04-Maven-SpringBootWeb 入门 /day04-Maven-SpringBootWeb 入门 / 讲义 / 02. SpringBootWeb 入门 /assets/image-20221201190541295.png" alt="image-20221201190541295" style="zoom:80%;" />
然后在 controller 包下新建一个类:HelloController
<img src="../../BaiduNetdiskDownload/day04-Maven-SpringBootWeb 入门 /day04-Maven-SpringBootWeb 入门 / 讲义 / 02. SpringBootWeb 入门 /assets/image-20221201190825439.png" alt="image-20221201190825439" style="zoom:80%;" />
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.2.3 运行测试
运行 SpringBoot 自动生成的引导类


打开浏览器,输入 http://localhost:8080/hello
<img src="../../BaiduNetdiskDownload/day04-Maven-SpringBootWeb 入门 /day04-Maven-SpringBootWeb 入门 / 讲义 / 02. SpringBootWeb 入门 /assets/image-20220823195048415.png" style="zoom:80%;" />
# 1.3 Web 分析

浏览器:
输入网址:
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 的资源
- 把资源发送给浏览器
我们在 JavaSE 阶段学习网络编程时,有讲过网络三要素:
- IP :网络中计算机的唯一标识
- 端口 :计算机中运行程序的唯一标识
- 协议 :网络中计算机之间交互的规则
问题:浏览器和服务器两端进行数据交互,使用什么协议?
答案:http 协议
# 2. HTTP 协议

HTTP:Hyper Text Transfer Protocol (超文本传输协议),规定了浏览器与服务器之间数据传输的规则。
- http 是互联网上应用最为广泛的一种网络协议
- http 协议要求:浏览器在向服务器发送请求数据时,或是服务器在向浏览器发送响应数据时,都必须按照固定的格式进行数据传输
如果想知道 http 协议的数据传输格式有哪些,可以打开浏览器,点击 F12
打开开发者工具,点击 Network
来查看

浏览器向服务器进行请求时:
- 服务器按照固定的格式进行解析

服务器向浏览器进行响应时:
- 浏览器按照固定的格式进行解析

所以,我们学习 HTTP 主要就是学习请求和响应数据的具体格式内容。
# 2.2.2 特点
我们刚才初步认识了 HTTP 协议,那么我们在看看 HTTP 协议有哪些特点:
** 基于 TCP 协议: ** 面向连接,安全
TCP 是一种面向连接的 (建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全
基于请求 - 响应模型: 一次请求对应一次响应(先请求后响应)
请求和响应是一一对应关系,没有请求,就没有响应
HTTP 协议是无状态协议: 对于数据没有记忆能力。每次请求 - 响应都是独立的
无状态指的是客户端发送 HTTP 请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。
- 缺点:多次请求间不能共享数据
- 优点:速度快
请求之间无法共享数据会引发的问题:
- 如:京东购物。加入购物车和去购物车结算是两次请求
- 由于 HTTP 协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
- 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据
具体使用的时候,我们发现京东是可以正常展示数据的,原因是 Java 早已考虑到这个问题,并提出了使用会话技术 (Cookie、Session) 来解决这个问题。具体如何来做,我们后面课程中会讲到。
刚才提到 HTTP 协议是规定了请求和响应数据的格式,那具体的格式是什么呢?
# 2.2 HTTP - 请求协议
浏览器和服务器是按照 HTTP 协议进行数据通信的。
HTTP 协议又分为:请求协议和响应协议
- 请求协议:浏览器将数据以请求格式发送到服务器
- 包括:请求行、请求头 、请求体
- 响应协议:服务器将数据以响应格式返回给浏览器
- 包括:响应行 、响应头 、响应体
在 HTTP1.1 版本中,浏览器访问服务器的几种方式:
| 请求方式 | 请求说明 |
| :------: | :----------------------------------------------------------- |
| GET | 获取资源。<br/> 向特定的资源发出请求。例:http://www.baidu.com/s?wd=itheima |
| POST | 传输实体主体。<br/> 向指定资源提交数据进行处理请求(例:上传文件),数据被包含在请求体中。 |
| OPTIONS | 返回服务器针对特定资源所支持的 HTTP 请求方式。<br/> 因为并不是所有的服务器都支持规定的方法,为了安全有些服务器可能会禁止掉一些方法,例如:DELETE、PUT 等。那么 OPTIONS 就是用来询问服务器支持的方法。 |
| HEAD | 获得报文首部。<br/>HEAD 方法类似 GET 方法,但是不同的是 HEAD 方法不要求返回数据。通常用于确认 URI 的有效性及资源更新时间等。 |
| PUT | 传输文件。<br/>PUT 方法用来传输文件。类似 FTP 协议,文件内容包含在请求报文的实体中,然后请求保存到 URL 指定的服务器位置。 |
| DELETE | 删除文件。<br/> 请求服务器删除 Request-URI 所标识的资源 |
| TRACE | 追踪路径。<br/> 回显服务器收到的请求,主要用于测试或诊断 |
| CONNECT | 要求用隧道协议连接代理。<br/>HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器 |
在我们实际应用中常用的也就是 :GET、POST
GET 方式的请求协议:

请求行 :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 请求的请求参数在请求行中,故不需要设置请求体
POST 方式的请求协议:

- 请求行 (以上图中红色部分):包含请求方式、资源路径、协议 / 版本
- 请求方式:POST
- 资源路径:/brand
- 协议 / 版本:HTTP/1.1
- 请求头 (以上图中黄色部分)
- 请求体 (以上图中绿色部分) :存储请求参数
- 请求体和请求头之间是有一个空行隔开(作用:用于标记请求头结束)
GET 请求和 POST 请求的区别:
| 区别方式 | GET 请求 | POST 请求 |
| ------------ | ------------------------------------------------------------ | -------------------- |
| 请求参数 | 请求参数在请求行中。<br/> 例:/brand/findAll?name=OPPO&status=1 | 请求参数在请求体中 |
| 请求参数长度 | 请求参数长度有限制 (浏览器不同限制也不同) | 请求参数长度没有限制 |
| 安全性 | 安全性低。原因:请求参数暴露在浏览器地址栏中。 | 安全性相对高 |
与 HTTP 的请求一样,HTTP 响应的数据也分为 3 部分:响应行、响应头 、响应体

响应行 (以上图中红色部分):响应数据的第一行。响应行由
协议及版本
、响应状态码
、状态码描述
组成- 协议 / 版本: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 ;
- 响应体 (以上图中绿色部分): 响应数据的最后一部分。存储响应的数据
响应体和响应头之间有一个空行隔开(作用:用于标记响应头结束)
# 2.3.2 响应状态码
| 状态码分类 | 说明 |
| ---------- | ------------------------------------------------------------ |
| 1xx | 响应中 --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 |
| 2xx | 成功 --- 表示请求已经被成功接收,处理已完成 |
| 3xx | 重定向 --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 |
| 4xx | 客户端错误 --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 |
| 5xx | 服务器端错误 --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP 版本不支持等 |
参考:资料 / SpringbootWeb / 响应状态码.md
关于响应状态码,我们先主要认识三个状态码,其余的等后期用到了再去掌握:
- 200 ok 客户端请求成功
- 404 Not Found 请求资源不存在
- 500 Internal Server Error 服务端发生不可预期的错误
# 2.4 HTTP - 协议解析
将资料中准备好的 Demo 工程,导入到我们的 IDEA 中,有一个 Server.java 类,这里面就是自定义的一个服务器代码,主要使用到的是 ServerSocket
和 Socket
说明:以下代码大家不需要自己写,我们主要是通过代码,让大家了解到服务器针对 HTTP 协议的解析机制
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 程序:

浏览器输入: http://localhost:8080
就会访问到 ServerSocket 程序
- ServerSocket 程序,会读取服务器上
html/a.html
文件,并把文件数据发送给浏览器 - 浏览器接收到 a.html 文件中的数据后进行解析,显示以下内容

现在大家知道了服务器是可以使用 java 完成编写,是可以接受页面发送的请求和响应数据给前端浏览器的,而在开发中真正用到的 Web 服务器,我们不会自己写的,都是使用目前比较流行的 web 服务器。如:Tomcat

# 3. WEB 服务器 - Tomcat
服务器硬件
- 指的也是计算机,只不过服务器要比我们日常使用的计算机大很多。

服务器,也称伺服器。是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。
服务器的构成包括处理器、硬盘、内存、系统总线等,和通用的计算机架构类似,但是由于需要提供高可靠的服务,因此在处理能力、稳定性、可靠性、安全性、可扩展性、可管理性等方面要求较高。
在网络环境下,根据服务器提供的服务类型不同,可分为:文件服务器,数据库服务器,应用程序服务器,WEB 服务器等。
服务器只是一台设备,必须安装服务器软件才能提供相应的服务。
服务器软件
服务器软件:基于 ServerSocket 编写的程序
- 服务器软件本质是一个运行在服务器设备上的应用程序
- 能够接收客户端请求,并根据请求给客户端响应数据

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

Web 服务器是安装在服务器端的一款软件,将来我们把自己写的 Web 项目部署到 Tomcat 服务器软件中,当 Web 服务器软件启动后,部署在 Web 服务器软件中的页面就可以直接通过浏览器来访问了。
Web 服务器软件使用步骤
- 准备静态资源
- 下载安装 Web 服务器软件
- 将静态资源部署到 Web 服务器上
- 启动 Web 服务器使用浏览器访问对应的资源
第 1 步:准备静态资源
- 在提供的资料中找到静态资源文件

第 2 步:下载安装 Web 服务器软件

第 3 步:将静态资源部署到 Web 服务器上

第 4 步:启动 Web 服务器使用浏览器访问对应的资源

浏览器输入: http://localhost:8080/demo/index.html

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

Tomcat 就是一款软件,我们主要是以学习如何去使用为主。具体我们会从以下这些方向去学习:
简介:初步认识下 Tomcat
基本使用:安装、卸载、启动、关闭、配置和项目部署,这些都是对 Tomcat 的基本操作
IDEA 中如何创建 Maven Web 项目
IDEA 中如何使用 Tomcat, 后面这两个都是我们以后开发经常会用到的方式
首选我们来认识下 Tomcat。
# 3.1.3 Tomcat
Tomcat 服务器软件是一个免费的开源的 web 应用服务器。是 Apache 软件基金会的一个核心项目。由 Apache,Sun 和其他一些公司及个人共同开发而成。
由于 Tomcat 只支持 Servlet/JSP 少量 JavaEE 规范,所以是一个开源免费的轻量级 Web 服务器。
JavaEE 规范: JavaEE => Java Enterprise Edition (Java 企业版)
avaEE 规范就是指 Java 企业级开发的技术规范总和。包含 13 项技术规范:JDBC、JNDI、EJB、RMI、JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF
因为 Tomcat 支持 Servlet/JSP 规范,所以 Tomcat 也被称为 Web 容器、Servlet 容器。JavaWeb 程序需要依赖 Tomcat 才能运行。
Tomcat 的官网: https://tomcat.apache.org/

# 3.2 基本使用
# 3.2.1 下载
直接从官方网站下载:https://tomcat.apache.org/download-90.cgi

Tomcat 软件类型说明:
- tar.gz 文件,是 linux 和 mac 操作系统下的压缩版本
- zip 文件,是 window 操作系统下压缩版本(我们选择 zip 文件)
大家可以自行下载,也可以直接使用资料中已经下载好的资源,
Tomcat 的软件程序 :/ 资料 / SpringbootWeb/apache-tomcat-9.0.27-windows-x64.zip

# 3.2.2 安装与卸载
安装: Tomcat 是绿色版,直接解压即安装
在 E 盘的 develop 目录下,将
apache-tomcat-9.0.27-windows-x64.zip
进行解压缩,会得到一个apache-tomcat-9.0.27
的目录,Tomcat 就已经安装成功。

注意,Tomcat 在解压缩的时候,解压所在的目录可以任意,但最好解压到一个不包含中文和空格的目录,因为后期在部署项目的时候,如果路径有中文或者空格可能会导致程序部署失败。
打开 apache-tomcat-9.0.27
目录就能看到如下目录结构,每个目录中包含的内容需要认识下

bin:目录下有两类文件,一种是以 .bat
结尾的,是 Windows 系统的可执行文件,一种是以 .sh
结尾的,是 Linux 系统的可执行文件。
webapps:就是以后项目部署的目录
** 卸载:** 卸载比较简单,可以直接删除目录即可
# 3.2.3 启动与关闭
启动 Tomcat
- 双击 tomcat 解压目录 /bin/startup.bat 文件即可启动 tomcat

注意: tomcat 服务器启动后,黑窗口不会关闭,只要黑窗口不关闭,就证明 tomcat 服务器正在运行

Tomcat 的默认端口为 8080,所以在浏览器的地址栏输入: http://127.0.0.1:8080
即可访问 tomcat 服务器
127.0.0.1 也可以使用 localhost 代替。如:
http://localhost:8080

- 能看到以上图片中 Apache Tomcat 的内容就说明 Tomcat 已经启动成功
注意事项 :Tomcat 启动的过程中,遇到控制台有中文乱码时,可以通常修改 conf/logging.prooperties 文件解决

关闭: 关闭有三种方式
1、强制关闭:直接 x 掉 Tomcat 窗口(不建议)

2、正常关闭:bin\shutdown.bat

3、正常关闭:在 Tomcat 启动窗口中按下 Ctrl+C
说明:如果按下 Ctrl+C 没有反映,可以多按几次
# 3.2.4 常见问题
问题 1:Tomcat 启动时,窗口一闪而过
- 检查 JAVA_HOME 环境变量是否正确配置

问题 2:端口号冲突

发生问题的原因:Tomcat 使用的端口被占用了。
解决方案:换 Tomcat 端口号
- 要想修改 Tomcat 启动的端口号,需要修改 conf/server.xml 文件
<img src="../../BaiduNetdiskDownload/day04-Maven-SpringBootWeb 入门 /day04-Maven-SpringBootWeb 入门 / 讲义 / 02. SpringBootWeb 入门 /assets/image-20220825084017185.png" alt="image-20220825084017185" style="zoom:80%;" />
注: HTTP 协议默认端口号为 80,如果将 Tomcat 端口号改为 80,则将来访问 Tomcat 时,将不用输入端口号。
# 3.3 入门程序解析
关于 web 开发的基础知识,我们可以告一段落了。下面呢,我们在基于今天的核心技术点 SpringBoot 快速入门案例进行分析。
# 3.3.1 Spring 官方骨架
之前我们创建的 SpringBoot 入门案例,是基于 Spring 官方提供的骨架实现的。
Spring 官方骨架,可以理解为 Spring 官方为程序员提供一个搭建项目的模板。

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




Spring 官方生成的 SpringBoot 项目,怎么使用呢?
- 解压缩后,就会得到一个 SpringBoot 项目工程


打开 pom.xml 文件,我们可以看到 springboot 项目中引入了 web 依赖和 test 依赖

结论:不论使用 IDEA 创建 SpringBoot 项目,还是直接在官方网站利用骨架生成 SpringBoot 项目,项目的结构和 pom.xml 文件中内容是相似的。
# 3.3.2 起步依赖
在我们之前讲解的 SpringBoot 快速入门案例中,同样也引用了:web 依赖和 test 依赖

spring-boot-starter-web 和 spring-boot-starter-test,在 SpringBoot 中又称为:起步依赖
而在 SpringBoot 的项目中,有很多的起步依赖,他们有一个共同的特征:就是以 spring-boot-starter-
作为开头。在以后大家遇到 spring-boot-starter-xxx 这类的依赖,都为起步依赖。
起步依赖有什么特殊之处呢,这里我们以入门案例中引入的起步依赖做为讲解:
- spring-boot-starter-web:包含了 web 应用开发所需要的常见依赖
- spring-boot-starter-test:包含了单元测试所需要的常见依赖
spring-boot-starter-web 内部把关于 Web 开发所有的依赖都已经导入并且指定了版本,只需引入
spring-boot-starter-web
依赖就可以实现 Web 开发的需要的功能
Spring 的官方提供了很多现成的 starter (起步依赖),我们在开发相关应用时,只需要引入对应的 starter 即可。
官方地址:https://docs.spring.io/spring-boot/docs/2.7.2/reference/htmlsingle/#using.build-systems.starters

每一个起步依赖,都用于开发一个特定的功能。
举例:当我们开发中需要使用 redis 数据库时,只需要在 SpringBoot 项目中,引入:spring-boot-starter-redis ,即可导入 redis 开发所需要的依赖。
# 3.3.2 SpringBoot 父工程
在我们之前开发的 SpringBoot 入门案例中,我们通过 maven 引入的依赖,是没有指定具体的依赖版本号的。

为什么没有指定 <version> 版本号,可以正常使用呢?
- 因为每一个 SpringBoot 工程,都有一个父工程。依赖的版本号,在父工程中统一管理。

# 3.3.3 内嵌 Tomcat
问题:为什么我们之前书写的 SpringBoot 入门程序中,并没有把程序部署到 Tomcat 的 webapps 目录下,也可以运行呢?
原因呢,是因为在我们的 SpringBoot 中,引入了 web 运行环境 (也就是引入 spring-boot-starter-web 起步依赖),其内部已经集成了内置的 Tomcat 服务器。
我们可以通过 IDEA 开发工具右侧的 maven 面板中,就可以看到当前工程引入的依赖。其中已经将 Tomcat 的相关依赖传递下来了,也就是说在 SpringBoot 中可以直接使用 Tomcat 服务器。

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

# SpringBootWeb 请求响应
# 前言
在上一次的课程中,我们开发了 springbootweb 的入门程序。 基于 SpringBoot 的方式开发一个 web 应用,浏览器发起请求 /hello 后 ,给浏览器返回字符串 “Hello World ~”。
其实呢,是我们在浏览器发起请求,请求了我们的后端 web 服务器 (也就是内置的 Tomcat)。而我们在开发 web 程序时呢,定义了一个控制器类 Controller,请求会被部署在 Tomcat 中的 Controller 接收,然后 Controller 再给浏览器一个响应,响应一个字符串 “Hello World”。 而在请求响应的过程中是遵循 HTTP 协议的。
但是呢,这里要告诉大家的时,其实在 Tomcat 这类 Web 服务器中,是不识别我们自己定义的 Controller 的。但是我们前面讲到过 Tomcat 是一个 Servlet 容器,是支持 Serlvet 规范的,所以呢,在 tomcat 中是可以识别 Servlet 程序的。 那我们所编写的 XxxController 是如何处理请求的,又与 Servlet 之间有什么联系呢?
其实呢,在 SpringBoot 进行 web 程序开发时,它内置了一个核心的 Servlet 程序 DispatcherServlet,称之为 核心控制器。 DispatcherServlet 负责接收页面发送的请求,然后根据执行的规则,将请求再转发给后面的请求处理器 Controller,请求处理器处理完请求之后,最终再由 DispatcherServlet 给浏览器响应数据。
那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达 tomcat 之后,tomcat 会负责解析这些请求数据,然后呢将解析后的请求数据会传递给 Servlet 程序的 HttpServletRequest 对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而 Tomcat,还给 Servlet 程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。
那上述所描述的这种浏览器 / 服务器的架构模式呢,我们称之为:BS 架构。
・BS 架构:Browser/Server,浏览器 / 服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。
# 1. 请求
在本章节呢,我们主要讲解,如何接收页面传递过来的请求数据。
# 1.1 Postman
之前我们课程中有提到当前最为主流的开发模式:前后端分离
在这种模式下,前端技术人员基于 "接口文档",开发前端程序;后端技术人员也基于 "接口文档",开发后端程序。
由于前后端分离,对我们后端技术人员来讲,在开发过程中,是没有前端页面的,那我们怎么测试自己所开发的程序呢?
方式 1:像之前 SpringBoot 入门案例中一样,直接使用浏览器。在浏览器中输入地址,测试后端程序。
- 弊端:在浏览器地址栏中输入地址这种方式都是 GET 请求,如何我们要用到 POST 请求怎么办呢?
- 要解决 POST 请求,需要程序员自己编写前端代码(比较麻烦)
方式 2:使用专业的接口测试工具(课程中我们使用 Postman 工具)
# 介绍
Postman 是一款功能强大的网页调试与发送网页 HTTP 请求的 Chrome 插件。
Postman 原是 Chrome 浏览器的插件,可以模拟浏览器向后端服务器发起任何形式 (如:get、post) 的 HTTP 请求
使用 Postman 还可以在发起请求时,携带一些请求参数、请求头等信息
作用:常用于进行接口测试
特征
- 简单
- 实用
- 美观
- 大方
# 1.2 简单参数
简单参数:在向服务器发起请求时,向服务器传递的是一些普通的请求数据。
那么在后端程序中,如何接收传递过来的普通参数数据呢?
我们在这里讲解两种方式:
- 原始方式
- SpringBoot 方式
# 1.2.1 原始方式
在原始的 Web 程序当中,需要通过 Servlet 中提供的 API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:
Tomcat 接收到 http 请求时:把请求的相关信息封装到 HttpServletRequest 对象中
在 Controller 中,我们要想获取 Request 对象,可以直接在方法的形参中声明 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 个请求参数) | |
// 第 1 个请求参数: name=Tom 参数名:name,参数值:Tom | |
// 第 2 个请求参数: age=10 参数名:age , 参数值:10 | |
String name = request.getParameter("name");//name 就是请求参数名 | |
String ageStr = request.getParameter("age");//age 就是请求参数名 | |
int age = Integer.parseInt(ageStr);// 需要手动进行类型转换 | |
System.out.println(name+" : "+age); | |
return "OK"; | |
} | |
} |
# 1.2.2 SpringBoot 方式
在 Springboot 的环境中,对原始的 API 进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。
@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"; | |
} | |
} |
postman 测试 (GET 请求):
postman 测试 (POST 请求):
结论:不论是 GET 请求还是 POST 请求,对于简单参数来讲,只要保证请求参数名和 Controller 方法中的形参名保持一致,就可以获取到请求参数中的数据值。
# 1.2.3 参数名不一致
如果方法形参名称与请求参数名称不一致,controller 方法中的形参还能接收到请求参数值吗?
@RestController | |
public class RequestController { | |
// http://localhost:8080/simpleParam?name=Tom&age=20 | |
// 请求参数名:name | |
//springboot 方式 | |
@RequestMapping("/simpleParam") | |
public String simpleParam(String username , Integer age ){// 请求参数名和形参名不相同 | |
System.out.println(username+" : "+age); | |
return "OK"; | |
} | |
} |
答案:运行没有报错。 controller 方法中的 username 值为:null,age 值为 20
- 结论:对于简单参数来讲,请求参数名和 controller 方法中的形参名不一致时,无法接收到请求数据
那么如果我们开发中,遇到了这种请求参数名和 controller 方法中的形参名不相同,怎么办?
解决方案:可以使用 Spring 提供的 @RequestParam 注解完成映射
在方法形参前面加上 @RequestParam 然后通过 value 属性执行请求参数名,从而完成映射。代码如下:
@RestController | |
public class RequestController { | |
// http://localhost:8080/simpleParam?name=Tom&age=20 | |
// 请求参数名:name | |
//springboot 方式 | |
@RequestMapping("/simpleParam") | |
public String simpleParam(@RequestParam("name") String username , Integer age ){ | |
System.out.println(username+" : "+age); | |
return "OK"; | |
} | |
} |
注意事项:
@RequestParam 中的 required 属性默认为 true(默认值也是 true),代表该请求参数必须传递,如果不传递将报错
如果该参数是可选的,可以将 required 属性设置为 false
@RequestMapping("/simpleParam") public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){ System.out.println(username+ ":" + age); return "OK";}
# 1.3 实体参数
在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端 controller 方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。
此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同
# 1.3.1 简单实体对象
定义 POJO 实体类:
public class User { | |
private String name; | |
private Integer age; | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public Integer getAge() { | |
return age; | |
} | |
public void setAge(Integer age) { | |
this.age = age; | |
} | |
@Override | |
public String toString() { | |
return "User{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
'}'; | |
} | |
} |
Controller 方法:
@RestController | |
public class RequestController { | |
// 实体参数:简单实体对象 | |
@RequestMapping("/simplePojo") | |
public String simplePojo(User user){ | |
System.out.println(user); | |
return "OK"; | |
} | |
} |
Postman 测试:
- 参数名和实体类属性名一致时
- 参数名和实体类属性名不一致时
# 1.3.2 复杂实体对象
上面我们讲的呢是简单的实体对象,下面我们在来学习下复杂的实体对象。
复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:
- User 类中有一个 Address 类型的属性(Address 是一个实体类)
复杂实体对象的封装,需要遵守如下规则:
- 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。
定义 POJO 实体类:
- Address 实体类
public class Address { | |
private String province; | |
private String city; | |
public String getProvince() { | |
return province; | |
} | |
public void setProvince(String province) { | |
this.province = province; | |
} | |
public String getCity() { | |
return city; | |
} | |
public void setCity(String city) { | |
this.city = city; | |
} | |
@Override | |
public String toString() { | |
return "Address{" + | |
"province='" + province + '\'' + | |
", city='" + city + '\'' + | |
'}'; | |
} | |
} |
- User 实体类
public class User { | |
private String name; | |
private Integer age; | |
private Address address; // 地址对象 | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public Integer getAge() { | |
return age; | |
} | |
public void setAge(Integer age) { | |
this.age = age; | |
} | |
public Address getAddress() { | |
return address; | |
} | |
public void setAddress(Address address) { | |
this.address = address; | |
} | |
@Override | |
public String toString() { | |
return "User{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", address=" + address + | |
'}'; | |
} | |
} |
Controller 方法:
@RestController | |
public class RequestController { | |
// 实体参数:复杂实体对象 | |
@RequestMapping("/complexPojo") | |
public String complexPojo(User user){ | |
System.out.println(user); | |
return "OK"; | |
} | |
} |
Postman 测试:
# 1.4 数组集合参数
数组集合参数的使用场景:在 HTML 的表单中,有一个表单项是支持多选的 (复选框),可以提交选择的多个值。
多个值是怎么提交的呢?其实多个值也是一个一个的提交。
后端程序接收上述多个值的方式有两种:
- 数组
- 集合
# 1.4.1 数组
数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
Controller 方法:
@RestController | |
public class RequestController { | |
// 数组集合参数 | |
@RequestMapping("/arrayParam") | |
public String arrayParam(String[] hobby){ | |
System.out.println(Arrays.toString(hobby)); | |
return "OK"; | |
} | |
} |
Postman 测试:
在前端请求时,有两种传递形式:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
# 1.4.2 集合
集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系
默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用 @RequestParam 绑定参数关系
Controller 方法:
@RestController | |
public class RequestController { | |
// 数组集合参数 | |
@RequestMapping("/listParam") | |
public String listParam(@RequestParam List<String> hobby){ | |
System.out.println(hobby); | |
return "OK"; | |
} | |
} |
Postman 测试:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
# 1.5 日期参数
上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:
因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过 @DateTimeFormat 注解,以及其 pattern 属性来设置日期的格式。
- @DateTimeFormat 注解的 pattern 属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。
- 后端 controller 方法中,需要使用 Date 类型或 LocalDateTime 类型,来封装传递的参数。
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"; | |
} | |
} |
Postman 测试:
# 1.6 JSON 参数
在学习前端技术时,我们有讲到过 JSON,而在前后端进行交互时,如果是比较复杂的参数,前后端通过会使用 JSON 格式的数据进行传输。 (JSON 是开发中最常用的前后端数据交互方式)
我们学习 JSON 格式参数,主要从以下两个方面着手:
- Postman 在发送请求时,如何传递 json 格式的请求参数
- 在服务端的 controller 方法中,如何接收 json 格式的请求参数
Postman 发送 JSON 格式数据:
服务端 Controller 方法接收 JSON 格式数据:
- 传递 json 格式的参数,在 Controller 中会使用实体类进行封装。
- 封装规则:JSON 数据键名与形参对象属性名相同,定义 POJO 类型形参即可接收参数。需要使用 @RequestBody 标识。
- @RequestBody 注解:将 JSON 数据映射到形参的实体类对象中(JSON 中的 key 和实体类中的属性名保持一致)
实体类:Address
public class Address { | |
private String province; | |
private String city; | |
// 省略 GET , SET 方法 | |
} |
实体类:User
public class User { | |
private String name; | |
private Integer age; | |
private Address address; | |
// 省略 GET , SET 方法 | |
} |
Controller 方法:
@RestController | |
public class RequestController { | |
//JSON 参数 | |
@RequestMapping("/jsonParam") | |
public String jsonParam(@RequestBody User user){ | |
System.out.println(user); | |
return "OK"; | |
} | |
} |
Postman 测试:
# 1.7 路径参数
传统的开发中请求参数是放在请求体 (POST 请求) 传递或跟在 URL 后面通过?key=value 的形式传递 (GET 请求)。
在现在的开发中,经常还会直接在请求的 URL 中传递参数。例如:
http://localhost:8080/user/1
http://localhost:880/user/1/0
上述的这种传递请求参数的形式呢,我们称之为:路径参数。
学习路径参数呢,主要掌握在后端的 controller 方法中,如何接收路径参数。
路径参数:
- 前端:通过请求 URL 直接传递参数
- 后端:使用 {…} 来标识该路径参数,需要使用 @PathVariable 获取路径参数
Controller 方法:
@RestController | |
public class RequestController { | |
// 路径参数 | |
@RequestMapping("/path/{id}") | |
public String pathParam(@PathVariable Integer id){ | |
System.out.println(id); | |
return "OK"; | |
} | |
} |
Postman 测试:
传递多个路径参数:
Postman:
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"; | |
} | |
} |
# 2. 响应
前面我们学习过 HTTL 协议的交互方式:请求响应模式(有请求就有响应)
那么 Controller 程序呢,除了接收请求外,还可以进行响应。
# 2.1 @ResponseBody
在我们前面所编写的 controller 方法中,都已经设置了响应数据。
controller 方法中的 return 的结果,怎么就可以响应给浏览器呢?
答案:使用 @ResponseBody 注解
@ResponseBody 注解:
- 类型:方法注解、类注解
- 位置:书写在 Controller 方法上或类上
- 作用:将方法返回值直接响应给浏览器
- 如果返回值类型是实体对象 / 集合,将会转换为 JSON 格式后在响应给浏览器
但是在我们所书写的 Controller 中,只在类上添加了 @RestController 注解、方法添加了 @RequestMapping 注解,并没有使用 @ResponseBody 注解,怎么给浏览器响应呢?
@RestController | |
public class HelloController { | |
@RequestMapping("/hello") | |
public String hello(){ | |
System.out.println("Hello World ~"); | |
return "Hello World ~"; | |
} | |
} |
原因:在类上添加的 @RestController 注解,是一个组合注解。
- @RestController = @Controller + @ResponseBody
@RestController 源码:
@Target({ElementType.TYPE}) // 元注解(修饰注解的注解) | |
@Retention(RetentionPolicy.RUNTIME) // 元注解 | |
@Documented // 元注解 | |
@Controller | |
@ResponseBody | |
public @interface RestController { | |
@AliasFor( | |
annotation = Controller.class | |
) | |
String value() default ""; | |
} |
结论:在类上添加 @RestController 就相当于添加了 @ResponseBody 注解。
- 类上有 @RestController 注解或 @ResponseBody 注解时:表示当前类下所有的方法返回值做为响应数据
- 方法的返回值,如果是一个 POJO 对象或集合时,会先转换为 JSON 格式,在响应给浏览器
下面我们来测试下响应数据:
@RestController | |
public class ResponseController { | |
// 响应字符串 | |
@RequestMapping("/hello") | |
public String hello(){ | |
System.out.println("Hello World ~"); | |
return "Hello World ~"; | |
} | |
// 响应实体对象 | |
@RequestMapping("/getAddr") | |
public Address getAddr(){ | |
Address addr = new Address();// 创建实体类对象 | |
addr.setProvince("广东"); | |
addr.setCity("深圳"); | |
return addr; | |
} | |
// 响应集合数据 | |
@RequestMapping("/listAddr") | |
public List<Address> listAddr(){ | |
List<Address> list = new ArrayList<>();// 集合对象 | |
Address addr = new Address(); | |
addr.setProvince("广东"); | |
addr.setCity("深圳"); | |
Address addr2 = new Address(); | |
addr2.setProvince("陕西"); | |
addr2.setCity("西安"); | |
list.add(addr); | |
list.add(addr2); | |
return list; | |
} | |
} |
在服务端响应了一个对象或者集合,那私前端获取到的数据是什么样子的呢?我们使用 postman 发送请求来测试下。测试效果如下:
# 2.2 统一响应结果
大家有没有发现一个问题,我们在前面所编写的这些 Controller 方法中,返回值各种各样,没有任何的规范。
如果我们开发一个大型项目,项目中 controller 方法将成千上万,使用上述方式将造成整个项目难以维护。那在真实的项目开发中是什么样子的呢?
在真实的项目开发中,无论是哪种方法,我们都会定义一个统一的返回结果。方案如下:
前端:只需要按照统一格式的返回结果进行解析 (仅一种解析方案),就可以拿到数据。
统一的返回结果使用类来描述,在这个结果中包含:
响应状态码:当前请求是成功,还是失败
状态码信息:给页面的提示信息
返回的数据:给前端响应的数据(字符串、对象、集合)
定义在一个实体类 Result 来包含以上信息。代码如下:
public class Result { | |
private Integer code;// 响应码,1 代表成功;0 代表失败 | |
private String msg; // 响应码 描述字符串 | |
private Object data; // 返回的数据 | |
public Result() { } | |
public Result(Integer code, String msg, Object data) { | |
this.code = code; | |
this.msg = msg; | |
this.data = data; | |
} | |
public Integer getCode() { | |
return code; | |
} | |
public void setCode(Integer code) { | |
this.code = code; | |
} | |
public String getMsg() { | |
return msg; | |
} | |
public void setMsg(String msg) { | |
this.msg = msg; | |
} | |
public Object getData() { | |
return data; | |
} | |
public void setData(Object data) { | |
this.data = 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); | |
} | |
} |
改造 Controller:
@RestController | |
public class ResponseController { | |
// 响应统一格式的结果 | |
@RequestMapping("/hello") | |
public Result hello(){ | |
System.out.println("Hello World ~"); | |
//return new Result(1,"success","Hello World ~"); | |
return Result.success("Hello World ~"); | |
} | |
// 响应统一格式的结果 | |
@RequestMapping("/getAddr") | |
public Result getAddr(){ | |
Address addr = new Address(); | |
addr.setProvince("广东"); | |
addr.setCity("深圳"); | |
return Result.success(addr); | |
} | |
// 响应统一格式的结果 | |
@RequestMapping("/listAddr") | |
public Result listAddr(){ | |
List<Address> list = new ArrayList<>(); | |
Address addr = new Address(); | |
addr.setProvince("广东"); | |
addr.setCity("深圳"); | |
Address addr2 = new Address(); | |
addr2.setProvince("陕西"); | |
addr2.setCity("西安"); | |
list.add(addr); | |
list.add(addr2); | |
return Result.success(list); | |
} | |
} |
使用 Postman 测试:
# 2.3 案例
下面我们通过一个案例,来加强对请求响应的学习。
# 2.3.1 需求说明
需求:加载并解析 xml 文件中的数据,完成数据处理,并在页面展示
获取员工数据,返回统一响应结果,在页面渲染展示
# 2.3.2 准备工作
案例准备:
XML 文件
- 已经准备好 (emp.xml),直接导入进来,放在 src/main/resources 目录下
工具类
- 已经准备好解析 XML 文件的工具类,无需自己实现
- 直接在创建一个包 com.itheima.utils ,然后将工具类拷贝进来
前端页面资源
- 已经准备好,直接拷贝进来,放在 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 下。
# 2.3.3 实现步骤
在 pom.xml 文件中引入 dom4j 的依赖,用于解析 XML 文件
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
引入资料中提供的:解析 XML 的工具类 XMLParserUtils、实体类 Emp、XML 文件 emp.xml
xxxxxxxxxx5 1public interface MyInterface {2 void regularMethod (); // 普通方法,实现类必须提供具体实现 3 default void defaultMethod () {4 System.out.println ("This is a default implementation."); // 默认方法,实现类可以选择性覆盖 5} java
引入资料中提供的静态页面文件,放在 resources 下的 static 目录下
创建 EmpController 类,编写 Controller 程序,处理请求,响应数据
# 2.3.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); | |
} | |
} |
统一返回结果实体类:
public class Result { | |
private Integer code ;//1 成功,0 失败 | |
private String msg; // 提示信息 | |
private Object data; // 数据 date | |
public Result() { | |
} | |
public Result(Integer code, String msg, Object data) { | |
this.code = code; | |
this.msg = msg; | |
this.data = data; | |
} | |
public Integer getCode() { | |
return code; | |
} | |
public void setCode(Integer code) { | |
this.code = code; | |
} | |
public String getMsg() { | |
return msg; | |
} | |
public void setMsg(String msg) { | |
this.msg = msg; | |
} | |
public Object getData() { | |
return data; | |
} | |
public void setData(Object data) { | |
this.data = data; | |
} | |
public static Result success(Object data){ | |
return new Result(1, "success", data); | |
} | |
public static Result success(){ | |
return new Result(1, "success", null); | |
} | |
public static Result error(String msg){ | |
return new Result(0, msg, null); | |
} | |
} |
# 2.3.5 测试
代码编写完毕之后,我们就可以运行引导类,启动服务进行测试了。
使用 Postman 测试:
打开浏览器,在浏览器地址栏输入: http://localhost:8080/emp.html
# 2.3.6 问题分析
上述案例的功能,我们虽然已经实现,但是呢,我们会发现案例中:解析 XML 数据,获取数据的代码,处理数据的逻辑的代码,给页面响应的代码全部都堆积在一起了,全部都写在 controller 方法中了。
当前程序的这个业务逻辑还是比较简单的,如果业务逻辑再稍微复杂一点,我们会看到 Controller 方法的代码量就很大了。
当我们要修改操作数据部分的代码,需要改动 Controller
当我们要完善逻辑处理部分的代码,需要改动 Controller
当我们需要修改数据响应的代码,还是需要改动 Controller
这样呢,就会造成我们整个工程代码的复用性比较差,而且代码难以维护。 那如何解决这个问题呢?其实在现在的开发中,有非常成熟的解决思路,那就是分层开发。
# 3. 分层解耦
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。
这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:
那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
- 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
- 逻辑处理:负责业务逻辑处理的代码。
- 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:
- Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
- Service:业务逻辑层。处理具体的业务逻辑。
- Dao:数据访问层 (Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
基于三层架构的程序执行流程:
- 前端发起的请求,由 Controller 层接收(Controller 响应数据给前端)
- Controller 层调用 Service 层来进行逻辑处理(Service 层处理完后,把处理结果返回给 Controller 层)
- Serivce 层调用 Dao 层(逻辑处理过程中需要用到的一些数据要从 Dao 层获取)
- Dao 层操作文件中的数据(Dao 拿到的数据会返回给 Service 层)
思考:按照三层架构的思想,如何要对业务逻辑 (Service 层) 进行变更,会影响到 Controller 层和 Dao 层吗?
答案:不会影响。 (程序的扩展性、维护性变得更好了)
# 3.1.2 代码拆分
我们使用三层架构思想,来改造下之前的程序:
- 控制层包名:xxxx.controller
- 业务逻辑层包名:xxxx.service
- 数据访问层包名:xxxx.dao
** 控制层:** 接收前端发送的请求,对请求进行处理,并响应数据
@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 -> { | |
// 处理 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("就业指导"); | |
} | |
}); | |
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; | |
} | |
} |
三层架构的好处:
- 复用性强
- 便于维护
- 利用扩展
# 3.2 分层解耦
刚才我们学习过程序分层思想了,接下来呢,我们来学习下程序的解耦思想。
解耦:解除耦合。
# 3.2.1 耦合问题
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
内聚:软件中各个功能模块内部的功能联系。
耦合:衡量软件中各个层 / 模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合。
高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素 (语句、程序段) 之间的联系程度越高,则内聚性越高,即 "高内聚"。
低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。
程序中高内聚的体现:
- EmpServiceA 类中只编写了和员工相关的逻辑处理代码
程序中耦合代码的体现:
- 把业务类变为 EmpServiceB 时,需要修改 controller 层中的代码
高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。
# 3.2.2 解耦思路
之前我们在编写代码时,需要什么对象,就直接 new 一个就可以了。 这种做法呢,层与层之间代码就耦合了,当 service 层的实现变了之后, 我们还需要修改 controller 层的代码。
那应该怎么解耦呢?
- 首先不能在 EmpController 中使用 new 对象。代码如下:
- 此时,就存在另一个问题了,不能 new,就意味着没有业务层对象(程序运行就报错),怎么办呢?
- 我们的解决思路是:
- 提供一个容器,容器中存储一些对象 (例:EmpService 对象)
- controller 程序从容器中获取 EmpService 类型的对象
- 我们的解决思路是:
我们想要实现上述解耦操作,就涉及到 Spring 中的两个核心概念:
控制反转: Inversion Of Control,简称 IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
对象的创建权由程序员主动创建转移到容器 (由容器创建、管理对象)。这个容器称为:IOC 容器或 Spring 容器
依赖注入: Dependency Injection,简称 DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源。
例:EmpController 程序运行时需要 EmpService 对象,Spring 容器就为其提供并注入 EmpService 对象
IOC 容器中创建、管理的对象,称之为:bean 对象
# 3.3 IOC&DI
上面我们引出了 Spring 中 IOC 和 DI 的基本概念,下面我们就来具体学习下 IOC 和 DI 的代码实现。
# 3.3.1 IOC&DI 入门
任务:完成 Controller 层、Service 层、Dao 层的代码解耦
- 思路:
- 删除 Controller 层、Service 层中 new 对象的代码
- Service 层及 Dao 层的实现类,交给 IOC 容器管理
- 为 Controller 及 Service 注入运行时依赖的对象
- Controller 程序中注入依赖的 Service 层对象
- Service 程序中注入依赖的 Dao 层对象
第 1 步:删除 Controller 层、Service 层中 new 对象的代码
第 2 步:Service 层及 Dao 层的实现类,交给 IOC 容器管理
- 使用 Spring 提供的注解:@Component ,就可以实现类交给 IOC 容器管理
第 3 步:为 Controller 及 Service 注入运行时依赖的对象
- 使用 Spring 提供的注解:@Autowired ,就可以实现程序运行时 IOC 容器自动注入需要的依赖对象
完整的三层代码:
- Controller 层:
@RestController | |
public class EmpController { | |
@Autowired // 运行时,从 IOC 容器中获取该类型对象,赋值给该变量 | |
private EmpService empService ; | |
@RequestMapping("/listEmp") | |
public Result list(){ | |
//1. 调用 service, 获取数据 | |
List<Emp> empList = empService.listEmp(); | |
//3. 响应数据 | |
return Result.success(empList); | |
} | |
} |
- Service 层:
@Component // 将当前对象交给 IOC 容器管理,成为 IOC 容器的 bean | |
public class EmpServiceA implements EmpService { | |
@Autowired // 运行时,从 IOC 容器中获取该类型对象,赋值给该变量 | |
private EmpDao empDao ; | |
@Override | |
public List<Emp> listEmp() { | |
//1. 调用 dao, 获取数据 | |
List<Emp> empList = empDao.listEmp(); | |
//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("就业指导"); | |
} | |
}); | |
return empList; | |
} | |
} |
Dao 层:
@Component // 将当前对象交给 IOC 容器管理,成为 IOC 容器的 bean | |
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; | |
} | |
} |
运行测试:
- 启动 SpringBoot 引导类,打开浏览器,输入:http://localhost:8080/emp.html
# 3.3.2 IOC 详解
通过 IOC 和 DI 的入门程序呢,我们已经基本了解了 IOC 和 DI 的基础操作。接下来呢,我们学习下 IOC 控制反转和 DI 依赖注入的细节。
# 3.3.2.1 bean 的声明
前面我们提到 IOC 控制反转,就是将对象的控制权交给 Spring 的 IOC 容器,由 IOC 容器创建及管理对象。IOC 容器创建的对象称为 bean 对象。
在之前的入门案例中,要把某个对象交给 IOC 容器管理,需要在类上添加一个注解:@Component
而 Spring 框架为了更好的标识 web 应用程序开发当中,bean 对象到底归属于哪一层,又提供了 @Component 的衍生注解:
- @Controller (标注在控制层类上)
- @Service (标注在业务层类上)
- @Repository (标注在数据访问层类上)
修改入门案例代码:
- Controller 层:
@RestController //@RestController = @Controller + @ResponseBody | |
public class EmpController { | |
@Autowired // 运行时,从 IOC 容器中获取该类型对象,赋值给该变量 | |
private EmpService empService ; | |
@RequestMapping("/listEmp") | |
public Result list(){ | |
//1. 调用 service, 获取数据 | |
List<Emp> empList = empService.listEmp(); | |
//3. 响应数据 | |
return Result.success(empList); | |
} | |
} |
- Service 层:
@Service | |
public class EmpServiceA implements EmpService { | |
@Autowired // 运行时,从 IOC 容器中获取该类型对象,赋值给该变量 | |
private EmpDao empDao ; | |
@Override | |
public List<Emp> listEmp() { | |
//1. 调用 dao, 获取数据 | |
List<Emp> empList = empDao.listEmp(); | |
//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("就业指导"); | |
} | |
}); | |
return empList; | |
} | |
} |
Dao 层:
@Repository | |
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; | |
} | |
} |
要把某个对象交给 IOC 容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Controller | @Component 的衍生注解 | 标注在控制器类上 |
@Service | @Component 的衍生注解 | 标注在业务类上 |
@Repository | @Component 的衍生注解 | 标注在数据访问类上(由于与 mybatis 整合,用的少) |
@Component | 声明 bean 的基础注解 | 不属于以上三类时,用此注解 |
查看源码:
在 IOC 容器中,每一个 Bean 都有一个属于自己的名字,可以通过注解的 value 属性指定 bean 的名字。如果没有指定,默认为类名首字母小写。
注意事项:
- 声明 bean 的时候,可以通过 value 属性指定 bean 的名字,如果没有指定,默认为类名首字母小写。
- 使用以上四个注解都可以声明 bean,但是在 springboot 集成 web 开发中,声明控制器 bean 只能用 @Controller。
# 3.3.2.2 组件扫描
问题:使用前面学习的四个注解声明的 bean,一定会生效吗?
答案:不一定。(原因:bean 想要生效,还需要被组件扫描)
下面我们通过修改项目工程的目录结构,来测试 bean 对象是否生效:
运行程序后,报错:
为什么没有找到 bean 对象呢?
- 使用四大注解声明的 bean,要想生效,还需要被组件扫描注解 @ComponentScan 扫描
@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,默认扫描的范围是 SpringBoot 启动类所在包及其子包。
- 解决方案:手动添加 @ComponentScan 注解,指定要扫描的包 (仅做了解,不推荐)
推荐做法(如下图):
- 将我们定义的 controller,service,dao 这些包呢,都放在引导类所在包 com.itheima 的子包下,这样我们定义的 bean 就会被自动的扫描到
# 3.3.3 DI 详解
上一小节我们讲解了控制反转 IOC 的细节,接下来呢,我们学习依赖注解 DI 的细节。
依赖注入,是指 IOC 容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
在入门程序案例中,我们使用了 @Autowired 这个注解,完成了依赖注入的操作,而这个 Autowired 翻译过来叫:自动装配。
@Autowired 注解,默认是按照类型进行自动装配的(去 IOC 容器中找某个类型的对象,然后完成注入操作)
入门程序举例:在 EmpController 运行的时候,就要到 IOC 容器当中去查找 EmpService 这个类型的对象,而我们的 IOC 容器中刚好有一个 EmpService 这个类型的对象,所以就找到了这个类型的对象完成注入操作。
那如果在 IOC 容器中,存在多个相同类型的 bean 对象,会出现什么情况呢?
- 程序运行会报错
如何解决上述问题呢?Spring 提供了以下几种解决方案:
@Primary
@Qualifier
@Resource
使用 @Primary 注解:当存在多个相同类型的 Bean 注入时,加上 @Primary 注解,来确定默认的实现。
使用 @Qualifier 注解:指定当前要注入的 bean 对象。 在 @Qualifier 的 value 属性中,指定注入的 bean 的名称。
- @Qualifier 注解不能单独使用,必须配合 @Autowired 使用
使用 @Resource 注解:是按照 bean 的名称进行注入。通过 name 属性指定要注入的 bean 的名称。
面试题 : @Autowird 与 @Resource 的区别
- @Autowired 是 spring 框架提供的注解,而 @Resource 是 JDK 提供的注解
- @Autowired 默认是按照类型注入,而 @Resource 是按照名称注入
# Mybatis
# 1.Mybatis 入门
# 1. 快速入门
需求:使用 Mybatis 查询所有用户数据。
# 1.1 入门程序分析
以前我们是在图形化客户端工具中编写 SQL 查询代码,发送给数据库执行,数据库执行后返回操作结果。
图形化工具会把数据库执行的查询结果,使用表格的形式展现出来
现在使用 Mybatis 操作数据库,就是在 Mybatis 中编写 SQL 查询代码,发送给数据库执行,数据库执行后返回结果。
Mybatis 会把数据库执行的查询结果,使用实体类封装起来(一行记录对应一个实体类对象)
Mybatis 操作数据库的步骤:
准备工作 (创建 springboot 工程、数据库表 user、实体类 User)
引入 Mybatis 的相关依赖,配置 Mybatis (数据库连接信息)
编写 SQL 语句 (注解 / XML)
# 1.2 入门程序实现
# 1.2.1 准备工作
# 1.2.1.1 创建 springboot 工程创建 springboot 工程,并导入 mybatis 的起步依赖、mysql 的驱动包。
项目工程创建完成后,自动在 pom.xml 文件中,导入 Mybatis 依赖和 MySQL 驱动依赖
<!-- 仅供参考:只粘贴了 pom.xml 中部分内容 -->
<dependencies>
<!-- 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>
<!-- spring 单元测试 (集成了 junit) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
# 1.2.2 配置 Mybatis
在之前使用图形化客户端工具,连接 MySQL 数据库时,需要配置:
连接数据库的四大参数:
- MySQL 驱动类
- 登录名
- 密码
- 数据库连接字符串
基于上述分析,在 Mybatis 中要连接数据库,同样也需要以上 4 个参数配置。
在 springboot 项目中,可以编写 application.properties 文件,配置数据库连接信息。我们要连接数据库,就需要配置数据库连接的基本信息,包括:driver-class-name、url 、username,password。
在入门程序中,大家可以直接这么配置,后面会介绍什么是驱动。
application.properties:
#驱动类名称
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=1234
上述的配置,可以直接复制过去,不要敲错了。 全部都是 spring.datasource.xxxx 开头。
# 1.2.3 编写 SQL 语句
在创建出来的 springboot 工程中,在引导类所在包下,在创建一个包 mapper。在 mapper 包下创建一个接口 UserMapper ,这是一个持久层接口(Mybatis 的持久层接口规范一般都叫 XxxMapper)。
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.2.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);
}
}
}
运行结果:
User{id=1, name='白眉鹰王', age=55, gender=1, phone='18800000000'} User{id=2, name='金毛狮王', age=45, gender=1, phone='18800000001'} User{id=3, name='青翼蝠王', age=38, gender=1, phone='18800000002'} User{id=4, name='紫衫龙王', age=42, gender=2, phone='18800000003'} User{id=5, name='光明左使', age=37, gender=1, phone='18800000004'} User{id=6, name='光明右使', age=48, gender=1, phone='18800000005'}
# 1.3 解决 SQL 警告与提示
默认我们在 UserMapper 接口上加的 @Select 注解中编写 SQL 语句是没有提示的。 如果想让 idea 给我们提示对应的 SQL 语句,我们需要在 IDEA 中配置与 MySQL 数据库的链接。
默认我们在 UserMapper 接口上的 @Select 注解中编写 SQL 语句是没有提示的。如果想让 idea 给出提示,可以做如下配置:
配置完成之后,发现 SQL 语句中的关键字有提示了,但还存在不识别表名 (列名) 的情况:
产生原因:Idea 和数据库没有建立连接,不识别表信息
解决方案:在 Idea 中配置 MySQL 数据库连接
在配置的时候指定连接那个数据库,如上图所示连接的就是 mybatis 数据库。
# 2. JDBC 介绍 (了解)
# 2.1 介绍
通过 Mybatis 的快速入门,我们明白了,通过 Mybatis 可以很方便的进行数据库的访问操作。但是大家要明白,其实 java 语言操作数据库呢,只能通过一种方式:使用 sun 公司提供的 JDBC 规范。
Mybatis 框架,就是对原始的 JDBC 程序的封装。
那到底什么是 JDBC 呢,接下来,我们就来介绍一下。
JDBC: (Java DataBase Connectivity),就是使用 Java 语言操作关系型数据库的一套 API。
本质:
sun 公司官方定义的一套操作所有关系型数据库的规范,即接口。
各个数据库厂商去实现这套接口,提供数据库驱动 jar 包。
我们可以使用这套接口 (JDBC) 编程,真正执行的代码是驱动 jar 包中的实现类。
# 2.2 代码
下面我们看看原始的 JDBC 程序是如何操作数据库的。操作步骤如下:
- 注册驱动
- 获取连接对象
- 执行 SQL 语句,返回执行结果
- 处理执行结果
- 释放资源
在 pom.xml 文件中已引入 MySQL 驱动依赖,我们直接编写 JDBC 代码即可
JDBC 具体代码实现:
import com.itheima.pojo.User;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
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 (类):数据库驱动管理类。
作用:
注册驱动
创建 java 代码和数据库之间的连接,即获取 Connection 对象
Connection (接口):建立数据库连接的对象
- 作用:用于建立 java 程序和数据库之间的连接
Statement (接口): 数据库操作对象 (执行 SQL 语句的对象)。
- 作用:用于向数据库发送 sql 语句
ResultSet (接口):结果集对象(一张虚拟表)
- 作用:sql 查询语句的执行结果会封装在 ResultSet 中
通过上述代码,我们看到直接基于 JDBC 程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用 Mybatis 这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率。
# 2.3 问题分析
原始的 JDBC 程序,存在以下几点问题:
- 数据库链接的四要素 (驱动、链接、用户名、密码) 全部硬编码在 java 代码中
- 查询结果的解析及封装非常繁琐
- 每一次查询数据库都需要获取连接,操作完毕后释放连接,资源浪费,性能降低
# 2.4 技术对比
分析了 JDBC 的缺点之后,我们再来看一下在 mybatis 中,是如何解决这些问题的:
数据库连接四要素 (驱动、链接、用户名、密码),都配置在 springboot 默认的配置文件 application.properties 中
查询结果的解析及封装,由 mybatis 自动完成映射封装,我们无需关注
在 mybatis 中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。
使用 SpringBoot+Mybatis 的方式操作数据库,能够提升开发效率、降低资源浪费
而对于 Mybatis 来说,我们在开发持久层程序操作数据库时,需要重点关注以下两个方面:
application.properties
#驱动类名称
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=1234
Mapper 接口(编写 SQL 语句)
@Mapper
public interface UserMapper {
@Select("select id, name, age, gender, phone from user")
public List<User> list();
}
# 3. 数据库连接池
在前面我们所讲解的 mybatis 中,使用了数据库连接池技术,避免频繁的创建连接、销毁连接而带来的资源浪费。
下面我们就具体的了解下数据库连接池。
# 3.1 介绍
没有使用数据库连接池:
- 客户端执行 SQL 语句:要先创建一个新的连接对象,然后执行 SQL 语句,SQL 语句执行后又需要关闭连接对象从而释放资源,每次执行 SQL 时都需要创建连接、销毁链接,这种频繁的重复创建销毁的过程是比较耗费计算机的性能。
数据库连接池是个容器,负责分配、管理数据库连接 (Connection)
- 程序在启动时,会在数据库连接池 (容器) 中,创建一定数量的 Connection 对象
允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
- 客户端在执行 SQL 时,先从连接池中获取一个 Connection 对象,然后在执行 SQL 语句,SQL 语句执行完之后,释放 Connection 时就会把 Connection 对象归还给连接池(Connection 对象可以复用)
释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
- 客户端获取到 Connection 对象了,但是 Connection 对象并没有去访问数据库 (处于空闲),数据库连接池发现 Connection 对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象
数据库连接池的好处:
- 资源重用
- 提升系统响应速度
- 避免数据库连接遗漏
# 3.2 产品
要怎么样实现数据库连接池呢?
官方 (sun) 提供了数据库连接池标准(javax.sql.DataSource 接口)
功能:获取连接
public Connection getConnection() throws SQLException;
第三方组织必须按照 DataSource 接口实现
常见的数据库连接池:
- C3P0
- DBCP
- Druid
- Hikari (springboot 默认)
现在使用更多的是:Hikari、Druid (性能更优越)
- Hikari(追光者) [默认的连接池]
Druid(德鲁伊)
Druid 连接池是阿里巴巴开源的数据库连接池项目
功能强大,性能优秀,是 Java 语言最好的数据库连接池之一
如果我们想把默认的数据库连接池切换为 Druid 数据库连接池,只需要完成以下两步操作即可:
参考官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
- 在 pom.xml 文件中引入依赖
<dependency> | |
<!-- Druid 连接池依赖 --> | |
<groupId>com.alibaba</groupId> | |
<artifactId>druid-spring-boot-starter</artifactId> | |
<version>1.2.8</version> | |
</dependency> |
- 在 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 |
Lombok 是一个实用的 Java 类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的 Java 代码。
通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString 等方法,并可以自动化生成日志变量,简化 java 开发、提高效率。
| 注解 | 作用 |
| ------------------- | ------------------------------------------------------------ |
| @Getter/@Setter | 为所有的属性提供 get/set 方法 |
| @ToString | 会给类自动生成易阅读的 toString 方法 |
| @EqualsAndHashCode | 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 |
| @Data | 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) |
| @NoArgsConstructor | 为实体类生成无参的构造器方法 |
| @AllArgsConstructor | 为实体类生成除了 static 修饰的字段之外带有各参数的构造器方法。 |
# 4.2 使用
第 1 步:在 pom.xml 文件中引入依赖
<!-- 在 springboot 的父工程中,已经集成了 lombok 并指定了版本号,故当前引入依赖时不需要指定 version --> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
</dependency> |
第 2 步:在实体类上添加注解
import lombok.Data; | |
@Data | |
public class User { | |
private Integer id; | |
private String name; | |
private Short age; | |
private Short gender; | |
private String phone; | |
} |
在实体类上添加了 @Data 注解,那么这个类在编译时期,就会生成 getter/setter、equals、hashcode、toString 等方法。
说明:@Data 注解中不包含全参构造方法,通常在实体类上,还会添加上:全参构造、无参构造
import lombok.Data; | |
@Data //getter 方法、setter 方法、toString 方法、hashCode 方法、equals 方法 | |
@NoArgsConstructor // 无参构造 | |
@AllArgsConstructor// 全参构造 | |
public class User { | |
private Integer id; | |
private String name; | |
private Short age; | |
private Short gender; | |
private String phone; | |
} |
Lombok 的注意事项:
- Lombok 会在编译时,会自动生成对应的 java 代码
- 在使用 lombok 时,还需要安装一个 lombok 的插件(新版本的 IDEA 中自带)
# 2.Mybatis 基本操作
# 1.1 删除
# 1.1.1 功能实现
页面原型:
当我们点击后面的 "删除" 按钮时,前端页面会给服务端传递一个参数,也就是该行数据的 ID。 我们接收到 ID 后,根据 ID 删除数据即可。
功能:根据主键删除数据
- SQL 语句
-- 删除id=17的数据 delete from emp where id = 17;
Mybatis 框架让程序员更关注于 SQL 语句
- 接口方法
@Mapper
public interface EmpMapper {
//@Delete("delete from emp where id = 17")
//public void delete();
// 以上 delete 操作的 SQL 语句中的 id 值写成固定的 17,就表示只能删除 id=17 的用户数据
//SQL 语句中的 id 值不能写成固定数值,需要变为动态的数值
// 解决方案:在 delete 方法中添加一个参数 (用户 id),将方法中的参数,传给 SQL 语句
/**
* 根据 id 删除数据
* @param id 用户 id
*/
@Delete("delete from emp where id = #{id}")// 使用 #{key} 方式获取方法中的参数值
public void delete(Integer id);
}
@Delete 注解:用于编写 delete 操作的 SQL 语句
如果 mapper 接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写,如:#{id}、#{value}。但是建议保持名字一致。
- 测试
- 在单元测试类中通过 @Autowired 注解注入 EmpMapper 类型对象
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired // 从 Spring 的 IOC 容器中,获取类型是 EmpMapper 的对象并注入
private EmpMapper empMapper;
@Test
public void testDel(){
// 调用删除方法
empMapper.delete(16);
}
}
# 1.1.2 日志输入
在 Mybatis 当中我们可以借助日志,查看到 sql 语句的执行、执行传递的参数以及执行结果。具体操作如下:
打开 application.properties 文件
开启 mybatis 的日志,并指定输出到控制台
#指定 mybatis 输出日志的位置,输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的 SQL 语句信息:
但是我们发现输出的 SQL 语句:delete from emp where id = ?,我们输入的参数 16 并没有在后面拼接,id 的值是使用?进行占位。那这种 SQL 语句我们称为预编译 SQL。
# 1.1.3 预编译 SQL
# 11.3.1 介绍
预编译 SQL 有两个优势:
- 性能更高
- 更安全 (防止 SQL 注入)
性能更高:预编译 SQL,编译一次之后会将编译后的 SQL 语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)
更安全 (防止 SQL 注入):将敏感字进行转义,保障 SQL 的安全性。
# 1.3.3.2 SQL 注入
SQL 注入:是通过操作输入的数据来修改事先定义好的 SQL 语句,以达到执行代码对服务器进行攻击的方法。
由于没有对用户输入进行充分检查,而 SQL 又是拼接而成,在用户输入参数时,在参数中添加一些 SQL 关键字,达到改变 SQL 运行结果的目的,也可以完成恶意攻击。
# 1.3.3.3 参数占位符
在 Mybatis 中提供的参数占位符有两种:${...} 、#
#
- 执行 SQL 时,会将 #{…} 替换为?,生成预编译 SQL,会自动设置参数值
- 使用时机:参数传递,都使用#
$
- 拼接 SQL。直接将参数拼接在 SQL 语句中,存在 SQL 注入问题
- 使用时机:如果对表名、列表进行动态设置时使用
注意事项:在项目开发中,建议使用 #{...},生成预编译 SQL,防止 SQL 注入安全。
# 1.4 新增
功能:新增员工信息
# 1.4.1 基本新增
员工表结构:
SQL 语句:
insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values ('songyuanqiao','宋远桥',1,'1.jpg',2,'2012-10-09',2,'2022-10-01 10:00:00','2022-10-01 10:00:00');
接口方法:
@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);
}
说明:#{...} 里面写的名称是对象的属性名
测试类:
import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Emp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.time.LocalDateTime;
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testInsert(){
// 创建员工对象
Emp emp = new Emp();
emp.setUsername("tom");
emp.setName("汤姆");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntrydate(LocalDate.of(2000,1,1));
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
// 调用添加方法
empMapper.insert(emp);
}
}
日志输出:
# 1.4.2 主键返回
概念:在数据添加成功后,需要获取插入数据库数据的主键。
如:添加套餐数据时,还需要维护套餐菜品关系表数据。
业务场景:在前面讲解到的苍穹外卖菜品与套餐模块的表结构,菜品与套餐是多对多的关系,一个套餐对应多个菜品。既然是多对多的关系,是不是有一张套餐菜品中间表来维护它们之间的关系。
在添加套餐的时候,我们需要在界面当中来录入套餐的基本信息,还需要来录入套餐与菜品的关联信息。这些信息录入完毕之后,我们一点保存,就需要将套餐的信息以及套餐与菜品的关联信息都需要保存到数据库当中。其实具体的过程包括两步,首先第一步先需要将套餐的基本信息保存了,接下来第二步再来保存套餐与菜品的关联信息。套餐与菜品的关联信息就是往中间表当中来插入数据,来维护它们之间的关系。而中间表当中有两个外键字段,一个是菜品的 ID,就是当前菜品的 ID,还有一个就是套餐的 ID,而这个套餐的 ID 指的就是此次我所添加的套餐的 ID,所以我们在第一步保存完套餐的基本信息之后,就需要将套餐的主键值返回来供第二步进行使用。这个时候就需要用到主键返回功能。
那要如何实现在插入数据之后返回所插入行的主键值呢?
- 默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在 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);
}
测试:
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testInsert(){
// 创建员工对象
Emp emp = new Emp();
emp.setUsername("jack");
emp.setName("杰克");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntrydate(LocalDate.of(2000,1,1));
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
// 调用添加方法
empMapper.insert(emp);
System.out.println(emp.getDeptId());
}
}
# 1.5 更新
功能:修改员工信息
点击 "编辑" 按钮后,会查询所在行记录的员工信息,并把员工信息回显在修改员工的窗体上 (下个知识点学习)
在修改员工的窗体上,可以修改的员工数据:用户名、员工姓名、性别、图像、职位、入职日期、归属部门
思考:在修改员工数据时,要以什么做为条件呢?
答案:员工 id
SQL 语句:
update emp set username = 'linghushaoxia', name = '令狐少侠', gender = 1 , image = '1.jpg' , job = 2, entrydate = '2012-01-01', dept_id = 2, update_time = '2022-10-01 12:12:12' where id = 18;
接口方法:
@Mapper
public interface EmpMapper {
/**
* 根据 id 修改员工信息
* @param emp
*/
@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);
}
测试类:
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testUpdate(){
// 要修改的员工信息
Emp emp = new Emp();
emp.setId(23);
emp.setUsername("songdaxia");
emp.setPassword(null);
emp.setName("老宋");
emp.setImage("2.jpg");
emp.setGender((short)1);
emp.setJob((short)2);
emp.setEntrydate(LocalDate.of(2012,1,1));
emp.setCreateTime(null);
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(2);
// 调用方法,修改员工数据
empMapper.update(emp);
}
}
# 1.6 查询
# 1.6.1 根据 ID 查询
在员工管理的页面中,当我们进行更新数据时,会点击 “编辑” 按钮,然后此时会发送一个请求到服务端,会根据 Id 查询该员工信息,并将员工数据回显在页面上。
SQL 语句:
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;
接口方法:
@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);
}
测试类:
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testGetById(){
Emp emp = empMapper.getById(1);
System.out.println(emp);
}
}
执行结果:
而在测试的过程中,我们会发现有几个字段 (deptId、createTime、updateTime) 是没有数据值的
# 1.6.2 数据封装
我们看到查询返回的结果中大部分字段是有值的,但是 deptId,createTime,updateTime 这几个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?
原因如下:
- 实体类属性名和数据库表查询返回的字段名一致,mybatis 会自动封装。
- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
解决方案:
- 起别名
- 结果映射
- 开启驼峰命名
起别名:在 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);
@Results 源代码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Results {
String id() default "";
Result[] value() default {}; //Result 类型的数组
}
@Result 源代码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Results.class)
public @interface Result {
boolean id() default false;// 表示当前列是否为主键(true: 是主键)
String column() default "";// 指定表中字段名
String property() default "";// 指定类中属性名
Class<?> javaType() default void.class;
JdbcType jdbcType() default JdbcType.UNDEFINED;
Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;
One one() default @One;
Many many() default @Many;
}
开启驼峰命名 (推荐):如果字段名与属性名符合驼峰命名规则,mybatis 会自动通过驼峰命名规则映射
驼峰命名规则: abc_xyz => abcXyz
- 表中字段名:abc_xyz
- 类中属性名:abcXyz
# 在 application.properties 中添加:
mybatis.configuration.map-underscore-to-camel-case=true
要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。
# 1.6.3 条件查询
在员工管理的列表页面中,我们需要根据条件查询员工信息,查询条件包括:姓名、性别、入职时间。
通过页面原型以及需求描述我们要实现的查询:
- 姓名:要求支持模糊匹配
- 性别:要求精确匹配
- 入职时间:要求进行范围查询
- 根据最后修改时间进行降序排序
SQL 语句:
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time
from emp
where name like '%张%'
and gender = 1
and entrydate between '2010-01-01' and '2020-01-01 '
order by update_time desc;
接口方法:
- 方式一
@Mapper
public interface EmpMapper {
@Select("select * from emp " +
"where name like '%${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 注入风险。
- 方式二(解决 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);
}
执行结果:生成的 SQL 都是预编译的 SQL 语句(性能高、安全)
# 1.6.4 参数名说明
在上面我们所编写的条件查询功能中,我们需要保证接口中方法的形参名和 SQL 语句中的参数占位符名相同。
当方法中的形参名和 SQL 语句中的占位符参数名不相同时,就会出现以下问题:
参数名在不同的 SpringBoot 版本中,处理方案还不同:
- 在 springBoot 的 2.x 版本(保证参数名一致)
springBoot 的父工程对 compiler 编译插件进行了默认的参数 parameters 配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以 #{…} 里面可以直接通过形参名获取对应的值
- 在 springBoot 的 1.x 版本 / 单独使用 mybatis(使用 @Param 注解来指定 SQL 语句中的参数名)
在编译时,生成的字节码文件当中,不会保留 Mapper 接口中方法的形参名称,而是使用 var1、var2、... 这样的形参名字,此时要获取参数值时,就要通过 @Param 注解来指定 SQL 语句中的参数名
# 3.Mybatis 的 XML 配置文件
Mybatis 的开发有两种方式:
- 注解
- XML
# 3.1 XML 配置文件规范
使用 Mybatis 的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的 SQL 功能,建议使用 XML 来配置映射语句,也就是将 SQL 语句写在 XML 配置文件中。
在 Mybatis 中使用 XML 映射文件方式开发,需要符合一定的规范:
XML 映射文件的名称与 Mapper 接口名称一致,并且将 XML 映射文件和 Mapper 接口放置在相同包下(同包同名)
XML 映射文件的 namespace 属性为 Mapper 接口全限定名一致
XML 映射文件中 sql 语句的 id 与 Mapper 接口中的方法名一致,并保持返回类型一致。
<select> 标签:就是用于编写 select 查询语句的。
- resultType 属性,指的是查询返回的单条记录所封装的类型。
# 3.2 XML 配置文件实现
第 1 步:创建 XML 映射文件
第 2 步:编写 XML 映射文件
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>
配置:XML 映射文件的 namespace 属性为 Mapper 接口全限定名
<?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="com.itheima.mapper.EmpMapper">
</mapper>
配置:XML 映射文件中 sql 语句的 id 与 Mapper 接口中的方法名一致,并保持返回类型一致
<?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="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>
运行测试类,执行结果:
# 3.3 MybatisX 的使用
MybatisX 是一款基于 IDEA 的快速开发 Mybatis 的插件,为效率而生。
MybatisX 的安装:
可以通过 MybatisX 快速定位:
MybatisX 的使用在后续学习中会继续分享
学习了 Mybatis 中 XML 配置文件的开发方式了,大家可能会存在一个疑问:到底是使用注解方式开发还是使用 XML 方式开发?
官方说明:https://mybatis.net.cn/getting-started.html
** 结论:** 使用 Mybatis 的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的 SQL 功能,建议使用 XML 来配置映射语句。
# 4.Mybatis 动态 SQL
# 1. 动态 SQL-if
<if>
:用于判断条件是否成立。使用 test 属性进行条件判断,如果条件为 true,则拼接 SQL。<if test="条件表达式">
要拼接的sql语句
</if>
# 1 条件查询
- 原有的 SQL 语句
<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>
动态 SQL 语句
<where>
只会在子元素有内容的情况下才插入 where 子句,而且会自动去除子句的开头的 AND 或 OR
<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>
测试方法:
@Test
public void testList(){
// 只有性别
List<Emp> list = empMapper.list(null, (short)1, null, null);
for(Emp emp : list){
System.out.println(emp);
}
}
执行的 SQL 语句:
# 2 更新员工
案例:完善更新员工功能,修改为动态更新员工数据信息
- 动态更新员工信息,如果更新时传递有值,则更新;如果更新时没有传递值,则不更新
- 解决方案:动态 SQL
修改 Mapper 接口:
@Mapper
public interface EmpMapper {
// 删除 @Update 注解编写的 SQL 语句
//update 操作的 SQL 语句编写在 Mapper 映射文件中
public void update(Emp emp);
}
<set>
:动态的在 SQL 语句中插入 set 关键字,并会删掉额外的逗号。(用于 update 语句中)
<?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="com.itheima.mapper.EmpMapper">
<!-- 更新操作 -->
<update id="update">
update emp
<!-- 使用 set 标签,代替 update 语句中的 set 关键字 -->
<set>
<if test="username != null">
username=#{username},
</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>
</mapper>
再次执行测试方法,执行的 SQL 语句:
小结
<if>
用于判断条件是否成立,如果条件为 true,则拼接 SQL
形式:
<if test="name != null"> … </if>
<where>
- where 元素只会在子元素有内容的情况下才插入 where 子句,而且会自动去除子句的开头的 AND 或 OR
<set>
- 动态地在行首插入 SET 关键字,并会删掉额外的逗号。(用在 update 语句中)
# 3 动态 SQL-foreach
案例:员工删除功能(既支持删除单条记录,又支持批量删除)
SQL 语句:
delete from emp where id in (1,2,3);
Mapper 接口:
@Mapper
public interface EmpMapper {
// 批量删除
public void deleteByIds(List<Integer> ids);
}
XML 映射文件:
- 使用
<foreach>
遍历 deleteByIds 方法中传递的参数 ids 集合
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符"
open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>
<?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="com.itheima.mapper.EmpMapper">
<!-- 删除操作 -->
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
执行的 SQL 语句:
# 4 动态 SQL-sql&include
问题分析:
- 在 xml 映射文件中配置的 SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码
我们可以对重复的代码片段进行抽取,将其通过
<sql>
标签封装到一个 SQL 片段,然后再通过<include>
标签进行引用。<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>