I open sourced the API scaffolding stater based on the rapid development of SpringBoot Web within the team

We are now using SpringBoot for web development, which is much more powerful than the previous SprngMvc set.
However, using SpringBoot Web for API development is still not concise enough.

Common functions of Web API need to be rewritten every time. Or copy the previous project code. So I encapsulated such a

Extract the modules and necessary functions that must be rewritten for each project of SpringBoot Web API .
And it expands all the tool libraries I use in my work.

Based on it, you can easily develop SpringBoot WEB API and improve efficiency. Don't care about some cumbersome things anymore. Duplicate work, but focus on business.

The current updated version to 1.5.2 features are as follows

  1. Support one-click configuration to customize RestFull API uniform format return
  2. Support RestFull API error internationalization
  3. Support global exception handling, global parameter verification processing
  4. Encapsulation of business error assertion tools, following the principle of returning errors first
  5. Encapsulate Redis key, value operation tool class. Unified key management spring cache cache implementation
  6. RestTemplate encapsulates POST, GET request tool
  7. Log integration. Customize the log path, classify according to the log level, support compression and file size segmentation. display by time
  8. The tool library integrates lombok, hutool, commons-lang3, and guava. No need to import them individually
  9. Integrated mybatisPlus one-click code generation
  10. Log records, service monitoring, support log link query. custom data source
  11. One-click configuration of OpenApi3 documents. Support for multiple documents and auto-configuration
  12. Interface current limit, IP city echo
  13. HttpUserAgent request device tool package
  14. RequestUtil parameter parsing and encapsulation tool

It will continue to be updated in the future. Reusable, must-have modules and tools in projects.

rest-api-spring-boot-starter is suitable for the rapid construction of SpringBoot Web API, allowing developers to quickly build a unified and standardized business RestFull API without worrying about some tediousness. Duplicate work, but focus on business.

quick start

  1. Introduce dependencies into the project pom
<dependency>
    <groupId>cn.soboys</groupId>
    <artifactId>rest-api-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>
  1. Enable rest-api via the @EnableRestFullApi annotation on the SpringBoot startup class or configuration class

@SpringBootApplication
@EnableRestFullApi
public class SuperaideApplication {
    
    

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

At this point you can use all the functions in your project.

RestFull API

In Controllerit, we write common request interfaces such as:

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

What is returned is the global unified RestFull API

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

Can also be Resultbuilt based on

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

pagination support

Our daily pagination is a special return. Also very commonly used.

@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. Build your own custom pagination data
ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
  1. ResultPage.buildSuccess(resultPage)return by doing a build

Return the unified response format

{
    
    
    "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
        }
    ]
}

Custom return format

{
    
    
    "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
        }
    ]
}

The above unified return format may not conform to the unified format of the interface in your project, such as:

{
    
    
    "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
            }
        ]
    }
}

pageThe pagination data is datainside you can define pageWrapthe property truewrapper to return the defined pageDatavalue keysuch as recordsetc.

You need to customize keyas msgyou may correspond message, successyou may correspond statusonly need to configure custom in the configuration filekey

Custom Return Success Value Your success return may be a value 200you can configurecode-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

When enabledenabled, it will read your custom configuration keysuch as

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

corresponding return content

{
    
    
    "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
            }
        ]
    }
}

custom return

Sometimes we need custom returns. Do not wrap the unified response RestFull APIformat

  1. @NoRestFulApiIt can be achieved through annotations such as
@GetMapping("/test")
@NoRestFulApi
public Map chatDialogue() {
    
    
    Map  m= new HashMap<>();
    m.put("name","judy");
    m.put("age",26);
    return m;
}
  1. By class scanning to achieve
    by default, the filter Stringtype is considered to be the page path.

Configuration files via properties include-packagesneed to return packages uniformly. exclude-packagesPackages that do not need to be returned uniformly

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

OpenApi document generation

Automatic support is already built in. swaggerdocument. and the latest OpenApi3 documentation. Accessible after project launch.

  1. swagger-ui.html documentation. path/swagger-ui.html

  2. Based on spring-doc document UI enhancement path/doc.html

  3. Interface document attribute information

  openapi:
    description:
    title:
    version:
    license: 
    contact:
      name:
      email:
      url: 
  • After starting the project, visit http://server:port/context-path/swagger-ui.html to enter the Swagger UI page, and the OpenAPI description will be provided in the following url in json format: http://server:port/context -path/v3/api-docs
  • server: domain name or IP
  • port: server port
  • context-path: the context path of the application, springboot is empty by default
  • Documentation is also available in yaml format at the following path: /v3/api-docs.yaml

If you don't like the official swagger-ui that is not beautiful, or you can't use it smoothly, you can choose to close the ui, and you can also eliminate the introduction of ui-related webjars.

springdoc:
  swagger-ui:
    enabled: false

OpenAPI document information can be obtained from this url by default: http://server:port/context-path/v3/api-docs.
Other tools that support the OpenAPI protocol can be used to display the API through this address, such as Apifox.
(Postman's api test can also use this address for import generation)

Knife4j (formerly swagger-bootstrap-ui) 3.x version provides partial support for the OpenAPI protocol.

::: tip
Knife4j is not implemented in accordance with the protocol specifications in many places, so there will be many problems when using it. In addition, the project has not been maintained for a long time, so it is not recommended to use.
:::

Since knife4j does not fully support specifications, it cannot directly use single-document source data, so grouping or urls must be specified.

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

or

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

The UI access address of Knife4j is different. The page is mapped under doc.htmlthe path. After starting the project, visithttp://server:port/context-path/doc.html

You can enter the Swagger UI page of Knife4j.

Global error interception, parameter verification

Help you encapsulate all common http errors, and all request parameter validation errors.

If the request is wrong

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

The requested resource does not exist, etc.

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

If you need to intercept the above error, please add it to the springboot configuration file

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

Parameter validation error

Validate Studen object parameters

/**
 * @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;
    }

request result

JSON Body parameter

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

wrong internationalization

The built-in encapsulation error supports both English and Chinese internationalization by default. You do not do any configuration to automatically support

If you need built-in support for more languages, just override it.

Customize your own error internationalization and language

  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 corresponds to the error prompt
and corresponds to internal_server_error customization.
The following language is defined by yourself and the front-end passes in the i18n-header. It will show that you have defined the wrong language

I don't pass the error. The default internationalization is Chinese. Configure it in default-lang: cn

When I pass in the specified language, it will return an error message according to the internationalization customization you configured

Log Link Tracking

The RestFull API uniformly returns a requestId unique identifier for each interface. Used for interface request log link tracking. Log query. like:

{
    
    
    "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
        }
    ]
}

By requestId you can easily locate each wrong request in your log file query.

Document that you want to document the request via Logannotation

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

The default logging data source of the system is the log file. like

2023-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],描述为 [接口限流] 的接口
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"
}

You can customize your own log data source to implement LogDataSourcethe interface log operation supports asynchronous. Needed in configuration class. Or add @EnableAsync
annotations to the startup class

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

Or you can inherit my default log data source implementation class LogFileDefaultDataSourceto override save(LogEntry logEntry)the method.

@Slf4j
public class LogFileDefaultDataSource implements LogDataSource {
    
    

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

If it is a custom log data source implementation, you need to reconfigure the file and configure the log data source. like:

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 City Records

Logging provides Ipcity echo records

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

By configuring ipCitythe properties, by default, truethe detailed information of the physical address corresponding to the IP will be recorded, that is, the country, city and other
Ipcities can be obtained through ip2regionquery.

You can configure properties locationto configure your own ip2region.xdbfiles.

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

If it is not configured by default, it will automatically generate one for you. Based on the latest data of 2.7.x,
get the latest custom ip data Github warehouse

Of course, it also helps you package. You can use HttpUserAgentthe static method of the tool class getIpToCityInfo(String ip)to obtain and query the city information corresponding to the ip

attribute configuration

Configure language internationalization, logs, etc.,

::: tip
does not need to configure any parameters by default. The default will be used, and the configuration in your project will be used if configured.
:::

default allocation

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: 请求资源不存在

Code generation configuration

Supports MybatisPlusone-click generation of code, does not introduce MybatisPlusgeneration dependencies by default and needs to be manually introduced

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

Directly used in Controller

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

Result build returns

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

pagination support

Our daily pagination is a special return. Also very commonly used.

@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. Build your own custom pagination data
ResultPage<List<EntityParam>> resultPage=new ResultPage<>();
  1. ResultPage.buildSuccess(resultPage)return by doing a build

Return the unified response format

{
    
    
    "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
        }
    ]
}

Custom return format

{
    
    
    "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
        }
    ]
}

The above unified return format may not conform to the unified format of the interface in your project, such as:

{
    
    
    "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
            }
        ]
    }
}

pageThe pagination data is datainside you can define pageWrapthe property truewrapper to return the defined pageDatavalue keysuch as recordsetc.

You need to customize keyas msgyou may correspond message, successyou may correspond statusonly need to configure custom in the configuration filekey

Custom Return Success Value Your success return may be a value 200you can configurecode-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

When enabledenabled, it will read your custom configuration keysuch as

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

corresponding return content

{
    
    
    "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
            }
        ]
    }
}

custom return

Sometimes we need custom returns. Do not wrap the unified response RestFull APIformat

  1. @NoRestFulApiIt can be achieved through annotations such as
@GetMapping("/test")
@NoRestFulApi
public Map chatDialogue() {
    
    
    Map  m= new HashMap<>();
    m.put("name","judy");
    m.put("age",26);
    return m;
}
  1. By class scanning to achieve
    by default, the filter Stringtype is considered to be the page path.

Configuration files via properties include-packagesneed to return packages uniformly. exclude-packagesPackages that do not need to be returned uniformly

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

error internationalization support

Common mistakes built in. You can see HttpStatus. The default error supports both Chinese and English internationalization. The configuration is as follows

  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: 请求资源不存在

Can be self-covered and extended

Global error interception and response

By default, all unknown error exceptions, validationparameter verification failure exceptions, and Http request exceptions are blocked. There is also the automatic integration of
global custom BusinessExceptionspring-boot-starter-validation business exceptions , and you do not need to introduce them separately in your project

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

There are also built-in extensions for many custom parameter verification references

third party request

Sometimes we need to call third-party interface services in our projects. Based on RestTemplate further encapsulation of direct POST, GET, requests.

Inject RestFulTemp where it needs to be used

@Resource
private RestFulTemp restFulTemp;

GET request

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

POST request

/**
 * 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 request

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

PUT request

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

Error exception customization

My built-in error exception and business exception may not be able to meet your own interface business exception needs. You can customize error exception class, and error response enumeration code.

Custom error enumeration needs to implement ResultCode interface

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

}

If you want to support internationalization, you need to implement the internationalization interface I18NKey. Please refer to my internal HttpStatus implementation.

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: 请求资源不存在

business assertion

Encapsulates the business error assertion tool. AssertFollow the error first return principle.

You need to customize your own business exceptions. Inheritance BusinessException
Override the corresponding method

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

    }
}

Logs are very commonly used in projects, and they are still necessary. The integration has been automatically configured and spring-boot-starter-loggingyou don't need to introduce it separately in the project

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

Default log configuration

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 # 日志数据源

Logging and Tracing

The RestFull API uniformly returns a requestId unique identifier for each interface. Used for interface request log link tracking. Log query. like:

{
    
    
    "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
        }
    ]
}

By requestId you can easily locate each wrong request in your log file query.

Document that you want to document the request via Logannotation

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

The default logging data source of the system is the log file. like

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

You can customize your own log data source to implement LogDataSourcethe interface log operation supports asynchronous. Needed in configuration class. Or add @EnableAsync
annotations to the startup class

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

Or you can inherit my default log data source implementation class LogFileDefaultDataSourceto override save(LogEntry logEntry)the method.

@Slf4j
public class LogFileDefaultDataSource implements LogDataSource {
    
    

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

If it is a custom log data source implementation, you need to reconfigure the file and configure the log data source. like:

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 # 日志数据源

cache and redis

Caching usage in projects is very common. The most used is based on Rediscaching. So I encapsulated Rediscommon operations for Key and Value.

::: tip
does not introduce Redis dependencies by default. If you want to use Redis, you need to import it separately
:::

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

project use

injectionredisTempUtil

@Autowired
private RedisTempUtil redisTempUtil;

as shown

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

Unified Cache Management

In the above, we directly redisTempUtildefine keyand store by ourselves directly through the tool class, which is not advisable if key很多随意定义就会很混乱. I provide a unified cache key management interface CacheTmpreference implementation CacheKeyBased on the enumeration form, all keys are managed centrally

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

}

use

  1. store for key
@GetMapping("/redis")
public Result chatDialogue() {
    
    
    CacheKey.PWD_RESET_CODE.valueSetAndExpire("test", 60l, TimeUnit.SECONDS, "judy");
    return Result.buildSuccess();
}
  1. Get the corresponding key
@GetMapping("/redis/get")
public Result redisGet() {
    
    
    String a = CacheKey.PWD_RESET_CODE.valueGet("judy");
    return Result.buildSuccess(a);
}

Spring Cache implementation

Encapsulates spring Cachethe configuration class or startup class in the project for further use @EnableCachingand can be used directly through annotations

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

Use of tool classes springCacheUtilSupports the use of methods that are not based on annotations

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

::: tip
does not introduce Redis dependencies by default, and the cache is implemented based on memory (after your project introduces redis dependencies, it will automatically switch the data source to Redis cache)
:::

redis configuration

Using one key for multiple projects or modules may cause confusion, so a global configuration key is provided.

  redis:
    key-prefix: rest 

Add a String type to the code key:testKey, and its actual key name stored in redis isrest:testKey

The configuration of the global key prefix does not affect other operations on the key. For example, when obtaining the corresponding value, it is still passed in testKeyinstead ofrest:testKey

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

OpenApi document generation

Automatic support is already built in. swaggerdocument. and the latest OpenApi3 documentation. Accessible after project launch.

  1. swagger-ui.html documentation. path/swagger-ui.html

  2. Based on spring-doc document UI enhancement path/doc.html

  3. Interface document attribute information

  openapi:
    description:
    title:
    version:
    license: 
    contact:
      name:
      email:
      url: 
  • After starting the project, visit http://server:port/context-path/swagger-ui.html to enter the Swagger UI page, and the OpenAPI description will be provided in the following url in json format: http://server:port/context -path/v3/api-docs
  • server: domain name or IP
  • port: server port
  • context-path: the context path of the application, springboot is empty by default
  • Documentation is also available in yaml format at the following path: /v3/api-docs.yaml

If you don't like the official swagger-ui that is not beautiful, or you can't use it smoothly, you can choose to close the ui, and you can also eliminate the introduction of ui-related webjars.

springdoc:
  swagger-ui:
    enabled: false

OpenAPI document information can be obtained from this url by default: http://server:port/context-path/v3/api-docs.
Other tools that support the OpenAPI protocol can be used to display the API through this address, such as Apifox.
(Postman's api test can also use this address for import generation)

Knife4j (formerly swagger-bootstrap-ui) 3.x version provides partial support for the OpenAPI protocol.

::: tip
Knife4j is not implemented in accordance with the protocol specifications in many places, so there will be many problems when using it. In addition, the project has not been maintained for a long time, so it is not recommended to use.
:::

Since knife4j does not fully support specifications, it cannot directly use single-document source data, so grouping or urls must be specified.

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

or

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

The UI access address of Knife4j is different. The page is mapped under doc.htmlthe path. After starting the project, visithttp://server:port/context-path/doc.html

You can enter the Swagger UI page of Knife4j.

code generation

In the project, we use mybatisor mybatisPlussome simple single-table business codes, adding, deleting, and changing. We can generate it with one click. No need to rewrite. I encapsulate mybatisPlusthe code generation tool

::: tip
does not introduce mybatisPluscode generation dependencies by default. If you want to use mybatisPluscode generation, you need to import it separately
:::

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

project use

  1. Code Generation Configuration Class
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;
}

For example:

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

common problem

Try to use the latest version during use. I will continue to update more content. It will be posted on my official account
Programmer Three Hours as soon as possible. Whole network with the same name

You can pay attention to the official account programmer three hours . Share with your heart and continue to output high-quality content. I hope it can bring you a little help

Guess you like

Origin blog.csdn.net/u011738045/article/details/131735222