• Spring Boot 集成MyBatis-Plus


    一、背景说明

    大部分的项目都需要进行数据的持久化,所以必然会使用到数据库。而关系型数据库是其中比较常见的数据库类型。目前使用比较多的关系型数据库有:MySQL、PostgreSQL和Oracle等。

    在古早的应用开发中,需要开发人员写许多的DAL(数据访问层)代码,需要自己管理数据库的连接与关闭,还需要自己从ResultSet中获取数据,然后再将其组装为对象。在其中会编写大量非业务代码,并且这些代码往往充满了重复。

    ORM的出现解决了前面提到的一系列问题,ORM的全称是 Object Relational Mapping ,翻译为对象关系模型。这里所说的对象是指业务领域的对象,关系 则指的是关系数据库(具体而言就是数据表和字段)。

    ORM可以实现自动将数据库中的表字段映射为对象的属性,其采用的方式是:使用映射元数据 来描述对象关系的映射。ORM充当了应用程序的业务逻辑层和数据库之间的桥梁。常见的ORM中间件有:Hibernate和ibatis(目前已更名为 MyBatis)。

    本篇所提及的 MyBatis-PlusMyBatis 的功能进行了增强。

    在这里插入图片描述

    二、集成过程

    在Spring Boot项目中集成 MyBatis-Plus 过程比较简单,大概分为三个步骤:

    • 引入maven依赖
    • 增加属性配置
    • 增加相关的自动配置类

    如果不知道如何创建Spring Boot项目,可以参考以往的文章:

    [Spring Boot实战] 如何快速地创建spring boot项目

    2.1 引入 maven 依赖

    为了使用 MyBatis-Plus,需要添加相关依赖:

     
     <dependency>
         <groupId>com.baomidougroupId>
         <artifactId>mybatis-plus-boot-starterartifactId>
         <version>${mybaits-plus.version}version>
     dependency>
    

    同时因为需要数据库进行数据的存储,所以还需要添加下面的依赖(我们选择的MySQL数据库):

     
     <dependency>
         <groupId>mysqlgroupId>
         <artifactId>mysql-connector-javaartifactId>
         <scope>runtimescope>
     dependency>
    

    如果仅限于集成 MyBatis-Plus,这两个依赖就已经足够了。

     

    2.2 增加属性配置

    相关的属性配置到 application.yml 文件中:

    spring:
      application:
        name: "demo-api"
      datasource:
        # MySql 8.0以上版本
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 兼容以前的配置
        jdbc-url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&useTimezone=true&serverTimezone=GMT%2B8&allowMultiQueries=true
        url: ${spring.datasource.jdbc-url}
        username: your_username
        password: your_password
    

     

    2.3 自动配置类

    前面我们已经提过:ORM使用映射元数据 来描述对象关系的映射。所以我们需要对相关信息进行配置,比如:

    • 使用的是什么数据库
    • 数据库连接是什么
    • Mapper 接口文件在哪里
    • MyBatis 所依赖的XML文件到哪里去找
    • MyBatis 的 SqlSessionFactory 如何创建(依赖数据源、MyBatis插件)

    Spring Boot 中的配置一般以 @Configuration 进行标识。相关代码如下:

    @Configuration
    @MapperScan(basePackages = "com.xhm.demo.api.mapper")
    public class DataSourceConfig {
    
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource dataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            //分页插件
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            //注册乐观锁插件
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            return interceptor;
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactory(DataSource dataSource, MybatisPlusInterceptor interceptor) throws Exception {
            MybatisSqlSessionFactoryBean ssfb = new MybatisSqlSessionFactoryBean();
            ssfb.setDataSource(dataSource);
            ssfb.setPlugins(interceptor);
            //到哪里找xml文件
            ssfb.setMapperLocations(new PathMatchingResourcePatternResolver()
                    .getResources("classpath:/mapper/*Mapper.xml"));
            return ssfb.getObject();
        }
    }
    

    配置类的 @MapperScan(basePackages = "com.xhm.demo.api.mapper") 指定了 Mapper 接口的存放位置

     
    第一个 Bean 用于创建数据源,会根据相关依赖自动判断数据库类型@ConfigurationProperties(prefix = "spring.datasource") 会将以 spring.datasource 开头的属性配置赋值给数据源对象的对应属性

     
    第二个 Bean 用于注册 MyBatis-Plus 的相关插件(这里暂时只注册了 分页插件和 乐观锁插件)

     
    第三个 Bean 用于创建 SqlSessionFactory 的实例。 SqlSessionFactory 是一个接口,负责数据库Session(会话)的管理,数据库会话通俗点讲就是客户端与数据库的单次连接。其依赖下面的信息:

    • 数据源
    • MyBatis-Plus 插件
    • xml文件的存放位置(资源路径)

    三、验证集成

    下面是常见的三层架构的目录结构:

    api
    ├── controller
    ├── dto
    ├── entity
    ├── mapper
    └── service
    

    其中最主要的目录自上而下依次是:

    • controller :控制器目录(表示层)
    • service:服务类目录(业务逻辑层)
    • mapper:DAO目录(数据访问层)

    另外两个目录的作用:

    • entity:实体类目录
    • dto:数据传输对象(DTO)目录

    创建对应的表:

    create table t_card_puncher
    (
       id                   int(10) not null auto_increment,
       name                 varchar(15) comment '打卡人姓名',
       nick                 varchar(50) comment '打卡人昵称',
       avatar               varchar(100) comment '头像',
       status               smallint(2) default 10 comment '打卡人状态(10-已保存;20-已启用;0-已作废)',
       account_id           int(10) comment '对应账号id',
       remark               varchar(500) comment '备注',
       added_by             int(10) comment '新增人id',
       added_by_name        varchar(20) comment '新增人姓名',
       added_time           datetime default CURRENT_TIMESTAMP comment '新增时间',
       last_modified_by     int(10) comment '最后修改人id',
       last_modified_by_name varchar(20) comment '最后修改人姓名',
       last_modified_time   datetime default CURRENT_TIMESTAMP comment '最后修改时间',
       last_modified_ip     varchar(50) comment '最后修改IP',
       valid                smallint(1) default 1 comment '是否有效(1-有效;0-无效)',
       primary key (id)
    );
    
    alter table t_card_puncher comment '习惯打卡人表';
    

    依次在上面的目录中创建相关类,实现一个简单的接口。我们将按照从上而下的顺序创建相关的类。

    3.1 控制器

    代码清单3.1-1:

    // CardPuncherController.java
    
    @RestController
    @RequestMapping("/cardPuncher")
    public class CardPuncherController {
        @Resource
        private CardPuncherService service;
    
        @PostMapping("/queryById")
        public BaseResponse<CardPuncher> queryById(@RequestBody @Valid IdRequest request){
            CardPuncher dto = service.queryCardPuncherById(request.getId());
            return BaseResponse.ok(dto);
        }
    }
    

    控制器类依赖下面三个类:

    1. IdRequest (DTO)
    2. CardPuncher (实体类)
    3. CardPuncherService (服务类–接口)

    代码清单3.1-2:

    // IdRequest.java
    
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import lombok.Data;
    
    import java.io.Serializable;
    import javax.validation.constraints.NotNull;
    
    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    public class IdRequest implements Serializable {
    
        private static final long serialVersionUID = 4111263664475615283L;
        /**
         * id
         */
        @NotNull(message = "ID不能为空!")
        private Integer id;
    }
    

    3.2 服务类

    控制器一般不含任何的业务逻辑,它只是将请求委托给相关的服务类。并且为了保持依赖的松散,控制器是不直接依赖于具体类的,而是依赖于接口。

    代码清单3.2-1:

    //打卡人服务类 :CardPuncherService.java
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.xhm.demo.api.entity.CardPuncher;
    
    public interface CardPuncherService extends IService<CardPuncher> {
    
        /**
         * 根据id查询打卡人
         *
         * @param id 打卡人id
         * @return 打卡人DTO
         */
        CardPuncher queryCardPuncherById(Integer id);
    }
    

    代码清单3.2-2,就是接口的对应实现:

    // 打卡人服务实现类:CardPuncherServiceImpl.java
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.toolkit.Wrappers;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.xhm.demo.api.entity.CardPuncher;
    import com.xhm.demo.api.enums.ValidEnum;
    import com.xhm.demo.api.mapper.CardPuncherMapper;
    import com.xhm.demo.api.service.CardPuncherService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    
    @Service
    @Slf4j
    public class CardPuncherServiceImpl extends ServiceImpl<CardPuncherMapper, CardPuncher> implements CardPuncherService {
    
        @Override
        public CardPuncher queryCardPuncherById(Integer id) {
            LambdaQueryWrapper<CardPuncher> queryWrapper = Wrappers.lambdaQuery();
            queryWrapper.eq(CardPuncher::getValid, ValidEnum.VALID.getCode());
            queryWrapper.eq(CardPuncher::getId, id);
            return this.getOne(queryWrapper);
        }
    }
    

    服务实现类依赖下面的类:

    1. ServiceImpl(MyBatis-Plus内置的服务实现类)
    2. CardPuncherService (服务接口类)
    3. CardPuncherMapper(Mapper接口)
    4. CardPuncher
    5. Wrappers (条件构造器

    3.3 Mapper接口类

    Mapper接口类比较简单,因为我们没有自定义脚本,而是调用了 ServiceImpl.getOne 方法。所以 Mapper 接口类中是没有自己的方法的。

    代码清单3.3-1:

    // 打卡人 Mapper 接口类:CardPuncherMapper.java
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    
    public interface CardPuncherMapper extends BaseMapper<CardPuncher> {
    
    }
    

    3.4 实体类

    实体类和数据表的字段是一一对应的,所以其是一个比较重要的类。相关代码如下:

    @Data
    @EqualsAndHashCode(callSuper = true)
    @Accessors(chain = true)
    @TableName("t_card_puncher")
    public class CardPuncher extends BaseEntity {
        private static final long serialVersionUID = 1L;
    
        /**
         * 打卡人姓名
         */
        private String name;
    }
    

    其中需要重点强调的是:

    当数据库中table的名称和实体类名称不一致时,一定需要使用注解 @TableName 进行显示声明表的名称。
     
    否则会提示相关的表不存在(例子中默认认为表名是 card_puncher,而我们的表是有前缀 t_ 的)

    其中的注解 @Accessors(chain = true) 的作用如下:

    被该注解修饰的类,setters方法返回的该类的实例(即this),而不是void。所以可以链式地调用setters方法。

    链式调用代码如下:

    CardPuncher entity = new CardPuncher().setName("老书生");
    

    上述类之间的关系如下图所示:
    在这里插入图片描述

     

    3.4 不要忘记XML文件

    在讲自动配置类时,我们提到过需要指定xml文件的存放位置:

    "classpath:/mapper/*Mapper.xml"

    那么我们必须在mapper中创建对应的xml文件,否则就会报如下错误:

    org.springframework.beans.factory.BeanCreationException: 
    Error creating bean with name 'cardPuncherController'
    : Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException
    : Error creating bean with name 'cardPuncherServiceImpl'
    : Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException
    : Error creating bean with name 'cardPuncherMapper' defined in file
    ...
    nested exception is java.io.FileNotFoundException: class path resource [mapper/] cannot be resolved to URL because it does not exist
    

    主要错误信息:

    java.io.FileNotFoundException: class path resource [mapper/] cannot be resolved to URL because it does not exist

    XML文件(CardPuncherMapper.xml)内容如下:

    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.xhm.demo.api.mapper.CardPuncherMapper">
    
    mapper>
    

    启动Spring Boot 项目,观察控制台,发现多了MyBatis的banner。并且没有任何报错信息。
    在这里插入图片描述
    移步到端点,可以发现多一个db项:
    db
    表明项目中使用了数据库,并且数据库的类型是MySQL。

    3.5 发起请求

    使用 Postman 发起接口请求。
    在这里插入图片描述
    因为此时表中还没有数据,所以结果如下:
    在这里插入图片描述
    插入1条数据:

    insert into my_database.t_card_puncher(`name`,nick,added_by,added_by_name,last_modified_by,last_modified_by_name)
    values('老书生','leon',-1,'系统',-1,'系统');
    

    再次请求,结果如下:
    在这里插入图片描述
    可以成功查出数据。到这里 MyBatis 的集成已经初步成功了。

    之所以说初步成功,是因为还有一些问题需要解决。如上图中时间格式的问题,2024-06-21T09:31:00 这种时间格式有自己的专有名词:ISO日期时间格式 / ISO 8601

     
    还有一个问题:当Id为空时,没有返回期望的错误信息,而是报400错误:
    400

    四、技巧拓展

    4.1 如何打印sql语句?

    发送请求后,在项目的控制台没有出现期望的sql语句,那么该如何处理才能成功打印sql语句呢?

    其实方法也很简单,在 application.yml 中添加如下配置:

    # 查看sql
    logging:
      level:
        com.xhm.demo.api.mapper: debug
    

    其中的 com.xhm.demo.api.mapper 是 Mapper 接口所在的包名。设置其日志级别为debug

    打印的sql脚本如下:

    2024-06-23 00:26:49.147 DEBUG 40300 --- [nio-8080-exec-6] c.x.d.a.m.CardPuncherMapper.selectOne    : ==>  Preparing: SELECT id,name,added_by,added_by_name,added_time,last_modified_by,last_modified_by_name,last_modified_time,last_modified_ip,valid FROM t_card_puncher WHERE (valid = ? AND id = ?)
    2024-06-23 00:26:49.188 DEBUG 40300 --- [nio-8080-exec-6] c.x.d.a.m.CardPuncherMapper.selectOne    : ==> Parameters: 1(Integer), 1(Integer)
    2024-06-23 00:26:49.226 DEBUG 40300 --- [nio-8080-exec-6] c.x.d.a.m.CardPuncherMapper.selectOne    : <==      Total: 1
    

     
    脚本是出现了,但是似乎不那么完美。如果查询条件比较多,拼接起来还是比较麻烦的。有无其他办法能够查看拼接好的脚本呢?

    可以试试一款IDEA插件:MyBatis Log Free
    在这里插入图片描述
    借助这款插件,可以查看完整的sql语句:
    在这里插入图片描述
    如果是第一次使用该插件,可以通过如下方式打开:

    【Tools】-> 【MyBatis Log Plugin】

    4.2 如何对参数增加非空验证?

    非空校验不生效,有两种情况:

    1. 空条件直接传到sql中
    2. 接口返回400错误,控制台有 WARN 日志

    第一种情况

    如果 @NotNull注解在请求类的属性 (如IdRequest类)上。检查Controller的方法的入参是否遗漏 @Valid 注解。
    在这里插入图片描述
    如果已经增加了注解,非空校验依然没有生效,则有可能是maven依赖不完整,可能 pom 中只增加了如下依赖:

    <dependency>
        <groupId>javax.validationgroupId>
        <artifactId>validation-apiartifactId>
    dependency>
    

    有两种解决方法:

    1. 增加 hibernate-validator 依赖
      validation-api 中只定义了相关注解,具体的校验逻辑在 hibernate-validator 中。所以需要添加如下依赖:
     
     <dependency>
         <groupId>org.hibernate.validatorgroupId>
         <artifactId>hibernate-validatorartifactId>
     dependency>
    
    1. 去除 validation-api 依赖,以下面的依赖进行替换:
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-validationartifactId>
    dependency>
    

    第二种情况

    因为没有对 MethodArgumentNotValidException 异常进行处理而导致校验信息没有返回。具体的现象是响应报文的结果如下:
    异常02

    所以需要增加 统一异常处理类 。相关代码如下:

    // exception/CommonExceptionHandler.java
    
    @Slf4j
    @ControllerAdvice
    public class CommonExceptionHandler {
        @ExceptionHandler({MethodArgumentNotValidException.class})
        @ResponseBody
        public BaseResponse<?> bindMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
            String traceId = MDC.get("traceId");
            StringBuilder errorMessage = new StringBuilder();
            List<FieldError> errors = ex.getBindingResult().getFieldErrors();
            for (FieldError error : errors) {
                //只取错误信息
                errorMessage.append(error.getDefaultMessage()).append(";");
            }
    
            errorMessage.deleteCharAt(errorMessage.length() - 1);
            log.error(String.format("traceId: %s, Exception: [%s] %s, request error, parameters invalid:%s", traceId, ex.getParameter().getParameterName(), ex, errorMessage));
            return BaseResponse.error(BaseResultCodeEnum.ERROR.getCode(), errorMessage.toString());
        }
    }
    

    BaseResponse.java代码如下:

    @Data
    public class BaseResponse<T> implements Serializable {
    
    
        private static final long serialVersionUID = -5330932746124338859L;
    
        /**
         * 响应状态码
         */
        private String code;
    
        /**
         * 响应消息
         */
        private String message;
    
        /**
         * 响应数据
         */
        private T data;
    
        public static <K> BaseResponse<K> ok() {
            return BaseResponse.result(BaseResultCodeEnum.SUCCESS.getCode(), BaseResultCodeEnum.SUCCESS.getMessage());
        }
    
        public static <K> BaseResponse<K> ok(K data) {
            BaseResponse<K> response = BaseResponse.ok();
            response.setData(data);
            return response;
        }
    
        public static <K> BaseResponse<K> error(String message) {
            return BaseResponse.result(BaseResultCodeEnum.ERROR.getCode(), message);
        }
    
        public static <K> BaseResponse<K> error(String code, String message) {
            return BaseResponse.result(code, message);
        }
    
        public static <K> BaseResponse<K> result(String code, String message) {
            BaseResponse<K> response = new BaseResponse<>();
            response.setCode(code);
            response.setMessage(message);
            return response;
        }
    }
    
    

    4.3 如何查看 Maven 依赖?

    有两种查看 Maven 依赖的方式:

    1. mvn dependency:tree命令
    2. 使用Idea的 Dependency Analyzer

    其中第二种方式依赖 IDEA的插件: Maven Helper

    第一种方式的演示:

    在终端输入如下命令:

    mvn dependency:tree
    

     
    输出结果如下:

    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building demo-api 0.0.1-SNAPSHOT
    [INFO] ------------------------------------------------------------------------
    [INFO]
    [INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) @ demo-api ---
    [INFO] com.xhm:demo-api:jar:0.0.1-SNAPSHOT
    ...
    [INFO] +- org.projectlombok:lombok:jar:1.18.20:compile (optional)
    ...
    [INFO] +- mysql:mysql-connector-java:jar:8.0.23:runtime
    [INFO] \- org.springframework.boot:spring-boot-starter-validation:jar:2.3.10.RELEASE:compile
    [INFO]    +- org.glassfish:jakarta.el:jar:3.0.3:compile
    [INFO]    \- org.hibernate.validator:hibernate-validator:jar:6.1.7.Final:compile
    [INFO]       +- jakarta.validation:jakarta.validation-api:jar:2.0.2:compile
    [INFO]       +- org.jboss.logging:jboss-logging:jar:3.4.1.Final:compile
    [INFO]       \- com.fasterxml:classmate:jar:1.5.1:compile
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 2.018 s
    [INFO] Finished at: 2024-06-25T16:12:36+08:00
    [INFO] Final Memory: 26M/304M
    [INFO] ------------------------------------------------------------------------
    

    第二种方式的演示:

    打开pom文件,从 Text 模式切换到 Dependency Analyzer 模式:

    • 选择 All Dependencies as Tree ,点击 “Refresh UI” 按钮
    • 收缩树形结构(全部)
    • 展开 spring-boot-starter-validation ,可以看到其依赖项。

    如图所示,其依赖于 hibernate-validator,而 hibernate-validator 又依赖于 jakarta.validation-api (替换validation-api 包含注解 javax.validation.constraints.NotNull

    在这里插入图片描述

    4.3 如何解决时间格式问题?

    前面我提到过接口返回的日期时间格式默认是:ISO日期时间格式。而很多时候我们需要指定的日期时间格式。如:

    • 当提到出生日期是,时间精度至少需要精确到天,在中国一般的日期格式是:2024-06-20
    • 但是当我们需要获知订单的创建时间时,可能需要这样的时间格式:2024-06-21 08:30:50

    要解决这个问题,其实也比较简单,可以使用 jackson-annotation 包中的注解 @JsonFormat 对日期时间格式进行自定义。

    下面使用该注解对 BaseEntity 进行功能增强:

    @Data
    public class BaseEntity implements Serializable {
    ...
    	/**
    	 * 新增时间
    	 */
    	@JsonFormat(pattern = "yyyy-MM-dd")
    	private LocalDateTime addedTime;
    	...
    	
        /**
         * 最后修改时间
         */
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime lastModifiedTime;
    }
    

    这里只是为了演示的方便,生产代码不建议这样写

    直接将实体类暴露给最终用户不是一个好的实践,因为一旦字段发生变动,就需要客户代码进行相应的调整
    普遍的做法是:引入DTO作为接口的数据载体。所以 @JsonFormat 注解也是打在DTO类上。

    4.4 如何快速增加 serialVersionUID 属性?

    对于可序列化的类,一般强烈建议显式声明 serialVersionUID 值。因为默认计算得到的 serialVersionUID 值可能会引发 InvalidClassException 。那么有没有什么快速的方法去完成这件事情呢?

    如果你使用的是 IDEA ,可以参考下面的方法:

    • 打开 Settings 窗口,输入搜索词: serial
    • 在【Editor】-【Inspections】中找到:【Java】- 【Serializatioin issues】。
    • 然后勾选其中的 Serializable class without serialVersionUID

    在这里插入图片描述
    上面设置的作用是:如果一个类实现了Serializable接口,但是没有声明 serialVersionUID 值,就会告警。
    xx
    F2 定位到告警信息后,再组合按键 Alt + Enter ,弹出上图的信息后,再按一次 Enter 后,就可以快速插入 serialVersionUID 值了。使用该方式生成的 serialVersionUID 能很好地避免重复的问题。

    五、总 结

    本文详细讲述了如何在Spring Boot 项目中集成 MyBatis-Plus,其中的设置和依赖都遵循最小功能集的原则。

    增加集成相关的配置和依赖后,我们又创建了一个简单的接口,用于验证本次集成是否成功。其中谈到了SSM项目中基本的项目结构,相关代码都是从生产代码中抽象出来的,具有很强的参考意义。文中还以类图的形式给出了各个类之间的关系,可以帮助读者更好地理解代码结构。

    接口测试时,我们使用了post工具,Postman是其中比较常见的一款。通过接口可以成功返回数据,但是数据的格式有一些问题。文章对这些问题的解决进行了细致地阐述。

    最后还分享了实际开发中可能会用到的一些小技巧,希望可以启动抛砖引玉的作用。有交流意愿的小伙伴可以在评论区进行留言。

     
     
     
     
     


    参考资料:

  • 相关阅读:
    代码随想录刷题day52 300.最长递增子序列;674. 最长连续递增序列;718. 最长重复子数组
    【接口测试】接口测试内容
    Java多线程/spring boot多线程
    如何利用CANN DVPP进行图片的等比例缩放?
    自动补全、
    如何评价一门编程语言的难易程度?
    Solon2 项目整合 Nacos 配置中心
    快速了解什么是:微服务
    webpack【实用教程】
    Java并发编程—java内存模型2
  • 原文地址:https://blog.csdn.net/xu7382/article/details/139811192