Maven

简介

Maven是一款管理和构建java项目的工具

Maven的作用?

  • 方便依赖管理

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

  • 统一项目结构

    提供标准,统一的项目结构

  • 项目构建

    标准跨平台(Linux、Windows、MacOS)的自动化项目结构方式

Maven坐标

什么是坐标?

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

Maven坐标主要组成

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

image-20240121152233994

注意:

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

依赖管理

依赖配置

依赖:指当前项目运行所需要的jar包。一个项目中可以引入多个依赖

例如:在当前工程中,我们需要用到logback来记录日志,此时就可以在maven工程的pom.xml文件中,引入logback的依赖。具体步骤如下:

  1. 在pom.xml中编写标签

  2. 标签中使用引入坐标

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

注意事项:

  1. 如果引入的依赖,在本地仓库中不存在,将会连接远程仓库 / 中央仓库,然后下载依赖(这个过程会比较耗时,耐心等待)
  2. 如果不知道依赖的坐标信息,可以到mvn的中央仓库(https://mvnrepository.com/)中搜索

依赖传递

依赖具有传递性

依赖传递可以分为:

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

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

image-20240121153936896

比如以上图中:

  • projectA依赖了projectB。对于projectA 来说,projectB 就是直接依赖。
  • 而projectB依赖了projectC及其他jar包。 那么此时,在projectA中也会将projectC的依赖传递下来。对于projectA 来说,projectC就是间接依赖。

排除依赖

问题:之前我们讲了依赖具有传递性。那么A依赖B,B依赖C,如果A不想将C依赖进来,是否可以做到?

答案:在maven项目中,我们可以通过排除依赖来实现。

什么是排除依赖?

  • 排除依赖:指主动断开依赖的资源。(被排除的资源无需指定版本)
1
2
3
4
5
6
7
8
9
10
11
12
13
<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包后,默认情况下,可以在任何地方使用。

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

作用范围:

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

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

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

scope 主程序 测试程序 打包(运行) 范例
compile(默认) Y Y Y log4j
test - Y - junit
provided Y Y - servlet-api
runtime - Y Y jdbc驱动

生命周期

Maven的生命周期就是为了对所有的构建过程进行抽象和统一。 描述了一次项目构建,经历哪些阶段。

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

  • clean:清理工作。

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

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

三套生命周期又包含哪些具体的阶段呢, 我们来看下面这幅图:

image-20220616124348972

我们看到这三套生命周期,里面有很多很多的阶段,这么多生命周期阶段,其实我们常用的并不多,主要关注以下几个:

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

• compile:编译项目源代码

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

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

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

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不属于同一套生命周期。

执行

在日常开发中,当我们要执行指定的生命周期时,有两种执行方式:

  1. 在idea工具右侧的maven工具栏中,选择对应的生命周期,双击执行
  2. 在DOS命令行中,通过maven命令执行

HTTP协议

  • 概念:Hyper Text Transfer Protocol ,超文本传输协议 规定了浏览器和服务器之间数据传输的规则
  • 特点:
    1. 基于TCP,面向连接,安全
    2. 基于请求-响应模型的:一次请求对应一次相应
    3. HTTP协议是无状态的协议:对于事务处理没有记忆能力,每次请求-相应都是独立的。
      • 缺点:多次请求之间不能共享数据
      • 优点:速度快

HTTP-请求数据格式

请求方式-GET:请求参数在请求行中,没有请求体,如:/brand/findAll?name=OPPO&status=1。GET请求大小是有限制的

请求方式-POST:请求参数在请求体中,POST请求大小是没有限制的

请求响应介绍-HTTP响应格式

image-20240125152927891

image-20240125152946173

状态码大类

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

常见的响应状态码

状态码 英文描述 解释
==200== OK 客户端请求成功,即处理成功,这是我们最想看到的状态码
302 Found 指示所请求的资源已移动到由Location响应头给定的 URL,浏览器会自动重新访问到这个页面
304 Not Modified 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向
400 Bad Request 客户端请求有语法错误,不能被服务器所理解
403 Forbidden 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源
==404== Not Found 请求资源不存在,一般是URL输入有误,或者网站资源被删除了
405 Method Not Allowed 请求方式有误,比如应该用GET请求方式的资源,用了POST
428 Precondition Required 服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头
429 Too Many Requests 指示用户在给定时间内发送了太多请求(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用
431 Request Header Fields Too Large 请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。
==500== Internal Server Error 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧
503 Service Unavailable 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好

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

Web服务器

  • 对HTTP协议操作进行封装,简化web程序开发
  • 部署web项目,对外提供网上信息浏览服务

Tomcat

  • 一个轻量级的web服务器,支持servlet、jsp等少了javaEE规范。
  • 也被称为web容器,servlet容器

基本使用

  • 安装
  • 卸载
  • 启动:bin/startup.bat
  • 停止:bin/shutdown.bat
  • 部署:把文件夹移动到webapps文件夹下

Springboot内嵌Tomcat

1.起步依赖

  • spring-boot-starter-web
  • spring-boot-starter-test

2.内嵌Tomcat服务器

  • 基于Springboot开发的web应用程序,内置了tomcat服务器,当启动类运行时,会自动启动内嵌的tomcat服务器**

请求响应

请求响应

  • 请求(HttpServletRequest):获取请求数据
  • 相应(HttpServletResponse):设置响应数据
  • BS架构:Browser/Server,浏览器/服务端架构模式,服务端只需要浏览器,应用程序的逻辑和数据都存储在服务器(维护方便 体验一般)
  • CS架构:Client/Server,客户端/服务端架构模式。(开发、维护麻烦 体验不错)

Postman

postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件

作用:常用于进行接口测试

请求

简单参数

1.原始方式获取请求参数

在原始的web程序,获取请求参数,需要通过HttpServletRequest对象手动获取。

  • Controller方法形参中声明HttpServletRequest对象
  • 调用对象的getParameter(参数名)
2.SpringBoot中接收简单参数
  • 请求参数名与方法形参变量名相同
  • 会自动进行类型转换
3.@RequestParam注解
  • 方法形参名称与请求参数名称不匹配,通过该注解完成映射
  • 该注解的required属性默认是true,代表请求参数必须传递,如果设置为false则代表可以不传递 会显示为null

实体参数

实体对象参数:请求参数名与形参对象属性名相同,定义PoJo接收即可

1.简单实体参数
  • 创建一个User的javabeen类 里面封装name、age

2.复杂实体参数
  • 创建一个User的javabeen类 里面封装name、age、adress
  • adress为Adress类 里面封装province、city

postman参数为:

数组集合参数

1.数组参数

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

确保参数名与方法中的形参名一致

2.集合参数

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

1
2
数组参数:请求参数名与形参数组名称相同,可以直接使用数组封装
集合参数:请求参数名与形参中结合变量名相同,通过@RequestParam绑定参数关系

日期参数

日期参数:使用@DataTimeFormat注解 完成日期参数格式转换

Json格式参数

json格式参数:json数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用@RequestBody表示绑定

路径参数

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

多路径:

总结

响应

@ResponseBody

类型:方法注解、类注解

位置:Controller类上/方法上

作用:将方法返回值直接响应,若返回值类型是 实体对象/集合 ,转JSON格式响应

说明:@RestController=@Controller + @ResponseBody

统一响应结果

Result(code、msg、data)

如:

http://localhost:8080/listAddr

案例:获取员工数据,并统一响应结果,在页面渲染

分层解耦

三层架构

介绍

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

单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。

这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。

我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:

image-20221204191650390

那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:

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

按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:

image-20221204193837678

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

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

image-20221204194207812

  • 前端发起的请求,由Controller层接收(Controller响应数据给前端)
  • Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
  • Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
  • Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)

思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?

答案:不会影响。 (程序的扩展性、维护性变得更好了)

代码拆分

我们使用三层架构思想,来改造下之前的程序:

  • 控制层包名:xxxx.controller
  • 业务逻辑层包名:xxxx.service
  • 数据访问层包名:xxxx.dao

image-20221204195812200

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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);
}
}

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

  • 业务接口
1
2
3
4
5
//业务逻辑接口(制定业务标准)
public interface EmpService {
//获取员工列表
public List<Emp> listEmp();
}
  • 业务实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//业务逻辑实现类(按照业务标准实现)
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;
}
}

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

  • 数据访问接口
1
2
3
4
5
//数据访问层接口(制定标准)
public interface EmpDao {
//获取员工列表数据
public List<Emp> listEmp();
}
  • 数据访问实现类
1
2
3
4
5
6
7
8
9
10
11
//数据访问实现类
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-20221204201342490

三层架构的好处:

  1. 复用性强
  2. 便于维护
  3. 利用扩展

分层解耦

刚才我们学习过程序分层思想了,接下来呢,我们来学习下程序的解耦思想。

解耦:解除耦合。

耦合问题

首先需要了解软件开发涉及到的两个概念:内聚和耦合。

  • 内聚:软件中各个功能模块内部的功能联系。

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

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

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

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

程序中高内聚的体现:

  • EmpServiceA类中只编写了和员工相关的逻辑处理代码

image-20221204202531571

程序中耦合代码的体现:

  • 把业务类变为EmpServiceB时,需要修改controller层中的代码

image-20221204203904900

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

解耦思路

之前我们在编写代码时,需要什么对象,就直接new一个就可以了。 这种做法呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。

image-20221204204916033

那应该怎么解耦呢?

  • 首先不能在EmpController中使用new对象。代码如下:

image-20221204205328069

  • 此时,就存在另一个问题了,不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?
    • 我们的解决思路是:
      • 提供一个容器,容器中存储一些对象(例:EmpService对象)
      • controller程序从容器中获取EmpService类型的对象

我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。

    对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器

  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

    程序运行时需要某个资源,此时容器就为其提供这个资源。

    例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象

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

IOC&DI

上面我们引出了Spring中IOC和DI的基本概念,下面我们就来具体学习下IOC和DI的代码实现。

IOC&DI入门

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

  • 思路:
    1. 删除Controller层、Service层中new对象的代码
    2. Service层及Dao层的实现类,交给IOC容器管理
    3. 为Controller及Service注入运行时依赖的对象
      • Controller程序中注入依赖的Service层对象
      • Service程序中注入依赖的Dao层对象

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

image-20221204212807207

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

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

image-20221204213328034

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

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

image-20221204213859112

完整的三层代码:

  • Controller层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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层:

1
2
3
4
5
6
7
8
9
10
11
@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;
}
}

运行测试:

image-20221204185455556

IOC详解

通过IOC和DI的入门程序呢,我们已经基本了解了IOC和DI的基础操作。接下来呢,我们学习下IOC控制反转和DI依赖注入的细节。

bean的声明

前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。

在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component

而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:

  • @Controller (标注在控制层类上)
  • @Service (标注在业务层类上)
  • @Repository (标注在数据访问层类上)

修改入门案例代码:

  • Controller层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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层:

1
2
3
4
5
6
7
8
9
10
11
@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的基础注解 不属于以上三类时,用此注解

查看源码:image-20221204221320230

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

image-20221204222650873

注意事项:

  • 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
  • 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
组件扫描

问题:使用前面学习的四个注解声明的bean,一定会生效吗?

答案:不一定。(原因:bean想要生效,还需要被组件扫描)

下面我们通过修改项目工程的目录结构,来测试bean对象是否生效:

image-20221204223602694

运行程序后,报错:

image-20221204223815554

为什么没有找到bean对象呢?

  • 使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描

@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,==默认扫描的范围是SpringBoot启动类所在包及其子包==。

image-20221204224643683

  • 解决方案:手动添加@ComponentScan注解,指定要扫描的包 (==仅做了解,不推荐==)

image-20221204225437297

推荐做法(如下图):

  • 将我们定义的controller,service,dao这些包呢,都放在引导类所在包com.itheima的子包下,这样我们定义的bean就会被自动的扫描到

image-20221204225815624

DI详解

上一小节我们讲解了控制反转IOC的细节,接下来呢,我们学习依赖注解DI的细节。

依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。

在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。

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

入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。

那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢?

image-20221204232154445

  • 程序运行会报错

image-20221204231616724

如何解决上述问题呢?Spring提供了以下几种解决方案:

  • @Primary

  • @Qualifier

  • @Resource

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

image-20221204232501679

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

  • @Qualifier注解不能单独使用,必须配合@Autowired使用

image-20221204233333606

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

image-20221204233637735

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

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

Mybatis入门

前言

在前面我们学习MySQL数据库时,都是利用图形化客户端工具(如:idea、datagrip),来操作数据库的。

在客户端工具中,编写增删改查的SQL语句,发给MySQL数据库管理系统,由数据库管理系统执行SQL语句并返回执行结果。

增删改操作:返回受影响行数

查询操作:返回结果集(查询的结果)

我们做为后端程序开发人员,通常会使用Java程序来完成对数据库的操作。Java程序操作数据库,现在主流的方式是:Mybatis。

什么是MyBatis?

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

  • MyBatis本是 Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。

  • 官网:https://mybatis.org/mybatis-3/zh/index.html

在上面我们提到了两个词:一个是持久层,另一个是框架。

  • 持久层:指的是就是数据访问层(dao),是用来操作数据库的。

image-20220901114951631

  • 框架:是一个半成品软件,是一套可重用的、通用的、软件基础代码模型。在框架的基础上进行软件开发更加高效、规范、通用、可拓展。

Mybatis课程安排:

  • Mybatis入门

  • Mybatis基础增删改查

  • Mybatis动态SQL

接下来,我们就通过一个入门程序,让大家快速感受一下通过Mybatis如何来操作数据库。

1. 快速入门

需求:使用Mybatis查询所有用户数据。

1.1 入门程序分析

以前我们是在图形化客户端工具中编写SQL查询代码,发送给数据库执行,数据库执行后返回操作结果。

image-20221209155704203

图形化工具会把数据库执行的查询结果,使用表格的形式展现出来

image-20220901121116813

现在使用Mybatis操作数据库,就是在Mybatis中编写SQL查询代码,发送给数据库执行,数据库执行后返回结果。

image-20221209155904370

Mybatis会把数据库执行的查询结果,使用实体类封装起来(一行记录对应一个实体类对象)

image-20221209161623051

Mybatis操作数据库的步骤:

  1. 准备工作(创建springboot工程、数据库表user、实体类User)

  2. 引入Mybatis的相关依赖,配置Mybatis(数据库连接信息)

  3. 编写SQL语句(注解/XML)

1.2 入门程序实现

1.2.1 准备工作

1.2.1.1 创建springboot工程

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

image-20221209162827242

image-20221209163123443

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 仅供参考:只粘贴了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数据库时,需要配置:

image-20221209172527630

连接数据库的四大参数:

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

基于上述分析,在Mybatis中要连接数据库,同样也需要以上4个参数配置。

在springboot项目中,可以编写application.properties文件,配置数据库连接信息。我们要连接数据库,就需要配置数据库连接的基本信息,包括:driver-class-name、url 、username,password。

在入门程序中,大家可以直接这么配置,后面会介绍什么是驱动。

application.properties:

1
2
3
4
5
6
7
8
#驱动类名称
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)。

image-20221209175843651

UserMapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
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注解直接将其注入进行,然后就可以测试了。

测试类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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
2
3
4
5
6
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给出提示,可以做如下配置:

image-20221210143348119

配置完成之后,发现SQL语句中的关键字有提示了,但还存在不识别表名(列名)的情况:

image-20221210143934318

产生原因:Idea和数据库没有建立连接,不识别表信息

解决方案:在Idea中配置MySQL数据库连接

image-20221210144139792

在配置的时候指定连接那个数据库,如上图所示连接的就是mybatis数据库。

2. JDBC介绍(了解)

2.1 介绍

通过Mybatis的快速入门,我们明白了,通过Mybatis可以很方便的进行数据库的访问操作。但是大家要明白,其实java语言操作数据库呢,只能通过一种方式:使用sun公司提供的 JDBC 规范。

Mybatis框架,就是对原始的JDBC程序的封装。

那到底什么是JDBC呢,接下来,我们就来介绍一下。

JDBC: ( Java DataBase Connectivity ),就是使用Java语言操作关系型数据库的一套API。

image-20221210144811961

本质:

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

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

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

2.2 代码

下面我们看看原始的JDBC程序是如何操作数据库的。操作步骤如下:

  1. 注册驱动
  2. 获取连接对象
  3. 执行SQL语句,返回执行结果
  4. 处理执行结果
  5. 释放资源

在pom.xml文件中已引入MySQL驱动依赖,我们直接编写JDBC代码即可

JDBC具体代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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(类):数据库驱动管理类。

  • 作用:

    1. 注册驱动

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

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

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

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

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

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

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

通过上述代码,我们看到直接基于JDBC程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用Mybatis这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率。

2.3 问题分析

原始的JDBC程序,存在以下几点问题:

  1. 数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
  2. 查询结果的解析及封装非常繁琐
  3. 每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低

image-20221210153407998

2.4 技术对比

分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:

  1. 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中

  2. 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注

  3. 在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。

image-20221210154324151

使用SpringBoot+Mybatis的方式操作数据库,能够提升开发效率、降低资源浪费

而对于Mybatis来说,我们在开发持久层程序操作数据库时,需要重点关注以下两个方面:

  1. application.properties

    1
    2
    3
    4
    5
    6
    7
    8
    #驱动类名称
    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
  2. Mapper接口(编写SQL语句)

    1
    2
    3
    4
    5
    @Mapper
    public interface UserMapper {
    @Select("select id, name, age, gender, phone from user")
    public List<User> list();
    }

3. 数据库连接池

在前面我们所讲解的mybatis中,使用了数据库连接池技术,避免频繁的创建连接、销毁连接而带来的资源浪费。

下面我们就具体的了解下数据库连接池。

3.1 介绍

image-20221210160341852

没有使用数据库连接池:

  • 客户端执行SQL语句:要先创建一个新的连接对象,然后执行SQL语句,SQL语句执行后又需要关闭连接对象从而释放资源,每次执行SQL时都需要创建连接、销毁链接,这种频繁的重复创建销毁的过程是比较耗费计算机的性能。

image-20221210161016314

数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象

允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

  • 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用)

释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏

  • 客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象

数据库连接池的好处:

  1. 资源重用
  2. 提升系统响应速度
  3. 避免数据库连接遗漏

3.2 产品

要怎么样实现数据库连接池呢?

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

    • 功能:获取连接

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

常见的数据库连接池:

  • C3P0
  • DBCP
  • Druid
  • Hikari (springboot默认)

现在使用更多的是:Hikari、Druid (性能更优越)

  • Hikari(追光者) [默认的连接池]

image-20220901144923251

  • Druid(德鲁伊)

    • Druid连接池是阿里巴巴开源的数据库连接池项目

    • 功能强大,性能优秀,是Java语言最好的数据库连接池之一

如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要完成以下两步操作即可:

参考官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

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

方式1:

1
2
3
4
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:

1
2
3
4
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

4.1 介绍

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

image-20221210164641266

通过注解的形式自动生成构造器、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文件中引入依赖

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

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

1
2
3
4
5
6
7
8
9
10
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等方法。

image-20221210170733921

说明:@Data注解中不包含全参构造方法,通常在实体类上,还会添加上:全参构造、无参构造

1
2
3
4
5
6
7
8
9
10
11
12
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中自带)

image-20221210165506359

5. Mybatis基础操作

学习完mybatis入门后,我们继续学习mybatis基础操作。

5.1 需求

需求说明:

  • 根据资料中提供的《tlias智能学习辅助系统》页面原型及需求,完成员工管理的需求开发。

image-20221210180155700

image-20221210180343288

image-20221210180515206

通过分析以上的页面原型和需求,我们确定了功能列表:

  1. 查询

    • 根据主键ID查询
    • 条件查询
  2. 新增

  3. 更新

  4. 删除

    • 根据主键ID删除
    • 根据主键ID批量删除

5.2 准备

实施前的准备工作:

  1. 准备数据库表
  2. 创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)
  3. application.properties中引入数据库连接信息
  4. 创建对应的实体类 Emp(实体类属性采用驼峰命名)
  5. 准备Mapper接口 EmpMapper

准备数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
-- 部门管理
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 '部门表';
-- 部门表测试数据
insert into dept (id, name, create_time, update_time)
values (1, '学工部', now(), now()),
(2, '教研部', now(), now()),
(3, '咨询部', now(), now()),
(4, '就业部', now(), now()),
(5, '人事部', now(), now());


-- 员工管理
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 '员工表';
-- 员工表测试数据
INSERT INTO emp (id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time)
VALUES
(1, 'jinyong', '123456', '金庸', 1, '1.jpg', 4, '2000-01-01', 2, now(), now()),
(2, 'zhangwuji', '123456', '张无忌', 1, '2.jpg', 2, '2015-01-01', 2, now(), now()),
(3, 'yangxiao', '123456', '杨逍', 1, '3.jpg', 2, '2008-05-01', 2, now(), now()),
(4, 'weiyixiao', '123456', '韦一笑', 1, '4.jpg', 2, '2007-01-01', 2, now(), now()),
(5, 'changyuchun', '123456', '常遇春', 1, '5.jpg', 2, '2012-12-05', 2, now(), now()),
(6, 'xiaozhao', '123456', '小昭', 2, '6.jpg', 3, '2013-09-05', 1, now(), now()),
(7, 'jixiaofu', '123456', '纪晓芙', 2, '7.jpg', 1, '2005-08-01', 1, now(), now()),
(8, 'zhouzhiruo', '123456', '周芷若', 2, '8.jpg', 1, '2014-11-09', 1, now(), now()),
(9, 'dingminjun', '123456', '丁敏君', 2, '9.jpg', 1, '2011-03-11', 1, now(), now()),
(10, 'zhaomin', '123456', '赵敏', 2, '10.jpg', 1, '2013-09-05', 1, now(), now()),
(11, 'luzhangke', '123456', '鹿杖客', 1, '11.jpg', 5, '2007-02-01', 3, now(), now()),
(12, 'hebiweng', '123456', '鹤笔翁', 1, '12.jpg', 5, '2008-08-18', 3, now(), now()),
(13, 'fangdongbai', '123456', '方东白', 1, '13.jpg', 5, '2012-11-01', 3, now(), now()),
(14, 'zhangsanfeng', '123456', '张三丰', 1, '14.jpg', 2, '2002-08-01', 2, now(), now()),
(15, 'yulianzhou', '123456', '俞莲舟', 1, '15.jpg', 2, '2011-05-01', 2, now(), now()),
(16, 'songyuanqiao', '123456', '宋远桥', 1, '16.jpg', 2, '2010-01-01', 2, now(), now()),
(17, 'chenyouliang', '123456', '陈友谅', 1, '17.jpg', NULL, '2015-03-21', NULL, now(), now());

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

image-20221210182008131

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

提示:可以把之前项目中已有的配置信息复制过来即可

1
2
3
4
5
6
7
8
#驱动类名称
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

1
2
3
4
5
6
7
/*@Mapper注解:表示当前接口为mybatis中的Mapper接口
程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理
*/
@Mapper
public interface EmpMapper {

}

完成以上操作后,项目工程结构目录如下:

image-20221210182500817

5.3 删除

5.3.1 功能实现

页面原型:

image-20221210183336095

当我们点击后面的”删除”按钮时,前端页面会给服务端传递一个参数,也就是该行数据的ID。 我们接收到ID后,根据ID删除数据即可。

功能:根据主键删除数据

  • SQL语句
1
2
-- 删除id=17的数据
delete from emp where id = 17;

Mybatis框架让程序员更关注于SQL语句

  • 接口方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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类型对象
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入
private EmpMapper empMapper;

@Test
public void testDel(){
//调用删除方法
empMapper.delete(16);
}

}

5.3.2 日志输入

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:

  1. 打开application.properties文件

  2. 开启mybatis的日志,并指定输出到控制台

1
2
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:

image-20220901164225644

但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL。

5.3.3 预编译SQL

5.3.3.1 介绍

预编译SQL有两个优势:

  1. 性能更高
  2. 更安全(防止SQL注入)

image-20221210202222206

性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)

更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。

5.3.3.2 SQL注入

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

由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。

测试1:使用资料中提供的程序,来验证SQL注入问题

image-20221210205419634

第1步:进入到DOS

image-20221211124744203

image-20221211124840720

第2步:执行以下命令,启动程序

1
2
#启动存在SQL注入的程序
java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar

image-20221210211605231

第3步:打开浏览器输入http://localhost:9090/login.html

image-20221210212406527

发现竟然能够登录成功:

image-20221210212511915

以上操作为什么能够登录成功呢?

  • 由于没有对用户输入内容进行充分检查,而SQL又是字符串拼接方式而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,从而完成恶意攻击。

image-20221210213311518

image-20221210214431228

用户在页面提交数据的时候人为的添加一些特殊字符,使得sql语句的结构发生了变化,最终可以在没有用户名或者密码的情况下进行登录。

测试2:使用资料中提供的程序,来验证SQL注入问题

第1步:进入到DOS

第2步:执行以下命令,启动程序:

1
2
#启动解决了SQL注入的程序
java -jar sql_prepared_demo-0.0.1-SNAPSHOT.jar

第3步:打开浏览器输入http://localhost:9090/login.html

image-20221210212406527

发现无法登录:

image-20221211125751981

以上操作SQL语句的执行:

image-20221211130011973

把整个' or '1'='1作为一个完整的参数,赋值给第2个问号(' or '1'='1进行了转义,只当做字符串使用)

5.3.3.3 参数占位符

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

  • #{…}

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

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

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

5.4 新增

功能:新增员工信息

image-20221211134239610

5.4.1 基本新增

员工表结构:

image-20221211134746319

SQL语句:

1
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');

接口方法:

1
2
3
4
5
6
7
@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);

}

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

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
}
}

日志输出:

image-20221211140222240

5.4.2 主键返回

概念:在数据添加成功后,需要获取插入数据库数据的主键。

如:添加套餐数据时,还需要维护套餐菜品关系表数据。

image-20221211150353385

业务场景:在前面讲解到的苍穹外卖菜品与套餐模块的表结构,菜品与套餐是多对多的关系,一个套餐对应多个菜品。既然是多对多的关系,是不是有一张套餐菜品中间表来维护它们之间的关系。

image-20221212093655389

在添加套餐的时候,我们需要在界面当中来录入套餐的基本信息,还需要来录入套餐与菜品的关联信息。这些信息录入完毕之后,我们一点保存,就需要将套餐的信息以及套餐与菜品的关联信息都需要保存到数据库当中。其实具体的过程包括两步,首先第一步先需要将套餐的基本信息保存了,接下来第二步再来保存套餐与菜品的关联信息。套餐与菜品的关联信息就是往中间表当中来插入数据,来维护它们之间的关系。而中间表当中有两个外键字段,一个是菜品的ID,就是当前菜品的ID,还有一个就是套餐的ID,而这个套餐的 ID 指的就是此次我所添加的套餐的ID,所以我们在第一步保存完套餐的基本信息之后,就需要将套餐的主键值返回来供第二步进行使用。这个时候就需要用到主键返回功能。

那要如何实现在插入数据之后返回所插入行的主键值呢?

  • 默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在Mapper接口中的方法上添加一个Options注解,并在注解中指定属性useGeneratedKeys=true和keyProperty=”实体类属性名”

主键返回代码实现:

1
2
3
4
5
6
7
8
9
@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);

}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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());
}
}

5.5 更新

功能:修改员工信息

image-20221212095605863

点击”编辑”按钮后,会查询所在行记录的员工信息,并把员工信息回显在修改员工的窗体上(下个知识点学习)

在修改员工的窗体上,可以修改的员工数据:用户名、员工姓名、性别、图像、职位、入职日期、归属部门

思考:在修改员工数据时,要以什么做为条件呢?

答案:员工id

SQL语句:

1
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;

接口方法:

1
2
3
4
5
6
7
8
9
10
@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);

}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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);
}
}

5.6 查询

5.6.1 根据ID查询

在员工管理的页面中,当我们进行更新数据时,会点击 “编辑” 按钮,然后此时会发送一个请求到服务端,会根据Id查询该员工信息,并将员工数据回显在页面上。

image-20221212101331292

SQL语句:

1
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;

接口方法:

1
2
3
4
5
@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);
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
@Autowired
private EmpMapper empMapper;

@Test
public void testGetById(){
Emp emp = empMapper.getById(1);
System.out.println(emp);
}
}

执行结果:

image-20221212103004961

而在测试的过程中,我们会发现有几个字段(deptId、createTime、updateTime)是没有数据值的

5.6.2 数据封装

我们看到查询返回的结果中大部分字段是有值的,但是deptId,createTime,updateTime这几个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?

image-20221212103124490

原因如下:

  • 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方案:

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

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

1
2
3
4
5
@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);

再次执行测试类:

image-20221212111027396

手动结果映射:通过 @Results及@Result 进行手动结果映射

1
2
3
4
5
@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源代码:

1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Results {
String id() default "";

Result[] value() default {}; //Result类型的数组
}

@Result源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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
1
2
# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。

5.6.3 条件查询

在员工管理的列表页面中,我们需要根据条件查询员工信息,查询条件包括:姓名、性别、入职时间。

image-20221212113422924

通过页面原型以及需求描述我们要实现的查询:

  • 姓名:要求支持模糊匹配
  • 性别:要求精确匹配
  • 入职时间:要求进行范围查询
  • 根据最后修改时间进行降序排序

SQL语句:

1
2
3
4
5
6
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;

接口方法:

  • 方式一
1
2
3
4
5
6
7
8
9
@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);
}

image-20221212115149151

以上方式注意事项:

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

  2. 模糊查询使用${…}进行字符串拼接,这种方式呢,由于是字符串拼接,并不是预编译的形式,所以效率不高、且存在sql注入风险。

  • 方式二(解决SQL注入风险)
    • 使用MySQL提供的字符串拼接函数:concat(‘%’ , ‘关键字’ , ‘%’)
1
2
3
4
5
6
7
8
9
10
11
12
@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语句(性能高、安全)

image-20221212120006242

5.6.4 参数名说明

在上面我们所编写的条件查询功能中,我们需要保证接口中方法的形参名和SQL语句中的参数占位符名相同。

当方法中的形参名和SQL语句中的占位符参数名不相同时,就会出现以下问题:

image-20221212150611796

参数名在不同的SpringBoot版本中,处理方案还不同:

  • 在springBoot的2.x版本(保证参数名一致)

image-20221212151156273

springBoot的父工程对compiler编译插件进行了默认的参数parameters配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}里面可以直接通过形参名获取对应的值

image-20221212151411154

  • 在springBoot的1.x版本/单独使用mybatis(使用@Param注解来指定SQL语句中的参数名)

image-20221212151628715

在编译时,生成的字节码文件当中,不会保留Mapper接口中方法的形参名称,而是使用var1、var2、…这样的形参名字,此时要获取参数值时,就要通过@Param注解来指定SQL语句中的参数名

image-20221212151736274

  1. Mybatis的XML配置文件

Mybatis的开发有两种方式:

  1. 注解
  2. XML

6.XML

6.1 XML配置文件规范

使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。

在Mybatis中使用XML映射文件方式开发,需要符合一定的规范:

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

  • XML映射文件的namespace属性为Mapper接口全限定名一致

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

image-20221212153529732

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

  • resultType属性,指的是查询返回的单条记录所封装的类型。

image-20240309163347721

由于XML映射文件中的id绑定的list方法 而list方法封装的是Emp 所以 resultType为Emp的全类名

image-20240309163504652

6.2 XML配置文件实现

第1步:创建XML映射文件

image-20221212154908306

image-20221212155304635

image-20221212155544404

第2步:编写XML映射文件

xml映射文件中的dtd约束,直接从mybatis官网复制即可

1
2
3
4
5
6
7
<?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接口全限定名

image-20221212160316644

1
2
3
4
5
6
7
<?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接口中的方法名一致,并保持返回类型一致

image-20221212163528787

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>

运行测试类,执行结果:

image-20221212163719534

6.3 MybatisX的使用

MybatisX是一款基于IDEA的快速开发Mybatis的插件,为效率而生。

MybatisX的安装:

image-20221213120923252

可以通过MybatisX快速定位:

image-20221213121521406

MybatisX的使用在后续学习中会继续分享

学习了Mybatis中XML配置文件的开发方式了,大家可能会存在一个疑问:到底是使用注解方式开发还是使用XML方式开发?

官方说明:https://mybatis.net.cn/getting-started.html

image-20220901173948645

结论:*使用Mybatis的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句*

7. Mybatis动态SQL

7.1 什么是动态SQL

在页面原型中,列表上方的条件是动态的,是可以不传递的,也可以只传递其中的1个或者2个或者全部。

image-20220901173203491

而在我们刚才编写的SQL语句中,我们会看到,我们将三个条件直接写死了。 如果页面只传递了参数姓名name 字段,其他两个字段 性别 和 入职时间没有传递,那么这两个参数的值就是null。

此时,执行的SQL语句为:

image-20220901173431554

这个查询结果是不正确的。正确的做法应该是:传递了参数,再组装这个查询条件;如果没有传递参数,就不应该组装这个查询条件。

比如:如果姓名输入了”张”, 对应的SQL为:

1
select *  from emp where name like '%张%' order by update_time desc;

如果姓名输入了”张”,,性别选择了”男”,则对应的SQL为:

1
select *  from emp where name like '%张%' and gender = 1 order by update_time desc;

SQL语句会随着用户的输入或外部条件的变化而变化,我们称为:动态SQL

image-20221213122623278

在Mybatis中提供了很多实现动态SQL的标签,我们学习Mybatis中的动态SQL就是掌握这些动态SQL标签。

7.2 动态SQL-if

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

1
2
3
<if test="条件表达式">
要拼接的sql语句
</if>

接下来,我们就通过<if>标签来改造之前条件查询的案例。

7.2.1 条件查询

示例:把SQL语句改造为动态SQL方式

  • 原有的SQL语句
1
2
3
4
5
6
7
<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语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="list" resultType="com.itheima.pojo.Emp">
select * from emp
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>

order by update_time desc
</select>

测试方法:

1
2
3
4
5
6
7
8
@Test
public void testList(){
//性别数据为null、开始时间和结束时间也为null
List<Emp> list = empMapper.list("张", null, null, null);
for(Emp emp : list){
System.out.println(emp);
}
}

执行的SQL语句:

image-20221213140353285

下面呢,我们修改测试方法中的代码,再次进行测试,观察执行情况:

1
2
3
4
5
6
7
8
@Test
public void testList(){
//姓名为null
List<Emp> list = empMapper.list(null, (short)1, null, null);
for(Emp emp : list){
System.out.println(emp);
}
}

执行结果:

image-20221213141139015

image-20221213141253355

再次修改测试方法中的代码,再次进行测试:

1
2
3
4
5
6
7
8
@Test
public void testList(){
//传递的数据全部为null
List<Emp> list = empMapper.list(null, null, null, null);
for(Emp emp : list){
System.out.println(emp);
}
}

执行的SQL语句:

image-20221213143854434

以上问题的解决方案:使用<where>标签代替SQL语句中的where关键字

  • <where>只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

测试方法:

1
2
3
4
5
6
7
8
@Test
public void testList(){
//只有性别
List<Emp> list = empMapper.list(null, (short)1, null, null);
for(Emp emp : list){
System.out.println(emp);
}
}

执行的SQL语句:

image-20221213141909455

7.2.2 更新员工

案例:完善更新员工功能,修改为动态更新员工数据信息

  • 动态更新员工信息,如果更新时传递有值,则更新;如果更新时没有传递值,则不更新
  • 解决方案:动态SQL

修改Mapper接口:

1
2
3
4
5
6
@Mapper
public interface EmpMapper {
//删除@Update注解编写的SQL语句
//update操作的SQL语句编写在Mapper映射文件中
public void update(Emp emp);
}

修改Mapper映射文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?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
<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>
where id=#{id}
</update>

</mapper>

测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testUpdate2(){
//要修改的员工信息
Emp emp = new Emp();
emp.setId(20);
emp.setUsername("Tom111");
emp.setName("汤姆111");

emp.setUpdateTime(LocalDateTime.now());

//调用方法,修改员工数据
empMapper.update(emp);
}

执行的SQL语句:

image-20221213152533851

再次修改测试方法,观察SQL语句执行情况:

1
2
3
4
5
6
7
8
9
10
@Test
public void testUpdate2(){
//要修改的员工信息
Emp emp = new Emp();
emp.setId(20);
emp.setUsername("Tom222");

//调用方法,修改员工数据
empMapper.update(emp);
}

执行的SQL语句:

image-20221213152850322

以上问题的解决方案:使用<set>标签代替SQL语句中的set关键字

  • <set>:动态的在SQL语句中插入set关键字,并会删掉额外的逗号。(用于update语句中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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语句:

image-20221213153329553

小结

  • <if>

    • 用于判断条件是否成立,如果条件为true,则拼接SQL

    • 形式:

      1
      <if test="name != null"></if>
  • <where>

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

    • 动态地在行首插入 SET 关键字,并会删掉额外的逗号。(用在update语句中)

7.3 动态SQL-foreach

案例:员工删除功能(既支持删除单条记录,又支持批量删除)

image-20220901181751004

SQL语句:

1
delete from emp where id in (1,2,3);

Mapper接口:

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

XML映射文件:

  • 使用<foreach>遍历deleteByIds方法中传递的参数ids集合
1
2
3
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符" 
open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>

image-20221213165710141

执行的SQL语句:

image-20221213164957636

7.4 动态SQL-sql&include

问题分析:

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

我们可以对重复的代码片段进行抽取,将其通过<sql>标签封装到一个SQL片段,然后再通过<include>标签进行引用。

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

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

image-20221213171244796

SQL片段: 抽取重复的代码

1
2
3
<sql id="commonSelect">
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>

然后通过<include> 标签在原来抽取的地方进行引用。操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
![image-20220826161735076](./JavaWeb/image-20220826161735076.png)<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>

登录校验

1
通常当我们访问某一个网页时 登陆后才能访问后端操作数据 但我们目前实现的这个 不管登不登陆都可以访问后端操作数据

为什么会出现这个问题 是因为我们缺少了最重要的一步:登录校验

什么是登录校验?

  • 所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。

  • 那应该怎么来实现登录校验的操作呢?具体的实现思路可以分为两部分:

    1. 在员工登录成功后,需要将用户登录成功的信息存起来,记录用户已经登录成功的标记。
    2. 在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。

我们要完成以上操作,会涉及到web开发中的两个技术:

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

会话技术

什么是会话?

  • 在我们日常生活当中,会话指的就是谈话、交谈。

  • 在web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。

在用户打开浏览器第一次访问服务器的时候,这个会话就建立了,直到有任何一方断开连接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。

比如:打开了浏览器来访问web服务器上的资源(浏览器不能关闭、服务器不能断开)

  • 第1次:访问的是登录的接口,完成登录操作
  • 第2次:访问的是部门管理接口,查询所有部门数据
  • 第3次:访问的是员工管理接口,查询员工数据

只要浏览器和服务器都没有关闭,以上3次请求都属于一次会话当中完成的。

image-20230105203827355

会话跟踪技术有两种:

  1. Cookie(客户端会话跟踪技术)
    • 数据存储在客户端浏览器当中
  2. Session(服务端会话跟踪技术)
    • 数据存储在储在服务端
  3. 令牌技术

cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。

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

服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

image-20230112101901417

接下来在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在,如果不存在这个cookie,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。

我刚才在介绍流程的时候,用了 3 个自动:

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

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

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

为什么这一切都是自动化进行的?

是因为 cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:

  • 响应头 Set-Cookie :设置Cookie数据的

  • 请求头 Cookie:携带Cookie数据的

image-20230112101804878

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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();
}
}

A. 访问c1接口,设置Cookie,http://localhost:8080/c1

image-20230112105410076

我们可以看到,设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器端。

image-20230112105538131

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

image-20230112105658486

优缺点

  • 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
  • 缺点:
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 不安全,用户可以自己禁用Cookie
    • Cookie不能跨域

跨域介绍:

image-20230112103840467

  • 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
  • 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
  • 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
  • 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
  • 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域

区分跨域的维度:

  • 协议
  • IP/协议
  • 端口

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

举例:

http://192.168.150.200/login.html ———-> https://192.168.150.200/login [协议不同,跨域]

http://192.168.150.200/login.html ———-> http://192.168.150.100/login [IP不同,跨域]

http://192.168.150.200/login.html ———-> http://192.168.150.200:8080/login [端口不同,跨域]

http://192.168.150.200/login.html ———-> http://192.168.150.200/login [不跨域]

方案二 - Session

前面介绍的时候,我们提到Session,它是服务器端会话跟踪技术,所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。

  • 获取Session

    image-20230112105938545

    如果我们现在要基于 Session 来进行会话跟踪,浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。

  • 响应Cookie (JSESSIONID)

    image-20230112110441075

    接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。

  • 查找Session

    image-20230112101943835

    接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。

    这样我们是不是就可以通过 Session 会话对象在同一次会话的多次请求之间来共享数据了?好,这就是基于 Session 进行会话跟踪的流程。

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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);
}
}

A. 访问 s1 接口,http://localhost:8080/s1

image-20230112111004447

请求完成之后,在响应头中,就会看到有一个Set-Cookie的响应头,里面响应回来了一个Cookie,就是JSESSIONID,这个就是服务端会话对象 Session 的ID。

B. 访问 s2 接口,http://localhost:8080/s2

image-20230112111137207

接下来,在后续的每次请求时,都会将Cookie的值,携带到服务端,那服务端呢,接收到Cookie之后,会自动的根据JSESSIONID的值,找到对应的会话对象Session。

那经过这两步测试,大家也会看到,在控制台中输出如下日志:

image-20230112111328117

两次请求,获取到的Session会话对象的hashcode是一样的,就说明是同一个会话对象。而且,第一次请求时,往Session会话对象中存储的值,第二次请求时,也获取到了。 那这样,我们就可以通过Session会话对象,在同一个会话的多次请求之间来进行数据共享了。

优缺点

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

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

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

image-20230112112557480

  • 首先第一点,我们现在所开发的项目,一般都不会只部署在一台服务器上,因为一台服务器会存在一个很大的问题,就是单点故障。所谓单点故障,指的就是一旦这台服务器挂了,整个应用都没法访问了。

image-20230112112740131

  • 所以在现在的企业项目开发当中,最终部署的时候都是以集群的形式来进行部署,也就是同一个项目它会部署多份。比如这个项目我们现在就部署了 3 份。

  • 而用户在访问的时候,到底访问这三台其中的哪一台?其实用户在访问的时候,他会访问一台前置的服务器,我们叫负载均衡服务器,我们在后面项目当中会详细讲解。目前大家先有一个印象负载均衡服务器,它的作用就是将前端发起的请求均匀的分发给后面的这三台服务器。

    image-20230112113558810

  • 此时假如我们通过 session 来进行会话跟踪,可能就会存在这样一个问题。用户打开浏览器要进行登录操作,此时会发起登录请求。登录请求到达负载均衡服务器,将这个请求转给了第一台 Tomcat 服务器。

    Tomcat 服务器接收到请求之后,要获取到会话对象session。获取到会话对象 session 之后,要给浏览器响应数据,最终在给浏览器响应数据的时候,就会携带这么一个 cookie 的名字,就是 JSESSIONID ,下一次再请求的时候,是不是又会将 Cookie 携带到服务端?

    好。此时假如又执行了一次查询操作,要查询部门的数据。这次请求到达负载均衡服务器之后,负载均衡服务器将这次请求转给了第二台 Tomcat 服务器,此时他就要到第二台 Tomcat 服务器当中。根据JSESSIONID 也就是对应的 session 的 ID 值,要找对应的 session 会话对象。

    我想请问在第二台服务器当中有没有这个ID的会话对象 Session, 是没有的。此时是不是就出现问题了?我同一个浏览器发起了 2 次请求,结果获取到的不是同一个会话对象,这就是Session这种会话跟踪方案它的缺点,在服务器集群环境下无法直接使用Session。

大家会看到上面这两种传统的会话技术,在现在的企业开发当中是不是会存在很多的问题。 为了解决这些问题,在现在的企业开发当中,基本上都会采用第三种方案,通过令牌技术来进行会话跟踪。接下来我们就来介绍一下令牌技术,来看一下令牌技术又是如何跟踪会话的。

方案三 - 令牌技术

这里我们所提到的令牌,其实它就是一个用户身份的标识,看似很高大上,很神秘,其实本质就是一个字符串。

image-20230112102022634

如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。

接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。

接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。

此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。

优缺点

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

针对于这三种方案,现在企业开发当中使用的最多的就是第三种令牌技术进行会话跟踪。而前面的这两种传统的方案,现在企业项目开发当中已经很少使用了。所以在我们的课程当中,我们也将会采用令牌技术来解决案例项目当中的会话跟踪问题。

JWT令牌技术

  • 全称Json Web Token

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

  • 组成

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

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

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

image-20240325110500548

  • 场景:登录认证
    1. 登录成功后,生成令牌
    2. 后续每个请求,都需要携带jwt令牌,系统在每次请求处理之前,先校验令牌,通过后,在处理

image-20240325110921704

image-20240325114101996

JWT令牌-生成

  • 要在pom文件中引入相关的依赖

image-20240325113642999

  • 编写测试类

    image-20240325113707625

JWT令牌-解析

image-20240325113912663

注意:

  • JWT校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥时配套的
  • 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法

Filter过滤器

1
2
3
-- 概念:Filter过滤器,是JavaWeb三大组件(Servlet、Listener、Filter)之一
-- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
-- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等

Filter快速入门

  • 定义Filter:定义一个类,实现Filter(javax.servlet)接口,并重写其所有方法
  • 配置Filter:Filter类上加@WebFilter注解,配置拦截资源的路径,引导类上加@ServletComponentScan开启Sevlet组件支持

Filter类

image-20240325152707043

引导类

image-20240325152730300

详解(执行流程、拦截路径、过滤器链)

1.执行流程

  • 请求==>放行前的逻辑==>放行==>访问Web资源==>放行后的逻辑

2.拦截路径

  • /login
  • /depts/*
  • /*

3.过滤器链

  • 一个web应用中,配置了多个过滤器,就形成了一个过滤器链
  • image-20240325154952548

登录校验-Filter

  • 所有的请求,拦截到了之后,都需要校验令牌吗?
    • 不是 有一个例外 登录请求 因为没登陆的时候是没有jwt令牌的
  • 拦截到请求后,什么情况下可以放行,执行业务操作?
    • 有令牌,且令牌校验通过(合法),否则都返回登录错误的结果

步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
HttpServletRequest req = (HttpServletRequest) Request;
HttpServletResponse resp = (HttpServletResponse) Response;
//1.获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}",url);
//2.判断请求url中是否含有login,如果包含,则说明是登录请求,放行
if (url.contains("login")) {
log.inf o("登陆操作,放行");
Chain.doFilter(Request,Response);
return;
}
//3.获取请求头中的令牌
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(jwt)) { //判断jwt令牌是否为null或空字符串
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换为json格式--->阿里巴巴提供的fastJSON工具类
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { //jwt解析失败
log.info("令牌解析失败,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换为json格式--->阿里巴巴提供的fastJSON工具类
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.放行
log.info("令牌合法,放行通过");
Chain.doFilter(Request,Response);

Interceptor拦截器

  • 概念:是一种动态拦截方法调用的机制,类似于过滤器,Spring框架中提供,用来动态拦截控制器方法的执行
  • 作用:拦截请求,在指定的方法调用前后,根据业务需求执行预先设定的代码

简介&快速入门

  1. 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法

    image-20240325215327083

  2. 注册拦截器

    image-20240325215313458

详解

  1. 拦截器-拦截器路径
    • 拦截器可以根据需求,配置不同的拦截路径
  2. 拦截器-执行流程
    • image-20240326085139308
1
2
3
Filter 和 Interceptor
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口
拦截范围不同:过滤器Filter会拦截所有资源,而Interceptor只会拦截SpringMVC环境中的资源

登录校验-interceptor

步骤:

​ 在Filter的基础上进行更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//1.获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}",url);
//2.判断请求url中是否含有login,如果包含,则说明是登录请求,放行
if (url.contains("login")) {
log.info("登陆操作,放行");
return true;
}
//3.获取请求头中的令牌
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(jwt)) { //判断jwt令牌是否为null或空字符串
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换为json格式--->阿里巴巴提供的fastJSON工具类
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { //jwt解析失败
log.info("令牌解析失败,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换为json格式--->阿里巴巴提供的fastJSON工具类
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行
log.info("令牌合法,放行通过");
return true;

总结

image-20240327183856718

异常处理

当前问题

登录功能和登录校验功能我们都实现了,下面我们学习下今天最后一块技术点:异常处理。首先我们先来看一下系统出现异常之后会发生什么现象,再来介绍异常处理的方案。

我们打开浏览器,访问系统中的新增部门操作,系统中已经有了 “就业部” 这个部门,我们再来增加一个就业部,看看会发生什么现象。

image-20230112125651073

点击确定之后,窗口关闭了,页面没有任何反应,就业部也没有添加上。 而此时,大家会发现,网络请求报错了。

image-20230112125737863

状态码为500,表示服务器端异常,我们打开idea,来看一下,服务器端出了什么问题。

image-20230112125826602

上述错误信息的含义是,dept部门表的name字段的值 就业部 重复了,因为在数据库表dept中已经有了就业部,我们之前设计这张表时,为name字段建议了唯一约束,所以该字段的值是不能重复的。

而当我们再添加就业部,这个部门时,就违反了唯一约束,此时就会报错。

我们来看一下出现异常之后,最终服务端给前端响应回来的数据长什么样。

image-20230112130253486

响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据。

接下来我们需要思考的是出现异常之后,当前案例项目的异常是怎么处理的?

  • 答案:没有做任何的异常处理

image-20230107121909087

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

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

解决方案

那么在三层构架项目中,出现了异常,该如何处理?

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

image-20230107122904214

全局异常处理器

我们该怎么样定义全局异常处理器?

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class GlobalExceptionHandler {

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

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

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

处理异常的方法返回值会转换为json后再响应给前端

重新启动SpringBoot服务,打开浏览器,再来测试一下添加部门这个操作,我们依然添加已存在的 “就业部” 这个部门:

image-20230112131232032

image-20230112131135272

此时,我们可以看到,出现异常之后,异常已经被全局异常处理器捕获了。然后返回的错误信息,被前端程序正常解析,然后提示出了对应的错误提示信息。

以上就是全局异常处理器的使用,主要涉及到两个注解:

  • @RestControllerAdvice //表示当前类为全局异常处理器
  • @ExceptionHandler //指定可以捕获哪种类型的异常进行处理

Spring事务管理

  • 注解:@Transactional
  • 位置:业务层(service)的方法上、类上、接口上
  • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;回滚事务

image-20240326091451610

事务属性

回滚rollback

  • @rollbackFor
  • 默认情况下,只有出现@RuntimeException才会回滚,rollbackFor属性用于控制出现何种异常类型,回滚事务
  • image-20240326093053231

传播行为propagation

事务传播行为:指的就是当一个事务方法被另一事务方法调用时,这个事务方法该如何进行事务控制

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

image-20240326095623453

例如:

image-20240328104531917

在上述完成删除部门操作时,不管失败与否 都要记录日志 那么deptLogService中的insert语句就要添加事务

image-20240326100618155

当Transaction的值为默认值时,加入delete中的语句报错 会发生回滚 到时会连带insert一起回滚

如果设置REQUIRES_NEW则不会 因为propagation的默认值会自动加入已存在的事务 REQUIRES_NEW则会创建新的事务

AOP

AOP基础

AOP概述

  • AOP:Aspect Oriented Progtamming(面向切面编程、面向方面编程),其实就是棉线特定方法编程

场景:

  • 案例部分佛南功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时
  • image-20240326110621621

实现:

  • 动态代理时面向切面编程最主流的实现,而SpringAOP时Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

底层原理:

  • 动态代理机制

Spring的AOP的底层用到了两种代理机制:

  • JDK的动态代理:针对实现接口的类产生代理
  • Cglib的动态代理:针对没有实现接口的类产生代理

快速入门

  • 导入依赖

    image-20240326111320941

  • 编写aop程序:针对于特定方法根据业务需要进行编程

    image-20240326112306354

AOP核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行的相关信息)
  • 通知:Advice, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:PointCut,匹配链接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知与切入点,的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象

image-20240326125641380

AOP执行流程

image-20240326130435084

AOP进阶

通知类型

  • @Around :环绕通知,此注解标注的通知方法在目标方法前、后都被执行
    • image-20240326144648642
  • @Before :前置通知,此注解标注的通知方法在目标方法前被执行
    • image-20240326144638119
  • @After重点):后置通知,此注解标注的通知方式在目标方法后被执行
    • image-20240326144656721
  • @AfterRuning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
    • image-20240326144703033
  • @AfterThrowing :异常后通知,此注解标注的通知方法发生异常后通知
    • image-20240326144709491

注意事项

1
2
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接受原始方法的返回值

抽取:

@PointCut注解

  • 该注解的作用是将公共的切点表达式抽取出来,需要使用的时引用该切点表达式即可

image-20240326144849843

image-20240326144931497

直接调用 别的类中的也可直接调用 这里取决于px的修饰符

  • private:仅能在当前切面类中引用该表达式
  • public:其他外部的切面类中也可以引用该表达式

通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通方法都会被执行

执行顺序:

  • 1.不同切面类中,默认按照切面类的类名字母排序

    • 目标方法前的通知方法:字母排名靠前的先执行

    • 目标方法后的通知方法:字母排名靠前的后执行

  • 2.用@Order(数字) 加在切面类上来控制顺序

    • 目标方法前的通知方法:数字小的先执行
    • 目标方法后的通知方法:数字小的后执行

切入点表达式

  • 切入点表达式:描述切入点方法的一种表达式
  • 作用:主要用来决定项目中的那些方法需要加入通知
  • 常见形式
    • execution(...) :根据方法的签名来匹配
    • @annotation(...) :根据注解匹配

executation

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

1
executation(访问修饰符? 返回值 包名.类名? 方法名(方法参数) throws 异常? )
  • 其中带?的表示可省略的部分
    • 访问修饰符:可省略(比如:public、protected)
    • 包名.类名:可省略
    • throw 异常:可省略 (注意是方法向上声明抛出的异常,不是实际抛出的异常)
  • 可以使用通配符描述切入点
    • * : 单个独立的任意符号,可以统配任意返回值、包名、类名、方法名、任意类型的一个参数、也可以统配包、类、方法名的一部分
      • image-20240326161142261
    • .. : 多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
      • image-20240326161240834

image-20240326161917056

image-20240326161540654

@annotation

@annotation 切入点表达式,用于匹配表示有特定注解的方法

1
@annotation(com.raehp.anno.Log)

实现步骤:

  • 创建Annoation注解

  • image-20240326162858318

  • 注解中添加@Retention注解、@Target注解

    image-20240326162938421

  • 在需要通知的方法上添加注解

    image-20240326163003775

  • 通过Pointcut修改切入点表达式 声明注解

image-20240326163051297

连接点

  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的信息,如目标类名,方法名,方法参数等。
    • 对于@Around 通知,获取连接点信息只能使用,ProceedingJoinPoint
    • 对于其他四种通知,获取连接点信息只能使用 JoinPoint ,他是 ProceedingJoinPoint 的父类型

image-20240326190904328

SpringBoot

配置优先级

配置

  • springboot中支持三种格式的配置文件

    1
    2
    3
    4
    5
    server.port=8081  												--properties
    server:
    port: 8082 --yml
    server:
    port: 8083 --yaml

    其中 :优先级properties > yml > yaml

1
虽然springboot支持多种格式配置文件,但是在项目开发过程中,推荐统一使用一种格式的配置 (yml是主流)
  • Springboot除了支持配置文件属性配置,还支持 Java系统属性 命令行参数 的方式进行属性配置

    1
    2
    3
    4
    # Java系统属性
    -Dserver.port=9000
    # 命令行参数
    --server.port=10010

    其中:优先级命令行参数 > Java系统属性

    image-20240401095305704

image-20240401095654133

那么,当我们打包成jar包之后 那我们 该如何设置Java系统属性 和命令行参数

  • 先打开jar包所在位置

    image-20240401095729789

  • 在当前文件地址打开cmd 输入

    image-20240401095818059

那么这5种配置方式的优先级大小呢?

1
命令行参数 > Java系统属性 > properties > yml > yaml

Bean管理

获取bean

  • 默认情况下,Spring项目启动时,会把bean都创建好放在IOC容器,如果想主动获取这些bean,可以通过如下方式

    • 注入ApplicaionContext

    • 根据name获取bean

      image-20240401102528247

    • 根据类型获取

      image-20240401102536962

    • 根据name获取bean(带转换类型)

运行结果:

image-20240401102653230

1
上述所说的 【Spring 项目启动时,会把其中的bean都创建好】 还会受到作用域及延迟初始化影响,这里主要针对于 默认的单例非延迟加载的bean而言

bean的作用域

  • Spring支持五种作用域 后三者在web环境才生效

    image-20240401104035633

  • 可以通过@Scope 注解来进行配置作用域

    image-20240401104123066

1
2
3
默认singleton 的bean  在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)
prototype的bean,第一次使用该bean的时候 都会创建一个新的实例
实际开发中,绝大部分的bean是单例的,也就是说绝大部分的bean不需要设置prototype

标注prototype的运行结果:

image-20240401104420321

默认值的运行结果

image-20240401104458244

添加@Lazy注解后的运行结果(scope为默认值的情况下):在调用的时候 初始化

image-20240401104748294

不加@Lazy注解:在调用前 初始化

image-20240401104833746

第三方bean

1
2
3
如果要管理的bean对象 来自第三方(不是自己定义的) 是无法使用@Component及衍生注解来声明bean的,就需要用到@Bean注解

若要管理第三方的bean对象,建议对这些bean集中管理分类配置,可以通过@Configuration注解来声明一个配置类
  • 在启动类中 使用@Bean 注解 来 声明第三方类
    • image-20240401111143619
  • 在通过@Autowired 注解来注入 测试
    • image-20240401111242081

但这个时候 我们会发现 声明第三方类的过程 是在启动类中实现的(不建议)

声明配置类的方法:

  • 先创建一个类 标注 @Configuration 注解 声明配置类

    • image-20240401111541227
  • 如果想要依赖注入 则直接以形参的方式 写到方法中即可

SpringBoot原理

起步依赖原理

起步依赖原理:Maven的依赖传递

自动配置原理

实现

  • 方案一:@ComponentScan 组件扫描

image-20240401122207578

  • 方案二:@Import 导入。使用@Import 导入的类会被Spring 加载到IOC容器中,导入形式主要有以下几种

    • 导入普通类

      • image-20240401122947934
    • 导入配置类

      • image-20240401123312638
    • 导入ImportSelector 接口实现类

      • image-20240401123344957
    • @EnableXxxx 注解,封装 @Import

      • image-20240401123509258

源码跟踪

@SpringbootApplication

Springboot的启动类上有个 @SpringBootApplication 注解 这个注解内还 有着 以下三个注解:

image-20240401151513118

自动配置的核心:@EnableAutoConfiguration

@SpringbootApplication 该注解标注在Springboot的工程引导类上,是Springboot中最最最重要的注解。该注解由三个部分组成:

  • @SpringbootConfiguration :该注解与 @Configruration 注解作用相同,用来声明当前也是一个配置类
  • @ComponentScan : 组件扫描,默认扫描当前引导类所在的包及其子包
  • @EnaleAutoConfiguration :SpringBoot 实现自动化配置的核心
@ConditionalMissingBean
  • 作用:按照一定条件进行判断,在满足给定条件后才会注册对应的bean对象到springboot的IOC容器中
  • 位置:方法、类
  • @Conditional 本身是一个父注解,派生出大量的子注解:
    • @ConditionalOnClass :判断环境中 是否有对应的字节码文件,才会注册bean到IOC容器中
      • image-20240401160505935
    • @ConditionalOnMissingBean :判断环境中没有对应的bean(类型 或 名称) 才注册bean到IOC容器
      • image-20240401160522629
    • @ConditionalOnProperty :判断配置文件中有对应属性和值,才注册bean到IOC容器
      • image-20240401160537920

start

  • 场景:在实际开发中,经常会定义一些公共组件,提供给各个团队使用,而在Springboot的项目中,一般会将这些公共组件封装为SpringBoot的starter。
  • image-20240401163617636
  • image-20240401163634021
1
2
注意:springboot官方提供的起步依赖中 spring-boot 在前   
其他技术提供: 功能在前

image-20240401163744139

图中,黄色背景中 有粉色背景的依赖 所以 当我加载黄色依赖的时候 它会自动加载里面引入的粉色依赖

自定义start

image-20240401163913668

image-20240401163920815

步骤:

  • 将之前工程中的 AliOSSProperties.java AliOSSUtils.java 复制到aliyun-oss-boot-autocongifure工程中 这个时候会发现报错了
    • AliOSSUtils.java 方法报错:MultipartFile 原因是没有导入spring-boot-starter-web 依赖
    • image-20240401195856740
    • image-20240401200005244
    • get set方法找不到 是因为AliyunOSSProperties中的 lombok依赖没有导入 这时候只需要重写以下get set方法就行了
  • 这个时候 发现 AliOSSProperties.java AliOSSUtils.java 上面的@compenent 注解都用没用了:因为 将来如过有用户使用 我们也不想让他们扫描到 所以 要把从@compenent 注解删掉 但是删掉后 又有错误
    • 因为不是IOC容器 所以不能使用@Autowired 注入 但是要求中 说我们要进行注入AliOSSUtils 所以就要用到 @Bean 注解
      • 声明一个 配置类 @Confirguration 来进行 配置 将其交给IOC容器
        • image-20240401201658838
    • AliOSSProperties.java 也会报错
      • image-20240401200727060
      • image-20240401200742532
      • 这时我们发现 缺少了 @EnableConfirgurationProperties 注解 所以我们要 在AliyunOSSConfirgue中添加 此注解
        • image-20240401201802085
  • 此外 还会发现 AliOSSUtils 中的 aliOSSProperties 我们并没有声明值 所以我们要给他写上get set方法
    • image-20240401201017568
    • 并且 在配置类的 @EnableConfirgurationProperties 注解内添加 并将AliOSSProperties.class类导入
      • image-20240401201108735
      • 进行注入 并将方法 中的值进行返回 注入 直接在方法形参内声明
        • image-20240401201146741
  • 最后 进行测试
    • image-20240401201326172
    • 引入测试类 并 在pom文件中 引入aliyun-oss-spring-boot-starter 的依赖 注意 不需要引入 aliyun-oss-spring-boot-autoconfigure 的 依赖 因为 aliyun-oss-spring-boot-starter 已经引入 又因为 依赖的传递性 所以 不用在引入了
    • image-20240401201513561
    • uploadController 中 注入aliOSSUtils 并调用upload方法 此时 就获得了 图像的url

Maven高级

分模块设计与开发

当我们很多人 共同开发一个项目的时候 如何做到数据共享呢? 这个时候就要将 我们要共i想的数据 封装到一个新模块中 当我们需要使用的 时候 直接导入对应的依赖即可

将之前完成的tlias包 进行分模块

  • 先分别创建存放pojo和utils的模块
    • image-20240402092430991
  • 将pojo包下的类 一同copy 到 tlias-pojo模块中 utils同理
    • image-20240402092640342
    • 再将 pojo类所需要的依赖 导入到pom文件中去
  • 最后 在这个项目中 导入 tlais-pojo 和tlais-utils 的依赖
    • image-20240402092833445
1
2
3
4
5
6
1.什么是分模块设计?
·将项目按照功能拆分成若干个子模块
2.为什么要分模块设计?
·方便项目的管理和维护、扩展,也方便模块间的相互调用,资源共享
3.注意事项:
·分模块设计需要先针对模块功能进行设计,在进行编码。不会先将工程开发完毕,然后进行拆分

继承与聚合

继承

  • 概念:继承描述的是两个工程间的关系,与Java中的继承相似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承
  • 作用:简化依赖配置、统一管理依赖
  • 实现:

继承关系

  1. 创建maven模块,tlias-parent,该工程为父工程,设置打包方式pom (默认jar)
    • image-20240402100153810
  2. 在子工程的pom.xml文件中,配置继承关系
    • image-20240402100221947
    • relativePath 父工程的相对路径
  3. 在父工程中配置各个工程共有的依赖(子工程会自动继承父工程的依赖)
    • image-20240402100324022

image-20240402100116088

1
2
3
注意:
在子工程中,配置了继承关系之后,坐标中的groupId是可以省略的,因为会自动创建父工程的
relativePath指定父工程的pom文件的相对位置(如果不指定,将从本地仓库/远程仓库查找该工程)

打包方式:

1
2
3
jar:普通模块打包,springboot项目基本都是jar包(内嵌tomcat运行)
war:普通web程序打包,需要部署在外部的tomcat服务器中运行
pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理

版本锁定

  • 在maven中,可以在父工程的pom文件中通过, 来统一管理版本
    • image-20240402103059965
    • 子工程引入依赖时,无需指定 版本号,父工程统一管理,变更依赖版本,只需要在父工程中变更
  • 自定义属性/引用属性
    • 如果 以后引入的依赖太多 一个个版本找起来也很麻烦 这个时候 就需要定义属性了
      • image-20240402104254199
      • image-20240402104303201
      • image-20240402104307962
      • image-20240402104434598
1
2
3
4
面试题:
<dependencyManager> 与 <dependencies> 的区别是什么?
<dependencies> 是直接依赖,在父工程中配置了依赖,子工程会直接继承下来
<dependencyManager> 是统一管理依赖版本,不会直接依赖,还需要在子工程中引入所需要的依赖(无需指定版本)

聚合

当我们要打包项目的时候 要一个个install然后再pakage 这个时候 会很繁琐 我们就要用到耦合

image-20240402112815627

  • maven 中可以通过 设置当前聚合工程所包含的子模块名称
    • image-20240402112947489
  • image-20240402113005262
    • 这个时候 直接运行 父工程中的package 就可实现 打包操作

总结:

  • 作用:
    • 聚合用于快速构建项目
    • 继承用于简化依配置、统一依赖管理
  • 相同点:
    • 聚合与继承的pom.xml 文件打包方式均为pom 额可以将两种关系制作到同一个pom文件中
    • 聚合与继承 均属于 设计性模块 并无实际的模块内容
  • 不同点:
    • 聚合实在聚合工程中配置,聚合可以感知到参与聚合的模块有那些
    • 继承是在子模块中配置关系,父模块无法感知那些子模块 继承了自己

私服

  • 私服是一种特殊的远程仓库,他是架设在局域网内的仓库服务,用来代替位于外部的中央仓库,用于解决团队内部的资源共享与资源同步问题
  • image-20240402115910067

创建私服 的方法较为固定 参考 [私服配置说明.md](E:\BaiduNetdiskDownload\最新版JavaWeb开发教程\资料\day15-maven高级\资料\01. maven高级\02. 私服配置案例\私服配置说明.md)