欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Springboot如何设计出优雅的后端(API)接口

程序员文章站 2022-06-15 13:53:59
项目背景现在绝大部分项目都是采用前后端分离的模式,对于前端来说,后端如果能有一个规范的、优雅的设计的api模式,那么前端的开发将会事倍功半,同时对于后端来说,统一的格式也有利于后期的维护和扩展(其实主要是甩手的时候,不至于下一个人看不懂=。=)。一个后端的接口分为四个部分:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。下面我们就从这四个部分分析需要的注意的点。后续我会把项目上传到github上,感兴趣的同学可以帮忙点个爱心。需要思考...

项目背景

现在绝大部分项目都是采用前后端分离的模式,对于前端来说,后端如果能有一个规范的、优雅的设计的api模式,那么前端的开发将会事倍功半,同时对于后端来说,统一的格式也有利于后期的维护和扩展(其实主要是甩手的时候,不至于下一个人看不懂=。=)。
一个后端的接口分为四个部分:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。下面我们就从这四个部分分析需要的注意的点。
后续我会把项目上传到github上,感兴趣的同学可以帮忙点个爱心。
附上csdn的下载地址:springboot优雅后端接口demo

需要思考的问题

  1. 统一返回接口格式。
  2. 全局异常处理。
  3. 参数常规非空校验。
  4. 通用的分页对象。
  5. 日志统一格式。
  6. 常用工具类。
  7. 接口文档。
  8. 接口的安全。

开始

这个是采用的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自信过头了。。。。。。
Springboot如何设计出优雅的后端(API)接口
我擦,忘了配置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

Springboot如何设计出优雅的后端(API)接口
访问地址:http://127.0.0.1:8082/cj-api/user?id=1,出现如下图所示,则表明成功。
Springboot如何设计出优雅的后端(API)接口
至此一个简单的接口就完成了,但是我们还需要很多流程去优化这个接口。

统一返回格式

常规而言(至少我是这么认为的,有意见的评论区见=。=),后端和前端的数据交互的格式都是采用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格式。启动项目,如下图所示:
Springboot如何设计出优雅的后端(API)接口
至此,任何接口的返回都必须遵守这个规则。
这里有个点:code可以不仅仅是0或者1,可以是自定义的一些错误码,这种好处呢就是好排查。看就错误码就大概知道哪边出错。但我本人喜欢只返回0或者1,为什么呢?因为tm的简单啊,我具体的错误内容我会放到msg里面:比如:用户名错误,什么的。

全局异常处理

我们再来看下刚才那个接口,让后台报错会怎样?如下所示:
Springboot如何设计出优雅的后端(API)接口
这种返回结果,根本一点都不友好。所以我们应该把后台的异常作统一处理,报错只能是后台报错,返回的接口,对于前端而言,必须还是刚才所说的格式。

@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
重启项目:访问刚才的地址,如下图所示:
Springboot如何设计出优雅的后端(API)接口
美滋滋,这样不管后台怎么样保证了返回的数据格式的一致性。

接口参数常规校验

@Valid

解决方案:采用Validator 注解进行参数校验。

  1. 在需要校验的参数上加上@NotNull注解。
  2. 在controller参数中加上注解@Valid
    Springboot如何设计出优雅的后端(API)接口
    Springboot如何设计出优雅的后端(API)接口
    附录:校验对应的注解:
    Springboot如何设计出优雅的后端(API)接口
    结果展示:
    这里因为是post请求,不能直接采用web方式访问,我们只能使用postman访问,如下图所示:
    Springboot如何设计出优雅的后端(API)接口
    返回格式还是responseBean这种JSON格式,但美中不足的是,返回的msg内容太多了,其实我只需要msg中的 default message [用户姓名不能为空]]
    如何处理呢?
    还记得我们上面讲的全局异常处理么?打开后台,看下后台异常报错:
    Springboot如何设计出优雅的后端(API)接口
    注意笔者标注红色部分,我们只需要对这种类型的异常进行特殊处理,获取到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

  1. 导入maven包:
    如何你是导入的我上面的maven文件的话,这边就不需要导入了。
 <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${page-helper.version}</version>
        </dependency>
  1. 请求中加入分页的参数: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语句:
Springboot如何设计出优雅的后端(API)接口
注意:本文不是介绍如何使用PageHelper,所以关于如何集成PageHelper的文明同以及PageHelper的用法大家可以出门右转百度。
其他:
其实更为方便的第三方库的话,我推荐使用 Mybatis-plus这个库,我也是最近才接触到的,他对于单表的操作实在是太方便了,一句sql都不需要要写,而且也自带分页功能。感兴趣的同学可以去看看。那有些杠精儿会问楼主了:你都推荐了,你为啥不用啊?楼主是因为用pageHelper用习惯了,所以没用,开发嘛,哪个用的习惯,用的顺手就用哪个。

总结

  1. 统一的返回格式:
    这边需要一个统一的返回格式,方便前后端进行数据的共享和交互。
  2. 全局异常处理:
    这里是为了在后台出现异常的时候,返回到前端的代码依旧是之前规定的数据格式,不然返回错误信息给前端,前端也无法进行判断。
  3. 常规性的接口校验:
    采用是springboot自带的@NotNull之类的参数判断,方便对一些数据、非空字段进行判断。
  4. 通用的分页请求对象
    这里我是采用的pageHelper类,其实我是把请求分为2类:
    一类是普通的请求。
    一类是分页的请求。
    我这里只是对分页请求进行了简单的处理:因为分页都是需要2个共同的参数:pageNum、pageSize。所以形成一个basePageRequestVO这个类。
    当然如果你业务逻辑都需要一个通用的请求参数,你也可以新建一个baseRequestVO,然后让其余的实体类都继承这个类。

结束语

其实写代码本来就是一个归纳总结的过程,整天复制粘贴的话,对自己而言其实没啥提升,我没有贬低复制粘贴这种做法,我自己也是复制粘贴,有现成的代码干嘛不用呢?但我希望是大家用脑子的复制粘贴,别复制粘贴过来,能运行就行。那这样的话,你永远都不会进步。

本文地址:https://blog.csdn.net/bicheng4769/article/details/110232665