接口设计
# 介绍
现代接口的设计,都需要遵循这么几点的要求:参数校验、幂等性、放置重复提交。才能保证安全性,可用性。
# 参数校验
平时在开发接口的时候,常常会需要对参数进行校验,这里提供两种处理校验逻辑的方式。使用Hibernate Validator来处理。 #Hibernate Validator Hibernate Validator是SpringBoot内置的校验框架,只要集成了SpringBoot就自动集成了它,我们可以通过在对象上面使用它提供的注解来完成参数校验。
# 常用注解
我们先来了解下常用的注解,对Hibernate Validator所提供的校验功能有个印象。
@Null:被注释的属性必须为null;
@NotNull:被注释的属性不能为null;
@AssertTrue:被注释的属性必须为true;
@AssertFalse:被注释的属性必须为false;
@Min:被注释的属性必须大于等于其value值;
@Max:被注释的属性必须小于等于其value值;
@Size:被注释的属性必须在其min和max值之间;
@Pattern:被注释的属性必须符合其regexp所定义的正则表达式;
@NotBlank:被注释的字符串不能为空字符串;
@NotEmpty:被注释的属性不能为空;
@Email:被注释的属性必须符合邮箱格式。
2
3
4
5
6
7
8
9
10
11
# 示例
@Getter
@Setter
public class TroubleRecordParam {
/**
* 问题描述
*/
@Size(max = 256, message = "troubleDesc 长度不能超过 256")
private String troubleDesc;
/**
* 设备IMEI
*/
@Size(max = 50, message = "imei 长度不能超过 50")
private String imei;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后在添加的接口中添加@Validated注解,并注入一个BindingResult参数
@Controller
@RequestMapping("/resultRecord")
public class ResultRecordController {
@Autowired
private ResultRecordService rrService;
@RequestMapping(value = "/create", method = RequestMethod.POST)
public CommonResult create(@Validated @RequestBody TroubleRecordParam troubleRecordParam, BindingResult result) {
CommonResult commonResult;
int count = rrService.createResultRecord(pmsBrand);
if (count == 1) {
commonResult = CommonResult.success(count);
} else {
commonResult = CommonResult.failed();
}
return commonResult;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
然后在整个Controller层创建一个切面,在其环绕通知中获取到注入的BindingResult对象,通过hasErrors方法判断校验是否通过,如果有错误信息直接返回错误信息,验证通过则放行;
@Aspect
@Component
@Order(2)
public class BindingResultAspect {
@Pointcut("execution(public * com.demo.test.controller.*.*(..))")
public void BindingResult() {
}
@Around("BindingResult()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult result = (BindingResult) arg;
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
if(fieldError!=null){
return CommonResult.validateFailed(fieldError.getDefaultMessage());
}else{
return CommonResult.validateFailed();
}
}
}
}
return joinPoint.proceed();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
使用切面的话,由于每个校验方法中都需要注入BindingResult对象,这样会导致很多重复工作,其实当校验失败时,SpringBoot默认会抛出MethodArgumentNotValidException或BindException异常,我们只要全局处理该异常依然可以得到校验失败信息。
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField()+fieldError.getDefaultMessage();
}
}
return CommonResult.failed(message);
}
@ResponseBody
@ExceptionHandler(value = BindException.class)
public R handleValidException(BindException e) {
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField()+fieldError.getDefaultMessage();
}
}
return Response.failed(message);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
由于SpringBoot 2.3版本默认移除了校验功能,如果想要开启的话需要添加如下依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2
3
4
# 优缺点
这种方式的优点是可以使用注解来实现参数校验,不需要一些重复的校验逻辑,但是也有一些缺点,比如需要在Controller的方法中额外注入一个BindingResult对象,只支持一些简单的校验,涉及到要查询数据库的校验就无法满足了。
# 幂等性
# 什么是幂等?
幂等是一个数学与计算机科学概念。
在数学中,幂等用函数表达式就是:f(x) = f(f(x))。比如求绝对值的函数,就是幂等的,abs(x) = abs(abs(x))。 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。
# 如何设计幂等
幂等意味着一条请求的唯一性。不管是你哪个方案去设计幂等,都需要一个全局唯一的ID,去标记这个请求是独一无二的。
# token令牌
token 令牌方案一般包括两个请求阶段:
- 客户端请求申请获取token,服务端生成token返回
- 客户端带着token请求,服务端校验token
流程图如下:
- 客户端发起请求,申请获取token。
- 服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。
- 客户端带着token,发起请求。
- 服务端去redis确认token是否存在,一般用 redis.del(token)的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。
| 这边文章里面,作者总结了8中接口幂等的方法 (opens new window)。总之,根据实际情况可以选择合适的方法进行实现。
# 重复提交
重复提交的思路其实也差不多雷同。单位时间内的唯一用户的唯一接口表示。
以下是一些常见的避免接口重复提交的方法:
Token 验证:在每个请求中添加一个 token,服务器验证 token 是否有效。如果 token 有效,则可以处理请求。如果 token 无效,则返回错误信息。当一个请求处理成功后,服务器可以将 token 标记为已使用,以防止同一请求被重复提交。
前端防重复提交:前端可以在表单提交时,禁用提交按钮,或者在提交时使用一些防止重复提交的技术,例如通过 AJAX 请求进行提交,或者在提交前使用一个标识符来防止重复提交。
服务端幂等性校验:服务端可以通过一些技术来判断接口的幂等性,从而避免重复提交。例如,可以在请求头中添加一个唯一标识符,或者在请求中添加一个时间戳,从而判断是否为同一请求。
接口限流:接口限流可以限制用户的请求速度和数量,从而避免接口被过多的请求打爆。可以使用一些开源的限流组件,例如 Guava、Hystrix 等。
使用 Post/Redirect/Get(PRG)模式:当用户提交表单时,服务端可以在成功处理请求后,重定向用户到一个新的页面,从而避免用户在刷新页面时,再次提交相同的表单数据。