1.四种API架构风格

两个单独的应用程序需要中介程序才能相互通信。因此,开发人员经常需要搭建桥梁——也就是应用程序编程接口(API),来允许一个系统访问另一个系统的信息或功能。为了快速、大规模地集成不同的应用程序,API 使用协议或规范来定义那些通过网络传输的消息的语义和信息。这些规范构成了 API 的体系结构。

1.1 RPC

RPC是远程过程调用(Remote Procedure Call)的缩写形式,远程过程调用是一种允许在不同上下文中远程执行函数的规范。RPC扩展了本地过程调用概念,并将其放在HTTP API的上下文中,使得远程过程调用如同调用本地方法一样简单。RPC代表性的框架有Grpc、Dubbo等等。

1.1.1 RPC工作机制

RPC的工作机制:客户端调用一个远程的过程,将参数和附加信息序列化为消息,然后将其发送至服务端。服务端接收消息后,将信息的内容进行反序列话,执行所请求的操作,然后将结果发送回客户端。最后客户端和服务端各自负责参数的序列化和反序列化。

1.1.2 RPC优缺点

RPC的优点:

  • 调用简单,清晰,透明。RPC不用像 REST 一样复杂,就像调用本地方法一样简单。
  • 高效低延迟、传输效率高。RPC可以基于TCP协议,也可以基于HTTP协议,使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率
  • 性能消耗低。高效的序列化协议可以支持高效的二进制传输。RPC可以基于thrift实现高效的二进制传输
  • 自带负载均衡和服务治理。RPC框架基本都自带了负载均衡策略,在服务治理方面,RPC能做到自动通知,不影响上游。

RPC的缺点:

  • 耦合性强。RPC跟底层系统是紧密耦合的,API 的抽象级别有助于其可重用性。API 与基础系统的耦合越紧密,对其他系统的可重用性就越差。RPC 与基础系统的紧密耦合不允许其在系统函数和外部 API 之间建立抽象层。这很容易引起安全问题,因为关于基础系统的细节实现很容易会泄漏到 API 中。RPC 的紧密耦合使得可伸缩性要求和松散耦合的团队难以实现。因此,客户端要么会担心调用特定端点的带来的任何可能的副作用,要么需要尝试弄清楚要调用的端点,因为客户端不了解服务器如何命名其函数。
  • 无法跨语言,平台敏感。Java 写的 RPC 微服务无法给 Python 调用,需要再实现一层 REST 来对外提供服务。

RPC应用场景:

  • RPC 适用于内网服务调用,对外提供服务推荐使用REST。
  • RPC适用于IO 密集的服务调用场景。
  • RPC适用于服务调用过于密集与复杂的场景。

    1.2 SOAP

    SOAP:SOAP 是一个 XML 格式的、高度标准化的网络通讯协议。在 XML-RPC 发布的一年后,SOAP 由微软发布、并继承了许多 XML-RPC 的特性。在 REST 紧随其后发布,一开始它们是被同时使用,但很快 REST 赢得了这次比赛,成为了更流行的协议。

    1.2.1 SOAP工作机制

    SOAP的消息由如下部分组成:

  • 一个信封标签:用于开始和结束每条消息。

  • 包含请求或响应的正文。
  • 一个标头:用于表示消息是否由某些规范或额外要求的来确认。
  • 故障通知:包含了可能在请求处理过程只能够发生的任何错误。

SOAP API 的逻辑由 Web 服务描述语言(WSDL)编写。该 API 描述语言定义了端点并描述了可以执行的所有过程。这使得不同的编程语言和 IDE 能够快速建立通信。SOAP 支持有状态和无状态消息传递。在有状态的情况下,服务器存储接收到的信息可能非常繁琐复杂。但这对于涉及多方和复杂交易的操作是合理的。

1.2.2 SOAP优缺点

SOAP的优点:

  • 独立于语言和平台 。内置创建 Web 服务的功能使得 SOAP 能够处理消息通信的同时发送独立于语言和平台响应。
  • 绑定到各种协议 。SOAP 在适用于多种场景的传输协议方面是十分灵活的。
  • 内置错误处理 。SOAP API 规范允许返回带有错误码及其说明的的 XML 重试消息。
  • 一系列的安全拓展 。SOAP 与 ES-Security 集成,因此 SOAP 可满足企业级事务要求。它在事务内部提供了隐私和完整性,同时允许在消息级别进行加密。

SOAP的缺点:

  • 仅使用 XML 。SOAP 消息包含大量的元数据,并且在请求和响应时仅支持繁冗的 XML 格式。
  • 重量级。由于 XML 文件的大小,SOAP 服务需要很大的带宽。
  • 非常专业化的知识。构建 SOAP API 服务器需要对所有涉及到的协议以及它们及其严格的限制都有很深的了解。
  • 乏味的消息更新。由于需要额外的工作来添加或者删除某个消息属性,这种死板的 SOAP 模式减慢了其被采用的速度。

SOAP的应用场景:

  • SOAP适用于过程调用。如果服务是作为一种功能提供,客户端调用服务是为了执行一个功能,推荐使用SOAP,比如常见的认证授权。而数据服务用REST。
  • 可以定义清晰明了的正式接口的情况下推荐使用SOAP,比如在企业应用中,系统间的耦合采用面向接口的方式。
  • 要更多的考虑非功能需求,比如安全、传输、协作等需求推荐使用SOAP。

    1.3 REST

    RESTful API如今是一种无需解释的 API 架构风格,它由一系列的架构约束所定义,旨在被广泛 API 使用者采用。
    当前最常见的 API 架构风格最初时由 Roy Fielding 在其博士论文中提出的。REST 使得服务端的数据可用,并以简单的格式(通常是 JSON 和 XML)来表示它。

    1.3.1 REST的工作机制

    REST 的定义并不像 SOAP 那样严格。RESTful 体系结构应该遵守如下六个体系结构约束:

  • 统一接口:无论设备或应用程序类型如何,都可以采用统一的方式与给定的服务端进行交互。

  • 无状态:请求本身包含处理该请求所需要的状态,并且服务端不存储与会话相关的任何内容。
  • 缓存。
  • 客户端 - 服务器体系结构:允许双方独立发展。
  • 应用程序的层级系统。
  • 服务端向客户端提供可执行代码的能力。

实际上,某些服务仅在某种程度上是 RESTful 的。而它们的内核采用了 RPC 样式,将较大的服务分解为资源,并有效地使用 HTTP 基础结构。但 REST 的关键部分是超媒体(又称 HATEOAS),是超文本作为应用程序状态引擎(Hypertext As The Enginer Of Application State)的缩写。基本来说,这意味着 REST API 在每个响应中都提供元数据,该元数据链接了有关如何使用该 API 的所有相关信息。这样便可以使客户端和服务端解耦。因此,API 提供者和 API 使用者都可以独立发展,而这并不会阻碍他们的交流。

1.3.2 RESTful优缺点

REST优点:

  • 客户端和服务端的解耦:由于 REST 尽可能地解耦了客户端和服务端,REST 相较于 RPC 可以提供更好的抽象性。具有抽象级别的系统能够封装其实现细节,以更好的标示和维持它的属性。这使得 REST API 足够灵活,可以随着时间的推移而发展,同时保持稳定的系统。
  • 可发现性:客户端和服务端之间的通信描述了所有内容,因此不需要外部文档即可了解如何与 REST API 进行交互。
  • 缓存友好:REST 重用了许多 HTTP 工具,也是唯一一种可以在 HTTP 层面上缓存数据的 API 架构风格。与其相对的是,在任何其他 API 上实现缓存都需要配置其他缓存模块。
  • 多种格式支持(跨语言):REST 拥有支持多种格式用于存储和交换数据的能力,这是它如今成为搭建公共 API 的主要选择的原因之一。

REST缺点:

  • 没有标准的 REST 结构:在构建 REST API 方面,没有具体的正确方法。如何对资源进行建模以及哪些资源需要建模取决于不同的情况。这使得 REST 在理论上很简单,但在实践中却很困难。
  • 庞大的负载:REST 会返回大量丰富的元数据,以便客户端可以仅从响应中了解有关应用程序状态的所有必要信息。对于具有大量带宽容量的大型网络系统来说,这种“啰嗦”的通信并不算很大的负载。但带宽容量并非总是足够的。这也是 Facebook 在 2012 年提出 GraphQL 架构风格的关键驱动因素。
  • 响应过度和响应不足问题。REST 的响应包含的数据会过多或不足,通常会导致客户端需要发送另一个请求。

RESTful应用场景:RESTful适用于公网传输、安全性高、跨语言等场景,例如PC端、移动端等客户端调用。

1.4 GraphQL

REST API 需要被多次调用才能返回所需要的资源。GraphQL 是一种用于 API 的查询语言,对 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,减少数据的冗余。

1.4.1 GraphQL的工作机制
GraphQL 从构建模式(Schema)开始。模式是对于用户可以在 GraphQL API 中进行的所有查询及其返回的所有类型的描述。模式构建非常困难,因为它需要使用模式定义语言(SDL)进行强类型化。因为在客户端进行查询之前已经定义好了模式,所以客户端可以验证其查询语句,以确保服务端能够对查询语句进行响应。在查询语句到达后端应用程序时,GraphQL 操作将根据整个模式进行解释,并向前端应用程序返回解析到的数据。API 向服务端发送一个庞大的查询,该 API 返回一个仅包含我们所需数据的 JSON 响应。

1.4.2 GraphQL优缺点

GraphQL的优点:
速度快:使用 GraphQL 的应用程序既快速又稳定,因为它们控制的是获取的数据,而不是服务器。
获取许多资源:GraphQL 查询不仅可以访问一种资源的属性,还可以平滑地跟踪它们之间的引用。虽然典型的 REST API 需要从多个 URL 加载,但 GraphQL API 可以在单个请求中获取您的应用程序所需的所有数据。即使在移动网络连接速度较慢的情况下,使用 GraphQL 的应用程序也可以很快。
一个端点:GraphQL API 是按照类型和字段组织的,而不是端点。从单个端点访问数据的全部功能。GraphQL 使用类型来确保应用程序只询问什么是可能的,并提供清晰和有用的错误。应用程序可以使用类型来避免编写手动解析代码。
可持续性:在不影响现有查询的情况下向 GraphQL API 添加新字段和类型。老化字段可以被弃用并从工具中隐藏。通过使用一个不断发展的版本,GraphQL API 使应用程序能够持续访问新功能,并鼓励使用更简洁、更易于维护的服务器代码。
无版本:返回数据的形状完全由客户端的查询决定,因此服务器变得更简单且易于泛化。添加新产品功能时,可以将其他字段添加到服务器,而现有客户端不受影响。当您停止使用旧功能时,相应的服务器字段可能会被弃用,但会继续运行。这种渐进的、向后兼容的过程消除了对递增版本号的需要。
强类型:GraphQL 查询的每个级别对应一个特定类型,每个类型描述一组可用字段。与 SQL 类似,这允许 GraphQL 在执行查询之前提供描述性错误消息。协议,而不是存储:服务器上的每个 GraphQL 字段都由任意函数支持。虽然 Facebook 正在构建 GraphQL 来支持新闻提要,但他们已经有了一个复杂的提要排名和存储模型,以及现有的数据库和业务逻辑。GraphQL 必须利用所有这些现有工作才能发挥作用,因此不会规定或提供任何后备存储。
自省:可以查询 GraphQL 服务器支持的类型。这为工具和客户端软件创建了一个强大的平台, 可以在这些信息的基础上构建静态类型语言的代码生成。

GraphQL的缺点:

  • 深度查询:GraphQL 不能无深度查询,所以如果你有一棵树并且想在不知道深度的情况下返回一个分支,你必须做一些分页。
  • 特定的响应结构:在 GraphQL 中,响应与查询的形状相匹配,因此如果您需要以非常特定的结构进行响应,则必须添加一个转换层来重塑响应。
  • 网络级缓存:由于 GraphQL 在 HTTP 上使用的常见方式(单个端点中的 POST),网络级缓存变得困难。解决它的一种方法是使用持久查询。
  • 处理文件上传:GraphQL 规范中没有关于文件上传的内容,并且突变不接受参数中的文件。为了解决这个问题,您可以使用其他类型的 API(如 REST)上传文件并将上传文件的 URL 传递给 GraphQL,或者将文件注入执行上下文,这样您就会在解析器函数中拥有该文件。
  • 不可预测的执行:GraphQL 的本质是您可以查询组合您想要的任何字段,但这种灵活性不是免费的。有一些问题值得了解,例如性能和 N+1 查询。
  • 额外的复杂度:如果你的服务暴露了一个非常简单的 API,GraphQL 只会增加额外的复杂性,所以一个简单的 REST API 会更好。

2.如何优雅的构建一个Rest full接口?

一个优雅的RESTful接口应具有可维护性、安全性和扩展性。大致分为领域模型、接口参数验证、接口统一响应体、接口错误处理、接口重试、api文档、api安全等方面构成。

2.1 领域模型

2.3 接口参数验证

接口参数校验。接口参数校验能大大提高应用的安全性和健壮性,如果缺少参数校验系统随时处于高危状态。Hibernate Validator是Java领域最为流行的验证库,由于使用SpringBoot的版本是2.3.5,spring-boot-starter-web依赖并没有包含Hibernate Validator依赖(之前的版本是包含的),所以需要我们手动引入Hibernate Validator依赖。

2.4 接口统一响应体

接口统一响应体。接口统一响应体能规范接口的响应结果,如果不这么做,响应结果五花八门,对于代码的可维护性是极差的。

2.5 接口错误处理

全局异常处理。SpringBoot支持全局异常处理,当程序发生错误或异常时全局异常处理能更友好的响应接口,而不是像以前的出现400、500等等提示不友好的结果。

2.6 接口重试

2.7 api文档

接口文档。如果一个接口没有说明,那么对于开发人员是懵逼的,好的接口文档能大大提高工作效率,减少撕逼。

2.8 api安全通讯

3.参数验证

3.1 为什么需要参数验证

在开发中参数校验能提高程序的健壮性,能大大减少程序出BUG的几率,一个请求发送到后端流程如下,前端会做一层参数校验,参数合法则请求到后端接口,传递的参数非法则不请求接口,但往往会有一些纰漏,因为请求的数据不一定百分之百合法,为了提交程序健壮性,后端的参数校验是最后一层保障。在开发中我们一般使用hibernate-validator(官网地址:http://hibernate.org/validator/)来提供参数校验,hibernate-validator实现了JSR-303规范,JSR是Java Specification Requests的缩写,意思是Java 规范提案,而JSR-303是java标准的验证规范,spring-boot-starter-web这个依赖包含了hibernate-validator,所以不需要我们再次引入hibernate-validator依赖,但在Springboot2.3.4以上版本(只试过2.3.4)中并未包含hibernate-validator,所以需要主动加入依赖。

hibernate-validator支持3种验证方式(下面会用注解形式验证参数):
(1).基于Java注解方式,优点是比较简单。
(2).基于Java Api方式和@Bean注解结合。
(3).基于ValidatorUtils工具类

3.2 hibernate-validator常用注解

注解 描述
@Null 必须为null
@NotNull 不能为null
@AssertTrue 必须为true
@AssertFalse 必须为false
@Min 必须为数字,其值大于或等于指定的最小值
@Max 必须为数字,其值小于或等于指定的最大值
@DecimalMin 必须为数字其值大于或等于指定的最小值
@DecimalMax 必须为数字,其值小于或等于指定的最大值
@Size 集合的长度
@Digits 必须为数字,其值必须再可接受的范围内
@Past 必须是过去的日期
@Future 必须是将来的日期
@Pattern 必须符合正则表达式
@Email 必须是邮箱格式
@Length 长度范围
@NotEmpty 不能为null,长度大于0
@Range 元素的大小范围
@NotBlank 不能为null,字符串长度大于0(限字符串)

@Pattern注解常用表达式,注意要转译
1 匹配首尾空格的正则表达式:(^\s)|(\s$)
2 整数或者小数:^[0-9]+.{0,1}[0-9]{0,2}$
3 只能输入数字:”^[0-9]$”。
4 只能输入n位的数字:”^\d{n}$”。
5 只能输入至少n位的数字:”^\d{n,}$”。
6 只能输入m~n位的数字:。”^\d{m,n}$”
7 只能输入零和非零开头的数字:”^(0|[1-9][0-9]
)$”。
8 只能输入有两位小数的正实数:”^[0-9]+(.[0-9]{2})?$”。
9 只能输入有1~3位小数的正实数:”^[0-9]+(.[0-9]{1,3})?$”。
10 只能输入非零的正整数:”^+?[1-9][0-9]$”。
11 只能输入非零的负整数:”^-[1-9][]0-9”
$。
12 只能输入长度为3的字符:”^.{3}$”。
13 只能输入由26个英文字母组成的字符串:”^[A-Za-z]+$”。
14 只能输入由26个大写英文字母组成的字符串:”^[A-Z]+$”。
15 只能输入由26个小写英文字母组成的字符串:”^[a-z]+$”。
16 只能输入由数字和26个英文字母组成的字符串:”^[A-Za-z0-9]+$”。
17 只能输入由数字、26个英文字母或者下划线组成的字符串:”^\w+$”。
18 验证用户密码:”^[a-zA-Z]\w{5,17}$”正确格式为:以字母开头,长度在6~18之间,只能包含字符、数字和下划线。
19 验证是否含有^%&’,;=?$\”等字符:”[^%&’,;=?$\x22]+”。
20 只能输入汉字:”^[\u4e00-\u9fa5]{0,}$”
21 验证Email地址:”^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)$”。
22 验证InternetURL:”^
[http://([\w-]+\.)+[\w-]+(/[\w-./?%&=](http://%28[/w-]+/.)+[/w-]+(/[/w-./?%&=)_]_)?$”。
23 验证电话号码:”^((\d{3,4}-)|\d{3.4}-)?\d{7,8}$”正确格式为:”XXX-XXXXXXX”、”XXXX-XXXXXXXX”、”XXX-XXXXXXX”、”XXX-XXXXXXXX”、”XXXXXXX”和”XXXXXXXX”。
24 验证身份证号(15位或18位数字):”^\d{15}|\d{18}$”。
25 验证一年的12个月:”^(0?[1-9]|1[0-2])$”正确格式为:”01”~”09”和”1”~”12”。
26 验证一个月的31天:”^((0?[1-9])|((1|2)[0-9])|30|31)$”正确格式为;”01”~”09”和”1”~”31”。
27 匹配中文字符的正则表达式: [\u4e00-\u9fa5]
28 匹配双字节字符(包括汉字在内):[^\x00-\xff]
29 应用:计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)
30 String.prototype.len=function(){return this.replace(/[^\x00-\xff]/g,”aa”).length;}
31 匹配空行的正则表达式:\n[\s| ]\r
32 匹配html标签的正则表达式:<(.
)>(.)<\/(.)>|<(.*)\/>

3.3 使用注解方式验证参数

在Student类属性上添加hibernate-validator注解定义验证规则:

  1. import lombok.ToString;
  2. import lombok.experimental.Accessors;
  3. import org.hibernate.validator.constraints.Range;
  4. import javax.validation.constraints.NotBlank;
  5. import javax.validation.constraints.Pattern;
  6. @Accessors(chain = true) //开启链式调用
  7. @AllArgsConstructor
  8. @NoArgsConstructor
  9. @Data
  10. @ToString
  11. public class Student {
  12. private Integer sid;
  13. @NotBlank(message ="stuName不能为空") //不能为空,字符串长度大于0(限字符串)
  14. private String stuName;
  15. @NotBlank
  16. private String stuNo;
  17. @Range(min = 0,max = 120,message ="age的值超出合法范围") //age的值在0到120之间,不然校验失败
  18. private int age;
  19. private Integer sex;
  20. private String address;
  21. @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}",message ="日期格式不正确") //校验日期格式,例如2020-02-02这种形式
  22. private String birthday;
  23. }

在Controller层验证参数是否合法,使用@Valid注解表示对该参数进行校验,如果对参数校验错误则会把错误信息注入到BindingResult中,例如下面:

package com.fly.controller;

import com.fly.entity.Student;
import com.fly.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@Validated
@RestController
public class StudentController {

    @Autowired
    private StudentService studentService;

    /*
    * @Valid注解表示对该参数进行校验,如果对参数校验错误则会把错误信息注入到BindingResult中
    * */
    @PostMapping("/addStudent")
    public String addStudent(@RequestBody @Valid Student student, BindingResult result){
        //判断绑定结果是否有错误
        if(result.hasErrors()){
            //遍历所有错误
            for (ObjectError allError : result.getAllErrors()) {
                //打印错误信息
                return "错误信息:"+allError.getDefaultMessage();
            }
        }
        int row=studentService.addStudent(student);
        return row>0?"添加成功!":"添加失败!";
    }
}

@Validated和@Valid都可用于参数验证,@Validated是Spring框架中的一个注解(Spring’s JSR-303 规范,是标准 JSR-303 的一个变种),@Valid是标准JSR-303规范,配合 BindingResult 可以直接提供参数验证结果。它们区别如下:
(1).@Validated注解具有分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,没有注明分组的属性默认分组Default.class,当一个类有部分有注明分组的属性和部分没有注明分组的属性时进行校验时注意是否需要添加Default.class,否则不会校验没有注明分组的属性。@Valid不支持分组。
(2).可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上,可以用在方法、构造函数、方法参数和成员属性(字段)上。

使用Postman测试接口并传入参数,由于Student的stuName属性验证规则不能为空且字符串长度大于0,Student的age属性验证规则为0到120范围之间,假设将stuName设为null,age值设为122。
aasdasd13.jpg
控制台抛出如下图错误,错误信息跟在验证注解的message一模一样。
Jietu20201112-115422.jpg

4.全局异常处理

当应用程序由于某种原因运行错误时,应用程序会抛出404、500等错误,为了提升应用程序健壮性与可用性,SpringBoot 支持使用@ControllerAdvice + @ExceptionHandler 进行全局异常处理。

@ControllerAdvice用于定义全局异常处理类。
@ExceptionHandler的作用主要在于声明一个或多个类型的异常,当符合条件的Controller抛出这些异常之后将会对这些异常进行捕获,然后按照其标注的方法的逻辑进行处理,从而改变返回的视图信息。

(1).首先自定义一个异常类,继承了Exception类即可

package com.fly.exception;
/*自定义异常类*/
public class CustomException extends Exception {
    /**异常信息*/
    private String message;
    /**异常原因*/
    private Throwable cause;
    public String getMessage(){
        return this.message;
    }
    public void setMessage(String message){
        this.message=message;
    }
    public Throwable getCause(){
        return this.cause;
    }
    public void setCause(Throwable cause){
         this.cause=cause;
    }
    public CustomException(){ }
    public CustomException(String message){
        super(message);
    }
    public CustomException(String message,Throwable cause){
        super(message,cause);
    }
}

(2).定义全局异常处理类

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
    /*声明一个CustomException类型的异常,当抛出CustomException异常时会对这些异常进行捕获*/
    @ExceptionHandler(CustomException.class)
    @ResponseBody
    public String customException(CustomException ex){
        System.out.println("异常信息:"+ex.getMessage());
        System.out.println("异常原因"+ex.getCause());
        return "自定义异常错误";
    }
}

5.接口统一响应结果

接口返回统一响应结果的优点在于接口更加规范,避免接口结果参差不齐,提升代码可维护性。

5.1 定义统一返回数据格式

DataPaging.class:

package com.fly.result;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.util.List;

/*数据分页类*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class DataPaging<T> {
    //当前页码
    private Integer current;
    //每页显示条数
    private Integer pageSize;
    //总条数
    private Integer total;
    //集合
    private List<T> list;

    //填充数据
    public DataPaging fillData(Integer current,Integer pageSize,List<T> list){
        this.current=current;
        this.pageSize=pageSize;
        this.total=list.size();
        this.list=list;
        return this;
    }
}

ResultCode.class:

package com.fly.result;

import org.springframework.http.HttpStatus;

/*响应状态码枚举类*/
public enum ResultCode {
    SUCCESS(HttpStatus.OK.value(),"Api操作成功"),
    FAIL(100,"Api操作失败"),
    ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(),"Api操作错误"),
    NOAUTHOR(HttpStatus.UNAUTHORIZED.value(),"资源未授权"),
    REQUEST_TIMEOUT(HttpStatus.REQUEST_TIMEOUT.value(),"Api超时"),
    GETEWAY_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT.value(),"网关超时"),
    Reject_Request(HttpStatus.NOT_ACCEPTABLE.value(),"拒绝请求")
    ;
    private final int code;
    private final String message;
    ResultCode(int code,String message){
        this.code=code;
        this.message=message;
    }
    public int getCode(){
        return  this.code;
    }
    public String getMessage(){
        return this.message;
    }
}

Result.class:

package com.fly.result;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Result<T>{
    //状态码
    private Integer code;
    //响应信息
    private String message;
    //响应数据主体
    private T data;

    public static Result success(String message,Object data){
        Result result=new Result();
        result.setMessage(message);
        result.setData(data);
        result.setCode(ResultCode.SUCCESS.getCode());
        return result;
    }
    public static Result success(Object data){
        Result result=new Result();
        result.setMessage("Api调用成功!");
        result.setData(data);
        result.setCode(ResultCode.SUCCESS.getCode());
        return result;
    }
    public static Result success(String msg){
        Result result=new Result();
        result.setMessage(msg);
        result.setCode(ResultCode.SUCCESS.getCode());
        return result;
    }
    public static Result error(String msg,Object data){
        Result result=new Result();
        result.setMessage(msg);
        result.setData(data);
        result.setCode(ResultCode.ERROR.getCode());
        return result;
    }
    public static Result error(Object data){
        Result result=new Result();
        result.setMessage("Api调用错误!");
        result.setData(data);
        result.setCode(ResultCode.ERROR.getCode());
        return result;
    }
    public static Result error(String message){
        Result result=new Result();
        result.setMessage(message);
        result.setCode(ResultCode.ERROR.getCode());
        return result;
    }
    public static Result create(Object data){
        Result result=new Result();
        result.setData(data);
        return result;
    }
    public static Result create(Object data,String msg){
        Result result=new Result();
        result.setData(data);
        result.setMessage(msg);
        return result;
    }
    public static Result create(Object data,String message,Integer code){
        Result result=new Result();
        result.setData(data);
        result.setMessage(message);
        result.setCode(code);
        return result;
    }
    public static Result create(String message,Integer code){
        Result result=new Result();
        result.setMessage(message);
        result.setCode(code);
        return result;
    }
}

经过这么封装Controller的方法全都返回Result类型的结果即可,但这里有个缺点,有些时候我们并不想Controller的方法返回Result类型,我们希望它可以是String、Integer甚至其他类型的对象,所以下面进行再次封装。

5.2 定义统一返回数据格式增强版

(1).首先定义一个注解来标识哪些类的方法需要进行返回值包装。注解没有内容纯来用标识。

package com.fly.annotation;
import org.springframework.web.bind.annotation.ResponseBody;
import java.lang.annotation.*;
/*
 * 表明此注解是生命周期是在运行时。注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
 */
@Retention(RetentionPolicy.RUNTIME)
/*
 * 作用于类和方法上
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
/**
 * @ResponseBody会返回的Object序列化成JSON字符串
 */
@ResponseBody
public @interface ResultBody {

}

(2).定义响应结果主体通知类。
ResultBodyAdvice.class:

package com.fly.result;

import com.fly.annotation.ResultBody;
import com.fly.exception.ResultException;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.annotation.Annotation;

//响应结果主体通知类
@RestControllerAdvice
public class ResultBodyAdvice implements ResponseBodyAdvice<Object> {

    private static final Class<? extends Annotation> ANNOTATION_TYPE= ResultBody.class;

    //判断类或方法上是否使用了@ResultBody注解
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(),ANNOTATION_TYPE)
                || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
    }


    /*
    * 当supports方法为true时就会调用此方法,相当于类或方法上使用了@ResultBody注解就会
    * 调用此方法
    * */
    @Override
    public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //如果方法的返回值是Result类型就直接返回,不做处理
        if(object instanceof  Result){
            return object;
        }
        return Result.success(object);
    }

    /*
    * 提供对标准Spring MVC异常的处理
    * ex表示当前异常
    * req表示当前请求
    * */
    @ExceptionHandler(Exception.class)
    public final Result exceptionHandler(Exception ex, WebRequest req){
        HttpHeaders headers=new HttpHeaders();

        //如果抛出了ResultException异常
        if(ex instanceof ResultException){
            this.handlerResultException((ResultException) ex,headers,req);
        }
        return Result.create(ex.getMessage(),ResultCode.ERROR.getCode());
    }

    //对ResultException异常进行处理
    protected  Result handlerResultException(ResultException ex,HttpHeaders httpHeaders,WebRequest req){
        System.out.println("http头信息:"+httpHeaders);
        System.out.println(req);
        return Result.create(ex.getMessage(),ResultCode.FAIL.getCode());
    }
}

ResultException.class:

package com.fly.exception;

public class ResultException extends Exception{
    /**异常信息*/
    private String message;
    /**异常原因*/
    private Throwable cause;
    public String getMessage(){
        return this.message;
    }
    public void setMessage(String message){
        this.message=message;
    }
    public Throwable getCause(){
        return this.cause;
    }
    public void setCause(Throwable cause){
        this.cause=cause;
    }
    public ResultException(){ }
    public ResultException(String message){
        super(message);
    }
    public ResultException(String message,Throwable cause){
        super(message,cause);
    }
}

6.接口文档

说到接口文档就不得不说Swagger,Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法、参数和模型紧密集成到服务器端的代码,允许 API 来始终保持同步。Swagger 让部署管理和使用功能强大的 API 从未如此简单。而knife4j相当于Swagger的升级版,提供了更多功能与特性帮助开发者提升效率。

添加knife4j坐标:

<!-- knife4j 坐标 -->
 <dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
   <version>3.0.2</version>
</dependency>

在application.properties中加入knife4j配置:

#开启knife4j增强
knife4j.enable=true
knife4j.setting.footerCustomContent="z乘风的API文档"

更多配置如下:

属性 默认值 说明值
knife4j.enable false 是否开启Knife4j增强模式
knife4j.cors false 是否开启一个默认的跨域配置,该功能配合自定义Host使用
knife4j.production false 是否开启生产环境保护策略,详情参考文档
knife4j.basic 对Knife4j提供的资源提供BasicHttp校验,保护文档
knife4j.basic.enable false 关闭BasicHttp功能
knife4j.basic.username basic用户名
knife4j.basic.password basic密码
knife4j.documents 自定义文档集合,该属性是数组
knife4j.documents.group 所属分组
knife4j.documents.name 类似于接口中的tag,对于自定义文档的分组
knife4j.documents.locations markdown文件路径,可以是一个文件夹(classpath:markdowns/*
),也可以是单个文件(classpath:md/sign.md
)
knife4j.setting 前端Ui的个性化配置属性
knife4j.setting.enableAfterScript true 调试Tab是否显示AfterScript功能,默认开启
knife4j.setting.language zh-CN Ui默认显示语言,目前主要有两种:中文(zh-CN)、英文(en-US)
knife4j.setting.enableSwaggerModels true 是否显示界面中SwaggerModel功能
knife4j.setting.swaggerModelName Swagger Models 重命名SwaggerModel名称,默认
knife4j.setting.enableDocumentManage true 是否显示界面中”文档管理”功能
knife4j.setting.enableReloadCacheParameter false 是否在每个Debug调试栏后显示刷新变量按钮,默认不显示
knife4j.setting.enableVersion false 是否开启界面中对某接口的版本控制,如果开启,后端变化后Ui界面会存在小蓝点
knife4j.setting.enableRequestCache true 是否开启请求参数缓存
knife4j.setting.enableFilterMultipartApis false 针对RequestMapping的接口请求类型,在不指定参数类型的情况下,如果不过滤,默认会显示7个类型的接口地址参数,如果开启此配置,默认展示一个Post类型的接口地址
knife4j.setting.enableFilterMultipartApiMethodType POST 具体接口的过滤类型
knife4j.setting.enableHost false 是否启用Host
knife4j.setting.enableHomeCustom false 是否开启自定义主页内容
knife4j.setting.homeCustomLocation 主页内容Markdown文件路径
knife4j.setting.enableSearch false 是否禁用Ui界面中的搜索框
knife4j.setting.enableFooter true 是否显示Footer
knife4j.setting.enableFooterCustom false 是否开启自定义Footer
knife4j.setting.footerCustomContent false 自定义Footer内容
knife4j.setting.enableDynamicParameter false 是否开启动态参数调试功能
knife4j.setting.enableDebug true 启用调试
knife4j.setting.enableOpenApi true 显示OpenAPI规范
knife4j.setting.enableGroup true 显示服务分组

定义Knife4j配置类:

package com.fly.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2 //启用Swagger2
@EnableKnife4j //启用Knife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfiguration {
    @Autowired
    private ApplicationContext applicationContext;

    public ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Knife4j文档管理")
                .description("Knife4j管理Api")
                .termsOfServiceUrl("http://www.group.com/")
                .contact(new Contact("z乘风","","2684837849@qq.com"))
                .version("1.0")
                .build();
    }

    @Bean
    public Docket docket01(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                //分组名称
                .groupName("分组1")
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.fly.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}

使用@ApiModel注解用于描述模型实体类,@ApiModelProperty用于描述实体类的属性或方法。

package com.fly.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@Accessors(chain = true) //开启链式调用
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
@ApiModel(value = "学生",description = "学生实体")
public class Student {

    @ApiModelProperty("学生id")
    private Integer sid;
    @ApiModelProperty(value = "学生名称",required = true)
    @NotBlank(message ="stuName不能为空") //不能为空,字符串长度大于0(限字符串)
    private String stuName;

    @ApiModelProperty(value = "学生编号",required = true)
    @NotBlank
    private String stuNo;
    @ApiModelProperty(value="学生年龄",required = true)
    @Range(min = 0,max = 120,message ="age的值超出合法范围") //age的值在0到120之间,不然校验失败
    private int age;

    @ApiModelProperty(value="性别",required = true)
    private Integer sex;
    @ApiModelProperty(value = "地址",required = false)
    private String address;
    @ApiModelProperty(value = "生日",required = false)
    @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}",message ="日期格式不正确") //校验日期格式,例如2020-02-02这种形式
    private String birthday;
}

使用@Api和@ApiOperation来描述Controller方法信息,以此来生成文档详情信息。

package com.fly.controller;

import com.fly.entity.Student;
import com.fly.result.Result;
import com.fly.service.StudentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;

//tags用于在左侧菜单显示的菜单项名称
@Api(value = "StudentController",tags="学生控制器")
@Validated
@RestController
public class StudentController {

    @Autowired
    private StudentService studentService;

    /*
    * @Valid注解表示对该参数进行校验,如果对参数校验错误则会把错误信息注入到BindingResult中
    * */
    @PostMapping("/addStudent")
    @ApiOperation(value="添加学生",nickname="zxp")
    public Result addStudent(@RequestBody @Valid Student student){
        return studentService.addStudent(student);
    }
}

运行项目后浏览器运行localhost:port+/doc.html即可进入knife4j界面,更多访问地址如下:

/doc.html Knife4j提供的文档访问地址
/v2/api-docs-ext Knife4j提供的增强接口地址,自2.0.6
版本后删除
/swagger-resources Springfox-Swagger提供的分组接口
/v2/api-docs Springfox-Swagger提供的分组实例详情接口
/swagger-ui.html Springfox-Swagger提供的文档访问地址
/swagger-resources/configuration/ui Springfox-Swagger提供
/swagger-resources/configuration/security Springfox-Swagger提供

当我们部署系统到生产系统,为了接口安全,需要屏蔽所有Swagger的相关资源,只需在application.properties 或application.yml加入以下配置即可屏蔽资源:

# 开启增强配置 
knife4j.enable=true
# 开启生产环境屏蔽
knife4j.production=true

显示界面效果:
pppppp.gif