从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置

从零开始SpringBoot项目搭建(二):Swagger接口文档配置以及其他全局配置

  • 前言
  • 一、Swagger接口文档工具集成使用
    • 1.1 概述
    • 1.2 导入依赖
    • 1.3 创建配置类Knife4jConfiguration
    • 1.4 Swagger常用注解
    • 1.5 自定义接口返回数据结构
      • 1.5.1 状态码枚举类
      • 1.5.2 通用接口自定义返回数据格式
      • 1.5.3 封装自定义异常类
    • 1.6 测试
      • 1.6.1 准备工作
      • 1.6.2 定义响应实体类
      • 1.6.3 Service核心业务实现
      • 1.6.4 Controller创建新接口
      • 1.6.5 测试结果
  • 二、MybatisPlus全局配置
    • 2.1 MybatisPlus配置类
    • 2.2 全局逻辑删除标志位
    • 2.3 业务实现自动填充基本字段信息
      • 2.3.1 构建所有实体类的基类
      • 2.3.2 实现元数据对象处理器接口
      • 2.3.3 测试

有关SpringBoot项目的搭建在上一篇文章中已经有过详细说明,有需要的小伙伴请查看:从零开始SpringBoot项目搭建(一)

前言

接上文,搭建好最基础的SpringBoot项目后。为了方便开发,我们需要:

  • 配置swagger文档用于接口文档自动生成。
  • 对于接口的返回数据,进行数据结构的统一化以及返回信息的规范化。
  • 增加MybatisPlus配置类,扩展自定义mapper方法
  • 配置MybatisPlus全局处理:新增数据、更新数据相关字段自动填充以及逻辑删除标记自动处理

一、Swagger接口文档工具集成使用

1.1 概述

swagger是比较流行的实时生成接口文档工具,这里我使用swagger2版本,并使用knife4j对接口文档进行美化加强。在前后端分离开发模式中,接口文档起到了不可忽视的作用,它相当于是前后端的约定协议,前端会根据后端出的接口文档进行开发。开发完成后,前后端再进行联调测试。

1.2 导入依赖

pom.xml文件中导入相关依赖:swagger2以及knife4j进行增强,请注意两者之间的版本兼容问题。一般swagger2.x的话,knife4j版本为2.0.0都会兼容,记得在swagger的两个依赖中都要排除swagger-modelsswagger-annotations,否则会造成依赖冲突。

<properties>
	<springfox-swagger2.version>2.9.2springfox-swagger2.version>
	<springfox-swagger-ui.version>2.9.2springfox-swagger-ui.version>
	<knife4j-spring-boot-starter.version>2.0.0knife4j-spring-boot-starter.version>
properties>
    
<dependency>
	<groupId>io.springfoxgroupId>
	<artifactId>springfox-swagger2artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.swaggergroupId>
			<artifactId>swagger-modelsartifactId>
		exclusion>
		<exclusion>
			<groupId>io.swaggergroupId>
			<artifactId>swagger-annotationsartifactId>
		exclusion>
	exclusions>
	<version>${springfox-swagger2.version}version>
dependency>

<dependency>
	<groupId>io.springfoxgroupId>
	<artifactId>springfox-swagger-uiartifactId>
	<exclusions>
		<exclusion>
			<groupId>io.swaggergroupId>
			<artifactId>swagger-modelsartifactId>
		exclusion>
		<exclusion>
			<groupId>io.swaggergroupId>
			<artifactId>swagger-annotationsartifactId>
		exclusion>
	exclusions>
	<version>${springfox-swagger-ui.version}version>
dependency>

<dependency>
	<groupId>com.github.xiaoymingroupId>
	<artifactId>knife4j-spring-boot-starterartifactId>
	<version>${knife4j-spring-boot-starter.version}version>
dependency>

1.3 创建配置类Knife4jConfiguration

  • application.properties配置文件中添加一条配置参数用于控制是否生成swagger文档:
### swagger2
swagger.enable=true
  • 创建一个配置类到config包下:注意需要添加三个注解@Configuration@EnableSwagger2@EnableKnife4j

请注意basePackage中是扫描controller请求路径的包所在位置,这里指定到core就好;指定PathSelectors.any()表示所有路径,这样core下的所有controller请求路径都会被扫描到并生成接口文档。

package com.study.core.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
@EnableKnife4j
public class Knife4jConfiguration {

    @Value("${swagger.enable}")
    private Boolean swaggerEnable;

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                                                      .enable(swaggerEnable)
                                                      .select()
                                                      .apis(RequestHandlerSelectors.basePackage("com.study.core"))
                                                      .paths(PathSelectors.any())
                                                      .build();
    }

    @Bean
    public ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("测试平台-测试平台接口文档")
                                   .version("v1.0.0")
                                   .description("测试平台接口文档")
                                   .build();
    }
}

配置完成,启动项目。在浏览器输入项目地址:http://localhost:8080/doc.html就能访问接口文档。如下图所示:

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第1张图片

1.4 Swagger常用注解

  • @Api:常用于Controller层,标记在类声明上,表示一个功能模块。使用属性tags为功能添加模块名称以及描述信息。@Api(tags = "xx模块-xx接口")
  • @ApiOperation:常用于Controller层,标记在方法声明上,表示一个接口。使用属性value为接口添加接口名称。@ApiOperation(value = "接口名")
  • @ApiModelProperty:常用于请求和响应的实体类中,表示传输对象中的一个字段。使用属性value为字段添加名称等信息。@ApiModelProperty(value = "字段名")

1.5 自定义接口返回数据结构

通常情况下,接口返回的数据结构分三个部分:状态码返回信息返回数据

1.5.1 状态码枚举类

package com.study.core.modular.common.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum HttpResultCode {
    //0~999
    SUCCESS(0, "成功"),
    SYSTEM_ERROR(999, "系统错误"),

    //1000~1999 表示参数错误
    PARAM_VALIDATED_FAILED(1000, "参数校验失败"),
    PARAM_EXCEPTION(1001, "参数异常"),
    TOKEN_VALIDATED_FAILED(1002, "Token校验失败"),
    TOKEN_EXPIRED(1003, "Token已过期"),

    //2000~2999 表示用户错误
    USER_ACCOUNT_OR_PASSWORD_VALIDATE_FAILED(2000, "用户账号或密码不正确"),
    USER_ACCOUNT_NOT_EXIST(2001, "用户账号不存在"),

    //3000~3999 表示基础业务错误
    DATA_NOT_EXISTS(3000, "数据不存在"),
    DATA_EXISTED(3001, "数据已存在"),
    BIZ_EXCEPTION(3002, "业务异常"),
    BIZ_DATA_EXCEPTION(3003, "业务数据异常")
    ;

    private final Integer code;
    private final String message;
}

1.5.2 通用接口自定义返回数据格式

需要注意的是,我们需要对该实体类进行序列化操作:实现Serializable接口。在完成该类的所有工作之后,不要忘了最后一步需要使其生成serialVersionUID。可以在IDEA中安装插件GenerateSerialVersionUID进行自动生成。

package com.study.core.modular.common.model;

import com.study.core.modular.common.enums.HttpResultCode;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult<T> implements Serializable {

    private static final long serialVersionUID = 9155082035856162703L;
    
    @ApiModelProperty(value = "状态码")
    private Integer code;

    @ApiModelProperty(value = "信息")
    private String message;

    @ApiModelProperty(value = "数据")
    private T data;

    public static <T> HttpResult<T> success() {
        return HttpResult.<T>builder()
                         .code(HttpResultCode.SUCCESS.getCode())
                         .message(HttpResultCode.SUCCESS.getMessage())
                         .build();
    }

    public static <T> HttpResult<T> success(T data) {
        return HttpResult.<T>builder()
                         .code(HttpResultCode.SUCCESS.getCode())
                         .message(HttpResultCode.SUCCESS.getMessage())
                         .data(data)
                         .build();
    }


    public static <T> HttpResult<T> failure(HttpResultCode httpResultCode) {
        return HttpResult.<T>builder()
                         .code(httpResultCode.getCode())
                         .message(httpResultCode.getMessage())
                         .build();
    }

    public static <T> HttpResult<T> failure(HttpResultCode httpResultCode, String message) {
        return HttpResult.<T>builder()
                         .code(httpResultCode.getCode())
                         .message(message)
                         .build();
    }

}

1.5.3 封装自定义异常类

根据状态码枚举自定义异常类,继承运行时异常RuntimeException,通样需要序列化,不要忘了最后生成serialVersionUID

package com.study.core.exception;

import com.study.core.modular.common.enums.HttpResultCode;
import lombok.Data;

@Data
public class BizException extends RuntimeException {

    private static final long serialVersionUID = 6169725979353182781L;
    
    private HttpResultCode httpResultCode;

    public BizException(HttpResultCode httpResultCode) {
        super(httpResultCode.getMessage());
        this.httpResultCode = httpResultCode;
    }

    public BizException(HttpResultCode httpResultCode, String message) {
        super(message);
        this.httpResultCode = httpResultCode;
    }

}

1.6 测试

1.6.1 准备工作

  • Hutool工具类库相关
    我在这里推荐一个非常好用的工具类库:Hutool工具类。此工具类库的妙用很多,在整个项目都会大量使用该类库中的方法。涵盖了各种工具方法:时间工具类DateUtil、字符串工具类StrUtil、集合工具类CollUtil、数组工具类ArrayUtil…具体的内容小伙伴们自行搜索相关文档进行学习,Maven坐标如下:
<dependencies>
	<dependency>
		<groupId>cn.hutoolgroupId>
        <artifactId>hutool-allartifactId>
        <version>${hutool-all.version}version>
	dependency>
dependencies>
  • 封装一些常用的方法,便于开发
    对于对象的转换操作,我主要是对springframwork中的BeanUtils进行了二次封装,将两个实体类相同名称属性的值进行复制转换:
    • 转换对象与被转换对象中的字段需要名称相同才会被复制其值
    • 假如被转换对象值为null则会被忽略
    • 可以手动指定需要忽略的字段

以下是具体实现,定义为JavaBeanUtils

package com.study.core.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.beans.PropertyDescriptor;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class JavaBeanUtils {

    public static <T> T map(Object source, Class<T> target, String... ignoreProperties) {
        try {
            T result = target.newInstance();
            copyNonNullProperties(source, result, ignoreProperties);
            return result;
        } catch (Exception e) {
            throw new BizException(HttpResultCode.BIZ_EXCEPTION, "对象转换异常!");
        }
    }

    public static void map(Object source, Object target, String... ignoreProperties) {
        copyNonNullProperties(source, target, ignoreProperties);
    }

    public static <T> List<T> mapList(List<?> sourceList, Class<T> target) {
        if (CollUtil.isEmpty(sourceList)) {
            return Collections.emptyList();
        }

        List<T> resultList = CollUtil.newArrayList();
        sourceList.forEach(src -> resultList.add(map(src, target)));
        return resultList;
    }

    private static void copyNonNullProperties(Object source, Object target, String... ignoreProperties) {
        BeanUtils.copyProperties(source, target, ArrayUtil.addAll(ignoreProperties, getNullPropertyNames(source)));
    }

    private static String[] getNullPropertyNames(Object source) {
        final BeanWrapper src = new BeanWrapperImpl(source);
        PropertyDescriptor[] pds = src.getPropertyDescriptors();

        Set<String> emptyNames = CollUtil.newHashSet();
        for (PropertyDescriptor pd : pds) {
            Object srcPropertyValue = src.getPropertyValue(pd.getName());
            if (srcPropertyValue == null) {
                emptyNames.add(pd.getName());
            }
        }
        return ArrayUtil.toArray(emptyNames, String.class);
    }

}

1.6.2 定义响应实体类

一般情况下我们不会直接将数据库中的实体类返回给前端,通常会返回一个封装过的响应实体类。service层是核心业务代码,处理后返回实体类(或实体类集合)后,再在controller层中对返回的结果进行处理,转换并返回对应的响应实体类(或响应实体类集合)。

此方式其实还挺契合MybatisPlus的,MybatisPlus是单表查询,数据库中需要构建好各表之间的关联关系。service层只查单表,后续返回的数据可在controller层中根据外键id去反查对应的关系表,然后填充与外键id有关系的值为null的数据。

在相应模块的model包下新建一个response包,所有响应实体类都放置在此包中,命名与业务相关,通常以xxxResponse结尾。如下,创建一个DemoResponse

package com.study.core.modular.demo.model.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoResponse {

    @ApiModelProperty(value = "主键id")
    private String id;

    @ApiModelProperty(value = "名称")
    private String name;

    @ApiModelProperty(value = "手机号")
    private String phone;

    @ApiModelProperty(value = "创建时间")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    @ApiModelProperty(value = "更新时间")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

}

1.6.3 Service核心业务实现

package com.study.core.modular.demo.service;

import cn.hutool.core.util.ObjectUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
@Slf4j
public class DemoService {

    private final DemoMapper demoMapper;

    public Demo get(String id) {
        Demo demo = demoMapper.selectById(id);
        if (ObjectUtil.isNull(demo)) {
            throw new BizException(HttpResultCode.DATA_NOT_EXISTS);
        }
        return demo;
    }
}

1.6.4 Controller创建新接口

package com.study.core.modular.demo.controller;

import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.response.DemoResponse;
import com.study.core.modular.demo.service.DemoService;
import com.study.core.util.JavaBeanUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.NotBlank;

@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {

    private final DemoService demoService;

    @ApiOperation(value = "查询数据详情Demo接口")
    @GetMapping("/get")
    public HttpResult<DemoResponse> get(@RequestParam @NotBlank String id) {
        return HttpResult.success(JavaBeanUtils.map(demoService.get(id), DemoResponse.class));
    }

}

1.6.5 测试结果

结果如下图显示,很完美!

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第2张图片

二、MybatisPlus全局配置

2.1 MybatisPlus配置类

  • MybatisPlusBaseMapper中提供了很多方法,但有时候还是不能满足所有的业务场景,所以需要在此基础上做扩充。可以在配置类中自定义类继承DefaultSqlInjector,重写getMethodList方法,进行自定义方法扩展。
  • 对于需要分页查询的业务场景,需要增加相应的拦截器。如果有需要还可以添加乐观锁拦截器。
package com.study.core.config;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
import com.baomidou.mybatisplus.extension.injector.methods.LogicDeleteByIdWithFill;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

@Configuration
public class MyBatisPlusConfig {

    @Bean
    public EasySqlInjector easySqlInjector() {
        return new EasySqlInjector();
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页查询拦截器
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        //乐观锁拦截器
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

    private class EasySqlInjector extends DefaultSqlInjector {

        @Override
        public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
            List<AbstractMethod> methodList = super.getMethodList(mapperClass);
            methodList.add(new LogicDeleteByIdWithFill());
            methodList.add(new LogicBatchDeleteWithFill());
            methodList.add(new InsertBatchSomeColumn());
            return methodList;
        }
    }

    private class LogicBatchDeleteWithFill extends AbstractMethod {

        /**
         * mapper 对应的方法名
         */
        private static final String MAPPER_METHOD = "deleteBatchWithFill";

        @Override
        public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
            String sql;
            SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE;
            if (tableInfo.isWithLogicDelete()) {
                List<TableFieldInfo> fieldInfos = tableInfo.getFieldList().stream()
                                                           .filter(i -> i.getFieldFill() == FieldFill.UPDATE || i.getFieldFill() == FieldFill.INSERT_UPDATE)
                                                           .collect(toList());
                if (CollectionUtils.isNotEmpty(fieldInfos)) {
                    String sqlSet = "SET " + fieldInfos.stream().map(i -> i.getSqlSet(ENTITY_DOT)).collect(joining(EMPTY))
                            + tableInfo.getLogicDeleteSql(false, false);
                    sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlSet,
                            sqlWhereEntityWrapper(true, tableInfo), "");
                } else {
                    sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
                            sqlWhereEntityWrapper(true, tableInfo), "");
                }
                SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
                return this.addUpdateMappedStatement(mapperClass, modelClass, MAPPER_METHOD, sqlSource);
            } else {
                sqlMethod = SqlMethod.DELETE;
                sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlWhereEntityWrapper(true, tableInfo), "");
                SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
                return this.addDeleteMappedStatement(mapperClass, MAPPER_METHOD, sqlSource);
            }
        }
    }

}

MybatisPlus配置类中扩展方法,有MP自身的扩展方法还有我们自己添加的自定义扩展方法:

  • LogicDeleteByIdWithFill类中的deleteByIdWithFill()方法:根据ID逻辑删除一条记录,传入参数是一个实体对象,id不能为空
  • LogicBatchDeleteWithFill类中的deleteBatchWithFill()方法:根据Wrapper条件构造器构造条件进行批量删除
  • InsertBatchSomeColumn类中的insertBatchSomeColumn()方法:批量插入数据

之后继承MybatisPlusBaseMapper接口,自定义一个扩展方法新接口MyBaseMapper,之后实体类的mapper都可以继承此接口,可以在对应的业务层进行批量删除、批量插入等扩展方法操作。新接口如下:

package com.study.core.modular.common.dao;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;

import java.util.Collection;

public interface MyBaseMapper<T> extends BaseMapper<T> {

    int deleteByIdWithFill(T entity);

    int deleteBatchWithFill(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> wrapper);

    Integer insertBatchSomeColumn(Collection<T> entityList);

}

2.2 全局逻辑删除标志位

application.properties配置文件中配置全局逻辑删除标志位,此配置项可取代实体类中的@TableLogic注解,配置一次则全部实体类中只要存在is_del字段,调用相应的查询、删除方法时都会加上对应的sql语句。(where is_del = '0' 或 set is_del = '1')

# mybatisplus
mybatis-plus.global-config.db-config.logic-delete-field=is_del
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.global-config.db-config.logic-delete-value=1

2.3 业务实现自动填充基本字段信息

所有实体类数据,都存在几个相同的字段:id创建人id创建时间更新人id更新时间以及逻辑删除标志位,有些还会有组织机构id等等。为了不想每次在读写数据库的时候,都去重复的更新相应的字段,我们可以做全局统一处理。

2.3.1 构建所有实体类的基类

首先我们要统一数据库表结构内的基础字段:

  • id:唯一标识
  • create_id:创建人id
  • create_time:创建时间
  • update_id:更新人id
  • update_time:更新时间
  • is_del:逻辑删除 0-否 1-是

然后把这几个字段抽出来定义成一个基类,所有的实体类都会继承基类:

id后续可以采用雪花算法或者号段模式去自动生成,他一定是新增数据时自动生成的默认值,现在暂时先不谈。
create_idcreate_time只有在新增数据时才会自动填充,所以使用INSERT
update_idupdate_time新增数据与更新数据时都要读写数据库,所以使用INSERT_UPDATE
is_del只有在删除时自动更新为1,新增数据时默认为0,所以只需要INSERT

package com.study.core.modular.common.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class BaseDO {

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    private String id;
    public static final String ID = "id";

    @ApiModelProperty(value = "创建人id")
    @TableField(value = "create_id", fill = FieldFill.INSERT)
    private String createId;
    public static final String CREATE_ID = "createId";

    @ApiModelProperty(value = "创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    public static final String CREATE_TIME = "createTime";

    @ApiModelProperty(value = "更新人id")
    @TableField(value = "update_id", fill = FieldFill.INSERT_UPDATE)
    private String updateId;
    public static final String UPDATE_ID = "updateId";

    @ApiModelProperty(value = "更新时间")
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    public static final String UPDATE_TIME = "updateTime";

    @ApiModelProperty(value = "逻辑删除 0-否 1-是")
    @TableField(value = "is_del", fill = FieldFill.INSERT)
    @TableLogic
    private Integer isDel;
    public static final String IS_DEL = "isDel";

}

创建基类后,修改Demo实体类,继承基类:

package com.study.core.modular.demo.model.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.study.core.modular.common.model.entity.BaseDO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;

@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tbl_demo")
public class Demo extends BaseDO {

    /**
     * 姓名
     */
    @TableField(value = "`name`")
    @ApiModelProperty(value = "姓名")
    private String name;

    /**
     * 手机号
     */
    @TableField(value = "phone")
    @ApiModelProperty(value = "手机号")
    private String phone;

}

2.3.2 实现元数据对象处理器接口

自定义一个处理类,实现元数据对象处理接口MetaObjectHandler。为新增数据与更新数据操作时,进行自动填充基本字段信息操作。
重写MetaObjectHandler的两个方法:

  • insertFill:新增数据自动填充逻辑处理
  • updateFill:更新数据自动填充逻辑处理

具体实现如下:

package com.study.core.handler;

import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.study.core.modular.common.enums.YesOrNoEnum;
import com.study.core.modular.common.model.entity.BaseDO;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class SetBasicInfoHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        Object o = metaObject.getOriginalObject();
        if (o instanceof BaseDO) {
            BaseDO baseDO = (BaseDO) o;
            //TODO 通过上下文获取当前登录用户信息后再填充创建人id或更新人id
            if (baseDO.getCreateTime() == null) {
                baseDO.setCreateTime(DateUtil.date());
            }
            if (baseDO.getUpdateTime() == null) {
                baseDO.setUpdateTime(DateUtil.date());
            }
            baseDO.setIsDel(YesOrNoEnum.N.getCode());
        } else {
            strictInsertFill(metaObject, BaseDO.CREATE_TIME, Date.class, DateUtil.date());
            strictInsertFill(metaObject, BaseDO.UPDATE_TIME, Date.class, DateUtil.date());
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        Object o = metaObject.getOriginalObject();
        if (o instanceof BaseDO) {
            BaseDO baseDO = (BaseDO) o;
            //TODO 通过上下文获取当前登录用户信息后再填充更新人id
            baseDO.setUpdateTime(DateUtil.date());
        } else {
            strictUpdateFill(metaObject, BaseDO.UPDATE_TIME, Date.class, DateUtil.date());
        }
    }
}

2.3.3 测试

(1)新增数据测试,观察是否自动填充create_time、update_time、is_del这三个字段

先定义请求实体类,新增数据和编辑数据两个接口的请求实体类可以是同一个,不同的校验规则可以通过校验组进行校验区分。在model包下新建request包,其中创建请求实体类DemoRequest

package com.study.core.modular.demo.model.request;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoRequest {

    @ApiModelProperty(value = "id")
    @NotBlank(message = "id不能为空", groups = { DemoRequest.Edit.class })
    private String id;

    @ApiModelProperty(value = "名称")
    @NotBlank(message = "名称不能为空")
    private String name;

    @ApiModelProperty(value = "手机号")
    @NotBlank(message = "手机号不能为空")
    private String phone;

    /**
     * 修改校验组
     */
    public interface Edit {
    }

}

编写核心业务逻辑

package com.study.core.modular.demo.service;

import com.study.core.modular.common.model.entity.BaseDO;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.util.JavaBeanUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@AllArgsConstructor
@Slf4j
public class DemoService {

    private final DemoMapper demoMapper;

    @Transactional(rollbackFor = Exception.class)
    public void add(DemoRequest request) {
        Demo demo = JavaBeanUtils.map(request, Demo.class, BaseDO.ID);
        demoMapper.insert(demo);
    }

}

创建新接口:新增数据接口

package com.study.core.modular.demo.controller;

import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {

    private final DemoService demoService;

    @ApiOperation(value = "新增数据Demo接口")
    @PostMapping("/add")
    public HttpResult<Void> add(@RequestBody @Validated DemoRequest request) {
        demoService.add(request);
        return HttpResult.success();
    }

}

swagger上测试接口,输入测试实例,发送请求,返回数据库实际结果:可以看到创建时间和更新时间都是自动记录的新增数据时的当前时间,逻辑删除标志位默认为否(0)。

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第3张图片
在这里插入图片描述

(2)编辑数据测试,观察update_time字段是否自动更新

同样使用新增数据时的请求实体类,开始编写核心业务逻辑

package com.study.core.modular.demo.service;

import cn.hutool.core.util.ObjectUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.util.JavaBeanUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@AllArgsConstructor
@Slf4j
public class DemoService {

    private final DemoMapper demoMapper;

    @Transactional(rollbackFor = Exception.class)
    public void edit(DemoRequest request) {
        Demo demo = demoMapper.selectById(request.getId());
        if (ObjectUtil.isNull(demo)) {
            throw new BizException(HttpResultCode.DATA_NOT_EXISTS);
        }
        JavaBeanUtils.map(request, demo);
        demoMapper.updateById(demo);
    }

}

创建新接口:编辑数据接口

package com.study.core.modular.demo.controller;

import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.request.DemoRequest;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {

    private final DemoService demoService;
    
    @ApiOperation(value = "编辑数据Demo接口")
    @PostMapping("/edit")
    public HttpResult<Void> edit(@RequestBody @Validated(DemoRequest.Edit.class) DemoRequest request) {
        demoService.edit(request);
        return HttpResult.success();
    }

}

swagger上测试接口,输入测试实例,发送请求,返回数据库实际结果:可以看到更新时间已被更改。

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第4张图片
在这里插入图片描述

(3)删除数据测试,观察is_del字段是否自动更新成1

首先定义一个删除数据接口的请求实体类(在这里提一下,一般写接口都是POST,读接口都是GET,POST接口中的请求参数都是放在请求体中的,一般需要对象进行包装,更方便)

package com.study.core.modular.demo.model.request;

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoDeleteRequest {
    
    @ApiModelProperty(value = "id")
    @NotBlank(message = "id不能为空")
    private String id;
    
}

编写核心业务

package com.study.core.modular.demo.service;

import cn.hutool.core.util.ObjectUtil;
import com.study.core.exception.BizException;
import com.study.core.modular.common.enums.HttpResultCode;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import com.study.core.modular.demo.model.request.DemoDeleteRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@AllArgsConstructor
@Slf4j
public class DemoService {

    private final DemoMapper demoMapper;

    @Transactional(rollbackFor = Exception.class)
    public void delete(DemoDeleteRequest request) {
        Demo demo = demoMapper.selectById(request.getId());
        if (ObjectUtil.isNull(demo)) {
            throw new BizException(HttpResultCode.DATA_NOT_EXISTS);
        }
        demoMapper.deleteByIdWithFill(demo);
    }

}

创建新接口:根据id删除数据

package com.study.core.modular.demo.controller;

import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.model.request.DemoDeleteRequest;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {

    private final DemoService demoService;

    @ApiOperation(value = "删除数据Demo接口")
    @PostMapping("/delete")
    public HttpResult<Void> delete(@RequestBody @Validated DemoDeleteRequest request) {
        demoService.delete(request);
        return HttpResult.success();
    }

}

swagger上测试接口,输入测试实例,发送请求,返回数据库实际结果:可以看到逻辑删除标志位已被更改为1,同时更新时间字段也被修改更新了。

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第5张图片
在这里插入图片描述

(4)修改批量删除数据接口,根据id删除单条记录改为批量删除指定记录,使用自定义扩展方法deleteBatchWithFill()

修改控制层接口参数

package com.study.core.modular.demo.controller;

import com.study.core.modular.common.model.HttpResult;
import com.study.core.modular.demo.service.DemoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.NotEmpty;
import java.util.List;

@Api(tags = "Demo模块-数据管理Demo接口")
@Validated
@AllArgsConstructor
@Slf4j
@RestController
@RequestMapping("/demoModel/demo")
public class DemoController {

    private final DemoService demoService;

    @ApiOperation(value = "批量删除数据Demo接口")
    @PostMapping("/delete")
    public HttpResult<Void> delete(@RequestBody @NotEmpty List<String> idList) {
        demoService.delete(idList);
        return HttpResult.success();
    }

}

核心业务实现

package com.study.core.modular.demo.service;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.study.core.modular.demo.dao.DemoMapper;
import com.study.core.modular.demo.model.entity.Demo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@AllArgsConstructor
@Slf4j
public class DemoService {

    private final DemoMapper demoMapper;

    @Transactional(rollbackFor = Exception.class)
    public void delete(List<String> idList) {
        demoMapper.deleteBatchWithFill(Demo.builder()
                                           .build(), Wrappers.lambdaQuery(Demo.class)
                                                             .in(Demo::getId, idList));
    }

}

开始测试,先新增几条数据,然后将这些数据的id全部传入测试用例中进行批量删除。单条数据删除也可以使用此接口,此时请求参数的id数组内元素只有一个。

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第6张图片
批量删除并返回结果:

从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第7张图片
从零开始SpringBoot项目搭建(二)——Swagger集成以及MybatisPlus配置_第8张图片

你可能感兴趣的:(spring,boot,后端,java)