全栈开发之后端脚手架:SpringBoot集成MybatisPlus代码生成,分页,雪花算法,统一响应,异常拦截,Swagger3接口文档

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 全栈开发之后端脚手架:SpringBoot集成MybatisPlus代码生成,分页,雪花算法,统一响应,异常拦截,Swagger3接口文档

背景


当需要快速实现一个想法时,如果采用 Java 技术栈,一般都是选择 SpringBoot 技术栈,虽然 SpringBoot 解决了传统 SpringMVC 配置等方面的问题,且其生态体系也非常强大,但是在实际使用时仍然需要集成最起码的数据库、响应封装、异常拦截、代码生成器、接口文档等基础组件,这时一般有两种手段:


  1. 使用开源的各类后台管理系统,这类系统一般模块完善,功能强大;然而很多无关紧要的模块又会带来一些干扰;


  1. 使用自建的简化版,集成最常用的模块即可,一方面便于掌控,同时在“重复造轮子”的过程中也熟悉下这些主流技术是如何整合在一起的。


这里,就从零开始搭建后端脚手架,以搭积木的方式将开源组件组装起来。后续的玩具项目都基于这个脚手架进行开发。


MybatisPlus代码生成器


参考官方的代码仓库以及文档mp.baomidou.com/guide/gener…

image.png

简单修改路径信息后,直接执行 MysqlGenerator 类的 main 方法。分别键入模块表以及表名即可生成 Entity , Mapper , Service , Controller 等对应的文件。


MybatisPlus雪花算法ID


image.png

MybatisPlus (3.3.1)的主键策略默认是雪花算法,如果不显式设置主键的话, MybatisPlus 通过代码自动通过雪花算法算出一个值,插入的时候就会将其作为id插入。


雪花算法( SnowFlake )是一个 Long 类型的 Java 长整型数字,一般对应 MySQL 中的类型为 BIGINT(20) ;具有趋势单调递增,且全局唯一的特点。


MybatisPlus分页


image.png

  • MybatisPlusConfig.java
@Configuration
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor().setCountSqlParser(new JsqlParserCountOptimize(true));
    }
}
  • PageUtils.java
@Data
public class PageUtils {
    //总记录数
    private long total;
    //每页记录数
    private long size;
    //总页数
    private long pages;
    //当前页数
    private long current;
    //列表数据
    private List<?> records;
    //灵活添加
    private Map<String,Object> data;
    /**
     * 分页
     * @param records        列表数据
     * @param total  总记录数
     * @param size    每页记录数
     * @param current    当前页数
     */
    public PageUtils(List<?> records, long total, long size, long current) {
        this.records = records;
        this.total = total;
        this.size = size;
        this.current = current;
        this.pages = (long)Math.ceil((double)total/size);
    }
    /**
     * 分页
     * @param records        列表数据
     * @param total  总记录数
     * @param size    每页记录数
     * @param current    当前页数
     */
    public PageUtils(List<?> records, long total, long size, long current, Map<String,Object> data) {
        this.records = records;
        this.total = total;
        this.size = size;
        this.current = current;
        this.data = data;
        this.pages = (long)Math.ceil((double)total/size);
    }
    /**
     * 分页
     */
    public PageUtils(Page<?> page) {
        this.records = page.getRecords();
        this.total = (long)page.getTotal();
        this.size = page.getSize();
        this.current = page.getCurrent();
        this.pages = (long)page.getPages();
    }
}
  • BookController.java
// 分页查询:使用自定义PageUtils
@GetMapping("list")
public Result<PageUtils> list(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
    PageUtils list = bookService.findList(new Page<>(page, size), params);
    return Result.success(list);
}
// 分页查询:使用MyBatisPlus的page方法
@GetMapping("page")
public Result<IPage<Book>> page(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
    QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
    queryWrapper.likeRight("read_date", params.get("readDate"));
    IPage<Book> list = bookService.page(new Page<>(page, size),queryWrapper);
    return Result.success(list);
}


统一响应封装


image.png

通过 RestControllerAdvice 注解,实现对请求的拦截,统一封装结果为 Result

@RestControllerAdvice
public class ResultAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o instanceof String) {
            return objectMapper.writeValueAsString(Result.success(o));
        }
        if (o instanceof Result) {
            return o;
        }
        return Result.success(o);
    }
}
  • Result.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    /** 结果状态 ,正常响应200,其他状态码都为失败*/
    private int code;
    private String msg;
    private T data;
    // Static methods
    /**
     * 成功时候的调用
     */
    public static <T> Result<T> success(T data) {
        return new Result<T>(data, CodeMsg.SUCCESS);
    }
    public static <T> Result<T> success() {
        return new Result<T>(CodeMsg.SUCCESS);
    }
    /**
     * 失败时候的调用
     */
    public static <T> Result<T> error(Integer code, String msg) {
        return new Result<T>(code, msg);
    }
    public static <T> Result<T> error(CodeMsg codeMsg) {
        return new Result<T>(codeMsg);
    }
    public static <T> Result<T> error(String msg) {
        CodeMsg codeMsg = new CodeMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
        return new Result<T>(codeMsg);
    }
    // Constructor
    private Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    private Result(T data, CodeMsg codeMsg) {
        this.data = data;
        if (codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }
    private Result(CodeMsg codeMsg) {
        if (codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }
}
  • CodeMsg.java
@Getter
public class CodeMsg {
    private int code;
    private String msg;
    // 通用的错误码
    public static final CodeMsg SUCCESS =new CodeMsg(HttpStatus.OK.value(), "success");
    public static final CodeMsg BAD_REQUEST = new CodeMsg(HttpStatus.BAD_REQUEST.value(), "请求无效");
    public static final CodeMsg SERVER_ERROR = new CodeMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务端异常");
    public static final CodeMsg NO_HANDLER_FOUND = new CodeMsg(HttpStatus.NOT_FOUND.value(), "未找到对应资源");
    public static final CodeMsg UNAUTHORIZED = new CodeMsg(HttpStatus.UNAUTHORIZED.value(), "未认证或登录状态过期");
    public static final CodeMsg FORBIDDEN = new CodeMsg(HttpStatus.FORBIDDEN.value(), "未授权");
    // 自定义错误码
    public static final CodeMsg PARAMETER_ERROR = new CodeMsg(4000, "参数不正确!");
    /*用户相关:验证码*/
    public static final CodeMsg CAPTCHA_EXPIRED = new CodeMsg(4001, "验证码不存在或已过期");
    public static final CodeMsg CAPTCHA_INVALID = new CodeMsg(4002, "验证码错误");
    /*用户相关:认证授权*/
    public static final CodeMsg BAD_CREDENTIAL = new CodeMsg(4003, "用户名或密码错误");
    public static final CodeMsg ACCOUNT_NOT_FOUND = new CodeMsg(4004, "账号不存在");
    public static final CodeMsg ACCOUNT_NOT_ACTIVATED = new CodeMsg(4005, "账号未激活");
    // 限流
    public static final CodeMsg RATE_LIMIT = new CodeMsg(4006,"达到阈值啦!");
    // 熔断
    public static final CodeMsg DEGRADE = new CodeMsg(4007,"熔断啦!");
    public static CodeMsg error(String msg){
        return new CodeMsg(HttpStatus.BAD_REQUEST.value(),msg);
    }
    public CodeMsg(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}


全局异常拦截


image.png

默认拦截所有异常(也可自定义异常进行封装),同样通过 RestControllerAdvice 注解,实现对异常响应的统一封装。


  • RestExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("Global exception: {}", null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(CodeMsg.SERVER_ERROR.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }
}


CRUD的Controller


@RestController
@RequestMapping("book")
@Api(tags = "测试Controller")
public class BookController {
    @Autowired
    IBookService bookService;
    @GetMapping("hello")
    @ApiOperation("哈喽")
    public String hello() {
        return "hello everyone.";
    }
    @GetMapping("list")
    public List<Book> list() {
        return bookService.list();
    }
    @PostMapping("save")
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }
    @GetMapping("detail/{id}")
    public Result detail(@PathVariable long id) {
        return Result.success(bookService.getById(id));
    }
    @GetMapping("error")
    public Result error() {
        int value = 8 / 0;
        return Result.success(value);
    }
    @GetMapping("page")
    public Result<IPage<Book>> page(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
        QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("read_date", params.get("readDate"));
        IPage<Book> list = bookService.page(new Page<>(page, size),queryWrapper);
        return Result.success(list);
    }
}


Swagger3接口文档


image.png

  • 引入依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>${swagger.version}</version>
</dependency>
  • 配置类
@Configuration
@EnableOpenApi
public class SwaggerConfig {
    private static final String VERSION = "1.0.0";
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.heartsuit.readingnotes.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SpringBoot+Swgger3.0后端服务接口文档")
                .contact(new Contact("Heartsuit", "https://blog.csdn.net/u013810234", "454670286@qq.com"))
                .description("基于Swagger3.0生成的接口文档")
                .termsOfServiceUrl("https://blog.csdn.net/u013810234")
                .license("The Apache License, Version 2.0")
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
                .version(VERSION)
                .build();
    }
}
  • 在控制器以及接口上添加注解
@Api(tags = "测试Controller")
@RestController
public class HelloController {
    @GetMapping("hello")
    @ApiOperation("哈喽")
    public String hello() {
        return "Hello SpringBoot with Swagger3.0";
    }
}
  • 启动服务,浏览器访问


没错,再没其他额外的注解了,直接启动服务,然后在浏览器访问即可。


Note:



实际中我们的接口文档只会在开发环境下使用,所以一般我们会在生产环境下关闭文档。


  • application.yml
spring:
  profiles:
    active: dev
  • application-dev.yml
springfox:
  documentation:
    enabled: true
  • application-prod.yml
springfox:
  documentation:
    enabled: false


遇到的问题


  • 问题1:控制台打印MyBatisPlusSQL日志


解决方法:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 问题2:Long类型的雪花算法ID传到前端后精度丢失


解决方法:在后端 JSON 返回前统一将 Long 转为字符串。

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }
}
  • 问题3:全局异常处理时多个异常处理器匹配顺序


解决方法:


如下,除了全局拦截的所有异常 Exception 之外,还有一个自定义的异常 CustomException ,那么,当出现 CustomException 时,当前两个异常该如何匹配呢?答案是子类异常处理器优先,即会被 customException 方法拦截,而不会被 exception 方法拦截。

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("Global exception: {}", null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(CodeMsg.SERVER_ERROR.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }
    @ExceptionHandler(CustomException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<String> customException(CustomException e) {
        log.error("Custom exception: {}", null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(e.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }
}
@Getter
public class CustomException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private Integer code;
    public CustomException(CodeMsg codeMsg) {
        super(codeMsg.getMsg());
        this.code = codeMsg.getCode();
    }
    public CustomException(Integer code, String msg){
        super(msg);
        this.code = code;
    }
}
  • 问题4:访问Swagger地址时报错:Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway. The base url is the root of where all the swagger resources are served. For e.g. if the api is available at example.org/api/v2/api-… then the base url is example.org/api/. Please enter the location manually

解决方法:


原因是我们使用 RestControllerAdvice 统一处理接口响应,导致给Swagger的返回值也包装了一层,最终在浏览器无法解析、渲染页面。


@RestControllerAdvice 改为: @RestControllerAdvice(basePackages = "com.heartsuit.*.controller")

即限制 RestControllerAdvice 的拦截范围,仅处理指定包下的接口响应。


项目依赖


<properties>
        <java.version>11</java.version>
        <mybatisplus.version>3.3.1</mybatisplus.version>
        <swagger.version>3.0.0</swagger.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--MySQL and ORM-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.version}</version>
        </dependency>
        <!--Swagger3.0-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <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>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>


配置文件


server:
  port: 8000
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      url: jdbc:mysql://localhost:3306/reading_notes?serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useSSL=false
      username: root
      password: root
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  typeAliasesPackage: com.heartsuit.*.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


IDEA相关插件


image.png

用到的插件:


  • Lombok :通过注解生成Getter, Setter, toString()以及日志打印;
  • MyBatis Log Plugin :从MyBatis以及MyBatisPlus控制台日志的SQL复原SQL+参数拼接;
  • RestfulToolkit :在IDEA中测试控制层的接口,无需再切换出IDE到浏览器或者Postman;
  • Free Mybatis plugin :链接Mapper接口与xml;


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
1月前
|
JSON JavaScript 前端开发
springboot中使用knife4j访问接口文档的一系列问题
本文作者是一位自学前端两年半的大一学生,分享了在Spring Boot项目中使用Knife4j遇到的问题及解决方案,包括解决Swagger请求404错误、JS错误等,详细介绍了依赖升级、注解替换及配置修改的方法。
89 1
|
1月前
|
JSON JavaScript 前端开发
springboot中使用knife4j访问接口文档的一系列问题
本文介绍了在Spring Boot项目中使用Knife4j访问接口文档时遇到的一系列问题及其解决方案。作者首先介绍了自己是一名自学前端的大一学生,熟悉JavaScript和Vue,正在向全栈方向发展。接着详细说明了如何解决Swagger请求404错误,包括升级Knife4j依赖、替换Swagger 2注解为Swagger 3注解以及修改配置类中的代码。最后,针对报JS错误的问题,提供了删除消息转换器代码的解决方法。希望这些内容能对读者有所帮助。
312 5
|
22天前
|
安全 Java API
实现跨域请求:Spring Boot后端的解决方案
本文介绍了在Spring Boot中处理跨域请求的三种方法:使用`@CrossOrigin`注解、全局配置以及自定义过滤器。每种方法都适用于不同的场景和需求,帮助开发者灵活地解决跨域问题,确保前后端交互顺畅与安全。
|
1月前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
52 1
|
2月前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
232 1
|
2月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
42 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
2月前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
2月前
|
Java 数据库连接 API
springBoot:后端解决跨域&Mybatis-Plus&SwaggerUI&代码生成器 (四)
本文介绍了后端解决跨域问题的方法及Mybatis-Plus的配置与使用。首先通过创建`CorsConfig`类并设置相关参数来实现跨域请求处理。接着,详细描述了如何引入Mybatis-Plus插件,包括配置`MybatisPlusConfig`类、定义Mapper接口以及Service层。此外,还展示了如何配置分页查询功能,并引入SwaggerUI进行API文档生成。最后,提供了代码生成器的配置示例,帮助快速生成项目所需的基础代码。
166 1
|
2月前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
342 10
|
2月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
185 6