我開源了團隊內部基於SpringBoot Web快速開發的API腳手架stater

2023-07-13 15:01:38

我們現在使用SpringBoot 做Web 開發已經比之前SprngMvc 那一套強大很多了。
但是 用SpringBoot Web 做API 開發還是不夠簡潔有一些。

每次Web API常用功能都需要重新寫一遍。或者複製之前專案程式碼。於是我封裝了這麼一個

抽出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>
  1. 在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<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<>();
  1. 通過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;
}
  1. 通過類掃描去實現
    預設會過濾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: 
  • 啟動專案後,存取 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<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<>();
  1. 通過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;
}
  1. 通過類掃描去實現
    預設會過濾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>

也內建擴充套件了許多自定義引數校驗參考

第三方請求

有時候我們專案中需要呼叫第三方介面服務。基於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: 請求資源不存在

業務斷言

封裝了業務錯誤斷言工具。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();
}
  1. 獲取對應的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: 
  • 啟動專案後,存取 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程式碼生成需自行單獨引入
:::

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

常見問題

在使用過程中儘量使用最新版本。我會持續更新更多的內容。 會第一時間釋出在我的公眾號
程式設計師三時。全網同名

可以關注 公眾號 程式設計師三時。用心分享持續輸出優質內容。希望可以給你帶來一點幫助