您的当前位置:首页正文

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

2024-11-30 来源:个人技术集锦

项目背景

需要思考的问题

开始

这个是采用的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自信过头了。。。。。。

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

统一返回格式

常规而言(至少我是这么认为的,有意见的评论区见=。=),后端和前端的数据交互的格式都是采用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格式。启动项目,如下图所示:

全局异常处理

我们再来看下刚才那个接口,让后台报错会怎样?如下所示:

@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);
    }
}

接口参数常规校验

@Valid

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

  1. 在需要校验的参数上加上@NotNull注解。
  2. 在controller参数中加上注解@Valid
 /**
     * 对参数校验的异常处理
     * @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,当然还有其他的,觉得不错的话,可以在下面评论区提出来。

  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语句:

注意:本文不是介绍如何使用PageHelper,所以关于如何集成PageHelper的文明同以及PageHelper的用法大家可以出门右转百度。
其他:
其实更为方便的第三方库的话,我推荐使用 Mybatis-plus这个库,我也是最近才接触到的,他对于单表的操作实在是太方便了,一句sql都不需要要写,而且也自带分页功能。感兴趣的同学可以去看看。那有些杠精儿会问楼主了:你都推荐了,你为啥不用啊?楼主是因为用pageHelper用习惯了,所以没用,开发嘛,哪个用的习惯,用的顺手就用哪个。

总结

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

结束语

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

显示全文