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

我们现在使用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中引入依赖
1<dependency> 2 <groupId>cn.soboys</groupId> 3 <artifactId>rest-api-spring-boot-starter</artifactId> 4 <version>1.5.0</version> 5</dependency>
  1. 在SpringBoot启动类或者配置类上通过 @EnableRestFullApi注解开启rest-api
1 2@SpringBootApplication 3@EnableRestFullApi 4public class SuperaideApplication { 5 6 public static void main(String[] args) { 7 SpringApplication.run(SuperaideApplication.class, args); 8 } 9}

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

RestFull API

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

1@PostMapping("/chat") 2public HashMap chatDialogue() { 3 HashMap m = new HashMap(); 4 m.put("age", 26); 5 m.put("name", "Judy"); 6 return m; 7}

返回的就是全局统一RestFull API

1{ 2 "success": true, 3 "code": "OK", 4 "msg": "操作成功", 5 "requestId": "IPbHLE5SZ1fqI0lgNXlB", 6 "timestamp": "2023-07-09 02:39:40", 7 "data": { 8 "name": "judy", 9 "hobby": "swing", 10 "age": 18 11 } 12}

也可以基于Result构建

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

分页支持

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

1@PostMapping("/page") 2@Log("分页查询用户数据") 3public Result page(@Validated EntityParam s) { 4 ResultPage<List<EntityParam>> resultPage=new ResultPage<>(); 5 List a=new ArrayList(); 6 a.add(s); 7 resultPage.setPageData(a); 8 return ResultPage.buildSuccess(resultPage); 9}
  1. 构建自定义自己的分页数据
1ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
  1. 通过ResultPage.buildSuccess(resultPage)进行构建返回

返回统一响应格式

1{ 2 "previousPage": 1, 3 "nextPage": 1, 4 "pageSize": 1, 5 "totalPageSize": 1, 6 "hasNext": "false", 7 "success": true, 8 "code": "OK", 9 "msg": "操作成功", 10 "requestId": "D9AMALgkZ6gVfe6Pi0Oh", 11 "timestamp": "2023-07-09 02:39:40", 12 "data": [ 13 { 14 "name": "judy", 15 "hobby": "swing", 16 "age": 18 17 } 18 ] 19}

自定义返回格式

1{ 2 "previousPage": 1, 3 "nextPage": 1, 4 "pageSize": 1, 5 "totalPageSize": 1, 6 "hasNext": "false", 7 "success": true, 8 "code": "OK", 9 "msg": "操作成功", 10 "requestId": "D9AMALgkZ6gVfe6Pi0Oh", 11 "timestamp": "2023-07-09 02:39:40", 12 "data": [ 13 { 14 "name": "judy", 15 "hobby": "swing", 16 "age": 18 17 } 18 ] 19}

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

1{ 2 "success": true, 3 "code": "OK", 4 "msg": "操作成功", 5 "requestId": "ztf4S-lP9yrtKPSiwldZ", 6 "timestamp": "2023-07-11 13:46:53", 7 "data": { 8 "previousPage": 1, 9 "nextPage": 1, 10 "pageSize": 1, 11 "totalPageSize": 1, 12 "hasNext": "false", 13 "pageData": [ 14 { 15 "name": "judy", 16 "hobby": "swing", 17 "age": 18 18 } 19 ] 20 } 21}

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

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

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

1rest-api: 2 enabled: false 3 msg: msg 4 code: code 5 code-success-value: OK 6 success: success 7 previousPage: previousPage 8 nextPage: nextPage 9 pageSize: pageSize 10 hasNext: hasNext 11 totalPageSize: totalPageSize 12 data: info

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

1rest-api: 2 enabled: true 3 msg: msg1 4 code: code1 5 code-success-value: 200 6 success: success1 7 previousPage: previousPage1 8 nextPage: nextPage1 9 pageSize: pageSize1 10 hasNext: hasNext1 11 totalPageSize: totalPageSize1 12 data: info

对应返回内容

1{ 2 "success": true, 3 "code": "OK", 4 "msg": "操作成功", 5 "requestId": "ztf4S-lP9yrtKPSiwldZ", 6 "timestamp": "2023-07-11 13:46:53", 7 "data": { 8 "previousPage": 1, 9 "nextPage": 1, 10 "pageSize": 1, 11 "totalPageSize": 1, 12 "hasNext": "false", 13 "pageData": [ 14 { 15 "name": "judy", 16 "hobby": "swing", 17 "age": 18 18 } 19 ] 20 } 21}

自定义返回

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

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

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

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

OpenApi文档生成

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

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

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

  3. 接口文档属性信息

1 openapi: 2 description: 3 title: 4 version: 5 license: 6 contact: 7 name: 8 email: 9 url:

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

1springdoc: 2 swagger-ui: 3 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 的指定。

1# urls 2springdoc: 3 swagger-ui: 4 urls: 5 - { name: 'sample', url: '/v3/api-docs' }

或者

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

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

即可进入 Knife4j 的 Swagger UI 页面。

全局错误拦截,参数校验

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

如请求错误

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

请求资源不存在等

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

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

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

参数校验错误

验证Studen对象参数

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

请求结果

JSON Body参数

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

错误国际化

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

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

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

1 i18n: 2 # 若前端无header传参则返回中文信息 3 i18n-header: Lang 4 default-lang: cn 5 message: 6 # admin 7 internal_server_error: 8 en: Internal Server Error 9 cn: 系统错误 10 not_found: 11 en: Not Found 12 cn: 请求资源不存在

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

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

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

日志链路追踪

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

1{ 2 "msg": "操作成功", 3 "code": "OK", 4 "previousPage": 1, 5 "success": true, 6 "requestId": "udYNdbbMFE45R84OPu9m", 7 "nextPage": 1, 8 "pageSize": 1, 9 "totalPageSize": 1, 10 "hasNext": "false", 11 "timestamp": "2023-07-09 03:00:27", 12 "info": [ 13 { 14 "name": "judy", 15 "hobby": "swing", 16 "age": 18 17 } 18 ] 19}

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

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

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

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

12023-07-13 11:21:25 INFO http-nio-8888-exec-2 cn.soboys.restapispringbootstarter.aop.LimitAspect IP:192.168.1.81 次访问key为 [_kenx:chat192.168.1.8],描述为 [接口限流] 的接口 22023-07-13 11:21:26 INFO http-nio-8888-exec-2 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource { 3 "description": "日志记录测试", 4 "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.chatDialogue()", 5 "params": { 6 }, 7 "logType": "INFO", 8 "requestIp": "192.168.1.8", 9 "path": "/chat", 10 "address": "0|0|0|内网IP|内网IP", 11 "time": 128, 12 "os": "Mac", 13 "browser": "Chrome", 14 "result": { 15 "success": true, 16 "code": "OK", 17 "msg": "操作成功", 18 "requestId": "5RgKzWGFNa9XSPwhw2Pi", 19 "timestamp": "2023-07-13 11:21:25", 20 "data": "接口限流测试" 21 }, 22 "apiType": "USER", 23 "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" 24}

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

1package cn.soboys.restapispringbootstarter.log; 2 3import org.springframework.scheduling.annotation.Async; 4 5import java.util.Map; 6 7/** 8 * @Author: kenx 9 * @Since: 2021/6/23 13:55 10 * @Description: 11 */ 12public interface LogDataSource { 13 14 /** 15 * 获取拓展数据 16 * @return 17 * @param logEntry 18 */ 19 @Async 20 void save(LogEntry logEntry); 21} 22

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

1@Slf4j 2public class LogFileDefaultDataSource implements LogDataSource { 3 4 /** 5 * 自定义保存数据源 6 * 7 * @param 8 * @return LogEntry 9 */ 10 @Override 11 public void save(LogEntry logEntry) { 12 log.info(JSONUtil.toJsonPrettyStr(logEntry)); 13 } 14}

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

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

Ip城市记录

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

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

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

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

1 ip2region: 2 external: false 3 location: classpath:ip2region/ip2region.xdb

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

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

属性配置

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

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

默认配置

1rest-api: 2 enabled: false 3 msg: msg 4 code: code 5 code-success-value: OK 6 success: success 7 previousPage: previousPage 8 nextPage: nextPage 9 pageSize: pageSize 10 hasNext: hasNext 11 totalPageSize: totalPageSize 12 data: info 13 include-packages: cn.soboys.superaide.controller 14 exclude-packages: xx.xxx.xxx 15 redis: 16 key-prefix: rest 17 openapi: 18 description: 19 title: 20 version: 21 license: 22 contact: 23 name: 24 email: 25 url: 26 logging: 27 path: ./logs #日志存储路径(服务器上绝对) 28 max-history: 90 # 保存多少天 29 max-file-size: 3MB # 每个文件大小 30 max-total-size-cap: 1GB #总文件大小超过多少压缩 31 level-root: INFO # 这里的INFO可以替换为其他日志等级,如DEBUG, WARN, ERROR, TRACE, FATAL, OFF等。 日志等级由低到高分别是debugger-info-warn-error 32 logDataSourceClass: cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource # 日志数据源 33 i18n: 34 # 若前端无header传参则返回中文信息 35 i18n-header: Lang 36 default-lang: cn 37 message: 38 # admin 39 internal_server_error: 40 en: Internal Server Error 41 cn: 系统错误 42 bad_gateway: 43 en: Bad Gateway 44 cn: 错误的请求 45 unauthorized: 46 en: Unauthorized 47 cn: 未授权 48 forbidden: 49 en: Forbidden 50 cn: 资源禁止访问 51 method_not_allowed: 52 en: Method Not Allowed 53 cn: 方法不被允许 54 request_timeout: 55 en: Request Timeout 56 cn: 请求超时 57 invalid_argument: 58 en: Invalid Argument {} 59 cn: 参数错误 {} 60 argument_analyze: 61 en: Argument Analyze {} 62 cn: 参数解析异常 {} 63 business_exception: 64 en: Business Exception 65 cn: 业务错误 66 not_found: 67 en: Not Found 68 cn: 请求资源不存在 69

代码生成配置

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

1package cn.soboys.restapispringbootstarter.config; 2 3import lombok.Data; 4 5/** 6 * @author 公众号 程序员三时 7 * @version 1.0 8 * @date 2023/7/5 00:05 9 * @webSite https://github.com/coder-amiao 10 */ 11@Data 12public class GenerateCodeConfig { 13 /** 14 * 数据库驱动 15 */ 16 private String driverName; 17 /** 18 * 数据库连接用户名 19 */ 20 private String username; 21 /** 22 * 数据库连接密码 23 */ 24 private String password; 25 /** 26 * 数据库连接url 27 */ 28 private String url; 29 /** 30 * 生成代码 保存路径。默认当前项目下。 31 * 如需修改,使用觉得路径 32 */ 33 private String projectPath; 34 /** 35 * 代码生成包位置 36 */ 37 private String packages; 38} 39

RestFull API

Controller中直接使用

1@PostMapping("/chat") 2public HashMap chatDialogue() { 3 HashMap m = new HashMap(); 4 m.put("age", 26); 5 m.put("name", "Judy"); 6 return m; 7}

Result构建返回

1@PostMapping("/chat") 2public Result chatDialogue() { 3 HashMap m = new HashMap(); 4 m.put("age", 26); 5 m.put("name", "Judy"); 6 return Result.buildSuccess(m); 7}

分页支持

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

1@PostMapping("/page") 2@Log("分页查询用户数据") 3public Result page(@Validated EntityParam s) { 4 ResultPage<List<EntityParam>> resultPage=new ResultPage<>(); 5 List a=new ArrayList(); 6 a.add(s); 7 resultPage.setPageData(a); 8 return ResultPage.buildSuccess(resultPage); 9}
  1. 构建自定义自己的分页数据
1ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
  1. 通过ResultPage.buildSuccess(resultPage)进行构建返回

返回统一响应格式

1{ 2 "previousPage": 1, 3 "nextPage": 1, 4 "pageSize": 1, 5 "totalPageSize": 1, 6 "hasNext": "false", 7 "success": true, 8 "code": "OK", 9 "msg": "操作成功", 10 "requestId": "D9AMALgkZ6gVfe6Pi0Oh", 11 "timestamp": "2023-07-09 02:39:40", 12 "data": [ 13 { 14 "name": "judy", 15 "hobby": "swing", 16 "age": 18 17 } 18 ] 19}

自定义返回格式

1{ 2 "previousPage": 1, 3 "nextPage": 1, 4 "pageSize": 1, 5 "totalPageSize": 1, 6 "hasNext": "false", 7 "success": true, 8 "code": "OK", 9 "msg": "操作成功", 10 "requestId": "D9AMALgkZ6gVfe6Pi0Oh", 11 "timestamp": "2023-07-09 02:39:40", 12 "data": [ 13 { 14 "name": "judy", 15 "hobby": "swing", 16 "age": 18 17 } 18 ] 19}

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

1{ 2 "success": true, 3 "code": "OK", 4 "msg": "操作成功", 5 "requestId": "ztf4S-lP9yrtKPSiwldZ", 6 "timestamp": "2023-07-11 13:46:53", 7 "data": { 8 "previousPage": 1, 9 "nextPage": 1, 10 "pageSize": 1, 11 "totalPageSize": 1, 12 "hasNext": "false", 13 "pageData": [ 14 { 15 "name": "judy", 16 "hobby": "swing", 17 "age": 18 18 } 19 ] 20 } 21}

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

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

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

1rest-api: 2 enabled: false 3 msg: msg 4 code: code 5 code-success-value: OK 6 success: success 7 previousPage: previousPage 8 nextPage: nextPage 9 pageSize: pageSize 10 hasNext: hasNext 11 totalPageSize: totalPageSize 12 data: info

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

1rest-api: 2 enabled: true 3 msg: msg1 4 code: code1 5 code-success-value: 200 6 success: success1 7 previousPage: previousPage1 8 nextPage: nextPage1 9 pageSize: pageSize1 10 hasNext: hasNext1 11 totalPageSize: totalPageSize1 12 data: info

对应返回内容

1{ 2 "success": true, 3 "code": "OK", 4 "msg": "操作成功", 5 "requestId": "ztf4S-lP9yrtKPSiwldZ", 6 "timestamp": "2023-07-11 13:46:53", 7 "data": { 8 "previousPage": 1, 9 "nextPage": 1, 10 "pageSize": 1, 11 "totalPageSize": 1, 12 "hasNext": "false", 13 "pageData": [ 14 { 15 "name": "judy", 16 "hobby": "swing", 17 "age": 18 18 } 19 ] 20 } 21}

自定义返回

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

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

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

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

错误国际化支持

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

1 i18n: 2 # 若前端无header传参则返回中文信息 3 i18n-header: Lang 4 default-lang: cn 5 message: 6 # admin 7 internal_server_error: 8 en: Internal Server Error 9 cn: 系统错误 10 bad_gateway: 11 en: Bad Gateway 12 cn: 错误的请求 13 unauthorized: 14 en: Unauthorized 15 cn: 未授权 16 forbidden: 17 en: Forbidden 18 cn: 资源禁止访问 19 method_not_allowed: 20 en: Method Not Allowed 21 cn: 方法不被允许 22 request_timeout: 23 en: Request Timeout 24 cn: 请求超时 25 invalid_argument: 26 en: Invalid Argument {} 27 cn: 参数错误 {} 28 argument_analyze: 29 en: Argument Analyze {} 30 cn: 参数解析异常 {} 31 business_exception: 32 en: Business Exception 33 cn: 业务错误 34 not_found: 35 en: Not Found 36 cn: 请求资源不存在

可以自行覆盖扩充

全局错误拦截和响应

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

1<!--参数校验--> 2<dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-validation</artifactId> 5</dependency>

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

第三方请求

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

在需要使用地方注入RestFulTemp

1@Resource 2private RestFulTemp restFulTemp;

GET请求

1@GetMapping("/doGet") 2public Result doGet() { 3 ResponseEntity<String> response = restFulTemp.doGet("http://127.0.0.1:9000/redis/get"); 4 return Result.buildSuccess(); 5}

POST 请求

1/** 2 * POST 请求参 数为body json体格式 3 * @return 4 */ 5@PostMapping("/doPost") 6public Result doPost() { 7 Student s=new Student(); 8 s.setHobby("swing"); 9 s.setNam("judy"); 10 //自动把对象转换为JSON 11 ResponseEntity<String> response = 12 restFulTemp.doPost("http://127.0.0.1:9000/redis/get",s); 13 return Result.buildSuccess(); 14}
1/** 2 * POST请求 参数为FORM 表单参数 3 * @return 4 */ 5@PostMapping("/doPost") 6public Result doPostForm() { 7 EntityParam s=new EntityParam(); 8 s.setAge(19); 9 s.setHobby("swing"); 10 s.setName("judy"); 11 12 ResponseEntity<String> response = 13 restFulTemp.doPostForm("http://127.0.0.1:8000/chat", BeanUtil.beanToMap(s)); 14 return Result.buildSuccess(response.getBody()); 15}

DELETE请求

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

PUT请求

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

错误异常自定义

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

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

1package cn.soboys.restapispringbootstarter; 2 3import cn.soboys.restapispringbootstarter.i18n.I18NKey; 4 5/** 6* @author 公众号 程序员三时 7* @version 1.0 8* @date 2023/6/26 10:21 9* @webSite https://github.com/coder-amiao 10* 响应码接口,自定义响应码,实现此接口 11*/ 12public interface ResultCode extends I18NKey { 13 14 String getCode(); 15 16 String getMessage(); 17 18}

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

1package cn.soboys.restapispringbootstarter; 2 3import cn.soboys.restapispringbootstarter.i18n.I18NKey; 4 5/** 6 * @author 公众号 程序员三时 7 * @version 1.0 8 * @date 2023/6/26 11:01 9 * @webSite https://github.com/coder-amiao 10 */ 11public enum HttpStatus implements ResultCode, I18NKey { 12 /** 13 * 系统内部错误 14 */ 15 INTERNAL_SERVER_ERROR("500", "internal_server_error"), 16 BAD_GATEWAY("502", "bad_gateway"), 17 NOT_FOUND("404", "not_found"), 18 UNAUTHORIZED("401", "unauthorized"), 19 FORBIDDEN("403", "forbidden"), 20 METHOD_NOT_ALLOWED("405", "method_not_allowed"), 21 REQUEST_TIMEOUT("408", "request_timeout"), 22 23 INVALID_ARGUMENT("10000", "invalid_argument"), 24 ARGUMENT_ANALYZE("10001", "argument_analyze"), 25 BUSINESS_EXCEPTION("20000", "business_exception"); 26 27 28 private final String value; 29 30 private final String message; 31 32 HttpStatus(String value, String message) { 33 this.value = value; 34 this.message = message; 35 } 36 37 38 @Override 39 public String getCode() { 40 return value; 41 } 42 43 @Override 44 public String getMessage() { 45 return message; 46 } 47 48 49 @Override 50 public String key() { 51 return message; 52 } 53} 54 55
1rest-api: 2 enabled: false 3 i18n: 4 # 若前端无header传参则返回中文信息 5 i18n-header: Lang 6 default-lang: cn 7 message: 8 # admin 9 internal_server_error: 10 en: Internal Server Error 11 cn: 系统错误 12 bad_gateway: 13 en: Bad Gateway 14 cn: 错误的请求 15 unauthorized: 16 en: Unauthorized 17 cn: 未授权 18 forbidden: 19 en: Forbidden 20 cn: 资源禁止访问 21 method_not_allowed: 22 en: Method Not Allowed 23 cn: 方法不被允许 24 request_timeout: 25 en: Request Timeout 26 cn: 请求超时 27 invalid_argument: 28 en: Invalid Argument {} 29 cn: 参数错误 {} 30 argument_analyze: 31 en: Argument Analyze {} 32 cn: 参数解析异常 {} 33 business_exception: 34 en: Business Exception 35 cn: 业务错误 36 not_found: 37 en: Not Found 38 cn: 请求资源不存在 39

业务断言

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

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

1package cn.soboys.restapispringbootstarter.exception; 2 3import cn.soboys.restapispringbootstarter.HttpStatus; 4import cn.soboys.restapispringbootstarter.ResultCode; 5import lombok.Data; 6 7/** 8 * @author 公众号 程序员三时 9 * @version 1.0 10 * @date 2023/6/26 16:45 11 * @webSite https://github.com/coder-amiao 12 */ 13@Data 14public class BusinessException extends RuntimeException { 15 16 /** 17 * 错误码 18 */ 19 private String code="20000"; 20 21 /** 22 * 错误提示 23 */ 24 private String message; 25 26 27 public BusinessException(String message) { 28 this.message = message; 29 30 } 31 32 public BusinessException(String message, String code) { 33 this.message = message; 34 this.code = code; 35 36 } 37 38 public BusinessException(ResultCode resultCode) { 39 this.message = resultCode.getMessage(); 40 this.code = resultCode.getCode(); 41 42 } 43} 44

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

1<!--日志集成--> 2<dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-logging</artifactId> 5</dependency>

默认日志配置

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

日志记录与追踪

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

1{ 2 "msg": "操作成功", 3 "code": "OK", 4 "previousPage": 1, 5 "success": true, 6 "requestId": "udYNdbbMFE45R84OPu9m", 7 "nextPage": 1, 8 "pageSize": 1, 9 "totalPageSize": 1, 10 "hasNext": "false", 11 "timestamp": "2023-07-09 03:00:27", 12 "info": [ 13 { 14 "name": "judy", 15 "hobby": "swing", 16 "age": 18 17 } 18 ] 19}

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

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

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

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

12023-07-09 03:00:32 INFO http-nio-8000-exec-2 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource { 2 "description": "查询用户数据", 3 "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.page()", 4 "logType": "INFO", 5 "time": 3, 6 "result": { 7 "success": true, 8 "code": "OK", 9 "msg": "操作成功", 10 "requestId": "udYNdbbMFE45R84OPu9m", 11 "timestamp": "2023-07-09 03:00:27", 12 "data": { 13 "previousPage": 1, 14 "nextPage": 1, 15 "pageSize": 1, 16 "totalPageSize": 1, 17 "hasNext": "false", 18 "pageData": [ 19 { 20 "name": "judy", 21 "hobby": "swing", 22 "age": 18 23 } 24 ], 25 "requestId": "qJTOejQmY-OOf7fagegB", 26 "timestamp": "2023-07-09 03:00:27" 27 } 28 }, 29 "apiType": "USER" 30} 312023-07-09 03:08:03 INFO http-nio-8000-exec-4 cn.soboys.restapispringbootstarter.log.LogFileDefaultDataSource { 32 "description": "查询用户数据", 33 "method": "cn.soboys.restapispringbootstarter.controller.ApiRestController.page()", 34 "logType": "INFO", 35 "time": 1, 36 "result": { 37 "success": true, 38 "code": "OK", 39 "msg": "操作成功", 40 "requestId": "kP3yPP-H7wI2x1ak6YFA", 41 "timestamp": "2023-07-09 03:00:27", 42 "data": { 43 "previousPage": 1, 44 "nextPage": 1, 45 "pageSize": 1, 46 "totalPageSize": 1, 47 "hasNext": "false", 48 "pageData": [ 49 { 50 "name": "judy", 51 "hobby": "swing", 52 "age": 18 53 } 54 ], 55 "requestId": "pGbbiEj8GQ1eTxQpF2Jr", 56 "timestamp": "2023-07-09 03:00:27" 57 } 58 }, 59 "apiType": "USER" 60}

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

1package cn.soboys.restapispringbootstarter.log; 2 3import org.springframework.scheduling.annotation.Async; 4 5import java.util.Map; 6 7/** 8 * @Author: kenx 9 * @Since: 2021/6/23 13:55 10 * @Description: 11 */ 12public interface LogDataSource { 13 14 /** 15 * 获取拓展数据 16 * @return 17 * @param logEntry 18 */ 19 @Async 20 void save(LogEntry logEntry); 21} 22

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

1@Slf4j 2public class LogFileDefaultDataSource implements LogDataSource { 3 4 /** 5 * 自定义保存数据源 6 * 7 * @param 8 * @return LogEntry 9 */ 10 @Override 11 public void save(LogEntry logEntry) { 12 log.info(JSONUtil.toJsonPrettyStr(logEntry)); 13 } 14}

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

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

缓存和redis

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

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

1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-data-redis</artifactId> 4</dependency>

项目使用

注入redisTempUtil

1@Autowired 2private RedisTempUtil redisTempUtil;

如示列

1@Autowired 2private RedisTempUtil redisTempUtil; 3 4@GetMapping("/redis") 5public Result chatDialogue( ) { 6 redisTempUtil.set("test","111"); 7 redisTempUtil.get("test"); 8 redisTempUtil.set("user","userObj",7200l); 9 redisTempUtil.getAllKey("xx"); //*表达式 10 redisTempUtil.clean(); 11 redisTempUtil.deleteObject("test"); 12 redisTempUtil.hasKey(""); 13 return Result.buildSuccess(); 14}

统一缓存管理

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

1package cn.soboys.restapispringbootstarter.cache; 2 3import lombok.Getter; 4 5/** 6 * @author 公众号 程序员三时 7 * @version 1.0 8 * @date 2023/7/2 11:04 9 * @webSite https://github.com/coder-amiao 10 * 缓存枚举 11 */ 12@Getter 13public enum CacheKey implements CacheTmp { 14 15 16 // 密码的重置码 17 PWD_RESET_CODE("reset:code:", true), 18 ; 19 20 private String key; 21 22 /** 23 * Key是否是Key前缀, true时直接取key=key,如果false时key=key+suffix 24 */ 25 private boolean hasPrefix; 26 27 CacheKey(String key, boolean hasPrefix) { 28 this.key = key; 29 this.hasPrefix = hasPrefix; 30 } 31 32 33 @Override 34 public Boolean getHasPrefix() { 35 return this.hasPrefix; 36 } 37 38 @Override 39 public String getKey() { 40 return this.key; 41 } 42 43}

使用

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

spring Cache实现

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

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

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

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

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

redis配置

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

1 redis: 2 key-prefix: rest

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

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

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

OpenApi文档生成

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

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

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

  3. 接口文档属性信息

1 openapi: 2 description: 3 title: 4 version: 5 license: 6 contact: 7 name: 8 email: 9 url:

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

1springdoc: 2 swagger-ui: 3 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 的指定。

1# urls 2springdoc: 3 swagger-ui: 4 urls: 5 - { name: 'sample', url: '/v3/api-docs' }

或者

1#分组 2springdoc: 3 group-configs: 4 - { 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代码生成需自行单独引入 :::

1<dependency> 2 <groupId>com.baomidou</groupId> 3 <artifactId>mybatis-plus-generator</artifactId> 4 <version>3.4.1</version> 5</dependency> 6<!-- MySQL --> 7<dependency> 8 <groupId>mysql</groupId> 9 <artifactId>mysql-connector-java</artifactId> 10 <version>8.0.28</version> 11</dependency> 12<!--代码生成依赖的模板引擎--> 13<dependency> 14 <groupId>org.freemarker</groupId> 15 <artifactId>freemarker</artifactId> 16 <version>2.3.31</version> 17</dependency>

项目使用

  1. 代码生成配置类
1package cn.soboys.restapispringbootstarter.config; 2 3import lombok.Data; 4 5/** 6 * @author 公众号 程序员三时 7 * @version 1.0 8 * @date 2023/7/5 00:05 9 * @webSite https://github.com/coder-amiao 10 */ 11@Data 12public class GenerateCodeConfig { 13 /** 14 * 数据库驱动 15 */ 16 private String driverName; 17 /** 18 * 数据库连接用户名 19 */ 20 private String username; 21 /** 22 * 数据库连接密码 23 */ 24 private String password; 25 /** 26 * 数据库连接url 27 */ 28 private String url; 29 /** 30 * 生成代码 保存路径。默认当前项目下。 31 * 如需修改,使用绝对路径 32 */ 33 private String projectPath; 34 /** 35 * 代码生成包位置 36 */ 37 private String packages; 38}

示列如:

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

常见问题

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

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

点赞
收藏

评论区

加载中...

相关推荐

SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决

前言我们在开发springboot项目时候,创建好SpringBoot项目就可以通过启动类直间启动,运行一个web项目,非常方便简单,不像我们之前通过SpringSpringMvc要运行启动一个web项目还需要要配置各种包扫描和tomcat才能启动我将应用分成了parentcommoncomponentapp这种模式,1.parent是一个单纯的p

SpringBoot定义优雅全局统一Restful API 响应框架

假如现在有一个Java项目,老板让你做项目组长,定义项目基础框架,系统技术架构选型,你应该如何设计一个规范的统一的RestfulAPI响应框架呢思考目前项目开发,都是基于前后端分离模式开发的,基于后端模板引擎那一套,可能已经不适用一些项目开发流程,和当下开

mybatis的mapper特殊字符转移以及动态SQL条件查询

前言我们知道在项目开发中之前使用数据库查询,都是基于jdbc,进行连接查询,然后是高级一点jdbcTemplate进行查询,但是我们发现还是不是很方便,有大量重复sql语句,与代码偶合,效率低下,于是就衍生出来ORM框架,如Mybatis,Hibernate,还有SpringBoot的,SpringDataJPA条件查询我们知道在mybatisma

SprintBoot配置Filter、Servlet

今天想要手动的实现以下Filter权限控制,但是发现使用springboot的filter和之前的不一样,之前使用Servlet3.0只需要使用@WebFilter注解或者在web.xml文件中配置以下即可。但是springboot中没有web.xml文件,于是我很自然的使用注解,然后发现怎么也配置不上去,那么怎么办呢,没错,查文档。经过一番查阅,原来s

Spring Boot 与 Kotlin 使用JdbcTemplate连接MySQL

之前介绍了一些Web层的例子,包括构建RESTfulAPI、使用Thymeleaf模板引擎渲染Web视图,但是这些内容还不足以构建一个动态的应用。通常我们做App也好,做Web应用也好,都需要内容,而内容通常存储于各种类型的数据库,服务端在接收到访问请求之后需要访问数据库获取并处理成展现给用户使用的数据形式。本文介绍在SpringBoot基础下配置数

@Autowired和@Resource注解的一个意外重要区别

今天上午,因为公司要跟客户展示最近开发的项目,然后安排了我重新构建一个template项目,用来向客户展示参考。基于已开发好的代码,我在进行一些简化抽取的时候出现了一个有趣的问题因为我们有一个springsecurity配置类时需要每个模块都使用,就是可能有些参数不同,现在我把他弄到一个公共的jar包,把之前类拷贝进去,然后把参数写活,结果出现了一些有