Mybatis-plus入门到精通

Why Mybatis-plus:

  • 少写 80% 重复代码:单表 CRUD 不用写 XML / 注解 SQL,继承 BaseMapper 直接调用,新增、查询、更新、删除一行代码搞定,告别 "复制粘贴" 式开发;
  • 动态 SQL 不用拼 :LambdaQueryWrapper 链式调用,条件查询像 "说话" 一样直观(比如ge(User::getAge, 18).like(User::getUserName, "张")),杜绝字段拼写错误和<if>/<where>标签繁琐配置;
  • 分页一键搞定 :内置物理分页插件,不用集成第三方工具,new Page(pageNum, pageSize)+selectPage()直接返回分页结果,性能比内存分页更优;
  • 代码生成省时间:AutoGenerator 一键生成 Entity、Mapper、Service、Controller 全套代码,新模块开发周期直接缩短 30%,不用手动建文件写模板;
  • 常用功能内置化:逻辑删除、乐观锁、主键策略、自动填充(创建时间 / 更新时间),不用手动写 SQL 控制,注解 + 简单配置就能实现,解决并发、数据安全等常见问题;
  • 零侵入兼容 MyBatis:完全保留 MyBatis 灵活性,复杂 SQL(多表联查、子查询)还能写 XML,既享受高效开发,又不牺牲自定义扩展能力;

快速上手:

1引入依赖:

java 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter。

2定义Mapper

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

因此我们自定义的Mapper只要实现了这个BaseMapper,就无需自己实现单表CRUD了。

例如:(需要指定BaseMapper需要操作的实体类类型)

java 复制代码
package com.itheima.mp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;

public interface UserMapper extends BaseMapper<User> {
}

测试:

之后就能使用BaseMapper里已经拥有的丰富的增删改查功能:

java 复制代码
package com.itheima.mp.mapper;

import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.List;

@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

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

    @Test
    void testSelectById() {
        User user = userMapper.selectById(5L);
        System.out.println("user = " + user);
    }

    @Test
    void testSelectByIds() {
        List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L));
        users.forEach(System.out::println);
    }

    @Test
    void testUpdateById() {
        User user = new User();
        user.setId(5L);
        user.setBalance(20000);
        userMapper.updateById(user);
    }

    @Test
    void testDelete() {
        userMapper.deleteById(5L);
    }
}

mp的默认规则:

MybatisPlus是根据BaseMapper中泛型PO实体(上面测试中的是User类)的信息来推断出表的信息,从而生成SQL的,我们想利用mp快速生成sql,就要遵守他的默认规则

默认情况下:

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名

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

  • MybatisPlus会把名为id的字段作为主键

注意:但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。

常用注解

**@TableName:**标识实体类对应的表

示例:

java 复制代码
@TableName("user")
public class User {
    private Long id;
    private String name;
}

TableName除了标识表名,还有其他属性:


**@TableId:**标识实体类中的主键字段

示例:

java 复制代码
@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
}

属性:

其中IdType常见类型:

  • AUTO:利用数据库的id自增长

  • INPUT:手动生成id

  • ASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略

**TableField:**普通字段注解:标识数据库中对应的字段

示例

java 复制代码
@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    @TableField(is_married")
    private Boolean isMarried;
    @TableField("`concat`")
    private String concat;
}

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

  • 成员变量名与数据库字段名不一致

  • 成员变量是以isXXX命名,并且类型为Boolean,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。

  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加转义字符:````````

常见其他属性:

mp的yaml配置:

MybatisPlus也支持基于yaml文件的自定义配置,官方文档:使用配置 | MyBatis-Plus

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:

  • 实体类的别名扫描包

  • 全局id类型

java 复制代码
mybatis-plus:
  type-aliases-package: com.dj.mp.domain.po
  global-config:
    db-config:
      id-type: auto # 全局id类型为自增长

MyBatisPlus也支持手写SQL的,而mapper文件的读取地址可以自己配置:

java 复制代码
mybatis-plus:
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。

核心功能:

条件构造器Wrapper--动态 SQL 的 "拼接神器"

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

Wrapper有很多默认实现

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

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

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

实际上我们只需要重点关注常见的三个

Wrapper 核心用法(LambdaQueryWrapper 为主,推荐)​

先明确核心原则:链式调用,条件方法自动拼接 AND/OR,无需手动处理逻辑运算符。

  1. 基础条件方法

1

  1. 逻辑运算符(AND/OR)​

默认多个条件用 AND 连接,如需 OR 可手动调用 .or() 方法:​

java 复制代码
// 示例:查询(年龄>=18 且 用户名含"张")OR(邮箱不为空)​

LambdaQueryWrapper new LambdaQueryWrapper​

.ge(User::getAge, 18)​

.like(User::getUserName, "张")​

.or() // 手动切换为OR​

.isNotNull(User::getEmail);​

// 对应SQL:WHERE age >= ? AND user_name LIKE ? OR email IS NOT NULL​

  1. 非空判断(自动过滤 NULL 值)​

Wrapper 会自动忽略值为 null 的条件,无需手动写 test="xxx != null">:​

java 复制代码
Integer minAge = null; // 前端未传该参数​

String userName = "张";​

​

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper .ge(minAge != null, User::getAge, minAge) // 若minAge为null,该条件不拼接​

.like(User::getUserName, userName);​

// 最终SQL:WHERE user_name LIKE '%张%'(age条件被忽略)​

第一个参数可传布尔值,控制条件是否生效(如 .ge(Boolean.TRUE, ...) 强制生效)。​

不同 Wrapper 实战示例​

  1. LambdaQueryWrapper(查询场景,推荐)​

// 需求:查询年龄18-30岁、用户名含"张"、邮箱不为空的用户,按创建时间降序​

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper .between(User::getAge, 18, 30)​

.like(User::getUserName, "张")​

.isNotNull(User::getEmail)​

.orderByDesc(User::getCreateTime);​

List = userMapper.selectList(wrapper);​

// 对应SQL:​

// SELECT id,user_name,age,email,create_time,update_time,is_deleted ​

// FROM user ​

// WHERE age BETWEEN ? AND ? ​

// AND user_name LIKE ? ​

// AND email IS NOT NULL ​

// ORDER BY create_time DESC​


2. QueryWrapper(非 Lambda,简单场景)​

// 注意:字段名需手动写字符串,容易拼错(不推荐复杂场景)​

QueryWrapper = new QueryWrapper​

.eq("age", 25)​

.like("user_name", "李")​

.orderByAsc("create_time");​

ListList = userMapper.selectList(wrapper);​


3. UpdateWrapper(动态更新场景)​

// 需求:更新用户(ID=1)的用户名和年龄,仅更新非空字段​

UpdateWrapper = new UpdateWrapper​

.set("user_name", "张三更新") // 强制更新该字段​

.set("age", 26)​

.eq("id", 1); // WHERE条件​

// 或用LambdaUpdateWrapper(避免字段名字符串):​

LambdaUpdateWrapper new LambdaUpdateWrapper​

.set(User::getUserName, "张三更新")​

.set(User::getAge, 26)​

.eq(User::getId, 1);​

userMapper.update(null, wrapper); // 第一个参数传null,用Wrapper的set条件​

// 对应SQL:UPDATE user SET user_name = ?, age = ? WHERE id = ?​


​多表关联:

理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。

例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户 要是自己基于mybatis实现SQL,大概是这样的:

XML 复制代码
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
      SELECT *
      FROM user u
      INNER JOIN address a ON u.id = a.user_id
      WHERE 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部分,实现多表查询。

查询条件这样来构建:

XML 复制代码
@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中自定义方法:

XML 复制代码
@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);

注意:通过${ew.customSqlSegment}来拼接我们wrapper的查询条件片段,其中@Param("ew")中ew是固定的约定写法,不能替换成别的词,如@Param("wrapper"),这样会报错


IService ------ Service 层的 "效率神器"

IService 是 MyBatis-Plus 提供的 Service 层接口,基于 BaseMapper 封装了更全面的 CRUD 方法,同时支持批量操作、链式调用、条件查询(结合 Wrapper)等高级特性。​

它解决了传统 Service 层的 2 大痛点:​

  • 无需手动编写 Service 接口和实现类的重复方法(如 getById、list、saveBatch);
  • 内置批量操作、分页查询等常用功能,不用自己封装工具类。

一:IService 核心价值:​

  • 接口 + 实现类一键生成,减少模板代码;
  • 方法命名更贴合业务(如 save 替代 insert、remove 替代 delete);
  • 支持 Lambda 条件查询,与 Wrapper 无缝衔接;
  • 批量操作默认优化性能(如分批插入)。

二、快速搭建 IService(3 步搞定)​

以之前的 User 实体为例,基于 Spring Boot + MP 环境,3 步搭建完整 Service 层:

1创建 UserService 接口,继承 MP 提供的 `IService 添加方法:​

XML 复制代码
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.mp.entity.User;

// 泛型指定实体类User
public interface UserService extends IService> {
    // 无需手动添加CRUD方法,IService已内置全套
    // 如需自定义业务方法,可在此添加(如 getUserByUserName)
}

2. 实现 Service 类(继承 ServiceImpl)​

创建 UserServiceImpl 实现类,继承 ServiceImpl>,并实现自定义接口:​

XML 复制代码
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;​

import com.example.mp.entity.User;​

import com.example.mp.mapper.UserMapper;​

import com.example.mp.service.UserService;​

import org.springframework.stereotype.Service;​

​

// 泛型参数1:对应的Mapper接口;参数2:实体类​

@Service // 注入Spring容器​

public class UserServiceImpl extends ServiceImpl, User> implements UserService {​

// 无需重写CRUD方法,ServiceImpl已实现所有IService接口方法​

// 如需自定义业务逻辑,在此实现(如重写或新增方法)​

}​

3. 注入使用(Controller / 其他 Service)​

直接在 Controller 中注入 UserService,即可调用内置方法:​

XML 复制代码
import com.example.mp.entity.User;​

import com.example.mp.service.UserService;​

import org.springframework.beans.factory.annotation.Autowired;​

import org.springframework.web.bind.annotation.*;​

​

import java.util.List;​

​

@RestController​

@RequestMapping("/user")​

public class UserController {​

​

@Autowired​

private UserService userService; // 注入IService实现类​

​

// 直接调用内置方法,无需手动写Service逻辑​

@GetMapping("/{id}")​

public User getById(@PathVariable Long id) {​

return userService.getById(id); // IService内置方法​

}​

}​

​

三、IService 核心内置方法(常用分类)​

IService 封装了近 50 个常用方法,覆盖查询、新增、更新、删除、批量操作、分页等场景,以下是高频用法分类:​

1. 单条数据操作​

|--------------------------|-------------------|---------------------------------------------------|
| 方法名​ | 作用​ | 示例代码​ |
| getById(Long id)​ | 根据 ID 查询单条数据​ | userService.getById(1L)​ |
| save(User entity)​ | 新增单条数据​ | userService.save(new User("张三", 25))​ |
| updateById(User entity)​ | 根据 ID 更新​ | userService.updateById(new User(1L, "张三更新", 26))​ |
| removeById(Long id)​ | 根据 ID 删除(支持逻辑删除)​ | userService.removeById(1L)​ |

2. 批量操作

|-------------------------------|------------------------|-----------------------------------------------------|
| 方法名​ | 作用​ | 示例代码​ |
| saveBatch(List)​ | 批量新增(默认分批插入,优化性能)​ | userService.saveBatch(Arrays.asList(user1, user2))​ |
| saveOrUpdateBatch(List list)​ | 批量新增或更新(存在则更新,不存在则新增)​ | userService.saveOrUpdateBatch(list)​ |
| updateBatchById(List> list)​ | 批量更新(根据 ID)​ | userService.updateBatchById(list)​ |
| removeByIds(Collection ids)​ | 批量删除(根据 ID 集合)​ | userService.removeByIds(Arrays.asList(1L, 2L))​ |

​对于批量新增,MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements,可以重写批处理的statement语句默认是关闭,我们可以将其打开,能够提升批处理速度

原因:会启用 MySQL Connector 驱动对批量语句的重写功能。具体来说:

它会将多条 SQL 语句合并成一条语句发送到数据库。

例如,原本需要执行多次的 INSERT 语句会被合并为一条批量插入语句。
如何使用? :

修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:

java 复制代码
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: MySQL123
  1. 条件查询(结合 Wrapper,核心)​

与 Wrapper 无缝衔接,支持 Lambda 条件构造,无需手动调用 Mapper:​

java 复制代码
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;​

​

// 示例1:条件查询列表​

LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>()​

.ge(User::getAge, 18)​

.like(User::getUserName, "张");​

List> userList = userService.list(wrapper); // 直接用Service查询​

​

// 示例2:条件查询单条数据​

User user = userService.getOne(wrapper); // 对应SELECT ONE​

​

// 示例3:条件删除​

boolean success = userService.remove(wrapper); // 对应DELETE WHERE​

  1. 分页查询(内置分页支持)​

结合 MP 分页插件,一行代码实现分页,无需手动处理 Page 对象:​

java 复制代码
import com.baomidou.mybatisplus.core.metadata.IPage;​

import com.baomidou.mybatisplus.core.toolkit.Wrappers;​

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;​

​

// 需求:分页查询年龄>=18的用户,第2页,每页10条​

Page page = new Page, 10); // 页码(从1开始)、每页条数​

LambdaQueryWrapper Wrappers.lambdaQuery(User.class)​

.ge(User::getAge, 18);​

​

IPage userService.page(page, wrapper); // 内置分页方法​

// 分页结果:userPage.getRecords()(数据列表)、userPage.getTotal()(总条数)、userPage.getPages()(总页数)​

​
  1. 其他实用方法​

|------------------------------------|-------------|-----------------------------------------------------------|
| 方法名​ | 作用​ | 示例代码​ |
| count()​ | 查询总记录数​ | long total = userService.count();​ |
| count(LambdaQueryWrapper wrapper)​ | 条件查询记录数​ | long count = userService.count(wrapper);​ |
| list()​ | 查询所有数据​ | List = userService.list();​ |
| chain()​ | 链式调用(高级用法)​ | userService.chain().like(User::getUserName, "张").list();​ |

四、自定义业务方法(扩展 IService)​

如果内置方法满足不了业务需求,可在接口中新增自定义方法,在实现类中实现:​

  1. 自定义接口方法​

java 复制代码
public interface UserService extends IService // 自定义方法:根据用户名查询用户(模糊匹配)​

List getUserByUserNameLike(String userName);​

}​

  1. 实现类中实现(可调用 baseMapper 或 Wrapper)​​
java 复制代码
@Service​

public class UserServiceImpl extends ServiceImpl User> implements UserService {​

​

@Override​

public ListUserNameLike(String userName) {​

// 方式1:调用 baseMapper(ServiceImpl内置属性,无需注入)​

return baseMapper.selectList(Wrappers.lambdaQuery(User.class)​

.like(User::getUserName, userName));​

​

// 方式2:直接用 Service 内置方法 + Wrapper(推荐)​

// return this.list(Wrappers.lambdaQuery(User.class).like(User::getUserName, userName));​

}​

}​

​
  1. 调用自定义方法​
java 复制代码
​



@GetMapping("/like")​

public ListLike(@RequestParam String userName) {​

return userService.getUserByUserNameLike(userName); // 调用自定义方法​

}​

​

扩展功能:

1代码生成插件mybatis-plus

1 分钟生成单表全套代码(Entity+Mapper+IService+ServiceImpl+Controller);

安装插件:

使用:先连接数据库

生成代码:

最终基础代码会自动生成

2静态工具Db:

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

3枚举处理器和Json处理器

数据库中常有 "状态类字段"(如 user_status:0 - 正常、1 - 禁用、2 - 冻结),传统做法是用 Integer 存储,代码中用常量判断(如 if (userStatus == 1)),存在 2 个痛点:​

  • 可读性差:数字无法直观表达含义;
  • 易出错:硬编码数字可能写错(如把 2 写成 3)。

MP 枚举处理器的价值:让数据库字段与 Java 枚举直接映射,无需手动转换,代码可读性和安全性大幅提升。

首先定义枚举:

java 复制代码
package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

然后把User类中的status字段的Inteager类型改为UserStatus 类型:

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

并且,在UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段:

最后配上枚举处理器

XML 复制代码
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

Json处理器

数据库中常有 "复杂结构字段"(如 ext_info 存储用户额外信息:{"address":"北京","hobby":["篮球","读书"]}),传统做法是用 varchar 存储 JSON 字符串,存在 2 个痛点:​

  • 代码冗余:需手动将 Java 对象序列化为 JSON 字符串,查询后再反序列化为对象;
  • 易出错:手动序列化可能导致 JSON 格式错误,查询时解析复杂。

MP JSON 处理器的价值:让 Java 对象与数据库 JSON 字段直接映射,自动完成序列化 / 反序列化,无需手动处理。

JSON 处理器使用步骤(3 步搞定)​

步骤 1:定义 JSON 对应的 Java 实体类​

创建存储 JSON 结构的实体类(如用户额外信息):

java 复制代码
package com.itheima.mp.domain.po;

import lombok.Data;

@Data
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
}

2接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:

同时,在User类上添加一个注解,声明自动映射:


分页插件

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

配置分页插件

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

java 复制代码
package com.itheima.mp.config;

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;

@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

测试:

java 复制代码
@Test
void testPageQuery() {
    // 1.分页查询,new Page()的两个参数分别是:页码、每页大小
    Page<User> p = userService.page(new Page<>(2, 2));
    // 2.总条数
    System.out.println("total = " + p.getTotal());
    // 3.总页数
    System.out.println("pages = " + p.getPages());
    // 4.数据
    List<User> records = p.getRecords();
    records.forEach(System.out::println);
}
相关推荐
木井巳9 小时前
【递归算法】二叉搜索树中第K小的元素
java·算法·leetcode·深度优先·剪枝
qq_297574679 小时前
【实战】POI 实现 Excel 多级表头导出(含合并单元格完整方案)
java·spring boot·后端·excel
星辰_mya9 小时前
Elasticsearch线上问题之慢查询
java·开发语言·jvm
南极星10059 小时前
我的创作纪念日--128天
java·python·opencv·职场和发展
郝学胜-神的一滴9 小时前
超越Spring的Summer(一): PackageScanner 类实现原理详解
java·服务器·开发语言·后端·spring·软件构建
摇滚侠9 小时前
Java,举例说明,函数式接口,函数式接口实现类,通过匿名内部类实现函数式接口,通过 Lambda 表达式实现函数式接口,演变的过程
java·开发语言·python
打工的小王9 小时前
java并发编程(七)ReentrantReadWriteLock
java·开发语言
lang201509289 小时前
Java并发革命:JSR-133深度解析
java·开发语言
abluckyboy9 小时前
基于 Java Socket 实现多人聊天室系统(附完整源码)
java·开发语言