【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段

devtools/2024/11/22 20:45:25/

文章目录

  • 一、MyBatis-Plus简介
  • 二、快速入门
    • 1、环境准备
    • 2、将mybatis项目改造成mybatis-plus项目
      • (1)引入MybatisPlus依赖,代替MyBatis依赖
      • (2)配置Mapper包扫描路径
      • (3)定义Mapper接口并继承BaseMapper<T>
    • 3、常见注解
      • (1)@TableName
      • (2)@TableId
      • (3)@TableField
      • (4)使用案例
    • 4、常见配置
  • 三、核心功能
    • 1、条件构造器
      • (1)QueryWrapper
      • (2)UpdateWrapper
      • (3)基于Lambda的Wrapper
    • 2、自定义SQL
      • (1)自定义SQL片段
      • (2)多表联查
    • 3、IService接口
      • (1)常用方法介绍
      • (2)基本用法
      • (3)Restful案例
      • (4)LambdaQuery和LambdaUpdate
      • (5)批量新增 & 批处理方案性能测试
  • 四、扩展功能
    • 1、代码生成
      • (1)安装插件
      • (2)使用步骤
      • (3)代码生成器配置
    • 2、静态工具
      • (1)案例一
      • (2)案例二
      • (3)案例三
    • 3、逻辑删除
      • (1)介绍
      • (2)使用步骤
      • (3)@TableLogic
    • 4、枚举处理器
      • (1)定义枚举,标记@EnumValue
      • (2)配置枚举处理器
    • 5、JSON类型处理器
      • (1)定义接收Json的实体类
      • (2)指定类型处理器
    • 6、yaml配置加密
      • (1)生成密钥
      • (2)修改配置
      • (3)配置密钥运行参数
      • (4)实现原理
    • 7、自动填充字段
      • (1)配置自动填充处理器
      • (2)添加@TableField的fill属性
  • 五、插件功能
    • 1、分页插件
      • (1)引入依赖
      • (2)配置分页内置拦截器
      • (3)分页API
    • 2、通用分页实体
      • (1)实体类设计
      • (2)开发接口
      • (3)改造PageDTO实体
      • (4)改造PageResult实体




一、MyBatis-Plus简介

MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

  • 框架结构

MyBatis-Plus官网:https://baomidou.com/

参考文档:https://mybatis.plus/ (网站访问速度稍慢,建议直接看官网文档)




二、快速入门

1、环境准备

  • 导入数据库表结构mp.sql,一共三张表useraddress,还有测试MP注解的表tb_user
-- --------------------------------------------------------
-- 主机:                           127.0.0.1
-- 服务器版本:                        8.0.28 - MySQL Community Server - GPL
-- 服务器操作系统:                      Win64
-- HeidiSQL 版本:                  12.2.0.6576
-- --------------------------------------------------------/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;-- 导出 mp 的数据库结构
CREATE DATABASE IF NOT EXISTS `mp` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `mp`;-- 导出  表 mp.address 结构
CREATE TABLE IF NOT EXISTS `address` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` bigint DEFAULT NULL COMMENT '用户ID',`province` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '省',`city` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '市',`town` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '县/区',`mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手机',`street` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '详细地址',`contact` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人',`is_default` bit(1) DEFAULT b'0' COMMENT '是否是默认 1默认 0否',`notes` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',`deleted` bit(1) DEFAULT b'0' COMMENT '逻辑删除',PRIMARY KEY (`id`) USING BTREE,KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;-- 正在导出表  mp.address 的数据:~11 rows (大约)INSERT INTO `address` (`id`, `user_id`, `province`, `city`, `town`, `mobile`, `street`, `contact`, `is_default`, `notes`, `deleted`) VALUES(59, 2, '北京', '北京', '朝阳区', '13900112222', '金燕龙办公楼', 'Rose', b'1', NULL, b'0'),(60, 1, '北京', '北京', '朝阳区', '13700221122', '修正大厦', 'Jack', b'0', NULL, b'0'),(61, 1, '上海', '上海', '浦东新区', '13301212233', '航头镇航头路', 'Jack', b'1', NULL, b'0'),(63, 2, '广东', '佛山', '永春', '13301212233', '永春武馆', 'Rose', b'0', NULL, b'0'),(64, 3, '浙江', '杭州', '拱墅区', '13567809102', '浙江大学', 'Hope', b'1', NULL, b'0'),(65, 3, '浙江', '杭州', '拱墅区', '13967589201', '左岸花园', 'Hope', b'0', NULL, b'0'),(66, 4, '湖北', '武汉', '汉口', '13967519202', '天天花园', 'Thomas', b'1', NULL, b'0'),(67, 3, '浙江', '杭州', '拱墅区', '13967589201', '左岸花园', 'Hopey', b'0', NULL, b'0'),(68, 4, '湖北', '武汉', '汉口', '13967519202', '天天花园', 'Thomas', b'1', NULL, b'0'),(69, 3, '浙江', '杭州', '拱墅区', '13967589201', '左岸花园', 'Hopey', b'0', NULL, b'0'),(70, 4, '湖北', '武汉', '汉口', '13967519202', '天天花园', 'Thomas', b'1', NULL, b'0');-- 导出  表 mp.user 结构
CREATE TABLE `user` (`id` BIGINT(19) NOT NULL AUTO_INCREMENT COMMENT '用户id',`username` VARCHAR(50) NOT NULL COMMENT '用户名' COLLATE 'utf8_general_ci',`password` VARCHAR(128) NOT NULL COMMENT '密码' COLLATE 'utf8_general_ci',`phone` VARCHAR(20) NULL DEFAULT NULL COMMENT '注册手机号' COLLATE 'utf8_general_ci',`info` JSON NOT NULL COMMENT '详细信息',`status` INT(10) NULL DEFAULT '1' COMMENT '使用状态(1正常 2冻结)',`balance` INT(10) NULL DEFAULT NULL COMMENT '账户余额',`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `username` (`username`) USING BTREE
)
COMMENT='用户表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
ROW_FORMAT=COMPACT
AUTO_INCREMENT=5
;-- 正在导出表  mp.user 的数据:~4 rows (大约)INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `status`, `balance`, `create_time`, `update_time`) VALUES(1, 'Jack', '123', '13900112224', '{"age": 20, "intro": "佛系青年", "gender": "male"}', 1, 1600, '2023-05-19 20:50:21', '2023-06-19 20:50:21'),(2, 'Rose', '123', '13900112223', '{"age": 19, "intro": "青涩少女", "gender": "female"}', 1, 600, '2023-05-19 21:00:23', '2023-06-19 21:00:23'),(3, 'Hope', '123', '13900112222', '{"age": 25, "intro": "上进青年", "gender": "male"}', 1, 100000, '2023-06-19 22:37:44', '2023-06-19 22:37:44'),(4, 'Thomas', '123', '17701265258', '{"age": 29, "intro": "伏地魔", "gender": "male"}', 1, 800, '2023-06-19 23:44:45', '2023-06-19 23:44:45');/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;create table tb_user
(user_id    bigint auto_increment comment '用户id',username   varchar(20)       null comment '用户名',password   varchar(20)       null comment '密码',is_deleted TINYINT default 0 null comment '逻辑删除',`order`    TINYINT           null comment '排序序号',constraint tb_user_pkprimary key (user_id),constraint tb_user_pk2unique (username)
)
comment '用户表(测试mp注解)';

数据库表结构如下:

  • 导入项目结构mp-demo

application.yaml中修改jdbc参数为自己的数据库参数:

spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456
logging:level:com.itheima: debugpattern:dateformat: HH:mm:ss
mybatis:mapper-locations: classpath*:mapper/*.xml

mybatismybatisplus_185">2、将mybatis项目改造成mybatis-plus项目

基于现有的mybatis项目将其改造成mybatis-plus实现如下功能:

  1. 新增用户功能
  2. 根据id查询用户
  3. 根据id批量查询用户
  4. 根据id更新用户
  5. 根据id删除用户

比如我们要实现User表的CRUD,只需要下面几步。

(1)引入MybatisPlus依赖,代替MyBatis依赖

MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。

<!-- springboot2的mybatis-plus依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version>
</dependency>

注意:如果是springboot3,引入的是mybatis-plus-spring-boot3-starter依赖。

<!-- springboot3的mybatis-plus依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version>
</dependency>

由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter。 最终,项目的依赖如下:

<!-- mysql -->
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
<!-- 单元测试 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version>
</dependency>

(2)配置Mapper包扫描路径

  • SpringBoot启动类上添加@MapperScan注解
java">import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.itheima.mp.mapper")
@SpringBootApplication
public class MpDemoApplication {public static void main(String[] args) {SpringApplication.run(MpDemoApplication.class, args);}
}

(3)定义Mapper接口并继承BaseMapper

  • 升级前的MyBatis版本增删改查

之前MyBatis的Mapper接口:

java">public interface UserMapper {void saveUser(User user);void deleteUser(Long id);void updateUser(User user);User queryUserById(@Param("id") Long id);List<User> queryUserByIds(@Param("ids") List<Long> ids);
}

之前MyBatis的Mapper接口的xml映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper"><!-- 新增用户 --><insert id="saveUser" parameterType="com.itheima.mp.domain.po.User">INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `balance`)VALUES(#{id}, #{username}, #{password}, #{phone}, #{info}, #{balance});</insert><!-- 修改用户 --><update id="updateUser" parameterType="com.itheima.mp.domain.po.User">UPDATE `user`<set><if test="username != null">`username`=#{username}</if><if test="password != null">`password`=#{password}</if><if test="phone != null">`phone`=#{phone}</if><if test="info != null">`info`=#{info}</if><if test="status != null">`status`=#{status}</if><if test="balance != null">`balance`=#{balance}</if></set>WHERE `id`=#{id};</update><!-- 删除用户 --><delete id="deleteUser" parameterType="com.itheima.mp.domain.po.User">DELETE FROM user WHERE id = #{id}</delete><!-- 根据用户id查询单个用户 --><select id="queryUserById" resultType="com.itheima.mp.domain.po.User">SELECT *FROM userWHERE id = #{id}</select><!-- 根据用户id数组批量查询多个用户 --><select id="queryUserByIds" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user<if test="ids != null">WHERE id IN<foreach collection="ids" open="(" close=")" item="id" separator=",">#{id}</foreach></if>LIMIT 10</select>
</mapper>

之前MyBatis的CRUD测试类:

java">@SpringBootTest
class MyBatisUserMapperTests {@Autowiredprivate UserMapper userMapper;@Testvoid testInsert() {User user = new User();user.setId(5L);user.setUsername("Lucy");user.setPassword("123");user.setPhone("18688990011");user.setBalance(200);user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());userMapper.saveUser(user);}@Testvoid testSelectById() {User user = userMapper.queryUserById(5L);System.out.println("user = " + user);}@Testvoid testQueryByIds() {List<User> users = userMapper.queryUserByIds(List.of(1L, 2L, 3L, 4L));users.forEach(System.out::println);}@Testvoid testUpdateById() {User user = new User();user.setId(5L);user.setBalance(20000);userMapper.updateUser(user);}@Testvoid testDeleteUser() {userMapper.deleteUser(5L);}
}
  • 升级后的MyBatisPlus版本的增删改查

为了简化单表CRUD,MybatisPlus提供了一个基础的BaseMapper接口,其中已经实现了单表的CRUD:

MyBatisPlus的Mapper接口:

java">import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;// 只需要继承BaseMapper就能省去所有的单表CRUD
public interface UserMapper extends BaseMapper<User> {
}

MyBatisPlus的CRUD测试类:

java">@SpringBootTest
class MyBatisPlusUserMapperTests {@Autowiredprivate UserMapper userMapper;@Testvoid testInsert() {User user = new User();user.setId(5L);user.setUsername("Lucy");user.setPassword("123");user.setPhone("18688990011");user.setBalance(200);user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());userMapper.insert(user);}@Testvoid testSelectById() {User user = userMapper.selectById(5L);System.out.println("user = " + user);}@Testvoid testSelectByIds() {List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L));users.forEach(System.out::println);}@Testvoid testUpdateById() {User user = new User();user.setId(5L);user.setBalance(20000);userMapper.updateById(user);}@Testvoid testDelete() {userMapper.deleteById(5L);}
}

可以看到在运行过程中打印出的SQL日志,以字段名进行查询而不是用*,非常标准。

11:52:03 DEBUG 22712 --- [           main] com.itheima.mp.mapper.UserMapper.insert  : ==>  Preparing: INSERT INTO user ( id, username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
11:52:03 DEBUG 22712 --- [           main] com.itheima.mp.mapper.UserMapper.insert  : ==> Parameters: 5(Long), Lucy(String), 123(String), 18688990011(String), {"age": 24, "intro": "英文老师", "gender": "female"}(String), 200(Integer), 2024-11-13T11:52:02.904878200(LocalDateTime), 2024-11-13T11:52:02.904878200(LocalDateTime)
11:52:03 DEBUG 22712 --- [           main] com.itheima.mp.mapper.UserMapper.insert  : <==    Updates: 111:54:23 DEBUG 8964 --- [           main] c.i.mp.mapper.UserMapper.selectBatchIds  : ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM user WHERE id IN ( ? , ? , ? , ? , ? )
11:54:23 DEBUG 8964 --- [           main] c.i.mp.mapper.UserMapper.selectBatchIds  : ==> Parameters: 1(Long), 2(Long), 3(Long), 4(Long), 5(Long)
11:54:23 DEBUG 8964 --- [           main] c.i.mp.mapper.UserMapper.selectBatchIds  : <==      Total: 5
User(id=1, username=Jack, password=123, phone=13900112224, info={"age": 20, "intro": "佛系青年", "gender": "male"}, status=1, balance=1600, createTime=2023-05-19T20:50:21, updateTime=2023-06-19T20:50:21)
User(id=2, username=Rose, password=123, phone=13900112223, info={"age": 19, "intro": "青涩少女", "gender": "female"}, status=1, balance=600, createTime=2023-05-19T21:00:23, updateTime=2023-06-19T21:00:23)
User(id=3, username=Hope, password=123, phone=13900112222, info={"age": 25, "intro": "上进青年", "gender": "male"}, status=1, balance=100000, createTime=2023-06-19T22:37:44, updateTime=2023-06-19T22:37:44)
User(id=4, username=Thomas, password=123, phone=17701265258, info={"age": 29, "intro": "伏地魔", "gender": "male"}, status=1, balance=800, createTime=2023-06-19T23:44:45, updateTime=2023-06-19T23:44:45)
User(id=5, username=Lucy, password=123, phone=18688990011, info={"age": 24, "intro": "英文老师", "gender": "female"}, status=1, balance=200, createTime=2024-11-13T11:52:03, updateTime=2024-11-13T11:52:03)

3、常见注解

在刚刚的入门案例中,我们仅仅引入了依赖,继承了BaseMapper就能使用MybatisPlus,非常简单。但是问题来了: MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢?

UserMapper在继承BaseMapper的时候指定了一个泛型

泛型中的User就是与数据库对应的PO实体类。

MybatisPlus底层通过反射,根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下(约定):

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名
  • MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MybatisPlus会把名为id的字段作为主键

但很多情况下,默认的实现与实际场景不符(实际情况与MP的约定不符合时使用),因此MybatisPlus提供了一些注解便于我们声明表信息。

  • @TableName:用来指定表名
  • @Tableld:用来指定表中的主键字段信息
  • @TableField:用来指定表中的普通字段信息


(1)@TableName

  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类

TableName注解除了指定表名以外,还可以指定很多其它属性:

属性类型必须指定默认值描述
valueString“”表名
schemaString“”schema
keepGlobalPrefixbooleanfalse是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMapString“”xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMapbooleanfalse是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludePropertyString[]{}需要排除的属性名 @since 3.3.1

(2)@TableId

  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段

TableId注解支持两个属性:

属性类型必须指定默认值描述
valueString“”表名
typeEnumIdType.NONE指定主键类型

枚举IdType支持的类型有:

描述
AUTO数据库 ID 自增
NONE无状态,该类型为未设置主键类型(如果全局配置中有 IdType 相关的配置,则会跟随全局配置。)当我们设置 @TableId 类型为NONE 时,且不手动设置主键值,MyBatisPlus将默认给出一个 Long 类型的字符串,因为全局配置默认为ASSIGN_ID。
INPUTinsert 前自行 set 主键值。当我们没有设置主键值时,MyBatisPlus并不设置 Long 类型的值,而是插入为null。
ASSIGN_ID分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)
ID_WORKER分布式全局唯一 ID 长整型类型 (please use ASSIGN_ID)
UUID32 位 UUID 字符串 (please use ASSIGN_UUID)
ID_WORKER_STR分布式全局唯一 ID 字符串类型 (please use ASSIGN_ID)

这里比较常见的有三种:

  • AUTO:利用数据库的id自增长
  • INPUT:手动生成id
  • ASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略

(3)@TableField

  • 描述:普通字段注解

一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:

  • 成员变量名与数据库字段名不一致
  • 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。
  • 成员变量名与数据库一致,但是数据库的关键字冲突。使用@TableField注解给字段名添加转义字符(两个反引号):````

支持的其它属性如下:

属性类型必填默认值描述
valueString“”数据库字段名
existbooleantrue是否为数据库表字段
conditionString“”字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window)
updateString“”字段 update set 部分注入,例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategyEnumFieldStrategy.DEFAULT举例:NOT_NULL insert into table_a(column) values (#{columnProperty})
updateStrategyEnumFieldStrategy.DEFAULT举例:IGNORED update table_a set column=#{columnProperty}
whereStrategyEnumFieldStrategy.DEFAULT举例:NOT_EMPTY where column=#{columnProperty}
fillEnumFieldFill.DEFAULT字段自动填充策略
selectbooleantrue是否进行 select 查询
keepGlobalFormatbooleanfalse是否保持使用全局的 format 进行处理
jdbcTypeJdbcTypeJdbcType.UNDEFINEDJDBC 类型 (该默认值不代表会按照该值生效)
typeHandlerTypeHander类型处理器 (该默认值不代表会按照该值生效)
numericScaleString“”指定小数点后保留的位数

(4)使用案例

tb_user为MP这三个常用注解的演示表

  • MpUser为PO实体类
java">@Data
@TableName("tb_user")
public class MpUser {// 用户id@TableId(value = "user_id", type = IdType.AUTO)private Long id;// 用户名@TableField("username")private String name;// 密码private String password;// 是否被逻辑删除@TableField("is_deleted")private Boolean isDeleted;// 排序字段@TableField("`order`")private Integer order;// 地址@TableField(exist = false)private String address;
}
  • MpUserMapper
java">public interface MpUserMapper extends BaseMapper<MpUser> {
}
  • MpUserMapperTests测试类
java">@SpringBootTest
public class MpUserMapperTests {@Autowiredprivate MpUserMapper mpUserMapper;@Testvoid testInsert() {MpUser mpUser = new MpUser();//mpUser.setId(1L);mpUser.setName("MP用户1");mpUser.setPassword("123");mpUser.setIsDeleted(false);mpUser.setOrder(1);mpUser.setAddress("https://baomidou.com/");mpUserMapper.insert(mpUser);}@Testvoid testSelectById() {MpUser mpUser = mpUserMapper.selectById(1L);System.out.println("mpUser = " + mpUser);}@Testvoid testSelectAll() {// selectList()方法的参数为MP内置的条件封装器Wrapper,所以不填写就是无任何条件,即查询全部List<MpUser> mpUserList = mpUserMapper.selectList(null);mpUserList.forEach(System.out::println);}@Testvoid testUpdateById() {MpUser mpUser = new MpUser();mpUser.setId(1L);mpUser.setIsDeleted(true);mpUserMapper.updateById(mpUser);}@Testvoid testDelete() {mpUserMapper.deleteById(1L);}
}

在新增时无论是否手动设置id,主键字段都被忽略,由数据库自增长

在查询时,字段名会添加别名,冲突字段加了转移符,并且查数据库不存在的字段也不会报错,而是null


4、常见配置

MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档使用配置。

mybatis-plus:type-aliases-package: com.itheima.mp.domain.po  # 别名包扫描,这项无默认值,需要自己指定mapper-locations: "classpath*:/mapper/**/*.xml" # mapper.xml映射文件地址,默认值configuration:map-underscore-to-camel-case: true  # 是否开启下划线和驼峰命名的映射,默认开启cache-enabled: true # mybatis二级缓存,默认开启global-config:db-config:id-type: assign_id  # 默认全局id生成策略为雪花算法update-strategy: not_null # 默认更新策略:只更新非null字段

mapper-locations:指定 MyBatis Mapper 对应的 XML 文件位置。如果在 Mapper 中有自定义方法(手写SQL或多表联查),需要配置此项。默认值为:"classpath*:/mapper/**/*.xml",也就是说我们只要把mapper.xml文件放置这个目录下就一定会被加载。

对于 Maven 多模块项目,扫描路径应以 classpath*: 开头,以加载多个 JAR 包中的 XML 文件。

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如实体类的别名扫描包type-aliases-package(默认值为null),用于给包中的类注册别名,注册后,在Mapper对应的XML文件中可以直接使用类名,无需使用全限定类名。

另外如果数据库的表大多数为主键自增,可以在全局配置中设置id-typeauto,之后如果有实体类的id属性是其他主键生成策略,再通过@TableId注解配置即可(优先级:指定注解 > 全局配置)。

mybatis-plus:type-aliases-package: com.itheima.mp.domain.poglobal-config:db-config:id-type: auto # 全局id类型为自增长



三、核心功能

刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了。

1、条件构造器

除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。

参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:

而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:

而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:

接下来,我们就来看看如何利用Wrapper实现复杂查询。

(1)QueryWrapper

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。接下来看一些例子:

查询:查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段。

java">// 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testQueryWrapper() {// 1.构建查询条件 where username like "%o%" AND balance >= 1000QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2.查询数据List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}

更新:更新用户名为jack的用户的余额为2000。

java">// 更新用户名为jack的用户的余额为2000。
@Test
void testUpdateByQueryWrapper() {// 1.设置要更新的数据User user = new User();user.setBalance(2000);// 2.构建更新条件 where username = "Jack"QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("username", "Jack");// 3.执行更新,user中非null字段都会作为set语句System.out.println(userMapper.update(user, queryWrapper) > 0);
}

(2)UpdateWrapper

基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例如:更新id为1,2,4的用户的余额,扣200,对应的SQL应该是:

UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql功能了:

java">@Test
void testUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLUpdateWrapper<User> updateWrapper = new UpdateWrapper<User>().setSql("balance = balance - 200")  // SET balance = balance - 200.in("id", ids); // WHERE id in (1, 2, 4)// 2.基于UpdateWrapper中的setSql来更新System.out.println(userMapper.update(updateWrapper) > 0);
}

(3)基于Lambda的Wrapper

无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。那怎么样才能不写字段名,又能知道字段名呢?

其中一种办法是基于变量的getter方法结合反射技术,我们只要将条件对应的字段的getter方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用和Lambda表达式。
因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:

  • LambdaQueryWrapper,对应QueryWrapper
  • LambdaUpdateWrapper,对应UpdateWrapper

其使用方式如下:

java">@Test
void testLambdaUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLLambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<User>().setSql("balance = balance - 200")  // SET balance = balance - 200.in(User::getId, ids); // WHERE id in (1, 2, 4)// 2.基于UpdateWrapper中的setSql来更新System.out.println(userMapper.update(lambdaUpdateWrapper) > 0);
}@Test
void testLambdaQueryWrapper() {// 1.构建查询条件 where username like "%o%" AND balance >= 1000LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2.查询数据List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}

总结:

  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊的情况才使用
  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper避免硬编码

2、自定义SQL

(1)自定义SQL片段

  • 问题引出

在演示的UpdateWrapper和LambdaUpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:

其中balance = balance - 200现在这种写法相当于把Mapper层的sql语句写在Service层了,这在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。 假如查询条件更复杂,动态SQL的编写也会更加复杂。

所以,MybatisPlus提供了自定义SQL片段功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL。

以当前案例来说,我们可以这样写:

java">@Test
void testCustomSQLUpdate() {// 更新条件List<Long> ids = List.of(1L, 2L, 4L);int amount = 200;// 定义条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 调用自定义SQL方法userMapper.updateBalanceByIds(wrapper, amount);
}

然后在UserMapper中自定义SQL:

java">import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;public interface UserMapper extends BaseMapper<User> {@Select("UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}")//void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);void updateBalanceByIds(@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
}
  • ${ew.customSqlSegment}:为自定义SQL片段,@Param("ew")其中参数ew必须叫这个,如果忘记了也可以用baomidou包下的常量类Constants.WRAPPER,其值等于"ew"

这样就省去了编写复杂查询条件的烦恼了,总结一下自定义SQL片段的使用场景:

  1. 更新时的特殊场景,不是更新具体不变的值,而是在原有值的基础上动态做增减(例如balance = balance - amount),完全使用MP只能在业务层拼接这条SQL语句。此时可以使用自定义SQL传值更新,更新的SQL定义在Mapper接口或Mapper.xml中,MP则更擅长处理where更新条件。
  2. 查询时的特殊场景(如下图),查询的字段结果是个别字段,而MP默认查询所有字段,只能通过QueryWrapper或LambdaQueryWrapper的select()方法去在业务层拼出查询字段,有时为了不违背企业开发规范,此时也可以使用自定义SQL片段


(2)多表联查

理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。 例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户信息,要是自己基于mybatis实现SQL,大概是这样的:

<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user uINNER JOIN address a ON u.id = a.user_idWHERE u.id<foreach collection="ids" separator="," item="id" open="IN (" close=")">#{id}</foreach>AND a.city = #{city}
</select>

可以看出其中最复杂的就是WHERE条件的编写,如果业务复杂一些,这里的SQL会更变态。

但是基于自定义SQL结合Wrapper的玩法,我们就可以利用Wrapper来构建查询条件,然后手写SELECT及FROM部分,实现多表查询。

查询条件这样来构建:

java">@Test
void testCustomJoinWrapper() {// 1.准备自定义查询条件QueryWrapper<User> wrapper = new QueryWrapper<User>().in("u.id", List.of(1L, 2L, 4L)).eq("a.city", "北京");// 2.调用mapper的自定义方法List<User> users = userMapper.queryUserByWrapper(wrapper);users.forEach(System.out::println);
}

然后在UserMapper中自定义方法:

java">@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);

当然,也可以在UserMapper.xml中写SQL:

<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>

3、IService接口

MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。


通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

(1)常用方法介绍

新增

  • save:新增单个元素
  • saveBatch:批量新增
  • saveOrUpdate:根据id判断,如果实体类中存在id就更新,不存在则新增
  • saveOrUpdateBatch:批量的新增或修改

删除

  • removeById:根据id删除
  • removeByIds:根据ids集合批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper<T>):根据Wrapper条件删除
  • removeBatchByIds:暂不支持

修改

  • updateById:根据id修改,只更新不为null的值
  • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含set和where部分
  • update(T,Wrapper):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

查一条

  • getById:根据id查询1条数据
  • getOne(Wrapper<T>):根据Wrapper查询1条数据
  • getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper

查多条

  • listByIds:根据id批量查询
  • list(Wrapper<T>):根据Wrapper条件查询多条数据
  • list():查询所有

查条数

  • count():统计所有数量
  • count(Wrapper<T>):统计符合Wrapper条件的数据数量

(2)基本用法

由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承MP的IService接口以拓展方法。让自定义的ServiceImpl实现类实现自定义的Service接口,同时继承MP的默认实现类 ServiceImpl,同时,这样就不用自己实现IService接口中的方法了。

  1. 自定义Service接口继承IService接口。定义IUserService继承IService
java">import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;public interface IUserService extends IService<User> {
}
  1. 自定义Service实现类,实现自定义接口并继承ServiceImpl类。创建UserServiceImpl类,继承ServiceImpl,实现UserService
java">import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

ServiceImpl<M, T>接口的泛型参数中,M是继承了BaseMapper的Mapper接口,T是PO实体类。


(3)Restful案例

案例:基于Restful风格实现下面的接口

编号接口请求方式请求路径请求参数返回值
1新增用户POST/users用户表单实体
2删除用户DELETE/users/{id}用户id
3根据id查询用户GET/users/{id}用户id用户VO
4根据id批量查询GET/users用户id集合用户VO集合
5根据id扣减余额PUT/users/{id}/deduction/{money}+ 用户id
+ 扣减金额

首先,我们在项目中引入Swagger和Web依赖:

<!-- swagger -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.1.0</version>
</dependency>
<!-- web -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

在yaml中配置swagger信息:

knife4j:enable: trueopenapi:title: 用户管理接口文档description: "用户管理接口文档"email: Aizen@qq.comconcat: 蓝染url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.itheima.mp.controller

然后,接口的接收和返回值分别需要定义两个实体:

  • UserFormDTO:代表新增时的用户表单
java">package com.itheima.mp.domain.dto;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("密码")private String password;@ApiModelProperty("注册手机号")private String phone;@ApiModelProperty("详细信息,JSON风格")private String info;@ApiModelProperty("账户余额")private Integer balance;
}
  • UserVO:代表查询的返回结果
java">package com.itheima.mp.domain.vo;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户VO实体")
public class UserVO {@ApiModelProperty("用户id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("详细信息")private String info;@ApiModelProperty("使用状态(1正常 2冻结)")private Integer status;@ApiModelProperty("账户余额")private Integer balance;
}

最后,按照Restful风格编写Controller接口方法

java">@Api(tags = "用户管理接口")
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor    // lombok注解:在构造方法中只会注入必须需要初始化的成员变量,例如加了final且未初始化的变量,将来不需要做注入的变量不加final即可
public class UserController {// Spring不推荐我们使用@Autowired进行属性注入,推荐我们使用构造器注入,但当需要注入的成员变量很多的时候,构造方法会显得特别长,因此我们可以将需要注入的变量加上final,并且使用lombok的@RequiredArgsConstructor注解提供必要参数构造器private final IUserService userService;@ApiOperation("新增用户接口")@PostMappingpublic void saveUser(@RequestBody UserFormDTO userDTO) {// 将DTO拷贝到POUser user = BeanUtil.copyProperties(userDTO, User.class);// 新增userService.save(user);}@ApiOperation("删除用户接口")@DeleteMapping("/{id}")public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {userService.removeById(id);}@ApiOperation("根据id查询用户接口")@GetMapping("/{id}")public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {// 查询用户POUser user = userService.getById(id);// 将PO拷贝到VOreturn BeanUtil.copyProperties(user, UserVO.class);}@ApiOperation("根据id批量查询用户接口")@GetMappingpublic List<UserVO> queryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {// 查询用户PO集合List<User> users = userService.listByIds(ids);// 将PO集合拷贝到VO集合return BeanUtil.copyToList(users, UserVO.class);}@ApiOperation("根据id扣减用户余额接口")@PutMapping("/{id}/deduction/{money}")public void deductBalanceById(@ApiParam("用户id") @PathVariable("id") Long id,@ApiParam("扣减的金额") @PathVariable("money") Integer money) {userService.deductBalanceById(id, money);}
}

可以看到前四个接口都直接在Controller实现即可,无需编写任何Service代码,非常方便。不过,一些带有业务逻辑的接口,比如第五个deductBalanceById接口,MP的Service没有提供业务逻辑,所以这些业务逻辑都要在Service层来做。另外更新余额需要自定义SQL,要在mapper中来实现。

  • UserService接口和UserServiceImpl实现类
java">public interface IUserService extends IService<User> {void deductBalanceById(Long id, Integer money);
}@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic void deductBalanceById(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常!");}// 校验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足!");}// 扣减余额baseMapper.deductBalance(id, money);}
}
  • mapper接口
java">public interface UserMapper extends BaseMapper<User> {@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
  • 访问http://localhost:8080/doc.html,测试接口


(4)LambdaQuery和LambdaUpdate

IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。

案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空

可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要动态做判断。

我们首先需要定义一个查询条件实体,UserQueryDTO实体:

java">@Data
@ApiModel(description = "用户查询条件实体")
public class UserQueryDTO {@ApiModelProperty("用户名关键字")private String name;@ApiModelProperty("用户状态:1-正常,2-冻结")private Integer status;@ApiModelProperty("余额最小值")private Integer minBalance;@ApiModelProperty("余额最大值")private Integer maxBalance;
}
  • 在UserController中定义方法
java">@ApiOperation("根据条件查询用户接口")
@GetMapping("/condition")
public List<UserVO> queryUserByCondition(UserQueryDTO queryDTO) {// 查询用户PO集合List<User> users = userService.queryUserByCondition(queryDTO);// 将PO集合拷贝到VO集合return BeanUtil.copyToList(users, UserVO.class);
}
  • UserService接口和UserServiceImpl实现类,基于lambdaQuery实现
java">public interface IUserService extends IService<User> {List<User> queryUserByCondition(UserQueryDTO queryDTO);
}// 基于Lambda查询
@Override
public List<User> queryUserByCondition(UserQueryDTO queryDTO) {String name = queryDTO.getName();Integer status = queryDTO.getStatus();Integer minBalance = queryDTO.getMinBalance();Integer maxBalance = queryDTO.getMaxBalance();return lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance).list();
}

MP对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuerylambdaUpdate方法。在组织查询条件的时候,我们加入了name != null这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个查询结果,list()表示查询结果返回一个List集合。可选的常用方法有:

  • one():最多1个结果
  • list():返回集合结果
  • count():返回计数结果
  • exist():返回查询的结果是否存在

与lambdaQuery方法类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务。

案例二:改造根据id修改用户余额的接口,要求如下

  • 完成对用户状态校验
  • 完成对用户余额校验
  • 如果扣减后余额为0,则将用户status修改为2,表示冻结状态(update语句的set部分是动态的)
  • 基于lambdaUpdate实现
java">@Override
@Transactional
public void deductBalanceById(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常!");}// 校验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足!");}// 扣减余额//baseMapper.deductBalance(id, money);int remainBalance = user.getBalance() - money;lambdaUpdate().set(User::getBalance, remainBalance)   // 更新余额.set(remainBalance == 0, User::getStatus, 2)    // 动态判断是否更新status.eq(User::getBalance, user.getBalance())    // CAS乐观锁.eq(User::getId, id)   // 根据id扣减对应用户的余额.update();  // 注意:LambdaUpdate做复杂更新时,最后必须记得加上.update()进行更新操作
}

(5)批量新增 & 批处理方案性能测试

IService中的批量新增功能使用起来非常方便,但有一点注意事项。

需求:批量插入10万条用户数据,并作出对比。

  • 方式一:普通for循环逐条插入
  • 方式二:IService的批量插入(默认不开启 jdbc 批处理参数)
  • 方式三:开启rewriteBatchedStatements=true参数

首先我们测试方式一,逐条插入数据

java">/*** 10w次插入意味着10w次网络请求,耗时最慢*/
@Test
void testSaveOneByOne() {long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {userService.save(buildUser(i));}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));
}private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123");user.setPhone("" + (18688190000L + i));user.setBalance(2000);user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(user.getCreateTime());return user;
}

执行结果耗时大约为551.9秒

可以看到速度非常慢。

再测试一下方式二,MybatisPlus的批处理:

java">/*** MP批处理采用的是JDBC底层的预编译方案PreparedStatement,将1000条数据统一打包执行save一并提交到MySQL,每1000条发送一次网络请求,插入100次共发送100次网络请求* MP如果不加JDBC连接参数rewriteBatchedStatements=true,底层还是打包逐条插入,只不过是从网络请求数量上减少了耗时* 而加上了MySQL的这个开启批处理参数后,MP调用的JDBC底层的批处理才能真正变成一次性批量插入多条数据*/
@Test
void testSaveBatch() {// 因为一次性new 10万条数据占用内存太多,并且向数据库请求的数据包有上限大小限制(一次网络传输的数据量是有限的)// 所以我们每次批量插入1000条件,插入100次即10万条数据// 准备一个容量为1000的集合List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {// 添加一个userlist.add(buildUser(i));// 每1000条批量插入一次if (i % 1000 == 0) {// 批量插入userService.saveBatch(list);// 清空集合,准备下一批数据list.clear();}}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));
}

执行结果耗时大约为27.6秒,打包逐条插入,从网络请求层面大大减少了耗时。

虽然上面的方式二减少了网络请求次数,但是底层还是打包逐条SQL插入。如果想要真正实现批量处理,有下面两种办法。

第一种实现就是利用MyBatis自定义SQL语句,用<foreach>标签遍历组装成下面一条SQL的形式

<!-- MyBatis批量插入 -->
<insert id="batchInsertUsers">INSERT INTO user (username, password, phone, info, balance, create_time, update_time)VALUES<foreach collection="list" item="user" separator=",">(#{name}, #{password}, #{phone}, #{info}, #{balance}, #{createTime}, #{updateTime})</foreach>
</insert>

第二种实现,就是还是利用MP的jdbc批处理,只不过MySQL本身默认没有开启这个批处理参数rewriteBatchedStatements=true,该参数在MySQL 3.1.13版本开始引入,默认值为false不开启。因此这个批处理操作其实底层是由MySQL驱动去做的,不是由MP来做的。所以我们只需在jdbc的url连接参数后添加该参数,MP的批处理才能成效。

spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456

jdbc批处理相比于Mybatis批处理效率更高

  • 开启参数后,测试耗时大约为6.5秒

拓展rewriteBatchedStatements=trueallowMultiQueries=true 的区别

  • rewriteBatchedStatements是重写sql语句达到发送一次sql的请求效果;allowMultiQueries是在mapper.xml中使用;分号分隔多条sql,允许多条语句一起发送执行的。
  • 对于insert批处理操作,开启rewriteBatchedStatements=true,驱动则会把多条sql语句重写成一条sql语句然后再发出去;而对于update和delete批处理操作,开启allowMultiQueries=true,驱动所做的事就是把多条sql语句累积起来再一次性发出去。

批处理方案总结:

  • 普通for循环逐条插入速度极差,不推荐
  • MP的批量新增,基于预编译的批处理,性能不错
  • 配置jdbc参数,开启rewriteBatchedStatements,性能最好



四、扩展功能

1、代码生成

在使用MybatisPlus以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成POMapperService等相关代码。只不过代码生成器同样要编码使用,也很麻烦。

这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。


(1)安装插件

在idea的plugins市场中搜索并安装MyBatisPlus插件:


(2)使用步骤

刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database,在弹出的窗口中填写数据库连接的基本信息:

点击OK保存。然后再次点击Idea顶部菜单中的other,然后选择Code Generator,在弹出的表单中填写信息:

最终,代码自动生成到指定的位置了


(3)代码生成器配置

如果不想用图形化界面方式配置生成代码,使用MyBatis-Plus官网提供的代码生成器模板也是可以的,但需要自己填写配置信息。

因为MP代码生成更新迭代速度很快,若本文的API被弃用,请以官网最新版本API为准:

MyBatis-Plus新代码生成器:https://baomidou.com/guides/new-code-generator/

代码生成器配置:https://baomidou.com/reference/new-code-generator-configuration/

  • 引入依赖
<!-- MP代码生成器 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.9</version>
</dependency>
  • 代码生成模板配置示例
java">import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;import java.util.Collections;
import java.util.List;public class CodeGenerator {/*** 数据库链接地址**/private static final String JDBC_URL_MAN = "jdbc:mysql://xxxxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8";/*** 数据库登录账号**/private static final String JDBC_USER_NAME = "xx";/*** 数据库登录密码**/private static final String JDBC_PASSWORD = "xxxx";public static void main(String[] args) {String dir = "\\xx\\xxx";String tablePrefix = "tb_";List<String> tables = List.of("tb_user");FastAutoGenerator.create(JDBC_URL_MAN, JDBC_USER_NAME, JDBC_PASSWORD).globalConfig(builder -> {builder.author("Aizen")                                                           // 作者.outputDir(System.getProperty("user.dir") + dir + "\\src\\main\\java")    // 输出路径(写到java目录).enableSwagger()                                                          // 开启swagger.commentDate("yyyy-MM-dd");                                       // 设置注释日期格式,默认值: yyyy-MM-dd}).packageConfig(builder -> {builder.parent("com.{company}")     // 设置父包名.moduleName("{model}")      // 设置父包模块名.entity("domain")           // 设置实体类包名.service("service")         // 设置Service接口包名.serviceImpl("service.impl")// 设置Service实现类包名.controller("controller")   // 设置Controller包名.mapper("mapper")           // 设置Mapper接口文件包名.xml("mappers")             // 设置Mapper XML文件包名.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + dir + "\\src\\main\\resources\\mapper"));}).strategyConfig(builder -> {builder.addInclude(tables)                              // 设置需要生成的表名.addTablePrefix(tablePrefix)                    // 设置表前缀.serviceBuilder()                               // 设置 Service 层模板.formatServiceFileName("%sService").formatServiceImplFileName("%sServiceImpl").entityBuilder()                                // 设置实体类模板.enableLombok()                                 // 启用 Lombok.logicDeleteColumnName("deleted")               // 逻辑删除字段名(数据库字段).enableTableFieldAnnotation()                   // 开启生成实体时生成字段注解.controllerBuilder().formatFileName("%sController").enableRestStyle()                              // 启用 REST 风格.mapperBuilder()                                // Mapper 策略配置.enableBaseResultMap()                          // 生成通用的resultMap.superClass(BaseMapper.class)                   // 设置父类.formatMapperFileName("%sMapper").enableMapperAnnotation().formatXmlFileName("%sMapper");})//.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板.execute();     // 执行生成}
}

2、静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

因为静态方法无法读取类上的泛型,所以MP在使用静态工具读取表信息时,需要传入PO实体类的Class字节码,MP再通过反射获取到表信息。其中新增和修改的方法由于需要传入实体类对象,因此不用传入实体类的Class字节码。下面是使用示例:

java">@Test
void testDbGet() {User user = Db.getById(1L, User.class);System.out.println(user);
}@Test
void testDbList() {// 利用Db实现复杂条件查询List<User> list = Db.lambdaQuery(User.class).like(User::getUsername, "o").ge(User::getBalance, 1000).list();list.forEach(System.out::println);
}@Test
void testDbUpdate() {Db.lambdaUpdate(User.class).set(User::getBalance, 2000).eq(User::getUsername, "Rose");
}

(1)案例一

  • 案例一:改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表

首先,我们要添加一个收货地址的VO对象AddressVO

java">@Data
@ApiModel(description = "收货地址VO")
public class AddressVO{@ApiModelProperty("id")private Long id;@ApiModelProperty("用户ID")private Long userId;@ApiModelProperty("省")private String province;@ApiModelProperty("市")private String city;@ApiModelProperty("县/区")private String town;@ApiModelProperty("手机")private String mobile;@ApiModelProperty("详细地址")private String street;@ApiModelProperty("联系人")private String contact;@ApiModelProperty("是否是默认 1默认 0否")private Boolean isDefault;@ApiModelProperty("备注")private String notes;
}

然后,改造原来的UserVO,添加一个用户的收获地址集合(对多)属性

java">@Data
@ApiModel(description = "用户VO实体")
public class UserVO {@ApiModelProperty("用户id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("详细信息")private String info;@ApiModelProperty("使用状态(1正常 2冻结)")private Integer status;@ApiModelProperty("账户余额")private Integer balance;@ApiModelProperty("用户的收获地址")private List<AddressVO> addresses;
}

修改UserController中根据id查询用户的业务接口

java">@ApiOperation("根据id查询用户接口")
@GetMapping("/{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {return userService.queryUserAndAddressById(id);
}

IUserService和UserServiceImpl

java">public interface IUserService extends IService<User> {UserVO queryUserAndAddressById(Long id);
}@Override
public UserVO queryUserAndAddressById(Long id) {// 查询用户POUser user = this.getById(id);if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户不存在或用户状态异常!");}// 查询地址POList<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();// 封装VO,将用户PO转为VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 如果地址PO不为空,将地址PO集合转化为地址VO集合,设置到用户VO中if (CollUtil.isNotEmpty(addresses)) {List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);userVO.setAddresses(addressVOList);}return userVO;
}

在查询地址时,我们采用了Db的静态方法,因此避免了注入AddressService,减少了循环依赖的风险。


(2)案例二

  • 案例二:改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址

代码实现:

java">@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {return userService.queryUserAndAddressByIds(ids);
}public interface IUserService extends IService<User> {List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {// 查询用户List<User> users = this.listByIds(ids);if (CollUtil.isEmpty(users)) {return Collections.emptyList();}// 查询地址// 获取用户id集合List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());// 根据用户id集合查询地址PO集合List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();// 地址PO集合转地址VO集合List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);// 用户地址集合分组处理,相同用户的放入一个集合(组)中Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);if (CollUtil.isNotEmpty(addressVOList)) {addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));}// PO集合转VO集合返回List<UserVO> userVOList = new ArrayList<>(users.size());for (User user : users) {// 转换用户PO为VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 为每个用户VO设置自己的地址集合userVO.setAddresses(addressMap.get(user.getId()));userVOList.add(userVO);}return userVOList;
}

(3)案例三

  • 案例三:实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常

代码实现:

java">@Api(tags = "地址管理接口")
@RestController
@RequestMapping("/address")
@RequiredArgsConstructor
public class AddressController {private final IAddressService addressService;@ApiOperation("根据用户id查询地址接口")@GetMapping("/{userId}")public List<AddressVO> queryAddressByUserId(@ApiParam("用户id") @PathVariable("userId") Long userId) {return addressService.queryAddressByUserId(userId);}
}public interface IAddressService extends IService<Address> {List<AddressVO> queryAddressByUserId(Long userId);
}@Service
public class AddressServiceImpl extends ServiceImpl<AddressMapper, Address> implements IAddressService {@Overridepublic List<AddressVO> queryAddressByUserId(Long userId) {// 查询用户,验证用户状态,冻结用户抛出异常User user = Db.getById(userId, User.class);if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户不存在或用户状态异常!");}// 查询该用户的收获地址List<Address> addresses = lambdaQuery().eq(Address::getUserId, userId).list();return BeanUtil.copyToList(addresses, AddressVO.class);}
}

3、逻辑删除

(1)介绍

对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:

  • 在表中添加一个字段标记数据是否被删除,逻辑删除字段的属性通常是IntegerBoolean类型。
  • 当删除数据时把标记置为1,1表示已删除
  • 查询时只查询标记为0的数据,0表示未删除

同理更新操作也需要加上deleted = 0,所以一旦采用了逻辑删除,所有的查询、删除、更新逻辑都要跟着变化,非常麻烦。

为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们只需要在application.yaml文件中配置逻辑删除的字段名称和值即可。

在这里插入图片描述

注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。


(2)使用步骤

例如,给address表添加一个逻辑删除字段

alter table address add deleted bit default b'0' null comment '逻辑删除';

Address实体类添加deleted属性

application.yaml中配置MP逻辑删除字段

mybatis-plus:global-config:db-config:logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)logic-delete-value: 1 # 逻辑已删除值(默认为 1)logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  • 测试删除和查询
java">@SpringBootTest
class IAddressServiceTest {@Autowiredprivate IAddressService addressService;@Testvoid testLogicDelete() {// 删除addressService.removeById(59L);// 查询Address address = addressService.getById(59L);System.out.println("address = " + address);}
}

对于没有逻辑删除字段的表不受影响,删除和查询还和之前一样。


(3)@TableLogic

  • @TableLogic注解用于标记实体类中的逻辑删除字段。
java">@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("address")
public class Address implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;// 省略.../*** 逻辑删除*/@TableLogic // @TableLogic注解用于标记实体类中的逻辑删除字段//@TableLogic(value = "0", delval = "1") // value表示默认逻辑未删除值,delval表示默认逻辑删除值 (这两个值可无、会自动获取全局配置)@TableField("deleted")private Boolean deleted;
}

使用这种方式就不用在application.yaml中配置MP逻辑删除字段了,直接在逻辑删除字段属性上加该注解即可。

总结:逻辑删除本身也有自己的问题,比如

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率
  • SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。


4、枚举处理器

当实体类属性是枚举类型,在与数据库的字段类型做转换时,底层默认使用的是MyBatis提供的EnumOrdinalTypeHandler枚举类型处理器。

但是这个并不好用,所以MP对类型处理器做了增强,其中增强后的枚举处理器叫MybatisEnumTypeHandler,JSON处理器叫AbstractJsonTypeHandler


(1)定义枚举,标记@EnumValue

定义一个用户状态的枚举

java">import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "冻结");@EnumValueprivate final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性值。因此我们需要给枚举中与数据库字段类型对应的属性值添加@EnumValue注解。

UserUserVO类中的status字段类型改为UserStatus 枚举类型。

(2)配置枚举处理器

在application.yaml文件中配置枚举处理器

mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  • 测试发现查询出的status字段会是枚举类型,默认显示的是枚举项的名称

如果我们想向前端返回指定的枚举属性,例如value状态值desc描述,SpringMVC负责处理响应数据,它在底层处理Json时用的是jackson,所以我们只需要使用jackson提供的@JsonValue注解,来标记JSON序列化后展示的字段。

java">import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "冻结");@EnumValueprivate final int value;@JsonValueprivate final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
  • 响应效果


5、JSON类型处理器

数据库的user表中有一个info字段,是JSON类型

info的格式像这样

{"age": 20, "intro": "佛系青年", "gender": "male"}

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器(SpringMVC底层默认也是使用的这个类)。

(1)定义接收Json的实体类

首先定义一个单独实体类UserInfo来与info字段的属性匹配

java">@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")  // 为了方便构建对象,为有参构造提供静态方法,名为of,UserInfo.of()
public class UserInfo {private Integer age;private String intro;private String gender;
}

(2)指定类型处理器

UserUserVO类的info字段修改为UserInfo类型,并声明类型处理器@TableField(typeHandler = JacksonTypeHandler.class)。另外,将info改为对象类型后出现对象嵌套,在复杂嵌套查询时需要使用resultMap结果集映射,否则无法映射。所以还需要再@TableName注解中添加autoResultMap=true确保能够正常映射。

java">@Data
@TableName(value = "user", autoResultMap = true)
public class User {/*** 用户id*///@TableId(type = IdType.AUTO)private Long id;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 注册手机号*/private String phone;/*** 详细信息*/@TableField(typeHandler = JacksonTypeHandler.class)private UserInfo info;/*** 使用状态(1正常 2冻结)*/private UserStatus status;/*** 账户余额*/private Integer balance;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}

如果启动mapper.xml报错,在info字段后加上, typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler就解决了。

  • 测试效果


6、yaml配置加密

目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。

我们以数据库的用户名和密码为例。

(1)生成密钥

首先,我们利用MP提供的AES工具生成一个随机秘钥,然后对用户名、密码加密。

java">import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class MpDemoApplicationTest {public static final String USERNAME = "root";public static final String PASSWORD = "123456";@Testvoid testEncrypt() {// 生成 16 位随机 AES 密钥String randomKey = AES.generateRandomKey();System.out.println("randomKey = " + randomKey); // randomKey = 7pSEa6F9TnYacTNJ// 利用密钥对用户名加密String username = AES.encrypt(USERNAME, randomKey);System.out.println("username = " + username);   // username = O4Yq+WKYGlPW5t8QvgrhUQ==// 利用密钥对用户名加密String password = AES.encrypt(PASSWORD, randomKey);System.out.println("password = " + password);   // password = cDYHnWysq07zUIAy1tcbRQ==}
}

(2)修改配置

修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文。

spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: mpw:O4Yq+WKYGlPW5t8QvgrhUQ== # 密文要以 mpw:开头password: mpw:cDYHnWysq07zUIAy1tcbRQ== # 密文要以 mpw:开头

(3)配置密钥运行参数

在启动项目的时候,需要把刚才生成的秘钥添加到Jar启动参数中,像这样:

--mpw.key=7pSEa6F9TnYacTNJ,新版本idea添加Program arguments中设置,界面如下

单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:@SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ")

然后随意运行一个单元测试,可以发现数据库查询正常,以上就是给SpringBoot的application.yaml配置文件中的敏感重要数据加密的实现步骤。

(4)实现原理

SpringBoot提供修改Spring环境后置处理器【EnvironmentPostProcessor】,允许在应用程序之前操作环境属性值,MyBatisPlus对其进行了重写实现。

java">package com.baomidou.mybatisplus.autoconfigure;import com.baomidou.mybatisplus.core.toolkit.AES;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.SimpleCommandLinePropertySource;import java.util.HashMap;/*** 安全加密处理器** @author hubin* @since 2020-05-23*/
public class SafetyEncryptProcessor implements EnvironmentPostProcessor {@Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {/*** 命令行中获取密钥*/String mpwKey = null;for (PropertySource<?> ps : environment.getPropertySources()) {if (ps instanceof SimpleCommandLinePropertySource) {SimpleCommandLinePropertySource source = (SimpleCommandLinePropertySource) ps;mpwKey = source.getProperty("mpw.key");break;}}/*** 处理加密内容*/if (StringUtils.isNotBlank(mpwKey)) {HashMap<String, Object> map = new HashMap<>();for (PropertySource<?> ps : environment.getPropertySources()) {if (ps instanceof OriginTrackedMapPropertySource) {OriginTrackedMapPropertySource source = (OriginTrackedMapPropertySource) ps;for (String name : source.getPropertyNames()) {Object value = source.getProperty(name);if (value instanceof String) {String str = (String) value;if (str.startsWith("mpw:")) {map.put(name, AES.decrypt(str.substring(4), mpwKey));}}}}}// 将解密的数据放入环境变量,并处于第一优先级上if (CollectionUtils.isNotEmpty(map)) {environment.getPropertySources().addFirst(new MapPropertySource("custom-encrypt", map));}}}
}

7、自动填充字段

MyBatis-Plus提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。

(1)配置自动填充处理器

自动填充功能通过实现 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 接口来实现。我们需要创建一个类来实现这个接口,并在其中定义插入和更新时的填充逻辑。添加@Component配置自动填充处理器类被Spring管理。

  • MyMetaObjectHandler实现MetaObjectHandler接口
java">@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {log.info("开始插入填充...");// 起始版本 3.3.3(推荐)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// Date类型填充//this.strictInsertFill(metaObject, "createTime", () -> new Date(), Date.class);// 起始版本 3.3.0(推荐使用)//this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// 也可以使用(3.3.0 该方法有bug)//this.fillStrategy(metaObject, "createTime", LocalDateTime.now());}@Overridepublic void updateFill(MetaObject metaObject) {log.info("开始更新填充...");// 起始版本 3.3.3(推荐)this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// Date类型填充//this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class);// 起始版本 3.3.0(推荐)//this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 也可以使用(3.3.0 该方法有bug)//this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)}
}

(2)添加@TableField的fill属性

在实体类中,你需要使用 @TableField 注解来标记哪些字段需要自动填充,并通过fill属性指定填充的策略。

  • User实体类
java">@Data
@TableName(value = "user", autoResultMap = true)
public class User {// 省略.../*** 创建时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 更新时间*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
  • FieldFill 枚举类
java">public enum FieldFill {DEFAULT,       // 默认不处理INSERT,        // 插入填充字段UPDATE,        // 更新填充字段INSERT_UPDATE  // 插入和更新填充字段
}

注意事项:

  1. 自动填充是直接给实体类的属性设置值,如果属性没有值,入库时会是null
  2. MetaObjectHandler 提供的默认方法策略是:如果属性有值则不覆盖,如果填充值为 null 则不填充。
  3. 字段必须声明 @TableField 注解,并设置 fill 属性来选择填充策略。
  4. update(T entity, Wrapper<T> updateWrapper) 时,entity 不能为空,否则自动填充失效。
  5. update(Wrapper<T> updateWrapper) 时不会自动填充,需要手动赋值字段条件。
  6. 使用 strictInsertFillstrictUpdateFill 方法可以根据注解 FieldFill.xxx、字段名和字段类型来区分填充逻辑。如果不需区分,可以使用 fillStrategy 方法。



五、插件功能

MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:

  • PaginationInnerInterceptor:自动分页
  • TenantLineInnerInterceptor:多租户
  • DynamicTableNameInnerInterceptor:动态表名
  • OptimisticLockerInnerInterceptor:乐观锁
  • IllegalSQLInnerInterceptor:sql 性能规范
  • BlockAttackInnerInterceptor:防止全表更新与删除

注意:使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

这里我们以最常用的分页插件为例来学习插件的用法。


1、分页插件

在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。 所以,我们必须配置分页插件。

(1)引入依赖

⚠ 注意,MyBatisPlus于 v3.5.9 起,PaginationInnerInterceptor已分离出来。如需使用,则需单独引入mybatis-plus-jsqlparser依赖!

<!-- MP分页插件 jdk 11+ 引入可选模块 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.9</version>
</dependency>
<!-- MP分页插件 jdk 8+ 引入可选模块 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

(2)配置分页内置拦截器

在项目中新建一个配置类MyBatisConfig

java">import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** MyBatis配置类*/
@Configuration
public class MybatisConfig {/*** MP拦截器*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(1000L);  // 设置单页分页条数最大限制interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}

(3)分页API

  • 测试分页查询
java">@Test
void testPageQuery() {// 准备分页条件int pageNo = 1, pageSize = 2;   // 页码、每页查询条数// 查询条件:无// 分页条件Page<User> page = Page.of(pageNo, pageSize);// 排序条件//page.addOrder(new OrderItem("id", true)); // MP老版本排序条件写法,true为升序,false为降序page.addOrder(OrderItem.desc("balance"));   // 新版MP直接用OrderItem的静态方法page.addOrder(OrderItem.asc("id")); // 可以添加多个排序条件// 分页查询page = userService.page(page);  // 这里返回的page对象其实和上面是同一个地址,只不过是封装好分页查询结果的page对象// 获取page中的查询结果long total = page.getTotal();   // 总条数System.out.println("total = " + total);long pages = page.getPages();   // 总页数System.out.println("pages = " + pages);List<User> records = page.getRecords(); // 分页数据records.forEach(System.out::println);
}
  • 分页查询效果


2、通用分页实体

现在要实现一个用户分页查询的接口,接口规范如下:

参数说明
请求方式GET
请求路径/users/page
请求参数json { "pageNo": 1, "pageSize": 5, "sortBy": "balance", "isAsc": false, "name": "o", "status": 1 }
返回值json { "total": 100006, "pages": 50003, "list": [ { "id": 1685100878975279298, "username": "user_9****", "info": { "age": 24, "intro": "英文老师", "gender": "female" }, "status": "正常", "balance": 2000 } ] }
特殊说明(1) 如果排序字段为空,默认按照更新时间排序
(2) 排序字段不为空,则按照排序字段排序

这里需要用到4个实体:

  • PageDTO:通用分页查询条件的实体,包含分页页码、每页查询条数、排序字段、是否升序
  • UserQueryDTO:接收用户查询条件实体,为了实现分页功能去继承PageDTO
  • PageResult:分页结果实体,包含总条数、总页数、当前页数据
  • UserVO:响应用户页面视图实体,将用户VO集合封装到PageResult中返回

(1)实体类设计

分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageDTO实体

  • PageDTO 通用分页查询条件的实体
java">@Data
@ApiModel(description = "通用分页查询实体")
public class PageDTO {@ApiModelProperty("页码")private Integer pageNo;@ApiModelProperty("每页查询条数")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;
}

PageDTO是接收前端提交的查询参数,一般包含四个属性:

  • pageNo:页码
  • pageSize:每页数据条数
  • sortBy:排序字段
  • isAsc:是否升序
  • UserQueryDTO 接收用户查询条件实体,继承PageDTO
java">@EqualsAndHashCode(callSuper = true)    // 当判断相等时先考虑父类属性再考虑子类属性,就是分页的时候把分页条件作为数据请求的的前提,然后再考虑查到了哪些匹配的数据
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQueryDTO extends PageDTO {@ApiModelProperty("用户名关键字")private String name;@ApiModelProperty("用户状态:1-正常,2-冻结")private Integer status;@ApiModelProperty("余额最小值")private Integer minBalance;@ApiModelProperty("余额最大值")private Integer maxBalance;
}
  • PageResult 分页结果实体
java">@Data
@ApiModel(description = "分页结果")
public class PageResult<T> {@ApiModelProperty("总条数")private Long total;@ApiModelProperty("总页数")private Long pages;@ApiModelProperty("结果集合")private List<T> list;
}
  • UserVO 响应用户页面视图实体,将用户VO集合封装到PageResult中返回,之前已经定义过了
java">@Data
@ApiModel(description = "用户VO实体")
public class UserVO {@ApiModelProperty("用户id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("详细信息")private UserInfo info;@ApiModelProperty("使用状态(1正常 2冻结)")private UserStatus status;@ApiModelProperty("账户余额")private Integer balance;@ApiModelProperty("用户的收获地址")private List<AddressVO> addresses;
}

(2)开发接口

  • UserController中定义分页条件查询用户的接口
java">// in UserController
@ApiOperation("根据条件分页查询用户接口")
@GetMapping("/condition/page")
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {return userService.queryUserByConditionAndPage(queryDTO);
}
  • IUserService接口和UserServiceImpl实现类
java">// in IUserService
PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO);// in UserServiceImpl
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 构建分页条件Page<User> page = Page.of(queryDTO.getPageNo(), queryDTO.getPageSize());// 构建排序条件if (StrUtil.isNotBlank(queryDTO.getSortBy())) { // 如果排序字段不为空page.addOrder(new OrderItem().setColumn(queryDTO.getSortBy()).setAsc(queryDTO.getIsAsc()));}else { // 如果排序字段为空,默认按照更新时间排序page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));}// 分页查询String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// 封装VO结果PageResult<UserVO> result = new PageResult<>();result.setTotal(p.getTotal());  // 总条数result.setPages(p.getPages());  // 总页数// 当前页数据List<User> records = p.getRecords();// 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 将用户集合拷贝为用户VO集合List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);result.setList(userVOList);return result;
}
  • 测试根据条件和分页查询用户接口

发送的分页请求

响应的分页数据


(3)改造PageDTO实体

在刚才的代码中,从PageDTOMybatisPlusPage之间转换的过程还是比较麻烦的。

对于PageDTO构建为MP的分页对象的部分,我们完全可以在PageDTO这个实体内部中定义一个转换方法,简化开发。

  • PageDTO
java">import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "通用分页查询实体")
public class PageDTO {@ApiModelProperty("页码")private Integer pageNo;@ApiModelProperty("每页查询条数")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;/*** 在PageDTO内部将PageDTO构建为MP的分页对象,并设置排序条件* @param items 排序条件(可以有一个或多个)* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPage(OrderItem... items) {// 构建分页条件Page<T> page = Page.of(pageNo, pageSize);// 构建排序条件if (sortBy != null && sortBy.trim().length() > 0) { // 如果排序字段不为空page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));}else if (items != null) { // 如果排序字段为空,且传入的默认OrderItem不为空,按照调用者传入的默认OrderItem排序page.addOrder(items);}return page;}/*** 将PageDTO构建为MP的分页对象* 如果调用者不想new OrderItem对象,可以调用该方法,传入默认的排序字段和排序规则即可* @param defaultSortBy 排序字段* @param defaultAsc 排序规则,true为asc,false为desc* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {return toMpPage(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));}/*** 将PageDTO构建为MP的分页对象,排序条件按update_time更新时间降序* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {return toMpPage("update_time", false);}/*** 将PageDTO构建为MP的分页对象,排序条件按create_time创建时间降序* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {return toMpPage("create_time", false);}
}

这样我们在开发也时就可以省去对从PageDTOPage的的转换:

java">// 构建分页条件对象
Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();

(4)改造PageResult实体

在查询出分页结果后,数据的非空校验,数据的VO转换都是模板代码,编写起来很麻烦。

我们完全可以将 PO分页对象转换为VO分页结果对象 的逻辑,封装到 PageResult 的内部方法中,简化整个过程。

  • PageResult
java">import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;@Data
@ApiModel(description = "分页结果")
public class PageResult<T> {@ApiModelProperty("总条数")private Long total;@ApiModelProperty("总页数")private Long pages;@ApiModelProperty("结果集合")private List<T> list;/*** 将PO分页对象转换为VO分页结果对象* @param page PO分页对象* @param clazz 目标VO的字节码对象* @return VO分页结果对象* @param <PO> PO实体* @param <VO> VO实体*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Class<VO> clazz) {// PO分页对象封装为VO结果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal());  // 总条数result.setPages(page.getPages());  // 总页数List<PO> records = page.getRecords();    // 当前页数据// 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 将PO集合拷贝为VO集合List<VO> userVOList = BeanUtil.copyToList(records, clazz);result.setList(userVOList);return result;}/*** 将PO分页对象转换为VO分页结果对象* @param page PO分页对象* @param convertor 自定义规则转换器* @return VO分页结果对象* @param <PO> PO实体* @param <VO> VO实体*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Function<PO, VO> convertor) {// PO分页对象封装为VO结果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal());  // 总条数result.setPages(page.getPages());  // 总页数List<PO> records = page.getRecords();    // 当前页数据// 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 将PO集合转换为VO集合,转换动作由调用者来传递List<VO> voList = records.stream().map(convertor).collect(Collectors.toList());result.setList(voList);return result;}
}

这两个改造都相当于定义通用业务工具类。最终,业务层的代码可以简化为:

java">@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 构建分页条件对象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分页查询String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// PO分页对象封装为VO结果return PageResult.of(p, UserVO.class);
}

如果是希望自定义PO到VO的转换过程,可以调用重载方法of(Page<PO> page, Function<PO, VO> convertor),convertor的转换器逻辑由调用者去编写传递:

java">@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 构建分页条件对象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分页查询String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// PO分页对象封装为VO结果return PageResult.of(p, user -> {// PO拷贝基础属性得到VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 对VO进行处理特殊逻辑String username = userVO.getUsername();// 例如用户名脱敏处理userVO.setUsername(username.length() > 2 ? StrUtil.fillAfter(username.substring(0, 2), '*', username.length()) : username.charAt(0) + "*");return userVO;});
}

自定义转换规则的场景,例如:

  • ① PO字段和VO字段不是包含关系,出现字段不一致。
  • ② 对VO中的属性做一些过滤、数据脱敏、加密等操作。
  • ③ 将VO中的属性继续设置数据,例如VO中的address属性,可以查询出用户所属的收获地址,设置后一并返回。

  • 最终的查询结果如下





http://www.ppmy.cn/devtools/136115.html

相关文章

React渲染流程与更新diff算法

React 的渲染流程从虚拟 DOM 树的生成到真实 DOM 的挂载和更新是一个层层递进的过程。以下是详细的解析&#xff1a; 渲染流程概述 React 的渲染流程可以分为两个阶段&#xff1a; 初次渲染&#xff08;Mounting&#xff09;&#xff1a; 将虚拟 DOM 树转换为真实 DOM&#x…

「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用

1. 谷歌浏览器安装及使用流程 1.1 准备篡改猴扩展程序包。 因为谷歌浏览器的扩展商城打不开&#xff0c;所以需要准备一个篡改猴压缩包。 其他浏览器只需打开扩展商城搜索篡改猴即可。 没有压缩包的可以进我主页下载。 也可直接点击下载&#xff1a;Chrome浏览器篡改猴(油猴…

应用系统开发(12) Zync中实现数字相敏检波

在 Xilinx Zynq 系列(如 Zynq-7000 或 Zynq UltraScale+)中实现数字相敏检波(DSP,Digital Synchronous Detection)可以通过硬件(PL部分,FPGA逻辑)和软件(PS部分,ARM Cortex-A 处理器)的协同工作来实现。以下是一个详细的设计方法,包括基本原理和 Zynq 的实现步骤。…

html兼容性问题处理

文章目录 HTML5兼容性问题及解决方法1. 标签支持问题2. 兼容性检测3. 属性值支持问题4. 媒体支持问题5. Web API支持问题6. CSS兼容性问题7. 特定浏览器问题的解决方法 HTML5兼容性问题及解决方法 HTML5作为一种新的标记语言&#xff0c;虽然带来了许多新特性和改进&#xff0…

典型的 SOME/IP 多绑定用例总结

SOME/IP 部署中 AP SWC 不自行打开套接字连接的原因 在典型的 SOME/IP 网络协议部署场景里&#xff0c;AP SWC 不太可能自己打开套接字连接与远程服务通信&#xff0c;因为 SOME/IP 被设计为尽可能少用端口。这一需求源于低功耗 / 低资源的嵌入式 ECU&#xff0c;并行管理大量…

Kafka怎么发送JAVA对象并在消费者端解析出JAVA对象--示例

1、在pom.xml中加入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-stream-kafka</artifactId><version>3.1.6</version></dependency> 2、配置application.yml 加入Kafk…

【AIGC】国内AI工具复现GPTs效果详解

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | GPTs应用实例 文章目录 &#x1f4af;前言&#x1f4af;本文所要复现的GPTs介绍&#x1f4af;GPTs指令作为提示词在ChatGPT实现类似效果&#x1f4af;国内AI工具复现GPTs效果可能出现的问题解决方法解决后的效果 &#x1…

JAVA接口代码示例

public class VehicleExample {// 定义接口public interface Vehicle {void start(); // 启动车辆void stop(); // 停止车辆void status();// 检查车辆状态}public interface InnerVehicleExample {void student();}// 实现接口的类&#xff1a;Carpublic static class Car imp…