一、Mybatis-Plus
1、快速入门
从项目资源中导入 demo 代码。
MyBatis-Plus 官方提供了 starter,其中集成了 Mybatis 和 Mybatis-Plus 的所有功能,并且实现了自动装配效果。因此我们可以用 Mybatis-Plus 的 starter 代替 Mybatis 的 starter。
Mybatis-Plus 简化了 Java 数据库操作,通过自动映射、条件构造器及插件功能提升开发效率。本文详解其快速入门、原理反射机制、Wrapper 用法、IService 接口扩展及逻辑删除等核心特性。同时涵盖 Docker 容器化基础,包括 MySQL 安装、镜像管理、数据卷挂载、自定义镜像构建及网络互联配置,助力实现应用环境的标准化部署。

从项目资源中导入 demo 代码。
MyBatis-Plus 官方提供了 starter,其中集成了 Mybatis 和 Mybatis-Plus 的所有功能,并且实现了自动装配效果。因此我们可以用 Mybatis-Plus 的 starter 代替 Mybatis 的 starter。
(2)继承 BaseMapper 类
继承 BaseMapper<User>是为了获得一组功能强大的、通用的数据库操作方法。<User>的作用是:为这个泛型接口 BaseMapper指定一个具体的类型。BaseMapper<User>的意思是:这个 Mapper 是专门用于操作 User实体类的。
(3)把原有的方法替换 测试一下,成功实现!

原理: Mybatis-Plus 通过扫描实体类,并基于反射获取【实体类信息】作为【数据库表信息】。
反射是 Java 语言的一种机制,它允许程序在运行时获取自身的信息,并能动态操作类或对象的属性、方法。反射的作用:在程序运行起来之后,通过一个类的完整路径名(如 com.example.User),可以动态地:获取这个类的所有信息(类名、修饰符、父类、接口等)。获取这个类中声明的所有字段(字段名、类型、修饰符等)。获取这个类中声明的所有方法(方法名、参数类型、返回类型等)。实例化这个类的对象。读取或修改一个对象的字段值(即使该字段是 private的)。调用一个对象的方法。
📍Mybatis-Plus 如何利用反射获取表/字段信息?
**扫描类路径:**MP 会扫描你配置的包路径(如 com.example.mapper),找到所有继承了 BaseMapper<T>的接口(如 UserMapper)。
**提取泛型参数:**通过反射 API(如 JavaType),MP 从 UserMapper接口上提取出它的泛型参数 <User>。这样 MP 就确定了这个 Mapper 要操作的实体类是 com.example.entity.User。
**加载并分析实体类:**MP 使用 Class.forName("com.example.entity.User")或类似方法,将 User类加载到内存中,并获取一个 Class<User>对象。这个 Class对象是反射的入口,它包含了 User类的完整元数据。
**获取表名信息:**MP 通过 Class对象的 getSimpleName()方法获取类的简单名称 "User"。MP 检查该类上是否有 @TableName注解。如果有,则使用注解指定的值作为表名;如果没有,则应用其默认命名策略(例如,将类名驼峰 "BaseUser"转换为下划线格式 "base_user"作为表名)。
**获取字段信息:**MP 通过 Class对象的 getDeclaredFields()方法,获取 User类中声明的所有字段(如 id, userName, password)对应的 Field对象数组。遍历每个 Field对象:通过 field.getName()获取字段名(如 "userName")。检查字段上是否有 @TableField或 @TableId注解,以确定它对应数据库的普通列还是主键列,以及自定义的列名是什么。如果没有注解,则应用默认变量命名策略(例如,将变量名驼峰 "userName"转换为 "user_name"作为字段名)。通过 field.getType()获取字段的 Java 类型(如 String.class),用于在 SQL 操作时进行类型转换。
**构建映射关系缓存:**MP 将以上分析结果(类 User-> 表 user,字段 userName-> 列 user_name等)缓存起来,形成一个完整的 TableInfo对象。之后所有通过 UserMapper进行的数据库操作,都会查询这个 TableInfo来动态生成正确的 SQL。
📍通过上述过程我们明白 MP 的默认转换规则:类名驼峰→下划线作为表名(如:BaseUser → base_user);变量名驼峰→下划线作为字段名(如:userName → user_name);名为 id 的变量作为主键。
📍那如果我们想自定义这些名称,需要用到下面的注解: @TableName:用来指定表名 @TableId:用来指定表中的主键字段信息 IdType.AUTO:数据库自增长 IdType.INPUT:通过 set 方法自行输入 IdType.ASSIGN_ID:分配 ID,接口 IdentifierGenerator 的方法 nextId 来生成 id,默认实现类为 DefaultIdentifierGenerator 雪花算法 @TableField:用来指定表中的普通字段信息成员变量名与数据库字段名不一致成员变量名以is开头,且是布尔值(如:isMarried)成员变量名与数据库关键字冲突(如:order)成员变量不是数据库字段(exist = false)
Mybatis-Plus 支持各种复杂的 where 条件。
(1)QueryWrapper(查询条件构造器) ① 查询出名字中带 o 的,存款大于等于 1000 元的人的 id、username、info、balance 字段。 ② 更新用户名为 jack 的用户的余额为 2000。
(2)UpdateWrapper(更新条件构造器) 更新 ID 为 1,2,4 的用户余额,扣 200。
(3)LambdaQueryWrapper(Lambda 表达式查询构造器) 查询出名字中带 o 的,存款大于等于 1000 元的人的 id、username、info、balance 字段。
(4)LambdaUpdateWrapper(Lambda 表达式更新构造器) 推荐优先使用 Lambda 表达式版本,因为它们在编译期就能发现字段名错误,更加安全可靠。
当 MyBatis-Plus 自带的条件构造器无法满足复杂需求(如多表关联、复杂计算、使用数据库特有功能)时,就必须使用自定义 SQL。
我们可以利用 MyBatisPlus 的Wrapper 来构建复杂的 Where 条件,然后自己定义 SQL 语句中剩下的部分。 📍需求:将 id 在指定范围的用户(1,2,4)的余额扣减指定值。
① 基于 Wrapper 构建 where 条件 ② 在 mapper 方法参数中用 Param 注解声明 wrapper 变量名称,必须是 ew ③ 自定义 SQL,并使用 Wrapper 条件
📍为什么要使用 IService?
封装常用业务逻辑:将常见的业务操作(如批量操作、链式查询、分页查询等)进行封装,避免在 Service 中重复编写简单代码。
提供更业务友好的方法:相比 Mapper 的数据库导向的方法,Service 层的方法命名更贴近业务。例如:saveUser()而不仅仅是 insert();getUserById()而不仅仅是 selectById();removeUserById()而不仅仅是 deleteById()。
实现复杂的批量操作:提供了 Mapper 层没有的批量操作方法,如 saveBatch()、updateBatchById()等。
UserService 继承 IService 接口,获得丰富的查询接口。UserServiceImpl 继承 ServiceImpl 实现类,获得接口所需要实现的实现类。
📍如何使用 IService? 第一步:创建 Service 接口,继承 IService。 第二步:创建 Service 实现类,继承 ServiceImpl。
【1】在 pom 文件引入依赖
【2】在 application 文件引入 swagger 配置
【3】导入 DTO、VO 文件
【4】编写 controller 层
这里依赖注入与@Autowired 类似,但更安全。作用:让 Spring 自动通过构造方法,为 UserController注入一个 IUserService的实现类实例,并且保证这个依赖在程序运行期间不可改变(final)。@RequiredArgsConstructor这是 Lombok 库提供的一个注解。作用:它在编译时会自动为类生成一个构造方法。生成规则:这个构造方法会包含所有必须初始化的字段作为参数。哪些是'必须初始化'的字段呢?就是所有被声明为 final的字段(比如上面的 userService)以及被 @NonNull注解的字段。
【5】上述是基础 CRUD,而我们实际开发中会遇到复杂的功能开发,会编写 service 层。 登录 swagger 文档测试一下:http://localhost:8080/doc.html
将 name、status 等参数打包成一个类(从资料中导入)。 【1】controller 层 【2】serviceImpl 层
在 serviceImpl 层修改之前的 deductBalance 根据 id 扣减余额代码。
由于查询和修改余额不是原子性操作,因此容易出现并发问题:两个用户同时扣减该账户余额、出现数据覆盖。
因此我们加入乐观锁,也就是 .eq(User::getBalance,user.getBalance())这行代码:乐观锁就是通过版本号或数据校验在更新时检查数据是否被修改,避免并发修改导致的数据不一致问题。在更新时,检查余额的当前值是否与查询时一致。如果余额未被其他线程修改,说明还没扣呢,更新成功。如果余额已被修改,说明已经有人改过了,不需要修改,更新返回 0 条记录。
但这样容易出现 aba 问题,但我们这里先不细究其他线程可能先扣款后又退款,余额数值回到原值虽然余额数值相同,但中间发生了业务变化。
📍【第一种】普通 for 循环插入性能极差,不推荐。每次插入都单独执行 SQL 语句。
📍【第二种】MyBatis-Plus 的批量新增基于预编译的批处理,性能不错。使用 MP 的 saveBatch()方法。
📍【第三种】配置 rewriteBatchedStatements 参数在 application 的 datasource 中 url 末尾添加配置。在 JDBC 连接字符串中加入 rewriteBatchedStatements=true,性能最好。让 MySQL 真正支持批量处理。rewriteBatchedStatements=true会让 JDBC 将多个 INSERT 语句重写为单个多值 INSERT 语句。

然后在【工具栏】内找到【Config Database】进行数据库配置,注意改数据库名称。同样在【工具栏】中打开【Code Generator】(有的版本在 Other 栏)。按下图填写。然后 check field 后直接 code generator,成功生成代码框架!
Db工具类在解决业务逻辑循环依赖方面具有独特优势,因为它不依赖 Spring 容器管理,是静态方法调用。假设有两个 Service:UserService需要调用 OrderService的方法;OrderService也需要调用 UserService的方法。这会形成典型的 Spring Bean 循环依赖。
Db的核心优势是:绕过 Service 层,直接查询数据。Db工具类可以解决技术上的循环依赖,但需要注意:适用场景:简单的、只读的、无业务逻辑的查询。不适用场景:涉及业务规则、事务管理、复杂逻辑的操作。
DB 静态工具的接口方法与 IService 类似。
① 根据 id 查询用户及其对应的所有地址
② 批量查询用户及其对应的地址列表
逻辑如下:listByIds()根据用户 ids 列表查询用户信息,封装为 VO。获取用户 id 集合,并根据 id 查询出每个用户对应的地址列表。将地址列表转换为 VO,此时的地址列表为混杂未分类形式。为了方便后续按【用户信息:地址列表】形式封装,需要将地址 VO 列表按 userId 分组。最后,封装 VO【用户 VO:对应地址列表 VO】。
③ 根据用户 id 查询收货地址,需要验证用户状态
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:在表中添加一个字段标记数据是否被删除。当删除数据时把标记置为 1。查询时只查询标记为 0 的数据。 而要把一起写的 delete 语句一个一个改很麻烦,因此我们使用 mybatisplus 提供的逻辑删除功能。 MybatisPlus 提供了逻辑删除功能无需改变方法调用的方式,而是在底层帮我们自动修改 CRUD 的语句。我们要做的就是在application.yaml 文件中配置逻辑删除的字段名称和值即可:在 application.yaml 文件中添加配置。 然后我们对 IAdressService 接口创建单元测试。按原来如果我们写 removeById 语句,底层执行的应该为 delete 语句。而经过 mp 逻辑删除配置,我们可以看到 mp 将原本的 delete 语句转换为 update 语句,将删除数据变为修改数据的逻辑删除字段,从而实现数据不显示但仍并未删除的功能。我们可以看到,查询时数据显示已为 null,但实际上这条数据并没有被删除。
问题场景:数据库的 user表中有一个 status字段,类型是 INT,用于表示用户状态(1 代表正常,2 代表冻结)。如果在 Java 代码中直接用整数(1, 2)来操作,代码可读性差,且容易传入错误的值(如 3, 4)。
解决方案:创建自定义枚举 UserStatus。枚举中明确定义了 NORMAL(1, "正常")和 FREEZE(2, "冻结")两个实例。在 User实体类中,将 status字段的类型从 Integer改为 UserStatus。
优势:类型安全:编译器能检查,避免传入无效的状态值。易读:使用 UserStatus.NORMAL比使用数字 1意义明确得多。易于维护:状态值集中管理,修改只需改枚举。
在 application.yaml 中配置全局枚举处理器:创建 enums 枚举类。然后修改 User 的状态类型。最后替换代码中的常量。
📍总结:如何使用 Mp 处理枚举 Enums 与数据库字段的映射
【第一步】标记枚举的数据库值。在枚举类中,用 @EnumValue注解标记哪个字段的值对应数据库存储。
【第二步】配置全局枚举处理器。在 application.yml中配置:这个配置会让 MyBatis-Plus 自动处理所有带 @EnumValue注解的枚举。
实现的效果保存数据时:UserStatus.NORMAL→ 数据库存入 1。查询数据时:数据库中的 1→ 自动转换为 UserStatus.NORMAL对象。代码更安全:避免了直接使用数字 1, 2,编译时就能发现错误。
当数据库的某个字段存储的是 JSON 格式的复杂数据时,我们希望在 Java 代码中能像操作普通对象一样操作这个 JSON 数据,而不是手动拼接字符串。
我们可以使用 MyBatis-Plus 自动完成 JSON ↔ Java 对象的转换:
【1】首先创建实体类 UserInfo。@AllArgsConstructor(staticName = "of")是一种创建对象的优雅方式,使代码更简洁、更函数式。
【2】替换 User 实体类中 info 的类型,从 String→UserInfo。
【1】配置分页插件作用:启用了 MyBatis-Plus 的分页功能。 【2】应用分页 API
【1】定义实体类需要定义 3 个实体:UserQuery:分页查询条件的实体,包含分页、排序参数等(画横线这几个可以继承 PageQuery)。PageDTO:分页结果实体,包含总条数、总页数、当前页数据。UserVO:用户页面视图实体。
UserQuery 类。这其中缺少分页查询相关参数(页码、每页数据条数等),因为别的业务也需要使用分页,因此我们需要单独创建一个 PageQuery 类,然后让 UserQuery 继承这个实体。
PageQuery 类。
【2】Controller 层定义用户分页接口
【3】Service 层编写分页逻辑
【1】在 PageQuery实体中封装分页工具通过上面 Service 层的逻辑代码,我们发现【构建分页条件】【数据非空校验】【数据转换】在每一个分页业务中都是一样的,我们可以将其封装成工具方法,以后需要使用时直接调用。最灵活(核心):toMpPage(OrderItem...)- 可传多个排序条件。中等:toMpPage(String, boolean)- 指定单个排序。最方便:toMpPageDefaultSortByXxx()- 直接调用,无需参数。
【2】在 PageDTO 中封装数据判空、转化工具empty()- 返回空结果,只保留分页信息。of(Class)- 自动反射转换,适合简单场景。of(Function)- 自定义转换,适合复杂场景。
【3】优化分页逻辑代码这样我们在 UserServiceImpl 中的分页查询业务层代码就能简化为:如果封装数据时希望自定义,可以这么编写:关于自定义 lambda 函数运行流程:Lambda 表达式在编译期检查字段名,避免运行时错误,且代码更简洁。
Docker 是一个容器化平台,可以让你把应用和它的运行环境打包在一起,在任何地方都能一致运行。 " 把我的整个运行环境一起打包给你 " 包含:代码 + 软件 + 配置 + 系统环境。 ❗️对于安装 docker 步骤我这里跳过(因为直接用的别人安装好 docker 的服务器)。
安装前保证:停止虚拟机中的 MySQL 服务。确保 Docker 已安装并启动。外部无法直接访问容器内部端口(3306),需要通过服务器的3307 端口被映射到容器的3306 端口。
注意:因为我的服务器 3306 端口被占用,所以我换成 3307 端口。
容器已成功创建,容器 ID 为:4dc488f4769e7facfe540d75fd842ad3338e2ad361c74faabccac58c2a6e7821。
用 Navicat 尝试连接数据库。连接成功!
# 启动 Docker(如果还没启动)
sudo systemctl start docker
# 验证 Docker 是否运行
docker -v
# 如果已经有原生 MySQL,先停止
sudo systemctl stop mysql
镜像:当我们利用 Docker 安装应用时,Docker 会自动搜索并下载应用镜像 (image)。镜像不仅包含应用本身,还包含应用运行所需要的环境、配置、系统函数库。 容器:Docker 会在运行镜像时创建一个隔离环境,称为容器 (container)。 镜像仓库:存储和管理镜像的平台,Docker 官方维护了一个公共仓库,里边包含了很多软件的镜像。 镜像命名规范:镜像名称由两部分组成,格式为:[repository]:[tag]。repository:镜像名。tag:版本号。没有指定 tag 时,默认 latest,代表最新版本。
docker build:作用:构建镜像。它读取 Dockerfile 和上下文中的文件,按照步骤执行,最终生成一个可用的本地镜像。流向:Dockerfile→ 本地镜像。 docker pull:作用:拉取镜像。从远端镜像仓库(如 Docker Hub)下载指定的镜像到本地镜像库。流向:镜像仓库 → 本地镜像。 docker push:作用:推送镜像。将本地的镜像上传到远端镜像仓库,供他人或其他机器使用。流向:本地镜像 → 镜像仓库。 docker images:作用:列出镜像。查看本地已经下载或构建的所有镜像列表及其信息。 docker rmi:作用:删除镜像。从本地镜像库中移除一个或多个不再需要的镜像,释放磁盘空间。 docker save:作用:保存镜像为文件。将一个或多个本地镜像打包成一个 tar 归档文件,便于离线分享或备份。
docker run:作用:创建并启动容器。它基于指定的本地镜像,创建并启动一个新的容器实例。如果本地没有该镜像,会先尝试执行 docker pull。流向:本地镜像 → 容器。 docker start:作用:启动容器。启动一个已存在但处于停止状态的容器,使其进入'运行中'状态。 docker stop:作用:停止容器。向一个'运行中'的容器发送停止信号,让其停止运行。 docker exec:作用:在运行中的容器内执行命令。进入一个正在运行的容器,并执行一个命令(例如 /bin/bash),常用于调试或执行临时操作。 docker logs:作用:查看容器日志。获取容器运行过程中产生的标准输出和错误日志,是排查问题、查看应用输出的主要方式。 docker ps:用于查看当前正在运行的容器列表。 因为 docker ps 返回的列表格式混乱,因此我们需要在指令后加命令格式。但显然每次加命令格式会很繁琐,因此我们需要设定命令别名。按回车进入 vim 编辑器,按 i 进入插入模式(下方会显示 INSERT),写完命令按 Esc 退出,然后输入 :wq保存并退出。最后在终端输入下面命令进行加载。以后输入 dps 就可以直接运行 docker ps ……一长串命令!
首先拉取 nginx 镜像。然后启动 nginx 容器。如果你的主机 80 端口被占用,先清理掉创建失败的容器,再重新创建。
官方 Nginx 镜像为了保持轻量,只包含运行 Nginx 所必需的最小化软件包。不包含 vi、vim、nano 等文本编辑器。不推荐在运行的容器内直接修改文件。 为了解决这个问题,我们引入数据卷: 数据卷(Volume)是一个虚拟目录,是容器目录与宿主机目录之间映射的桥梁,方便我们操作容器内的文件,也方便迁移容器产生的数据。图中的连接线就表示这种映射关系。双向同步:任何一边的修改都会实时反映到另一边。持久化:容器删除后,数据仍在宿主机保留。共享:多个容器可以挂载同一个数据卷。
**如何挂载数据卷?**在 docker run命令中,通过 -v 数据卷名称:容器内目录参数可以实现数据卷挂载。如果挂载时指定的数据卷不存在,Docker 会自动创建这个数据卷。
1.先删除 nginx。
2.进行数据挂载。
3.然后确定一下数据卷有没有成功创建。
4.查看数据卷详情。
5.获取宿主机目录后,转到宿主机目录下。
6.这下用 vi 命令编辑 index.html 目录就没有问题了。这是 vim 编辑器,按 i 进入编辑,改完按 esc,最后输入:wq 退出并保存。
7.然后访问 nginx 主页就会发现修改完成!
我们会发现 mysql 自动创建了一个数据卷,用于数据持久化存储。如果用户不指定挂载,Docker 会自动创建匿名数据卷。这是为了保证数据库数据不会因容器删除而丢失。但匿名卷难以管理,建议在生产环境中使用命名数据卷。
先删掉 mysql。确定一下我们本次需要实现 3 项文件的挂载,包括其对应容器内路径:挂载 /root/mysql/data 到容器内的 /var/lib/mysql 目录。挂载 /root/mysql/init 到容器内的 /docker-entrypoint-initdb.d 目录,携带课前资料准备的 SQL 脚本。挂载 /root/mysql/conf 到容器内的 /etc/mysql/conf.d 目录,携带课前资料准备的配置文件。 【1】创建本地目录 /mysql 下的/data、/init、/conf先回到根目录下,然后通过 mkdir 指令创建目录,创建完用 ls 查看目录。 【2】将课程资料中的脚本、配置文件放入本地目录这里我是直接拖动文件,其他的存入方法可以询问 AI。 【3】进行本地目录挂载注:看你【根目录】名称叫什么,一般是 root,我这里是/home/roye,注意修改!在执行 docker run 命令时,使用 -v 本地目录:容器内目录 可以完成本地目录挂载。本地目录必须以"/"或"."开头,如果直接以名称开头,会被识别为数据卷而非本地目录。-v mysql:/var/lib/mysql 会被识别为一个数据卷叫 mysql。-v ./mysql:/var/lib/mysql 会被识别为当前目录下的 mysql 目录。 输入 dps 查看 mysql 容器是否成功运行。 【4】检查是否运行成功如果运行成功,原来新建的 data 目录下会有相关文件生成,还有 hmall 数据库。进入 mysql 查看数据库。查看编码表,是 utf8mb4 没有问题。查看数据库。切换到 hmall 数据库查看表。说明数据挂载成功!
镜像就是包含了应用程序、程序运行的系统函数库、运行配置等文件的文件包。构建镜像的过程其实就是把上述文件打包的过程。构建一个 Java 镜像的步骤:准备一个 Linux 运行环境。安装 JRE 并配置环境变量。拷贝 Jar 包。编写运行脚本。
Dockerfile是一个文本文件,包含一系列用于构建 Docker 镜像的指令。它定义了如何从基础镜像开始,逐步添加配置、安装软件、复制文件,最终生成一个可运行的容器镜像。
通过上一部分学习,我们已经知道,如何构建一个 Java 应用镜像。现在我们只需要把打包好的镜像上传到 docker 并命名使用。
【1】从资料包中获取 demo 包,可以看到已经打包好的镜像 docker-demo.jar:Java 应用程序的可执行 JAR 包。Dockerfile:镜像构建配置文件。
【2】把整个 demo 目录拖进本地目录。
【3】基础镜像涉及的 jdk 包需要下载,而资料包已经提供了 jdk 包,因此我们直接拖进本地目录即可。
【4】加载镜像接着通过 docker images 查看镜像。构建一个名为 docker-demo的 Docker 镜像。自定义镜像构建完成,如果想要启动则运行下面的命令。接着查看日志。成功运行!
默认情况下,所有容器都是以 docker 初始化的网桥连接的。但是容器间互联会非常麻烦,需要手动获取 IP 地址,容器重启后 IP 会变化。因此我们需要掌握【自定义网络互联】这样就不需要知道 IP 地址,直接通过容器名称就可以进行容器间互联。下面是一些 docker 网络的指令。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online