我开源了团队内部基于SpringBoot Web快速开发的API脚手架stater

2023年 7月 14日 66.8k 0

我们现在使用SpringBoot 做Web 开发已经比之前SprngMvc 那一套强大很多了。
但是 用SpringBoot Web 做API 开发还是不够简洁有一些。

每次Web API常用功能都需要重新写一遍。或者复制之前项目代码。于是我封装了这么一个

抽出SpringBoot Web API 每个项目必备需要重复写的模块,和必备功能。
并且扩展了我工作中用到的 所有工具库。

基于它,你可以轻松开发SpringBoot WEB API,提高效率。不在去关心一些繁琐。重复工作,而是把重点聚焦到业务。

目前更新版本到1.5.1 功能如下

  • 支持一键配置自定义RestFull API 统一格式返回
  • 支持RestFull API 错误国际化
  • 支持全局异常处理,全局参数验证处理
  • 业务错误断言工具封装,遵循错误优先返回原则
  • 封装Redis key,value 操作工具类。统一key管理 spring cache缓存实现
  • RestTemplate 封装 POST,GET 请求工具
  • 日志集成。自定义日志路径,按照日志等级分类,支持压缩和文件大小分割。按时间显示
  • 工具库集成 集成了lombok,hutool,commons-lang3,guava。不需要自己单个引入
  • 集成mybatisPlus一键代码生成
  • 日志记录,服务监控,支持日志链路查询。自定义数据源
  • OpenApi3文档一键配置。支持多种文档和自动配置
  • 后续会持续更新。项目中重复使用,必备模块和工具。

    • GitHub 地址
    • gitee 地址

    rest-api-spring-boot-starter 适用于SpringBoot Web API 快速构建让开发人员快速构建统一规范的业务RestFull API 不在去关心一些繁琐。重复工作,而是把重点聚焦到业务。

    快速开始

  • 项目pom中引入依赖
  • 
        cn.soboys
        rest-api-spring-boot-starter
        1.5.0
    
    
  • 在SpringBoot启动类或者配置类上通过 @EnableRestFullApi注解开启rest-api
  • 
    @SpringBootApplication
    @EnableRestFullApi
    public class SuperaideApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SuperaideApplication.class, args);
        }
    }
    

    到此你项目中就可以使用所有的功能了。

    RestFull API

    Controller中我们写普通的请求接口如:

    @PostMapping("/chat")
    public HashMap chatDialogue() {
        HashMap m = new HashMap();
        m.put("age", 26);
        m.put("name", "Judy");
        return m;
    }
    

    返回的就是全局统一RestFull API

    {
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "IPbHLE5SZ1fqI0lgNXlB",
        "timestamp": "2023-07-09 02:39:40",
        "data": {
            "name": "judy",
            "hobby": "swing",
            "age": 18
        }
    }
    

    也可以基于Result构建

    @PostMapping("/chat")
    public Result chatDialogue(@Validated EntityParam s) {
        return Result.buildSuccess(s);
    }
    

    分页支持

    我们在日常中分页是一个比较特殊返回。也是非常常用的。

    @PostMapping("/page")
    @Log("分页查询用户数据")
    public Result page(@Validated EntityParam s) {
        ResultPage resultPage=new ResultPage();
        List a=new ArrayList();
        a.add(s);
        resultPage.setPageData(a);
        return ResultPage.buildSuccess(resultPage);
    }
    
  • 构建自定义自己的分页数据
  • ResultPage resultPage=new ResultPage();
    
  • 通过ResultPage.buildSuccess(resultPage)进行构建返回
  • 返回统一响应格式

    {
        "previousPage": 1,
        "nextPage": 1,
        "pageSize": 1,
        "totalPageSize": 1,
        "hasNext": "false",
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "D9AMALgkZ6gVfe6Pi0Oh",
        "timestamp": "2023-07-09 02:39:40",
        "data": [
            {
                "name": "judy",
                "hobby": "swing",
                "age": 18
            }
        ]
    }
    

    自定义返回格式

    {
        "previousPage": 1,
        "nextPage": 1,
        "pageSize": 1,
        "totalPageSize": 1,
        "hasNext": "false",
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "D9AMALgkZ6gVfe6Pi0Oh",
        "timestamp": "2023-07-09 02:39:40",
        "data": [
            {
                "name": "judy",
                "hobby": "swing",
                "age": 18
            }
        ]
    }
    

    上述统一返回格式,可能不符合你项目中接口统一格式如:

    {
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "ztf4S-lP9yrtKPSiwldZ",
        "timestamp": "2023-07-11 13:46:53",
        "data": {
            "previousPage": 1,
            "nextPage": 1,
            "pageSize": 1,
            "totalPageSize": 1,
            "hasNext": "false",
            "pageData": [
                {
                    "name": "judy",
                    "hobby": "swing",
                    "age": 18
                }
            ]
        }
    }
    

    page分页数据是在data里面你可以定义pageWrap属性true包装返回定义pageDatakey值如records

    你需要自定义keymsg你可能对应message,success你可能对应status只需要在配置文件中配置自定义key

    自定义返回成功值你的成功返回可能是200你可以配置code-success-value

    rest-api:
      enabled: false
      msg: msg
      code: code
      code-success-value: OK
      success: success
      previousPage: previousPage
      nextPage: nextPage
      pageSize: pageSize
      hasNext: hasNext
      totalPageSize: totalPageSize
      data: info
    

    enabled开启后会读取你自定义配置的key

    rest-api:
      enabled: true
      msg: msg1
      code: code1
      code-success-value: 200
      success: success1
      previousPage: previousPage1
      nextPage: nextPage1
      pageSize: pageSize1
      hasNext: hasNext1
      totalPageSize: totalPageSize1
      data: info
    

    对应返回内容

    {
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "ztf4S-lP9yrtKPSiwldZ",
        "timestamp": "2023-07-11 13:46:53",
        "data": {
            "previousPage": 1,
            "nextPage": 1,
            "pageSize": 1,
            "totalPageSize": 1,
            "hasNext": "false",
            "pageData": [
                {
                    "name": "judy",
                    "hobby": "swing",
                    "age": 18
                }
            ]
        }
    }
    

    自定义返回

    有时候我们需要自定义返回。不去包装统一响应RestFull API格式

  • 可以通过注解@NoRestFulApi实现如
  • @GetMapping("/test")
    @NoRestFulApi
    public Map chatDialogue() {
        Map  m= new HashMap();
        m.put("name","judy");
        m.put("age",26);
        return m;
    }
    
  • 通过类扫描去实现
    默认会过滤String类型认为是页面路径。
  • 通过属性配置文件include-packages需要统一返回包。exclude-packages不需统一返回的包

    include-packages: cn.soboys.superaide.controller
    exclude-packages: xx.xxx.xxx
    

    OpenApi文档生成

    已经内置自动支持。swagger文档。和最新的OpenApi3 文档。项目启动后即可访问。

  • swagger-ui.html 文档。路径/swagger-ui.html

  • 基于spring-doc 文档UI增强 路径/doc.html

  • 接口文档属性信息

  •   openapi:
        description:
        title:
        version:
        license: 
        contact:
          name:
          email:
          url: 
    
    • 启动项目后,访问 http://server:port/context-path/swagger-ui.html 即可进入 Swagger UI 页面,OpenAPI 描述将在以下 json 格式的 url 中 提供:http://server:port/context-path/v3/api-docs
    • server:域名 或 IP
    • port:服务器端口
    • context-path:应用程序的上下文路径,springboot 默认为空
    • 文档也可以 yaml 格式提供,位于以下路径:/v3/api-docs.yaml

    如果嫌弃官方提供的 swagger-ui 不美观,或者使用不顺手,可以选择关闭 ui,还可以剔除掉 ui 相关的 webjar 的引入。

    springdoc:
      swagger-ui:
        enabled: false
    

    OpenAPI 文档信息,默认可在此 url 中获取: http://server:port/context-path/v3/api-docs。
    可以利用其他支持 OpenAPI 协议的工具,通过此地址,进行 API 展示,如 Apifox。
    ( Postman 的 api 测试也可以利用此地址进行导入生成 )

    Knife4j (原 swagger-bootstrap-ui) 3.x 版本提供了对于 OpenAPI 协议的部分支持。

    ::: tip
    Knife4j 很多地方没有按照协议规范实现,所以使用起来会有很多问题,另外项目也很久没有维护了,不推荐使用。
    :::

    由于 knife4j 对于规范支持的不全面,无法直接使用单文档源数据,所以必须进行分组或者 urls 的指定。

    # urls
    springdoc:
      swagger-ui:
        urls:
          - { name: 'sample', url: '/v3/api-docs' }
    

    或者

    #分组
    springdoc:
      group-configs:
        - { group: 'sample', packages-to-scan: 'com.example' }
    

    Knife4j 的 UI 访问地址有所不同,页面映射在 doc.html 路径下,启动项目后,访问 http://server:port/context-path/doc.html

    即可进入 Knife4j 的 Swagger UI 页面。

    全局错误拦截,参数校验

    帮你封装好了所有http常见错误,和所有请求参数验证错误。

    如请求错误

    {
        "success": false,
        "code": "405",
        "msg": "方法不被允许",
        "timestamp": "2023-07-03 22:36:47",
        "data": "Request method 'GET' not supported"
    }
    

    请求资源不存在等

    {
        "success": false,
        "code": "404",
        "msg": "请求资源不存在",
        "timestamp": "2023-07-03 22:42:35",
        "data": "/api"
    }
    

    如果需要拦截上面错误请在springboot 配置文件中加入

    #出现错误时, 直接抛出异常
    spring.mvc.throw-exception-if-no-handler-found=true
    #不要为我们工程中的资源文件建立映射
    spring.web.resources.add-mappings=false
    

    参数校验错误

    验证Studen对象参数

    /**
     * @author 公众号 程序员三时
     * @version 1.0
     * @date 2023/6/26 22:10
     * @webSite https://github.com/coder-amiao
     */
    @Data
    public class Student {
        @NotBlank
        private String nam;
        @NotBlank
        private String hobby;
    }
    
        @PostMapping("/chat")
        public HashMap chatDialogue(@Validated  Student student) {
            HashMap m = new HashMap();
            m.put("age", 26);
            m.put("name", "Judy");
            return m;
        }
    

    请求结果

    JSON Body参数

        @PostMapping("/chat")
        public HashMap chatDialogue(@RequestBody @Validated  Student student) {
            HashMap m = new HashMap();
            m.put("age", 26);
            m.put("name", "Judy");
            return m;
        }
    

    错误国际化

    内置封装错误默认支持英文和中文两种国际化。你不做任何配置自动支持

    如果需要内置支持更多语言,覆盖即可。

    自定义自己错误国际化和语言

      i18n:
        # 若前端无header传参则返回中文信息
        i18n-header: Lang
        default-lang: cn
        message:
          # admin
          internal_server_error:
            en: Internal Server Error
            cn: 系统错误
          not_found:
            en: Not Found
            cn: 请求资源不存在
    

    message 对应错误提示
    对应internal_server_error 自定义
    下面语言自己定义 和前端传入i18n-header 对应上,就显你定义错误语言

    我不传错误国际化默认就是中文在 default-lang: cn
    进行配置

    当我传入 指定语言 就会按照你配置的国际化自定义返回错误提示

    日志链路追踪

    RestFull API 统一返回有一个requestId 它是每个接口唯一标识。用于接口请求日志链路追踪。日志查询。 如:

    {
        "msg": "操作成功",
        "code": "OK",
        "previousPage": 1,
        "success": true,
        "requestId": "udYNdbbMFE45R84OPu9m",
        "nextPage": 1,
        "pageSize": 1,
        "totalPageSize": 1,
        "hasNext": "false",
        "timestamp": "2023-07-09 03:00:27",
        "info": [
            {
                "name": "judy",
                "hobby": "swing",
                "age": 18
            }
        ]
    }
    

    通过requestId你可以很轻松的在你的日志文件查询定位到每次错误的请求。

    通过Log注解记录你想要记录请求

    @PostMapping("/page")
    @Log(value = "查询用户数据",apiType= LogApiTypeEnum.USER,CURDType= LogCURDTypeEnum.RETRIEVE)
    public Result page(@Validated EntityParam s) {
        ResultPage resultPage=new ResultPage();
        List a=new ArrayList();
        a.add(s);
        resultPage.setPageData(a);
        return ResultPage.buildSuccess(resultPage);
    }
    

    系统默认日志记录数据源为日志文件。如

    2023-07-09 03:00:32 INFO  http-nio-8000-exec-2 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource {
        "description": "查询用户数据",
        "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.page()",
        "logType": "INFO",
        "time": 3,
        "result": {
            "success": true,
            "code": "OK",
            "msg": "操作成功",
            "requestId": "udYNdbbMFE45R84OPu9m",
            "timestamp": "2023-07-09 03:00:27",
            "data": {
                "previousPage": 1,
                "nextPage": 1,
                "pageSize": 1,
                "totalPageSize": 1,
                "hasNext": "false",
                "pageData": [
                    {
                        "name": "judy",
                        "hobby": "swing",
                        "age": 18
                    }
                ],
                "requestId": "qJTOejQmY-OOf7fagegB",
                "timestamp": "2023-07-09 03:00:27"
            }
        },
        "apiType": "USER"
    }
    2023-07-09 03:08:03 INFO  http-nio-8000-exec-4 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource {
        "description": "查询用户数据",
        "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.page()",
        "logType": "INFO",
        "time": 1,
        "result": {
            "success": true,
            "code": "OK",
            "msg": "操作成功",
            "requestId": "kP3yPP-H7wI2x1ak6YFA",
            "timestamp": "2023-07-09 03:00:27",
            "data": {
                "previousPage": 1,
                "nextPage": 1,
                "pageSize": 1,
                "totalPageSize": 1,
                "hasNext": "false",
                "pageData": [
                    {
                        "name": "judy",
                        "hobby": "swing",
                        "age": 18
                    }
                ],
                "requestId": "pGbbiEj8GQ1eTxQpF2Jr",
                "timestamp": "2023-07-09 03:00:27"
            }
        },
        "apiType": "USER"
    }
    

    你可以自定义自己的日志数据源实现LogDataSource接口 日志操作支持异步。需要在配置类。或者启动类加上@EnableAsync
    注解

    package cn.soboys.restapispringbootstarter.log;
    
    import org.springframework.scheduling.annotation.Async;
    
    import java.util.Map;
    
    /**
     * @Author: kenx
     * @Since: 2021/6/23 13:55
     * @Description:
     */
    public interface LogDataSource {
    
        /**
         * 获取拓展数据
         * @return
         * @param logEntry
         */
        @Async
        void  save(LogEntry logEntry);
    }
    
    

    或者你可以继承我默认的日志数据源实现类LogFileDefaultDataSource 重写save(LogEntry logEntry)方法。

    @Slf4j
    public class LogFileDefaultDataSource implements LogDataSource {
    
        /**
         * 自定义保存数据源
         *
         * @param
         * @return LogEntry
         */
        @Override
        public void save(LogEntry logEntry) {
            log.info(JSONUtil.toJsonPrettyStr(logEntry));
        }
    }
    

    如果是自定义日志数据源实现需要再配置文件,配置日志数据源。如:

    logging:
        path: ./logs   #日志存储路径(服务器上绝对)
        max-history: 90 # 保存多少天
        max-file-size: 3MB  # 每个文件大小
        max-total-size-cap: 1GB  #总文件大小超过多少压缩
        level-root: INFO    # 这里的INFO可以替换为其他日志等级,如DEBUG, WARN, ERROR, TRACE, FATAL, OFF等。 日志等级由低到高分别是debugger-info-warn-error
        logDataSourceClass: cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource # 日志数据源
    

    属性配置

    配置语言国际化,日志等,

    ::: tip
    默认不用配置任何参数。会使用默认的,配置了会使用你项目中的配置。
    :::

    默认配置

    rest-api:
      enabled: false
      msg: msg
      code: code
      code-success-value: OK
      success: success
      previousPage: previousPage
      nextPage: nextPage
      pageSize: pageSize
      hasNext: hasNext
      totalPageSize: totalPageSize
      data: info
      include-packages: cn.soboys.superaide.controller
      exclude-packages: xx.xxx.xxx
      redis:
        key-prefix: rest
      openapi:
        description:
        title:
        version:
        license: 
        contact:
          name:
          email:
          url:
      logging:
        path: ./logs   #日志存储路径(服务器上绝对)
        max-history: 90 # 保存多少天
        max-file-size: 3MB  # 每个文件大小
        max-total-size-cap: 1GB  #总文件大小超过多少压缩
        level-root: INFO    # 这里的INFO可以替换为其他日志等级,如DEBUG, WARN, ERROR, TRACE, FATAL, OFF等。 日志等级由低到高分别是debugger-info-warn-error
        logDataSourceClass: cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource # 日志数据源
      i18n:
        # 若前端无header传参则返回中文信息
        i18n-header: Lang
        default-lang: cn
        message:
          # admin
          internal_server_error:
            en: Internal Server Error
            cn: 系统错误
          bad_gateway:
            en: Bad Gateway
            cn: 错误的请求
          unauthorized:
            en: Unauthorized
            cn: 未授权
          forbidden:
            en: Forbidden
            cn: 资源禁止访问
          method_not_allowed:
            en: Method Not Allowed
            cn: 方法不被允许
          request_timeout:
            en: Request Timeout
            cn: 请求超时
          invalid_argument:
            en: Invalid Argument {}
            cn: 参数错误 {}
          argument_analyze:
            en: Argument Analyze {}
            cn: 参数解析异常 {}
          business_exception:
            en: Business Exception
            cn: 业务错误
          not_found:
            en: Not Found
            cn: 请求资源不存在
    
    

    代码生成配置

    支持MybatisPlus代码一键生成 默认不引入MybatisPlus生成依赖需要手动引入

    package cn.soboys.restapispringbootstarter.config;
    
    import lombok.Data;
    
    /**
     * @author 公众号 程序员三时
     * @version 1.0
     * @date 2023/7/5 00:05
     * @webSite https://github.com/coder-amiao
     */
    @Data
    public class GenerateCodeConfig {
        /**
         * 数据库驱动
         */
        private String driverName;
        /**
         * 数据库连接用户名
         */
        private String username;
        /**
         * 数据库连接密码
         */
        private String password;
        /**
         * 数据库连接url
         */
        private String url;
        /**
         * 生成代码 保存路径。默认当前项目下。
         * 如需修改,使用觉得路径
         */
        private String projectPath;
        /**
         * 代码生成包位置
         */
        private String packages;
    }
    
    

    RestFull API

    Controller中直接使用

    @PostMapping("/chat")
    public HashMap chatDialogue() {
        HashMap m = new HashMap();
        m.put("age", 26);
        m.put("name", "Judy");
        return m;
    }
    

    Result构建返回

    @PostMapping("/chat")
    public Result chatDialogue() {
        HashMap m = new HashMap();
        m.put("age", 26);
        m.put("name", "Judy");
        return Result.buildSuccess(m);
    }
    

    分页支持

    我们在日常中分页是一个比较特殊返回。也是非常常用的。

    @PostMapping("/page")
    @Log("分页查询用户数据")
    public Result page(@Validated EntityParam s) {
        ResultPage resultPage=new ResultPage();
        List a=new ArrayList();
        a.add(s);
        resultPage.setPageData(a);
        return ResultPage.buildSuccess(resultPage);
    }
    
  • 构建自定义自己的分页数据
  • ResultPage resultPage=new ResultPage();
    
  • 通过ResultPage.buildSuccess(resultPage)进行构建返回
  • 返回统一响应格式

    {
        "previousPage": 1,
        "nextPage": 1,
        "pageSize": 1,
        "totalPageSize": 1,
        "hasNext": "false",
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "D9AMALgkZ6gVfe6Pi0Oh",
        "timestamp": "2023-07-09 02:39:40",
        "data": [
            {
                "name": "judy",
                "hobby": "swing",
                "age": 18
            }
        ]
    }
    

    自定义返回格式

    {
        "previousPage": 1,
        "nextPage": 1,
        "pageSize": 1,
        "totalPageSize": 1,
        "hasNext": "false",
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "D9AMALgkZ6gVfe6Pi0Oh",
        "timestamp": "2023-07-09 02:39:40",
        "data": [
            {
                "name": "judy",
                "hobby": "swing",
                "age": 18
            }
        ]
    }
    

    上述统一返回格式,可能不符合你项目中接口统一格式如:

    {
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "ztf4S-lP9yrtKPSiwldZ",
        "timestamp": "2023-07-11 13:46:53",
        "data": {
            "previousPage": 1,
            "nextPage": 1,
            "pageSize": 1,
            "totalPageSize": 1,
            "hasNext": "false",
            "pageData": [
                {
                    "name": "judy",
                    "hobby": "swing",
                    "age": 18
                }
            ]
        }
    }
    

    page分页数据是在data里面你可以定义pageWrap属性true包装返回定义pageDatakey值如records

    你需要自定义keymsg你可能对应message,success你可能对应status只需要在配置文件中配置自定义key

    自定义返回成功值你的成功返回可能是200你可以配置code-success-value

    rest-api:
      enabled: false
      msg: msg
      code: code
      code-success-value: OK
      success: success
      previousPage: previousPage
      nextPage: nextPage
      pageSize: pageSize
      hasNext: hasNext
      totalPageSize: totalPageSize
      data: info
    

    enabled开启后会读取你自定义配置的key

    rest-api:
      enabled: true
      msg: msg1
      code: code1
      code-success-value: 200
      success: success1
      previousPage: previousPage1
      nextPage: nextPage1
      pageSize: pageSize1
      hasNext: hasNext1
      totalPageSize: totalPageSize1
      data: info
    

    对应返回内容

    {
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "ztf4S-lP9yrtKPSiwldZ",
        "timestamp": "2023-07-11 13:46:53",
        "data": {
            "previousPage": 1,
            "nextPage": 1,
            "pageSize": 1,
            "totalPageSize": 1,
            "hasNext": "false",
            "pageData": [
                {
                    "name": "judy",
                    "hobby": "swing",
                    "age": 18
                }
            ]
        }
    }
    

    自定义返回

    有时候我们需要自定义返回。不去包装统一响应RestFull API格式

  • 可以通过注解@NoRestFulApi实现如
  • @GetMapping("/test")
    @NoRestFulApi
    public Map chatDialogue() {
        Map  m= new HashMap();
        m.put("name","judy");
        m.put("age",26);
        return m;
    }
    
  • 通过类扫描去实现
    默认会过滤String类型认为是页面路径。
  • 通过属性配置文件include-packages需要统一返回包。exclude-packages不需统一返回的包

    include-packages: cn.soboys.superaide.controller
    exclude-packages: xx.xxx.xxx
    

    错误国际化支持

    内置常见的错误。可以看HttpStatus。默认错误支持中文和英文两种国际化。配置如下

      i18n:
        # 若前端无header传参则返回中文信息
        i18n-header: Lang
        default-lang: cn
        message:
          # admin
          internal_server_error:
            en: Internal Server Error
            cn: 系统错误
          bad_gateway:
            en: Bad Gateway
            cn: 错误的请求
          unauthorized:
            en: Unauthorized
            cn: 未授权
          forbidden:
            en: Forbidden
            cn: 资源禁止访问
          method_not_allowed:
            en: Method Not Allowed
            cn: 方法不被允许
          request_timeout:
            en: Request Timeout
            cn: 请求超时
          invalid_argument:
            en: Invalid Argument {}
            cn: 参数错误 {}
          argument_analyze:
            en: Argument Analyze {}
            cn: 参数解析异常 {}
          business_exception:
            en: Business Exception
            cn: 业务错误
          not_found:
            en: Not Found
            cn: 请求资源不存在
    

    可以自行覆盖扩充

    全局错误拦截和响应

    默认拦所有未知错误异常和validation参数校验失败异常,以及Http请求异常。
    还有全局自定义BusinessException 业务异常 自动集成spring-boot-starter-validation 你项目中不需要再单独引入

    
    
        org.springframework.boot
        spring-boot-starter-validation
    
    

    也内置扩展了许多自定义参数校验参考

    第三方请求

    有时候我们项目中需要调用第三方接口服务。基于RestTemplate 进一步封装了直接的POST,GET,请求。

    在需要使用地方注入RestFulTemp

    @Resource
    private RestFulTemp restFulTemp;
    

    GET请求

    @GetMapping("/doGet")
    public Result doGet() {
        ResponseEntity response = restFulTemp.doGet("http://127.0.0.1:9000/redis/get");
        return Result.buildSuccess();
    }
    

    POST 请求

    /**
     * POST 请求参 数为body json体格式
     * @return
     */
    @PostMapping("/doPost")
    public Result doPost() {
        Student s=new Student();
        s.setHobby("swing");
        s.setNam("judy");
        //自动把对象转换为JSON
        ResponseEntity response = 
                restFulTemp.doPost("http://127.0.0.1:9000/redis/get",s);
        return Result.buildSuccess();
    }
    
    /**
     * POST请求 参数为FORM 表单参数
     * @return
     */
    @PostMapping("/doPost")
    public Result doPostForm() {
        EntityParam s=new EntityParam();
        s.setAge(19);
        s.setHobby("swing");
        s.setName("judy");
    
        ResponseEntity response =
                restFulTemp.doPostForm("http://127.0.0.1:8000/chat", BeanUtil.beanToMap(s));
        return Result.buildSuccess(response.getBody());
    }
    

    DELETE请求

    @GetMapping("/doDelete")
    public Result doDelete() {
        restFulTemp.doDelete("http://127.0.0.1:8000/chat");
        return Result.buildSuccess();
    }
    

    PUT请求

    @GetMapping("/doPut")
    public Result doPut() {
        EntityParam s=new EntityParam();
        restFulTemp.doPut("http://127.0.0.1:8000/chat",s);
        return Result.buildSuccess(s);
    }
    

    错误异常自定义

    我内置错误异常和业务异常可能无法满足你自身接口业务异常需要。你可以自定义错误异常类,和错误响应枚举码。

    自定义错误枚举 需要实现ResultCode接口

    package cn.soboys.restapispringbootstarter;
    
    import cn.soboys.restapispringbootstarter.i18n.I18NKey;
    
    /**
    * @author 公众号 程序员三时
    * @version 1.0
    * @date 2023/6/26 10:21
    * @webSite https://github.com/coder-amiao
    * 响应码接口,自定义响应码,实现此接口
    */
    public interface ResultCode extends I18NKey {
    
       String getCode();
    
       String getMessage();
    
    }
    

    如果要支持国际化还需要实现国际化接口I18NKey 参考我内部HttpStatus实现即可

    package cn.soboys.restapispringbootstarter;
    
    import cn.soboys.restapispringbootstarter.i18n.I18NKey;
    
    /**
     * @author 公众号 程序员三时
     * @version 1.0
     * @date 2023/6/26 11:01
     * @webSite https://github.com/coder-amiao
     */
    public enum HttpStatus implements ResultCode, I18NKey {
        /**
         * 系统内部错误
         */
        INTERNAL_SERVER_ERROR("500", "internal_server_error"),
        BAD_GATEWAY("502", "bad_gateway"),
        NOT_FOUND("404", "not_found"),
        UNAUTHORIZED("401", "unauthorized"),
        FORBIDDEN("403", "forbidden"),
        METHOD_NOT_ALLOWED("405", "method_not_allowed"),
        REQUEST_TIMEOUT("408", "request_timeout"),
    
        INVALID_ARGUMENT("10000", "invalid_argument"),
        ARGUMENT_ANALYZE("10001", "argument_analyze"),
        BUSINESS_EXCEPTION("20000", "business_exception");
    
    
        private final String value;
    
        private final String message;
    
        HttpStatus(String value, String message) {
            this.value = value;
            this.message = message;
        }
    
    
        @Override
        public String getCode() {
            return value;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    
    
        @Override
        public String key() {
            return message;
        }
    }
    
    
    
    rest-api:
      enabled: false
      i18n:
        # 若前端无header传参则返回中文信息
        i18n-header: Lang
        default-lang: cn
        message:
          # admin
          internal_server_error:
            en: Internal Server Error
            cn: 系统错误
          bad_gateway:
            en: Bad Gateway
            cn: 错误的请求
          unauthorized:
            en: Unauthorized
            cn: 未授权
          forbidden:
            en: Forbidden
            cn: 资源禁止访问
          method_not_allowed:
            en: Method Not Allowed
            cn: 方法不被允许
          request_timeout:
            en: Request Timeout
            cn: 请求超时
          invalid_argument:
            en: Invalid Argument {}
            cn: 参数错误 {}
          argument_analyze:
            en: Argument Analyze {}
            cn: 参数解析异常 {}
          business_exception:
            en: Business Exception
            cn: 业务错误
          not_found:
            en: Not Found
            cn: 请求资源不存在
    
    

    业务断言

    封装了业务错误断言工具。Assert 遵循错误优先返回原则。

    你要自定义自己的业务异常。继承BusinessException
    重写对应方法

    package cn.soboys.restapispringbootstarter.exception;
    
    import cn.soboys.restapispringbootstarter.HttpStatus;
    import cn.soboys.restapispringbootstarter.ResultCode;
    import lombok.Data;
    
    /**
     * @author 公众号 程序员三时
     * @version 1.0
     * @date 2023/6/26 16:45
     * @webSite https://github.com/coder-amiao
     */
    @Data
    public class BusinessException extends RuntimeException {
    
        /**
         * 错误码
         */
        private String code="20000";
    
        /**
         * 错误提示
         */
        private String message;
    
    
        public BusinessException(String message) {
            this.message = message;
    
        }
    
        public BusinessException(String message, String code) {
            this.message = message;
            this.code = code;
    
        }
    
        public BusinessException(ResultCode resultCode) {
            this.message = resultCode.getMessage();
            this.code = resultCode.getCode();
    
        }
    }
    
    

    项目中日志是非常常用的,而且还是必须的。已经自动配置集成spring-boot-starter-logging 你不需要在项目中单独引入

    
    
        org.springframework.boot
        spring-boot-starter-logging
    
    

    默认日志配置

    logging:
        path: ./logs   #日志存储路径(服务器上绝对)
        max-history: 90 # 保存多少天
        max-file-size: 3MB  # 每个文件大小
        max-total-size-cap: 1GB  #总文件大小超过多少压缩
        level-root: INFO    # 这里的INFO可以替换为其他日志等级,如DEBUG, WARN, ERROR, TRACE, FATAL, OFF等。 日志等级由低到高分别是debugger-info-warn-error
        logDataSourceClass: cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource # 日志数据源
    

    日志记录与追踪

    RestFull API 统一返回有一个requestId 它是每个接口唯一标识。用于接口请求日志链路追踪。日志查询。 如:

    {
        "msg": "操作成功",
        "code": "OK",
        "previousPage": 1,
        "success": true,
        "requestId": "udYNdbbMFE45R84OPu9m",
        "nextPage": 1,
        "pageSize": 1,
        "totalPageSize": 1,
        "hasNext": "false",
        "timestamp": "2023-07-09 03:00:27",
        "info": [
            {
                "name": "judy",
                "hobby": "swing",
                "age": 18
            }
        ]
    }
    

    通过requestId你可以很轻松的在你的日志文件查询定位到每次错误的请求。

    通过Log注解记录你想要记录请求

    @PostMapping("/page")
    @Log(value = "查询用户数据",apiType= LogApiTypeEnum.USER,CURDType= LogCURDTypeEnum.RETRIEVE)
    public Result page(@Validated EntityParam s) {
        ResultPage resultPage=new ResultPage();
        List a=new ArrayList();
        a.add(s);
        resultPage.setPageData(a);
        return ResultPage.buildSuccess(resultPage);
    }
    

    系统默认日志记录数据源为日志文件。如

    2023-07-09 03:00:32 INFO  http-nio-8000-exec-2 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource {
        "description": "查询用户数据",
        "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.page()",
        "logType": "INFO",
        "time": 3,
        "result": {
            "success": true,
            "code": "OK",
            "msg": "操作成功",
            "requestId": "udYNdbbMFE45R84OPu9m",
            "timestamp": "2023-07-09 03:00:27",
            "data": {
                "previousPage": 1,
                "nextPage": 1,
                "pageSize": 1,
                "totalPageSize": 1,
                "hasNext": "false",
                "pageData": [
                    {
                        "name": "judy",
                        "hobby": "swing",
                        "age": 18
                    }
                ],
                "requestId": "qJTOejQmY-OOf7fagegB",
                "timestamp": "2023-07-09 03:00:27"
            }
        },
        "apiType": "USER"
    }
    2023-07-09 03:08:03 INFO  http-nio-8000-exec-4 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource {
        "description": "查询用户数据",
        "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.page()",
        "logType": "INFO",
        "time": 1,
        "result": {
            "success": true,
            "code": "OK",
            "msg": "操作成功",
            "requestId": "kP3yPP-H7wI2x1ak6YFA",
            "timestamp": "2023-07-09 03:00:27",
            "data": {
                "previousPage": 1,
                "nextPage": 1,
                "pageSize": 1,
                "totalPageSize": 1,
                "hasNext": "false",
                "pageData": [
                    {
                        "name": "judy",
                        "hobby": "swing",
                        "age": 18
                    }
                ],
                "requestId": "pGbbiEj8GQ1eTxQpF2Jr",
                "timestamp": "2023-07-09 03:00:27"
            }
        },
        "apiType": "USER"
    }
    

    你可以自定义自己的日志数据源实现LogDataSource接口 日志操作支持异步。需要在配置类。或者启动类加上@EnableAsync
    注解

    package cn.soboys.restapispringbootstarter.log;
    
    import org.springframework.scheduling.annotation.Async;
    
    import java.util.Map;
    
    /**
     * @Author: kenx
     * @Since: 2021/6/23 13:55
     * @Description:
     */
    public interface LogDataSource {
    
        /**
         * 获取拓展数据
         * @return
         * @param logEntry
         */
        @Async
        void  save(LogEntry logEntry);
    }
    
    

    或者你可以继承我默认的日志数据源实现类LogFileDefaultDataSource 重写save(LogEntry logEntry)方法。

    @Slf4j
    public class LogFileDefaultDataSource implements LogDataSource {
    
        /**
         * 自定义保存数据源
         *
         * @param
         * @return LogEntry
         */
        @Override
        public void save(LogEntry logEntry) {
            log.info(JSONUtil.toJsonPrettyStr(logEntry));
        }
    }
    

    如果是自定义日志数据源实现需要再配置文件,配置日志数据源。如:

    logging:
        path: ./logs   #日志存储路径(服务器上绝对)
        max-history: 90 # 保存多少天
        max-file-size: 3MB  # 每个文件大小
        max-total-size-cap: 1GB  #总文件大小超过多少压缩
        level-root: INFO    # 这里的INFO可以替换为其他日志等级,如DEBUG, WARN, ERROR, TRACE, FATAL, OFF等。 日志等级由低到高分别是debugger-info-warn-error
        logDataSourceClass: cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource # 日志数据源
    

    缓存和redis

    项目中缓存使用是非常常见的。用的最多的是基于Redis缓存。于是我封装了对于RedisKey和Value常用操作。

    ::: tip
    默认不引入Redis依赖,如果要使用Redis需要自己单独引入
    :::

    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    

    项目使用

    注入redisTempUtil

    @Autowired
    private RedisTempUtil redisTempUtil;
    

    如示列

    @Autowired
    private RedisTempUtil redisTempUtil;
    
    @GetMapping("/redis")
    public Result chatDialogue( ) {
        redisTempUtil.set("test","111");
        redisTempUtil.get("test");
        redisTempUtil.set("user","userObj",7200l);
        redisTempUtil.getAllKey("xx");  //*表达式
        redisTempUtil.clean();
        redisTempUtil.deleteObject("test");
        redisTempUtil.hasKey("");
        return Result.buildSuccess();
    }
    

    统一缓存管理

    上面我们是直接通过工具类redisTempUtil直接自己定义key然后去存储,这种方式是不可取的如果key很多随意定义就会很混乱。我提供了统一缓存key管理接口CacheTmp 参考实现CacheKey 基于枚举形式,把所有key集中管理

    package cn.soboys.restapispringbootstarter.cache;
    
    import lombok.Getter;
    
    /**
     * @author 公众号 程序员三时
     * @version 1.0
     * @date 2023/7/2 11:04
     * @webSite https://github.com/coder-amiao
     * 缓存枚举
     */
    @Getter
    public enum CacheKey implements CacheTmp {
    
    
        // 密码的重置码
        PWD_RESET_CODE("reset:code:", true),
        ;
    
        private String key;
    
        /**
         * Key是否是Key前缀, true时直接取key=key,如果false时key=key+suffix
         */
        private boolean hasPrefix;
    
        CacheKey(String key, boolean hasPrefix) {
            this.key = key;
            this.hasPrefix = hasPrefix;
        }
    
    
        @Override
        public Boolean getHasPrefix() {
            return this.hasPrefix;
        }
    
        @Override
        public String getKey() {
            return this.key;
        }
    
    }
    

    使用

  • 存储对于key
  • @GetMapping("/redis")
    public Result chatDialogue() {
        CacheKey.PWD_RESET_CODE.valueSetAndExpire("test", 60l, TimeUnit.SECONDS, "judy");
        return Result.buildSuccess();
    }
    
  • 获取对应的key
  • @GetMapping("/redis/get")
    public Result redisGet() {
        String a = CacheKey.PWD_RESET_CODE.valueGet("judy");
        return Result.buildSuccess(a);
    }
    

    spring Cache实现

    封装了spring Cache进一步使用 项目中在配置类或者启动类通过注解@EnableCaching开启直接使用即可

    @Cacheable(cacheNames = "testCache", keyGenerator = "keyGeneratorStrategy")
    @GetMapping("/redis/springCache")
    public Result springCache() {
        String a = "test cache";
        return Result.buildSuccess(a);
    }
    

    工具类使用springCacheUtil 支持提供不是基于注解的使用方式

    @GetMapping("/redis/springCache")
    public Result redisSpringCache() {
        String a = "111344";
        springCacheUtil.putCache("test","key","121e1");
        return Result.buildSuccess(a);
    }
    

    ::: tip
    默认不引入Redis依赖,缓存基于内存实现(你项目引入redis依赖后会自定切换数据源为Redis缓存)
    :::

    redis配置

    多个项目或者模块使用一个key可能会造成混乱,于是提供了一个全局配置key。

      redis:
        key-prefix: rest 
    

    代码中添加一个 String 类型的 key:testKey,其实际在 redis 中存储的 key name 为 rest:testKey

    全局 key 前缀的配置,并不影响对 key 的其他操作,例如获取对应的 value 时,依然是传入 testKey,而不是 rest:testKey

    String key = "testKey";
    String value = redisTempUtil.get(key);
    String value1 = CacheKey.PWD_RESET_CODE.valueGet(key);
    

    OpenApi文档生成

    已经内置自动支持。swagger文档。和最新的OpenApi3 文档。项目启动后即可访问。

  • swagger-ui.html 文档。路径/swagger-ui.html

  • 基于spring-doc 文档UI增强 路径/doc.html

  • 接口文档属性信息

  •   openapi:
        description:
        title:
        version:
        license: 
        contact:
          name:
          email:
          url: 
    
    • 启动项目后,访问 http://server:port/context-path/swagger-ui.html 即可进入 Swagger UI 页面,OpenAPI 描述将在以下 json 格式的 url 中 提供:http://server:port/context-path/v3/api-docs
    • server:域名 或 IP
    • port:服务器端口
    • context-path:应用程序的上下文路径,springboot 默认为空
    • 文档也可以 yaml 格式提供,位于以下路径:/v3/api-docs.yaml

    如果嫌弃官方提供的 swagger-ui 不美观,或者使用不顺手,可以选择关闭 ui,还可以剔除掉 ui 相关的 webjar 的引入。

    springdoc:
      swagger-ui:
        enabled: false
    

    OpenAPI 文档信息,默认可在此 url 中获取: http://server:port/context-path/v3/api-docs。
    可以利用其他支持 OpenAPI 协议的工具,通过此地址,进行 API 展示,如 Apifox。
    ( Postman 的 api 测试也可以利用此地址进行导入生成 )

    Knife4j (原 swagger-bootstrap-ui) 3.x 版本提供了对于 OpenAPI 协议的部分支持。

    ::: tip
    Knife4j 很多地方没有按照协议规范实现,所以使用起来会有很多问题,另外项目也很久没有维护了,不推荐使用。
    :::

    由于 knife4j 对于规范支持的不全面,无法直接使用单文档源数据,所以必须进行分组或者 urls 的指定。

    # urls
    springdoc:
      swagger-ui:
        urls:
          - { name: 'sample', url: '/v3/api-docs' }
    

    或者

    #分组
    springdoc:
      group-configs:
        - { group: 'sample', packages-to-scan: 'com.example' }
    

    Knife4j 的 UI 访问地址有所不同,页面映射在 doc.html 路径下,启动项目后,访问 http://server:port/context-path/doc.html

    即可进入 Knife4j 的 Swagger UI 页面。

    代码自动生成

    项目中我们使用mybatis 或者mybatisPlus 一些简单的单表业务代码,增删改成。我们可以一键生成。不需要重复写。 我封装了mybatisPlus 代码生成工具

    ::: tip
    默认不引入mybatisPlus代码生成依赖,如果要使用mybatisPlus代码生成需自行单独引入
    :::

    
        com.baomidou
        mybatis-plus-generator
        3.4.1
    
    
    
        mysql
        mysql-connector-java
        8.0.28
    
    
    
        org.freemarker
        freemarker
        2.3.31
    
    

    项目使用

  • 代码生成配置类
  • package cn.soboys.restapispringbootstarter.config;
    
    import lombok.Data;
    
    /**
     * @author 公众号 程序员三时
     * @version 1.0
     * @date 2023/7/5 00:05
     * @webSite https://github.com/coder-amiao
     */
    @Data
    public class GenerateCodeConfig {
        /**
         * 数据库驱动
         */
        private String driverName;
        /**
         * 数据库连接用户名
         */
        private String username;
        /**
         * 数据库连接密码
         */
        private String password;
        /**
         * 数据库连接url
         */
        private String url;
        /**
         * 生成代码 保存路径。默认当前项目下。
         * 如需修改,使用绝对路径
         */
        private String projectPath;
        /**
         * 代码生成包位置
         */
        private String packages;
    }
    

    示列如:

    public class Test {
        public static void main(String[] args) {
            GenerateCodeConfig config=new GenerateCodeConfig();
            config.setDriverName("com.mysql.cj.jdbc.Driver");
            config.setUsername("root");
            config.setPassword("root");
            config.setUrl("jdbc:mysql://127.0.0.1:3306/ry?useUnicode=true&useSSL=false&characterEncoding=utf8");
            //config.setProjectPath("superaide");
            config.setPackages("cn.soboys.superaide");
            MyBatisPlusGenerator.generate(config);
        }
    }
    

    常见问题

    在使用过程中尽量使用最新版本。我会持续更新更多的内容。 会第一时间发布在我的公众号
    程序员三时。全网同名

    可以关注 公众号 程序员三时。用心分享持续输出优质内容。希望可以给你带来一点帮助

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论