- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章SpringBoot后端接口的实现(看这一篇就够了)由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
摘要:本文演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松.
一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个优秀的后端接口和一个糟糕的后端接口对比起来差异还是蛮大的,其中最重要的关键点就是看是否规范.
本文就一步一步演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松.
所需依赖包 。
这里用的是SpringBoot配置项目,本文讲解的重点是后端接口,所以只需要导入一个spring-boot-starter-web包就可以了: <!--web依赖包,web应用必备--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> 。
本文还用了swagger来生成API文档,lombok来简化类,不过这两者不是必须的,可用可不用.
参数校验 。
一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了.
业务层校验 。
首先我们来看一下最常见的做法,就是在业务层进行参数校验:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
String addUser(User user) {
if
(user ==
null
|| user.getId() ==
null
|| user.getAccount() ==
null
|| user.getPassword() ==
null
|| user.getEmail() ==
null
) {
return
"对象或者对象字段不能为空"
;
}
if
(StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return
"不能输入空字符串"
;
}
if
(user.getAccount().length() <
6
|| user.getAccount().length() >
11
) {
return
"账号长度必须是6-11个字符"
;
}
if
(user.getPassword().length() <
6
|| user.getPassword().length() >
16
) {
return
"密码长度必须是6-16个字符"
;
}
if
(!Pattern.matches(
"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"
, user.getEmail())) {
return
"邮箱格式不正确"
;
}
// 参数校验完毕后这里就写上业务逻辑
return
"success"
;
}
|
这样做当然是没有什么错的,而且格式排版整齐也一目了然,不过这样太繁琐了,这还没有进行业务操作呢光是一个参数校验就已经这么多行代码,实在不够优雅.
我们来改进一下,使用Spring Validator和Hibernate Validator这两套Validator来进行方便的参数校验!这两套Validator依赖包已经包含在前面所说的web依赖包里了,所以可以直接使用.
Validator + BindResult进行校验 。
Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Data
public
class
User {
@NotNull
(message =
"用户id不能为空"
)
private
Long id;
@NotNull
(message =
"用户账号不能为空"
)
@Size
(min =
6
, max =
11
, message =
"账号长度必须是6-11个字符"
)
private
String account;
@NotNull
(message =
"用户密码不能为空"
)
@Size
(min =
6
, max =
11
, message =
"密码长度必须是6-16个字符"
)
private
String password;
@NotNull
(message =
"用户邮箱不能为空"
)
@Email
(message =
"邮箱格式不正确"
)
private
String email;
}
|
校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上@Valid注解,并添加BindResult参数即可方便完成验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@RestController
@RequestMapping
(
"user"
)
public
class
UserController {
@Autowired
private
UserService userService;
@PostMapping
(
"/addUser"
)
public
String addUser(
@RequestBody
@Valid
User user, BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for
(ObjectError error : bindingResult.getAllErrors()) {
return
error.getDefaultMessage();
}
return
userService.addUser(user);
}
}
|
这样当请求数据传递到接口的时候Validator就自动完成校验了,校验的结果就会封装到BindingResult中去,如果有错误信息我们就直接返回给前端,业务逻辑代码也根本没有执行下去.
此时,业务层里的校验代码就已经不需要了:
1
2
3
4
|
public
String addUser(User user) {
// 直接编写业务逻辑
return
"success"
;
}
|
现在可以看一下参数校验效果。我们故意给这个接口传递一个不符合校验规则的参数,先传递一个错误数据给接口,故意将password这个字段不满足校验条件:
1
2
3
4
5
6
|
{
"account"
:
"12345678"
,
"email"
:
"123@qq.com"
,
"id"
: 0,
"password"
:
"123"
}
|
再来看一下接口的响应数据:
这样是不是方便很多?不难看出使用Validator校验有如下几个好处:
(1)简化代码,之前业务层那么一大段校验代码都被省略掉了.
(2)使用方便,那么多校验规则可以轻而易举的实现,比如邮箱格式验证,之前自己手写正则表达式要写那么一长串,还容易出错,用Validator直接一个注解搞定。(还有更多校验规则注解,可以自行去了解哦) 。
(3)减少耦合度,使用Validator能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来.
使用Validator+ BindingResult已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个BindingResult参数,然后再提取错误信息返回给前端.
这样有点麻烦,并且重复代码很多(尽管可以将这个重复代码封装成方法)。我们能否去掉BindingResult这一步呢?当然是可以的! 。
Validator + 自动抛出异常 。
我们完全可以将BindingResult这一步给去掉:
1
2
3
4
|
@PostMapping
(
"/addUser"
)
public
String addUser(
@RequestBody
@Valid
User user) {
return
userService.addUser(user);
}
|
去掉之后会发生什么事情呢?直接来试验一下,还是按照之前一样故意传递一个不符合校验规则的参数给接口。此时我们观察控制台可以发现接口已经引发MethodArgumentNotValidException异常了:
其实这样就已经达到我们想要的效果了,参数校验不通过自然就不执行接下来的业务逻辑,去掉BindingResult后会自动引发异常,异常发生了自然而然就不会执行业务逻辑。也就是说,我们完全没必要添加相关BindingResult相关操作嘛.
不过事情还没有完,异常是引发了,可我们并没有编写返回错误信息的代码呀,那参数校验失败了会响应什么数据给前端呢?
我们来看一下刚才异常发生后接口响应的数据:
没错,是直接将整个错误对象相关信息都响应给前端了!这样就很难受,不过解决这个问题也很简单,就是我们接下来要讲的全局异常处理! 。
全局异常处理 。
参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理,不然还不如用之前BindingResult方式呢。又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果! 。
基本使用 。
首先,我们需要新建一个类,在这个类上加上@ControllerAdvice或@RestControllerAdvice注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller还是@RestController来决定) 。
然后在类中新建方法,在方法上加上@ExceptionHandler注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理! 。
我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException全局处理:
1
2
3
4
5
6
7
8
9
10
11
12
|
@RestControllerAdvice
public
class
ExceptionControllerAdvice {
@ExceptionHandler
(MethodArgumentNotValidException.
class
)
public
String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(
0
);
// 然后提取错误提示信息进行返回
return
objectError.getDefaultMessage();
}
}
|
我们再来看下这次校验失败后的响应数据:
没错,这次返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!更多的校验思路:SpringBoot实现通用的接口参数校验 。
自定义异常 。
全局处理当然不会只能处理一种异常,用途也不仅仅是对一个参数校验方式进行优化。在实际开发中,如何对异常处理其实是一个很麻烦的事情。传统处理异常一般有以下烦恼:
以上这些问题都可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理代表什么?代表规范!规范有了,很多问题就会迎刃而解! 。
全局异常处理的基本使用方式大家都已经知道了,我们接下来更进一步的规范项目中的异常处理方式:自定义异常.
在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,我这时候就可以手动抛出异常从而触发事务回滚。那手动抛出异常最简单的方式就是throw new RuntimeException("异常信息")了,不过使用自定义会更好一些:
我们现在就来开始写一个自定义异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Getter
//只要getter方法,无需setter
public
class
APIException
extends
RuntimeException {
private
int
code;
private
String msg;
public
APIException() {
this
(
1001
,
"接口错误"
);
}
public
APIException(String msg) {
this
(
1001
, msg);
}
public
APIException(
int
code, String msg) {
super
(msg);
this
.code = code;
this
.msg = msg;
}
}
|
在刚才的全局异常处理类中记得添加对我们自定义异常的处理:
1
2
3
4
|
@ExceptionHandler
(APIException.
class
)
public
String APIExceptionHandler(APIException e) {
return
e.getMsg();
}
|
这样就对异常的处理就比较规范了,当然还可以添加对Exception的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做.
现在全局异常处理和自定义异常已经弄好了,不知道大家有没有发现一个问题,就是当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这就要引申出我们接下来要讲的东西了:数据统一响应 。
数据统一响应 。
现在我们规范好了参数校验方式和异常处理方式,然而还没有规范响应数据!比如我要获取一个分页信息数据,获取成功了呢自然就返回的数据列表,获取失败了后台就会响应异常信息,即一个字符串,就是说前端开发者压根就不知道后端响应过来的数据会是啥样的!所以,统一响应数据是前后端规范中必须要做的! 。
自定义统一响应体 。
统一数据响应第一步肯定要做的就是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!那么如何定义响应体呢?关于异常的设计:如何更优雅的设计异常 。
可以参考我们自定义异常类,也来一个响应信息代码code和响应信息说明msg:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@Getter
public
class
ResultVO<T> {
/**
* 状态码,比如1000代表响应成功
*/
private
int
code;
/**
* 响应信息,用来说明响应情况
*/
private
String msg;
/**
* 响应的具体数据
*/
private
T data;
public
ResultVO(T data) {
this
(
1000
,
"success"
, data);
}
public
ResultVO(
int
code, String msg, T data) {
this
.code = code;
this
.msg = msg;
this
.data = data;
}
}
|
然后我们修改一下全局异常处理那的返回值:
1
2
3
4
5
6
7
8
9
10
11
12
|
@ExceptionHandler
(APIException.
class
)
public
ResultVO<String> APIExceptionHandler(APIException e) {
// 注意哦,这里返回类型是自定义响应体
return
new
ResultVO<>(e.getCode(),
"响应失败"
, e.getMsg());
}
@ExceptionHandler
(MethodArgumentNotValidException.
class
)
public
ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(
0
);
// 注意哦,这里返回类型是自定义响应体
return
new
ResultVO<>(
1001
,
"参数校验失败"
, objectError.getDefaultMessage());
}
|
我们再来看一下此时如果发生异常了会响应什么数据给前端:
OK,这个异常信息响应就非常好了,状态码和响应说明还有错误提示数据都返给了前端,并且是所有异常都会返回相同的格式!异常这里搞定了,别忘了我们到接口那也要修改返回类型,我们新增一个接口好来看看效果:
1
2
3
4
5
6
7
8
9
10
|
@GetMapping
(
"/getUser"
)
public
ResultVO<User> getUser() {
User user =
new
User();
user.setId(1L);
user.setAccount(
"12345678"
);
user.setPassword(
"12345678"
);
user.setEmail(
"123@qq.com"
);
return
new
ResultVO<>(user);
}
|
看一下如果响应正确返回的是什么效果:
这样无论是正确响应还是发生异常,响应数据的格式都是统一的,十分规范! 。
数据格式是规范了,不过响应码code和响应信息msg还没有规范呀!大家发现没有,无论是正确响应,还是异常响应,响应码和响应信息是想怎么设置就怎么设置,要是10个开发人员对同一个类型的响应写10个不同的响应码,那这个统一响应体的格式规范就毫无意义!所以,必须要将响应码和响应信息给规范起来.
响应码枚举 。
要规范响应体中的响应码和响应信息用枚举简直再恰当不过了,我们现在就来创建一个响应码枚举类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Getter
public
enum
ResultCode {
SUCCESS(
1000
,
"操作成功"
),
FAILED(
1001
,
"响应失败"
),
VALIDATE_FAILED(
1002
,
"参数校验失败"
),
ERROR(
5000
,
"未知错误"
);
private
int
code;
private
String msg;
ResultCode(
int
code, String msg) {
this
.code = code;
this
.msg = msg;
}
}
|
然后修改响应体的构造方法,让其只准接受响应码枚举来设置响应码和响应信息:
1
2
3
4
5
6
7
8
9
|
public
ResultVO(T data) {
this
(ResultCode.SUCCESS, data);
}
public
ResultVO(ResultCode resultCode, T data) {
this
.code = resultCode.getCode();
this
.msg = resultCode.getMsg();
this
.data = data;
}
|
然后同时修改全局异常处理的响应码设置方式:
1
2
3
4
5
6
7
8
9
10
11
12
|
@ExceptionHandler
(APIException.
class
)
public
ResultVO<String> APIExceptionHandler(APIException e) {
// 注意哦,这里传递的响应码枚举
return
new
ResultVO<>(ResultCode.FAILED, e.getMsg());
}
@ExceptionHandler
(MethodArgumentNotValidException.
class
)
public
ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(
0
);
// 注意哦,这里传递的响应码枚举
return
new
ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}
|
这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!这些可以参考:Java项目构建基础:统一结果,统一异常,统一日志 。
全局处理响应数据 。
接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但还是有可以优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每一个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?当然是有滴,还是要用到全局处理.
首先,先创建一个类加上注解使其成为全局处理类。然后继承ResponseBodyAdvice接口重写其中的方法,即可对我们的controller进行增强操作,具体看代码和注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@RestControllerAdvice
(basePackages = {
"com.rudecrab.demo.controller"
})
// 注意哦,这里要加上需要扫描的包
public
class
ResponseControllerAdvice
implements
ResponseBodyAdvice<Object> {
@Override
public
boolean
supports(MethodParameter returnType, Class<?
extends
HttpMessageConverter<?>> aClass) {
// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
return
!returnType.getGenericParameterType().equals(ResultVO.
class
);
}
@Override
public
Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<?
extends
HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
// String类型不能直接包装,所以要进行些特别的处理
if
(returnType.getGenericParameterType().equals(String.
class
)) {
ObjectMapper objectMapper =
new
ObjectMapper();
try
{
// 将数据包装在ResultVO里后,再转换为json字符串响应给前端
return
objectMapper.writeValueAsString(
new
ResultVO<>(data));
}
catch
(JsonProcessingException e) {
throw
new
APIException(
"返回String类型错误"
);
}
}
// 将原本的数据包装在ResultVO里
return
new
ResultVO<>(data);
}
}
|
重写的这两个方法是用来在controller将数据进行返回前进行增强操作,supports方法要返回为true才会执行beforeBodyWrite方法,所以如果有些情况不需要进行增强操作可以在supports方法里进行判断。对返回数据进行真正的操作还是在beforeBodyWrite方法中,我们可以直接在该方法里包装数据,这样就不需要每个接口都进行数据包装了,省去了很多麻烦.
我们可以现在去掉接口的数据包装来看下效果:
1
2
3
4
5
6
7
8
9
10
|
@GetMapping
(
"/getUser"
)
public
User getUser() {
User user =
new
User();
user.setId(1L);
user.setAccount(
"12345678"
);
user.setPassword(
"12345678"
);
user.setEmail(
"123@qq.com"
);
// 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装
return
user;
}
|
然后我们来看下响应数据:
成功对数据进行了包装! 。
注意:beforeBodyWrite方法里包装数据无法对String类型的数据直接进行强转,所以要进行特殊处理,这里不讲过多的细节,有兴趣可以自行深入了解.
总结 。
到此这篇关于SpringBoot后端接口的实现(看这一篇就够了)的文章就介绍到这了,更多相关SpringBoot 后端接口内容请搜索我以前的文章或继续浏览下面的相关文章希望大家以后多多支持我! 。
原文链接:https://blog.csdn.net/devcloud/article/details/108598751 。
最后此篇关于SpringBoot后端接口的实现(看这一篇就够了)的文章就讲到这里了,如果你想了解更多关于SpringBoot后端接口的实现(看这一篇就够了)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在尝试在我的代码库中为我正在编写的游戏服务器更多地使用接口(interface),并了解高级概念以及何时应该使用接口(interface)(我认为)。在我的例子中,我使用它们将我的包相互分离,并使
我有一个名为 Widget 的接口(interface),它在我的整个项目中都在使用。但是,它也用作名为 Widget 的组件的 Prop 。 处理此问题的最佳方法是什么?我应该更改我的 Widget
有一个接口(interface)可以是多个接口(interface)之一 interface a {x:string} interface b {y:string} interface c {z:st
我遇到了一种情况,我需要调用第三方服务来获取一些信息。这些服务对于不同的客户可能会有所不同。我的界面中有一个身份验证功能,如下所示。 interface IServiceProvider { bool
在我的例子中,“RequestHandlerProxy”是一个结构,其字段为接口(interface)“IAdapter”,接口(interface)有可能被调用的方法,该方法的输入为结构“Reque
我有一个接口(interface)Interface1,它已由类A实现,并且设置了一些私有(private)变量值,并且我将类A的对象发送到下一个接受输入作为Interface2的类。那么我怎样才能将
假设我有这样的类和接口(interface)结构: interface IService {} interface IEmailService : IService { Task SendAs
有人知道我在哪里可以找到 XML-RPC 接口(interface)的定义(在 OpenERP 7 中)?我想知道创建或获取对象需要哪些参数和对象属性。每个元素的 XML 示例也将非常有帮助。 最佳答
最近,我一直在阅读有关接口(interface)是抽象的错误概念的文章。一篇这样的帖子是http://blog.ploeh.dk/2010/12/02/InterfacesAreNotAbstract
如果我有一个由第三方实现的现有 IInterface 后代,并且我想添加辅助例程,Delphi 是否提供了任何简单的方法来实现此目的,而无需手动重定向每个接口(interface)方法?也就是说,给定
我正在尝试将 Article 数组分配给我的 Mongoose 文档,但 Typescript 似乎不喜欢这样,我不知道为什么它显示此警告/错误,表明它不可分配. 我的 Mongoose 模式和接口(
我有两个接口(interface): public interface IController { void doSomething(IEntity thing); } public inte
是否可以创建一个扩展 Serializable 接口(interface)的接口(interface)? 如果是,那么扩展接口(interface)的行为是否会像 Serilizable 接口(int
我试图在两个存储之间创建一个中间层,它从存储 A 中获取数据,将其转换为相应类型的存储 B,然后存储它。由于我需要转换大约 50-100 种类型,我希望使用 map[string]func 并根据 s
我正在处理一个要求,其中我收到一个 JSON 对象,其中包含一个日期值作为字符串。我的任务是将 Date 对象存储在数据库中。 这种东西: {"start_date": "2019-05-29", "
我们的方法的目标是为我们现有的 DAO 和模型类引入接口(interface)。模型类由各种类型的资源 ID 标识,资源 ID 不仅仅是随机数,还带有语义和行为。因此,我们必须用对象而不是原始类型来表
Collection 接口(interface)有多个方法。 List 接口(interface)扩展了 Collection 接口(interface)。它声明与 Collection 接口(int
我有一个 Java 服务器应用程序,它使用 Jackson 使用反射 API 对 DTO 进行一般序列化。例如对于这个 DTO 接口(interface): package com.acme.libr
如果我在 Kotlin 中有一个接口(interface): interface KotlinInterface { val id: String } 我可以这样实现: class MyCla
我知道Java中所有访问修饰符之间的区别。然而,有人问了我一个非常有趣的问题,我很难找到答案:Java 中的 private 接口(interface)和 public 接口(interface)有什
我是一名优秀的程序员,十分优秀!