Springboot如何设计出优雅的后端(API)接口
项目背景
现在绝大部分项目都是采用前后端分离的模式,对于前端来说,后端如果能有一个规范的、优雅的设计的api模式,那么前端的开发将会事倍功半,同时对于后端来说,统一的格式也有利于后期的维护和扩展(其实主要是甩手的时候,不至于下一个人看不懂=。=)。
一个后端的接口分为四个部分:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。下面我们就从这四个部分分析需要的注意的点。
后续我会把项目上传到github上,感兴趣的同学可以帮忙点个爱心。
附上csdn的下载地址:springboot优雅后端接口demo
需要思考的问题
- 统一返回接口格式。
- 全局异常处理。
- 参数常规非空校验。
- 通用的分页对象。
- 日志统一格式。
- 常用工具类。
- 接口文档。
- 接口的安全。
开始
这个是采用的springboot配置项目,我先附上需要依赖的包
<properties>
<java.version>1.8</java.version>
<spring-mybatis.version>1.3.2</spring-mybatis.version>
<spring-druid.version>1.1.10</spring-druid.version>
<spring-jdbc.version>2.0.6</spring-jdbc.version>
<mysql-connector.version>8.0.16</mysql-connector.version>
<commons-lang.version>3.8.1</commons-lang.version>
<fastjson.version>1.2.51</fastjson.version>
<jwt.version>3.4.0</jwt.version>
<page-helper.version>1.2.7</page-helper.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- mysql connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${spring-mybatis.version}</version>
</dependency>
<!-- druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>
<!-- swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.api.gateway</groupId>
<artifactId>sdk-core-java</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${page-helper.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
顿时傻眼,一看这么多东西,怎么办?兄弟们不要慌,跟着我一步一步来。
首先第一步,我们先写一个简单的controller,先把项目运行起来。
@RestController
public class UserController {
@GetMapping("/user")
public String getUserInfo(@RequestParam("id") Integer id) {
return "cj" + id;
}
}
启动项目,等一会儿将会出现success的提示语。。。。。。。。。。。。。。。
wait、wait自信过头了。。。。。。
我擦,忘了配置mysql的信息的(因为我maven中加入和mysql的驱动),我们配置上信息,重新启动
server:
port: 8082
tomcat:
uri-encoding: UTF-8
servlet:
session:
timeout: 600000
context-path: /cj-api
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone:
GMT+8
#应用名称
application:
name: cj-api
datasource:
url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&useSSL=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initialSize: 2
minIdle: 2
maxActive: 30
访问地址:http://127.0.0.1:8082/cj-api/user?id=1,出现如下图所示,则表明成功。
至此一个简单的接口就完成了,但是我们还需要很多流程去优化这个接口。
统一返回格式
常规而言(至少我是这么认为的,有意见的评论区见=。=),后端和前端的数据交互的格式都是采用JSON格式,那么json格式的话,一般都会有固定的几个字段比如:code、data、msg。
code:一般是返回错误码,前端会根据这个错误码进行下一步操作。
data:code成功之后,返回的具体数据,这里一般是个泛型。
msg:错误信息、成功信息(根据code来的)。
新建ResponseBean类
/**
* 描述:统一返回前端的实体类
*
* @author caojing
* @create 2020-11-27-13:56
*/
public class ResponseBean<T> {
/**
* 状态码,0 success,1 fail 3第一次登陆
*/
@ApiModelProperty("状态码,0 success,1 fail,2 wait")
private int code = 0;
/**
* 返回信息
*/
@ApiModelProperty("返回信息")
private String msg;
/**
* 返回的数据
*/
@ApiModelProperty("返回数据")
private T data;
public ResponseBean() {
}
public ResponseBean(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public void buildSuccessResponse(T data) {
this.code = 0;
this.data = data;
this.msg = "成功";
}
public void buildFailedResponse() {
this.code = 1;
this.msg = "失败";
}
public void buildFailedResponse(String msg) {
this.code = 1;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
新增一个userbean类
package com.cj.demo.bean.user;
/**
* 描述:
*
* @author caojing
* @create 2020-11-27-16:06
*/
public class UserBean {
public UserBean(int id, String name, int age, String email) {
this.id = id;
this.name = name;
this.age = age;
this.email = email;
}
private int id;
private String name;
private int age;
private String email;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Controller类改为如下所示:
@RestController
public class UserController {
@GetMapping("/user")
public ResponseBean<UserBean> getUserInfo(@RequestParam("id") Integer id) {
ResponseBean responseBean = new ResponseBean();
responseBean.setCode(0);
responseBean.setData(new UserBean(1,"cj",12,"106067690@qq.com") );
responseBean.setMsg("成功");
return responseBean;
}
}
@RestController
这个注解其实就是@controller
+@ResponseBody
,会自动帮我们把实体类转化为json格式。启动项目,如下图所示:
至此,任何接口的返回都必须遵守这个规则。
这里有个点:code可以不仅仅是0或者1,可以是自定义的一些错误码,这种好处呢就是好排查。看就错误码就大概知道哪边出错。但我本人喜欢只返回0或者1,为什么呢?因为tm的简单啊,我具体的错误内容我会放到msg里面:比如:用户名错误,什么的。
全局异常处理
我们再来看下刚才那个接口,让后台报错会怎样?如下所示:
这种返回结果,根本一点都不友好。所以我们应该把后台的异常作统一处理,报错只能是后台报错,返回的接口,对于前端而言,必须还是刚才所说的格式。
@RestControllerAdvice
附上全局异常处理类:
package com.cj.demo.controller;
import com.cj.demo.bean.ResponseBean;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @author cj
* 异常捕获,遵循restful风格
*/
@RestControllerAdvice
public class ExceptionController {
private Logger logger = LoggerFactory.getLogger(ExceptionController.class);
// /**
// * 捕捉shiro的异常
// */
// @ResponseStatus(HttpStatus.UNAUTHORIZED)
// @ExceptionHandler(ShiroException.class)
// public ResponseBean handle401(ShiroException e) {
// if (e instanceof UnauthorizedException) {
// return Tools.buildResFail("无对应权限");
// } else if (e instanceof AuthenticationException) {
// return Tools.buildResFail(e.getMessage());
// }
// return new ResponseBean(401, "Shiro错误," + e.getMessage(), null);
// }
//
//
// @ResponseStatus(HttpStatus.UNAUTHORIZED)
// @ExceptionHandler(IllegalAccessException.class)
// public ResponseBean handle403() {
// return new ResponseBean(1, "非法访问", null);
// }
/**
* 捕捉其他所有异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
logger.error("异常:", ex);
return new ResponseBean(1, ex.getMessage(), null);
}
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
}
注释的部分先忽略不计(毕竟我还没讲到shiro这块的,但是大家感兴趣的话可以去这边看下:SpringBoot2.0集成Shiro)
重启项目:访问刚才的地址,如下图所示:
美滋滋,这样不管后台怎么样保证了返回的数据格式的一致性。
接口参数常规校验
@Valid
解决方案:采用Validator 注解进行参数校验。
- 在需要校验的参数上加上
@NotNull
注解。 - 在controller参数中加上注解
@Valid
。
附录:校验对应的注解:
结果展示:
这里因为是post请求,不能直接采用web方式访问,我们只能使用postman访问,如下图所示:
返回格式还是responseBean这种JSON格式,但美中不足的是,返回的msg内容太多了,其实我只需要msg中的default message [用户姓名不能为空]]
如何处理呢?
还记得我们上面讲的全局异常处理么?打开后台,看下后台异常报错:
注意笔者标注红色部分,我们只需要对这种类型的异常进行特殊处理,获取到msg的消息内容,然后返回ResponseBean就行了,具体如下所示:
/**
* 对参数校验的异常处理
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseBean MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return new ResponseBean(1, objectError.getDefaultMessage(), null);
}
这个是加到@RestControllerAdvice
注解的那个类里面。
通用的分页对象
利用pageHelper进行分页
分页也是我们在后端开发的时候经常遇到的一个功能,那么在不用这些第三方插件的前提下我们是如何操作的呢?
第一步:一般是根据条件查询出对应的数据然后最后加上limit进行分页。
第二步:是同样的sql去除limit只查询出符合条件的总数。
常规是需要这2个sql就可以进行分页了。但这样的话sql会有重复的代码,一种解决方案是利用<sql></sql>
来提取通用的内容。一种是借助第三方插件来实现。
第三方插件的话:我经常使用的是这个PageHelper,当然还有其他的,觉得不错的话,可以在下面评论区提出来。
PageHelper
- 导入maven包:
如何你是导入的我上面的maven文件的话,这边就不需要导入了。
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${page-helper.version}</version>
</dependency>
- 请求中加入分页的参数:pageNum、pageSize
新建一个BasePageRequestVO
(任何分页请求的实体类都要继承这个类)
package com.cj.demo.bean.request;
import io.swagger.annotations.ApiModelProperty;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 描述:
*
* @author caojing
* @create 2019-12-03-11:41
*/
public class BasePageRequestVO {
/**
是否需要分页,默认需要
**/
private Boolean enablePage = true;
/**
第N页
**/
private int pageNum;
/**
每页M条数
**/
private int pageSize;
/**
是否需要统计总数,默认需要
**/
private Boolean enableCount = true;
public Boolean getEnablePage() {
return enablePage;
}
public void setEnablePage(Boolean enablePage) {
this.enablePage = enablePage;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public Boolean getEnableCount() {
return enableCount;
}
public void setEnableCount(Boolean enableCount) {
this.enableCount = enableCount;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
新建一个UserRequestVO
类
package com.cj.demo.bean.request;
/**
* 描述:
*
* @author caojing
* @create 2020-12-02-14:29
*/
public class UserRequestVO extends BasePageRequestVO {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return super.toString();
}
}
UserController 增加如下代码
@PostMapping("/user/page")
public ResponseBean<PageInfo<List<UserBean>>> getUserInfoPage(@RequestBody UserRequestVO userRequestVO) {
PageHelper.startPage(userRequestVO.getPageNum(), userRequestVO.getPageSize());
List<UserBean> userBean = userService.selectPage();
PageInfo pageInfo = new PageInfo(userBean);
pageInfo.setList(userBean);
ResponseBean responseBean = new ResponseBean();
responseBean.setData(pageInfo);
responseBean.setCode(0);
return responseBean;
}
我们可以看下mapper的内容:
<select id="selectPage" resultMap="BaseResultMap">
select *
from user
</select>
并没有分页语句,其实上面controller最主要的一句话是:PageHelper.startPage()
这句话后面要紧跟着我们的查询语句,这样就可以实现分页效果啦,可以看下控制台打印的sql语句:
注意:本文不是介绍如何使用PageHelper,所以关于如何集成PageHelper的文明同以及PageHelper的用法大家可以出门右转百度。
其他:
其实更为方便的第三方库的话,我推荐使用 Mybatis-plus这个库,我也是最近才接触到的,他对于单表的操作实在是太方便了,一句sql都不需要要写,而且也自带分页功能。感兴趣的同学可以去看看。那有些杠精儿会问楼主了:你都推荐了,你为啥不用啊?楼主是因为用pageHelper用习惯了,所以没用,开发嘛,哪个用的习惯,用的顺手就用哪个。
总结
- 统一的返回格式:
这边需要一个统一的返回格式,方便前后端进行数据的共享和交互。 - 全局异常处理:
这里是为了在后台出现异常的时候,返回到前端的代码依旧是之前规定的数据格式,不然返回错误信息给前端,前端也无法进行判断。 - 常规性的接口校验:
采用是springboot自带的@NotNull之类的参数判断,方便对一些数据、非空字段进行判断。 - 通用的分页请求对象
这里我是采用的pageHelper类,其实我是把请求分为2类:
一类是普通的请求。
一类是分页的请求。
我这里只是对分页请求进行了简单的处理:因为分页都是需要2个共同的参数:pageNum、pageSize。所以形成一个basePageRequestVO
这个类。
当然如果你业务逻辑都需要一个通用的请求参数,你也可以新建一个baseRequestVO,然后让其余的实体类都继承这个类。
结束语
其实写代码本来就是一个归纳总结的过程,整天复制粘贴的话,对自己而言其实没啥提升,我没有贬低复制粘贴这种做法,我自己也是复制粘贴,有现成的代码干嘛不用呢?但我希望是大家用脑子的复制粘贴,别复制粘贴过来,能运行就行。那这样的话,你永远都不会进步。
本文地址:https://blog.csdn.net/bicheng4769/article/details/110232665
上一篇: void相关注意事项
下一篇: 数据库建模范式理解