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

kenx
• 阅读 316

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

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

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

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

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

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

  1. 支持一键配置自定义RestFull API 统一格式返回
  2. 支持RestFull API 错误国际化
  3. 支持全局异常处理,全局参数验证处理
  4. 业务错误断言工具封装,遵循错误优先返回原则
  5. 封装Redis key,value 操作工具类。统一key管理 spring cache缓存实现
  6. RestTemplate 封装 POST,GET 请求工具
  7. 日志集成。自定义日志路径,按照日志等级分类,支持压缩和文件大小分割。按时间显示
  8. 工具库集成 集成了lombok,hutool,commons-lang3,guava。不需要自己单个引入
  9. 集成mybatisPlus一键代码生成
  10. 日志记录,服务监控,支持日志链路查询。自定义数据源
  11. OpenApi3文档一键配置。支持多种文档和自动配置
  12. 接口限流,Ip城市回显
  13. HttpUserAgent请求设备工具封装
  14. RequestUtil参数解析封装工具

后续会持续更新。项目中重复使用,必备模块和工具。

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

快速开始

  1. 项目pom中引入依赖

    <dependency>
     <groupId>cn.soboys</groupId>
     <artifactId>rest-api-spring-boot-starter</artifactId>
     <version>1.5.0</version>
    </dependency>
  2. 在SpringBoot启动类或者配置类上通过 @EnableRestFullApi注解开启rest-api

@SpringBootApplication @EnableRestFullApi public class SuperaideApplication {

public static void main(String[] args) {
    SpringApplication.run(SuperaideApplication.class, args);
}

}


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

## RestFull API
在`Controller`中我们写普通的请求接口如:
```java
@PostMapping("/chat")
public HashMap chatDialogue() {
    HashMap m = new HashMap();
    m.put("age", 26);
    m.put("name", "Judy");
    return m;
}
```
返回的就是全局统一RestFull API
```json
{
    "success": true,
    "code": "OK",
    "msg": "操作成功",
    "requestId": "IPbHLE5SZ1fqI0lgNXlB",
    "timestamp": "2023-07-09 02:39:40",
    "data": {
        "name": "judy",
        "hobby": "swing",
        "age": 18
    }
}
```

也可以基于`Result`构建
```java
@PostMapping("/chat")
public Result chatDialogue(@Validated EntityParam s) {
    return Result.buildSuccess(s);
}
```
## 分页支持

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

```java
@PostMapping("/page")
@Log("分页查询用户数据")
public Result page(@Validated EntityParam s) {
    ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
    List a=new ArrayList();
    a.add(s);
    resultPage.setPageData(a);
    return ResultPage.buildSuccess(resultPage);
}
  1. 构建自定义自己的分页数据
    ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
  2. 通过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格式

  1. 可以通过注解@NoRestFulApi实现如
    @GetMapping("/test")
    @NoRestFulApi
    public Map chatDialogue() {
     Map  m= new HashMap<>();
     m.put("name","judy");
     m.put("age",26);
     return m;
    }
  2. 通过类扫描去实现 默认会过滤String类型认为是页面路径。

通过属性配置文件include-packages需要统一返回包。exclude-packages不需统一返回的包

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

OpenApi文档生成

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

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

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

  3. 接口文档属性信息

    openapi:
     description:
     title:
     version:
     license: 
     contact:
       name:
       email:
       url: 

如果嫌弃官方提供的 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;
    }

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

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;
    }

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

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

错误国际化

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

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

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

  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 进行配置

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

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

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

日志链路追踪

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<List<EntityParam>> resultPage=new ResultPage<>();
    List a=new ArrayList();
    a.add(s);
    resultPage.setPageData(a);
    return ResultPage.buildSuccess(resultPage);
}

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

2023-07-13 11:21:25 INFO  http-nio-8888-exec-2 cn.soboys.restapispringbootstarter.aop.LimitAspect IP:192.168.1.8 第 1 次访问key为 [_kenx:chat192.168.1.8],描述为 [接口限流] 的接口
2023-07-13 11:21:26 INFO  http-nio-8888-exec-2 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource {
    "description": "日志记录测试",
    "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.chatDialogue()",
    "params": {
    },
    "logType": "INFO",
    "requestIp": "192.168.1.8",
    "path": "/chat",
    "address": "0|0|0|内网IP|内网IP",
    "time": 128,
    "os": "Mac",
    "browser": "Chrome",
    "result": {
        "success": true,
        "code": "OK",
        "msg": "操作成功",
        "requestId": "5RgKzWGFNa9XSPwhw2Pi",
        "timestamp": "2023-07-13 11:21:25",
        "data": "接口限流测试"
    },
    "apiType": "USER",
    "device": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
}

你可以自定义自己的日志数据源实现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 # 日志数据源

Ip城市记录

日志记录提供Ip城市回显记录

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

通过配置ipCity属性,默认是true会记录IP对应物理地址详细信息即国家城市等 Ip城市查询通过ip2region获取。

你可配置属性 location 配置自己的ip2region.xdb文件。

  ip2region:
    external: false
    location: classpath:ip2region/ip2region.xdb

默认不配置会帮你自动生成一个。基于2.7.x最新的数据 获取最新自定义ip数据 Github 仓库

当然也帮你封装了。你可以通过工具类HttpUserAgent的静态方法getIpToCityInfo(String ip) 去获取查询ip对应城市信息

属性配置

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

::: 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<List<EntityParam>> resultPage=new ResultPage<>();
    List a=new ArrayList();
    a.add(s);
    resultPage.setPageData(a);
    return ResultPage.buildSuccess(resultPage);
}
  1. 构建自定义自己的分页数据
    ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
  2. 通过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格式

  1. 可以通过注解@NoRestFulApi实现如
    @GetMapping("/test")
    @NoRestFulApi
    public Map chatDialogue() {
     Map  m= new HashMap<>();
     m.put("name","judy");
     m.put("age",26);
     return m;
    }
  2. 通过类扫描去实现 默认会过滤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 你项目中不需要再单独引入

<!--参数校验-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

也内置扩展了许多自定义参数校验参考 我开源了团队内部基于SpringBoot Web快速开发的API脚手架stater

第三方请求

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

在需要使用地方注入RestFulTemp

@Resource
private RestFulTemp restFulTemp;

GET请求

@GetMapping("/doGet")
public Result doGet() {
    ResponseEntity<String> 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<String> 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<String> 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: 请求资源不存在

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

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

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

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

业务断言

封装了业务错误断言工具。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 你不需要在项目中单独引入

<!--日志集成-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

默认日志配置

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<List<EntityParam>> 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需要自己单独引入 :::

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

项目使用

注入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;
    }

}

使用

  1. 存储对于key
    @GetMapping("/redis")
    public Result chatDialogue() {
     CacheKey.PWD_RESET_CODE.valueSetAndExpire("test", 60l, TimeUnit.SECONDS, "judy");
     return Result.buildSuccess();
    }
  2. 获取对应的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文档。项目启动后即可访问。

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

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

  3. 接口文档属性信息

    openapi:
     description:
     title:
     version:
     license: 
     contact:
       name:
       email:
       url: 

如果嫌弃官方提供的 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代码生成需自行单独引入 :::

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
</dependency>
<!-- MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<!--代码生成依赖的模板引擎-->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

项目使用

  1. 代码生成配置类
    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);
    }
}

常见问题

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

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

点赞
收藏
评论区
推荐文章
kenx kenx
3年前
SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决
前言我们在开发springboot项目时候,创建好SpringBoot项目就可以通过启动类直间启动,运行一个web项目,非常方便简单,不像我们之前通过SpringSpringMvc要运行启动一个web项目还需要要配置各种包扫描和tomcat才能启动我将应用分成了parentcommoncomponentapp这种模式,1.parent是一个单纯的p
liam liam
2年前
先写API文档还是先写代码?
代码未动,文档先行其实大家都知道API文档先行的重要性,但是在实践过程中往往会遇到很多困难。程序员最讨厌的两件事:1.写文档,2.别人不写文档。大多数开发人员不愿意写API文档的原因是写文档短期收益远低于付出的成本,然而并不是所有人都能够坚持做有长期收益的事情的。作为一个前后端分离模式开发的团队,我们经常会看到这样的场景:前端开发和后端开发在一起
kenx kenx
3年前
mybatis的mapper特殊字符转移以及动态SQL条件查询
前言我们知道在项目开发中之前使用数据库查询,都是基于jdbc,进行连接查询,然后是高级一点jdbcTemplate进行查询,但是我们发现还是不是很方便,有大量重复sql语句,与代码偶合,效率低下,于是就衍生出来ORM框架,如Mybatis,Hibernate,还有SpringBoot的,SpringDataJPA条件查询我们知道在mybatisma
kenx kenx
1年前
SpringBoot定义优雅全局统一Restful API 响应框架
假如现在有一个Java项目,老板让你做项目组长,定义项目基础框架,系统技术架构选型,你应该如何设计一个规范的统一的RestfulAPI响应框架呢思考目前项目开发,都是基于前后端分离模式开发的,基于后端模板引擎那一套,可能已经不适用一些项目开发流程,和当下开
Easter79 Easter79
3年前
SprintBoot配置Filter、Servlet
今天想要手动的实现以下Filter权限控制,但是发现使用springboot的filter和之前的不一样,之前使用Servlet3.0只需要使用@WebFilter注解或者在web.xml文件中配置以下即可。但是springboot中没有web.xml文件,于是我很自然的使用注解,然后发现怎么也配置不上去,那么怎么办呢,没错,查文档。经过一番查阅,原来s
Wesley13 Wesley13
3年前
Spring Boot 与 Kotlin 使用JdbcTemplate连接MySQL
之前介绍了一些Web层的例子,包括构建RESTfulAPI、使用Thymeleaf模板引擎渲染Web视图,但是这些内容还不足以构建一个动态的应用。通常我们做App也好,做Web应用也好,都需要内容,而内容通常存储于各种类型的数据库,服务端在接收到访问请求之后需要访问数据库获取并处理成展现给用户使用的数据形式。本文介绍在SpringBoot基础下配置数
Wesley13 Wesley13
3年前
@Autowired和@Resource注解的一个意外重要区别
今天上午,因为公司要跟客户展示最近开发的项目,然后安排了我重新构建一个template项目,用来向客户展示参考。基于已开发好的代码,我在进行一些简化抽取的时候出现了一个有趣的问题因为我们有一个springsecurity配置类时需要每个模块都使用,就是可能有些参数不同,现在我把他弄到一个公共的jar包,把之前类拷贝进去,然后把参数写活,结果出现了一些有
kenx kenx
1年前
我开源了团队内部基于SpringBoot Web快速开发的API脚手架v1.6.0更新
什么是restapispringbootstarterrestapispringbootstarter适用于SpringBootWebAPI快速构建让开发人员快速构建统一规范的业务RestFullAPI不在去关心一些繁琐。重复工作,而是把重点聚焦到业务。动
liam liam
2年前
为什么越来越多的开发者放弃使用Postman,而选择Apifox
一、API调试常用解决方案1、PostmanSwaggerMockJMeter作为一个后端开发,我做的大部分项目一般都是基于Swagger来管理API文档,基于Postman来做接口调试,基于JMeter来做接口性能测试,基于RAP等工具MockAPI数据。\2、存在的问题(1)多系统数据不互通API设计者、前
kenx kenx
1年前
SpringBoot定义优雅全局统一Restful API 响应框架完结撒花篇封装starter组件
之前我们已经,出了一些列文章。讲解如何封统一全局响应RestfulAPI。感兴趣的可以看我前面几篇文章(整个starter项目发展史)后续我萌生里新的想法,SpringBoot不是提供了自己的starter。我们也可以自定义starter吗,于是我定义了r