Mushroom Notes Mushroom Notes
🍄首页
  • JavaSE

    • 基础篇
    • 数据结构
    • IO流
    • Stream流
    • 函数式接口
    • JUC
    • 反射
    • 网络编程
    • 设计模式
  • JavaEE

    • Servlet
    • JDBC
    • 会话技术
    • 过滤器监听器
    • 三层架构
  • JDK

    • 总览
  • JVM

    • 总览
  • 常用mate
  • CSS
  • JavaScript
  • rds 数据库

    • MySQL
    • MySQL 进阶
    • MySQL 库表规范
  • nosql 数据库

    • Redis
    • Redis 进阶
    • Redis 底层
    • MongoDB
  • Spring生态

    • Spring
    • Spring MVC
    • Spring boot
    • Spring Validation
  • Spring Cloud生态

    • Spring Cloud
    • 服务治理
    • 远程调用
    • 网关路由
    • 服务保护
    • 分布式事务
    • 消息中间件
  • 数据库

    • Mybatis
    • Mybatis Plus
    • Elasticsearch
    • Redisson
  • 通信

    • Netty
📚技术
  • 方案专题
  • 算法专题
  • BUG专题
  • 安装专题
  • 网安专题
  • 面试专题
  • 常用网站
  • 后端常用
  • 前端常用
  • 分类
  • 标签
  • 归档

kinoko

一位兴趣使然的热心码农
🍄首页
  • JavaSE

    • 基础篇
    • 数据结构
    • IO流
    • Stream流
    • 函数式接口
    • JUC
    • 反射
    • 网络编程
    • 设计模式
  • JavaEE

    • Servlet
    • JDBC
    • 会话技术
    • 过滤器监听器
    • 三层架构
  • JDK

    • 总览
  • JVM

    • 总览
  • 常用mate
  • CSS
  • JavaScript
  • rds 数据库

    • MySQL
    • MySQL 进阶
    • MySQL 库表规范
  • nosql 数据库

    • Redis
    • Redis 进阶
    • Redis 底层
    • MongoDB
  • Spring生态

    • Spring
    • Spring MVC
    • Spring boot
    • Spring Validation
  • Spring Cloud生态

    • Spring Cloud
    • 服务治理
    • 远程调用
    • 网关路由
    • 服务保护
    • 分布式事务
    • 消息中间件
  • 数据库

    • Mybatis
    • Mybatis Plus
    • Elasticsearch
    • Redisson
  • 通信

    • Netty
📚技术
  • 方案专题
  • 算法专题
  • BUG专题
  • 安装专题
  • 网安专题
  • 面试专题
  • 常用网站
  • 后端常用
  • 前端常用
  • 分类
  • 标签
  • 归档
  • Spring

  • SpringCloud

  • 数据库

    • Mybatis
    • Mybatis Plus
      • 依赖
      • CRUD操作API
        • 分页查询IPage使用示例
        • 按条件查询
        • lambda格式按条件查询
        • 并且关系(and)
        • 或者关系(or)
        • if语句控制条件追加
        • 查询结果包含模型类中部分属性
        • 查询结果包含模型类中未定义的属性
        • 查询条件
        • 排序和limit
      • 乐观锁(update)
      • 注解配置
      • YML全局配置
      • 逆向工程
    • Elasticsearch
    • Redisson
  • 通信

  • 框架
  • 数据库
kinoko
2023-12-17
目录

Mybatis Plus

# 依赖


<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.5.6</version>
</parent>

<dependencies>
  
  <!--springboot-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
  
  <!-- spring整合test -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
  </dependency>
  
  <!-- mybatis-plus的驱动包 -->
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
  </dependency>
  
  <!-- 连接池 -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
  </dependency>
  
  <!-- mysql要选择正确版本的驱动-->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
    <scope>runtime</scope>
  </dependency>
  
  <!-- lombok -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# CRUD操作API


内置通用 BaseMapper,少量配置即可实现单表CRUD 操作(如果只做单表增删查改不需要你写任何的sql)

功能 MP接口
新增 int insert(T t)
删除 int deleteById(Serializable id)
批量删除 int deleteBatchIds(Collection idList)
修改 int updateById(T t)
根据Id查询 T selectById(Serializable id)
查询全部 List<T> selectList()
分页查询 IPage<T> selectPage(IPage<T> page)
按条件查询 IPage<T> selectPage(Wrapper<T> queryWrapper)

# 分页查询IPage使用示例

使用mp的分页功能必须存在一个配置类,配置分页拦截器

/**
* 如果需要使用到mybatis-plus的分页功能,必须存在一个配置类
* 该配置类创建Mybatis的拦截器,这个拦截器的作用就是在你执行selectPage的方法的时候
* 对sql进行拦截,然后拼接limit语句实现分页。
*/
@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor createMybatisPlusInterceptor(){
        //1. 创建Mybatisplus拦截器
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //2. 往拦截器中添加分页拦截器
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        
        //3 .返回
        return mybatisPlusInterceptor;
    }
    
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

测试

/**
* 分页查询
*/
@Test
public void testPage(){
    //1. 设置当前页与页面大小
    Page<User> page =new Page<>(1,2); //当前页1  页面大小是2
    
    //2. 创建分页需要条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    
    //3. 调用selectPage方法实现分页,分页的结果会被封装到Page对象中
    userMapper.selectPage(page,queryWrapper);
    
    System.out.println("页面大小:"+page.getSize());
    System.out.println("页面数据:"+page.getRecords());
    System.out.println("当前页:"+page.getCurrent());
    System.out.println("总记录数:"+page.getTotal());
    System.out.println("总页数:"+page.getPages());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

若是自定义方法也想使用mp的分页功能需要满足以下几点:

  1. 如果自己实现的sql语句需要使用分页功能,那么该方法必须接收一个Page对象。
  2. 方法的返回值必须也是一个Page对象
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

public interface UserMapper extends BaseMapper<User> {

    @Select("select * from user where age>#{age}")
    public Page<User> selectByAge(Page<User> page, @Param("age") Integer age);
}
1
2
3
4
5
6
7

当SQL语句非常复杂的时候,比如需要分组、条件、排序,使用mp就需要调用较多的api,并且不可复用,这里就可以自己通过xml实现sql语句来进行复用。

# 按条件查询

QueryMrapper 和 LambdaQueryWrapper 的区别

**相同点:**设置查询条件、查询字段等信息
不同点:

  1. QueryMrapper 需要设置字段名字字符串的形式编写代码,功能强大

优点:

  1. 给查询的字段起别名
  2. 可以执行聚合函数
  3. LambdaQueryWrapper 通过lambda表达式写字段名字,有智能提示

查询年龄大于18岁的用户

@Test
void testQueryWrapper() {
    //1. 创建查询条件封装对象,可以指定泛型
    QueryWrapper<User> wrapper = new QueryWrapper();
    //2.字段age大于18
    wrapper.gt("age", 18);
    //3.执行查询
    List<User> users = userMapper.selectList(wrapper);
    //4.输出结果
    users.forEach(System.out::println);
}
1
2
3
4
5
6
7
8
9
10
11

# lambda格式按条件查询

查询年龄小于10的用户

@Test
void testLambdaQueryWrapper() {
    //1.创建lambda查询包装器,支持泛型
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper();
    //2. 使用lambda参数,相当于调用 user -> user.getAge()方法,获取列名
    wrapper.le(User::getAge, 10);
    //3.查询
    List<User> users = userMapper.selectList(wrapper);
    //4.输出结果
    users.forEach(System.out::println);
}
1
2
3
4
5
6
7
8
9
10
11

# 并且关系(and)

查询年龄小于30岁,而且大于10岁的用户

@Test
void testAnd() {
    //并且关系
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    //支持链式写法
    wrapper.lt(User::getAge, 30).gt(User::getAge, 10);
    List<User> userList = userMapper.selectList(wrapper);
    System.out.println(userList);
}
1
2
3
4
5
6
7
8
9

生成的SQL语句: SELECT id,name,gender,password,age,tel FROM user WHERE (age < ? AND age > ?)

# 或者关系(or)

查询年龄小于10岁或者大于30岁的用户

@Test
void testOr() {
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    //或者关系:小于10岁或者大于30岁
    wrapper.lt(User::getAge, 10).or().gt(User::getAge, 30);
    List<User> userList = userMapper.selectList(wrapper);
    System.out.println(userList);
}
1
2
3
4
5
6
7
8

生成的SQL语句: SELECT id,name,gender,password,age,tel FROM user WHERE (age < ? OR age > ?)

# if语句控制条件追加

  • 如果最小年龄不为空,则查询大于这个年龄的用户
  • 如果最大年龄不为空,则查询小于这个年龄的用户
@Test
void testCondition() {
    Integer minAge=10;  //将来有用户传递进来,此处简化成直接定义变量了
    Integer maxAge=null;  //将来有用户传递进来,此处简化成直接定义变量了
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    //参数1:如果表达式为true,那么查询才使用该条件,也支持链式编程
    wrapper.gt(minAge != null, User::getAge, minAge);
    wrapper.lt(maxAge != null, User::getAge, maxAge);
    //查询
    List<User> userList = userMapper.selectList(wrapper);
    //输出
    userList.forEach(System.out::println);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 查询结果包含模型类中部分属性

查询所有用户,只显示id, name, age三个属性,不是全部列。

使用select(列名...)方法,查询的结果如果封装成实体类,则只有这三个属性有值,其它属性为NULL

@Test
void testSameColumn() {
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    //查询所有用户,只显示id, name, age三个属性,不是全部列
    wrapper.select(User::getId, User::getName, User::getAge);
    List<User> userList = userMapper.selectList(wrapper);
    System.out.println(userList);
}
1
2
3
4
5
6
7
8

生成的SQL语句:**SELECT id,name,age FROM user**

# 查询结果包含模型类中未定义的属性

如果查询结果包含模型类中未定义的属性,可以将每个元素封装成Map对象。一行数据代表一个map集合对象,表字段为key,字段内容为value。

**需求:**按性别进行分组,统计每组的人数。只显示统计的人数和性别这两个字段

使用QueryWrapper包装对象的select方法

@Test
void testCountGender() {
    //使用QueryWrapper包装对象
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    //查询2列:人数, 性别。 将聚合函数定义别名做为Map中的键
    wrapper.select("count(*) as count, gender");
    //按sex分组
    wrapper.groupBy("gender");
    //这里的查询方法使用selectMaps
    List<Map<String, Object>> list = userMapper.selectMaps(wrapper);
    list.forEach(System.out::println);
}
1
2
3
4
5
6
7
8
9
10
11
12

# 查询条件

  • 购物设定价格区间、户籍设定年龄区间(le ge匹配 或 between匹配)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>();
//范围查询 lt le gt ge eq between
//2. 使用lambda参数,相当于调用 user -> user.getAge()方法,获取列名
        // le 小于等于
        // lt 小于
        // gt 大于
        // ge 大于等于
        // eq 等于
wrapper.between(User::getAge, 10, 30);
List<User> userList = userMapper.selectList(wrapper);
System.out.println(userList);
1
2
3
4
5
6
7
8
9
10
11
  • 查信息,搜索新闻(非全文检索版:like匹配)
  /**
     * 需求: 查询姓张的用户
     *    select * from user where name like '张%'
     */
    @Test
    public void testFindByLike(){
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        // Wrapper.likeRight(字段,值) 模糊查询生成格式:'值%'
        // Wrapper.likeLeft(字段,值) 模糊查询生成格式:'%值'
        // Wrapper.like(字段,值) 模糊查询生成格式:'%值%'
        lambdaQueryWrapper.likeRight(User::getName,"张");
        List<User> userList = userMapper.selectList(lambdaQueryWrapper);
        userList.forEach(System.out::println);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 统计报表(分组查询聚合函数)
QueryWrapper<User> qw = new QueryWrapper<User>();
qw.select("gender", "count(*) as nums");
qw.groupBy("gender");
List<Map<String, Object>> maps = userMapper.selectMaps(qw);
System.out.println(maps);
1
2
3
4
5

# 排序和limit

题目:显示年龄最大的5个用户

  • 说明:
    ①:提示:对年龄进行降序排序
    ②:仅获取前5条数据(提示:使用分页功能控制数据显示数量)
  • last()方法的说明:
    无视优化规则直接拼接到 sql 的最后(有sql注入的风险,请谨慎使用),注意只能调用一次,多次调用以最后一次为准
    /**
     * 需求: 查询年龄大于18岁的前三位
     *      select * from user where age>18 order by age desc limit 3;
     *
     */
    @Test
    public void testFindByLimit(){
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        // wrapper.orderByDesc(字段),  按照指定的字段降序排序
        // wrapper.orderByAsc(字段),  按照指定的字段升序排序
        // wrapper.last("sql片段"),  用于拼接limit关键字的sql片段,将这个片段放在所有sql语句的最后面。
        lambdaQueryWrapper.gt(User::getAge,18).orderByDesc(User::getAge).last("limit 3");
        List<User> userList = userMapper.selectList(lambdaQueryWrapper);
        userList.forEach(System.out::println);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

生成的SQL **SELECT id,name,gender,password,age,tel FROM user WHERE (age > ?) ORDER BY age DESC limit 3**

# 乐观锁(update)


数据库中悲观锁的实现是通过 FOR UPDATE关键字来开启行锁,锁住当前行数据
如:

-- 手动开启事务
START TRANSACTION
SELECT * FROM tbl_user WHERE id=1 FOR UPDATE	-- 开启行锁
-- 增、删、改的事情
UPDATE tbl_user SET age=age+1 WHERE id=1
COMMIT/ROLLBACK
1
2
3
4
5
6

数据库的乐观锁的实现则是简单粗暴,给表添加字段version,控制字段版本,每进行一次修改,version+1

假设当前数据库tbl_user表状态为 age = 12, version =0;
线程A:
update tbl_user set age=age+1,version=version+1 where id=1 and version=1
若A先成功,age=13,version=1
线程B:
update tbl_user set age=age+1,version=version+1 where id=1 and version=1
由于此时最新的version=2,所以没有符合条件的数据,sql执行失败

**sql语句执行过程:**编译sql语句=>执行修改sql=>根据条件查询数据=>修改数据=>提交事务=>更新到磁盘

**注意:**数据库在执行sql语句的时候是原子操作

mp实现乐观锁的步骤:

  1. 数据库的表必须添加version字段
  2. 实体类也得有version属性,并且加上@Version注解表示该属性为版本更新字段
  3. 添加乐观锁的拦截器
  4. 执行update语句的时候记得要先把待修改记录的version查出来

配置乐观锁拦截器实现锁机制对应的动态SQL语句拼装

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        //1.定义Mp拦截器
        MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
        
        //2.添加乐观锁拦截器
        mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        
        return mpInterceptor;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testUpdateTwo() {
    // 先通过要修改的数据id将当前数据以及version查询出来
    User user1 = userMapper.selectById(2L);     //version=2
    User user2 = userMapper.selectById(2L);    //version=2
    user1.setName("Jack aaa");
    userMapper.updateById(user1);              //version=>3
    user2.setName("Jack bbb");
    userMapper.updateById(user2);               //verion=2 更新失败
}
1
2
3
4
5
6
7
8
9
10

image.png

# 注解配置


注解 属性 说明
@TableField value:设置字段映射关系,起别名
exist:false 设置不查询该字段
select:false 设置该字段不参与查询,但是增删改参与
建立实体类属性名与表字段名的映射关系;在生成sql语句的时候,会根value属性查找到表字段设置与实体类属性名相同的别名
@TableName value:设置表别名 建立实体类名与表名的映射关系
@TableId type:设置主键属性的生成策略,值参照IdType枚举值
- AUTO(0):使用数据库的id自增策略
- NONE(1):不设置id生成策略
- INPUT(2):用户手动输入id
- ASSIGN_ID(3):雪花算法生成id【默认】【推荐】
- ASSIGN_UUID(4):以uuid算法生成id
设置主键id生成策略
@TableLogic value:未删除时的值
delval:删除了的值
逻辑删除字段,标记当前记录是否被删除,当删除记录时不会真的删除该记录,而是更新该字段的值。

# YML全局配置


mybatis-plus:
  global-config:
    db-config:
      # 设置全局id生成策略
      id-type: assign_id
      # 设置全局表名映射
      table-prefix: tbl_
      # 逻辑删除字段名
      logic-delete-field: deleted
      # 逻辑删除字面值:未删除为0
      logic-not-delete-value: 0
      # 逻辑删除字面值:删除为1
      logic-delete-value: 1
1
2
3
4
5
6
7
8
9
10
11
12
13

# 逆向工程


package cn.kk;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {

    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }


    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        String moduleName = scanner("模块名");
        gc.setOutputDir(projectPath + "/"+moduleName+"/src/main/java");
        //代码的作者
        gc.setAuthor("kk");
        gc.setOpen(false);
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/db2?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(scanner("功能模块名"));
        //设置父级包名
        pc.setParent("cn.kk");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/"+moduleName+"/src/main/resources/mapper/" + pc.getModuleName()
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        // 公共父类
        // strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
        // strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        String preName = scanner("请输入表前缀名");
        strategy.setTablePrefix(preName); // 设置表前缀
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());

        // 执行
        mpg.execute();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#mybatisPlus#mb
上次更新: 2023/12/29 11:32:56
Mybatis
Elasticsearch

← Mybatis Elasticsearch→

最近更新
01
JVM 底层
09-13
02
JVM 理论
09-13
03
JVM 应用
09-13
更多文章>
Theme by Vdoing | Copyright © 2022-2024 kinoko | MIT License | 粤ICP备2024165634号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式