0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot 后端接口规范(中)

jf_78858299 来源:架构师 作者:架构师 2023-05-05 17:01 次阅读

四、全局异常处理

参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理。但我们又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用[SpringBoot]全局异常处理来达到一劳永逸的效果!

1、基本使用

首先,我们需要新建一个类,在这个类上加上@ControllerAdvice@RestControllerAdvice注解,这个类就配置成全局处理类了。

这个根据你的Controller层用的是@Controller还是@RestController来决定。

然后在类中新建方法,在方法上加上@ExceptionHandler注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException全局处理:

package com.csdn.demo1.global;

import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@ResponseBody
public class ExceptionControllerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然后提取错误提示信息进行返回
        return objectError.getDefaultMessage();
    }
    
     /**
     * 系统异常 预期以外异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultVO? handleUnexpectedServer(Exception ex) {
        log.error("系统异常:", ex);
        // GlobalMsgEnum.ERROR是我自己定义的枚举类
        return new ResultVO<>(GlobalMsgEnum.ERROR);
    }

    /**
     * 所以异常的拦截
     */
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultVO? exception(Throwable ex) {
        log.error("系统异常:", ex);
        return new ResultVO<>(GlobalMsgEnum.ERROR);
    }
}

我们再次进行测试,这次返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!

以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!

2、自定义异常

在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,而使用自定义异常有诸多优点:

  • 自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
  • 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
  • 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。

我们现在就来开始写一个自定义异常:

package com.csdn.demo1.global;

import lombok.Getter;

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

然后在刚才的全局异常类中加入如下:

//自定义的全局异常
  @ExceptionHandler(APIException.class)
  public String APIExceptionHandler(APIException e) {
      return e.getMsg();
  }

这样就对异常的处理就比较规范了,当然还可以添加对Exception的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。

另外,当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这还需要配合数据统一响应。

如果在多模块使用,全局异常等公共功能抽象成子模块,则在需要的子模块中需要将该模块包扫描加入,@SpringBootApplication(scanBasePackages = {"com.xxx"})

五、数据统一响应

统一数据响应是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!这里我包括了响应信息代码code和响应信息说明msg,首先可以设置一个枚举规范响应体中的响应码和响应信息。

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

自定义响应体

package com.csdn.demo1.global;

import lombok.Getter;

@Getter
public class ResultVO<T> {
    /**
     * 状态码,比如1000代表响应成功
     */
    private int code;
    /**
     * 响应信息,用来说明响应情况
     */
    private String msg;
    /**
     * 响应的具体数据
     */
    private T data;
    
    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;
    }
}

最后需要修改全局异常处理类的返回类型

@RestControllerAdvice
public class ExceptionControllerAdvice {

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

最后在controller层进行接口信息数据的返回

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

经过测试,这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!

还有一种全局返回类如下

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Msg {
    //状态码
    private int code;
    //提示信息
    private String msg;
    //用户返回给浏览器的数据
    private Map<String,Object> data = new HashMap<>();

    public static Msg success() {
        Msg result = new Msg();
        result.setCode(200);
        result.setMsg("请求成功!");
        return result;
    }

    public static Msg fail() {
        Msg result = new Msg();
        result.setCode(400);
        result.setMsg("请求失败!");
        return result;
    }

    public static Msg fail(String msg) {
        Msg result = new Msg();
        result.setCode(400);
        result.setMsg(msg);
        return result;
    }

    public Msg(ReturnResult returnResult){
        code = returnResult.getCode();
        msg = returnResult.getMsg();
    }

    public Msg add(String key,Object value) {
        this.getData().put(key, value);
        return this;
    }
}

六、全局处理响应数据(可选择)

接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但还是有可以优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每一个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?

当然是有的,还是要用到全局处理。但是为了扩展性,就是允许绕过数据统一响应(这样就可以提供多方使用),我们可以自定义注解,利用注解来选择是否进行全局响应包装

首先创建自定义注解,作用相当于全局处理类开关:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // 表明该注解只能放在方法上
public @interface NotResponseBody {
}

其次创建一个类并加上注解使其成为全局处理类。然后继承ResponseBodyAdvice接口重写其中的方法,即可对我们的controller进行增强操作,具体看代码和注释:

package com.csdn.demo1.global;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice(basePackages = {"com.scdn.demo1.controller"}) // 注意哦,这里要加上需要扫描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class? class="hljs-keyword"extends HttpMessageConverter?> aClass) {
       // 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
        // 如果方法上加了我们的自定义注解也没有必要进行额外的操作
        return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class? class="hljs-keyword"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方法中,我们可以直接在该方法里包装数据,这样就不需要每个接口都进行数据包装了,省去了很多麻烦。此时controller只需这样写就行了:

@GetMapping("/getUser")
//@NotResponseBody  //是否绕过数据统一响应开关
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;
}

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    8422

    浏览量

    150662
  • URL
    URL
    +关注

    关注

    0

    文章

    138

    浏览量

    15286
  • 后端
    +关注

    关注

    0

    文章

    31

    浏览量

    2190
  • SpringBoot
    +关注

    关注

    0

    文章

    173

    浏览量

    157
收藏 人收藏

    评论

    相关推荐

    《CC2480接口规范》()

    《CC2480接口规范》()欢迎研究ZigBee的朋友和我交流。。。
    发表于 08-12 12:35

    SpringBoot的Druid介绍

    SpringBootDruid数据源配置
    发表于 05-07 09:21

    SpringBoot知识总结

    SpringBoot干货学习总结
    发表于 08-01 10:40

    怎么学习SpringBoot

    SpringBoot学习之路(X5)- 整合JPA
    发表于 06-10 14:52

    怎样去使用springboot

    怎样去使用springboot呢?学习springboot需要懂得哪些?
    发表于 10-25 07:13

    SpringBoot应用启动运行run方法

    )、refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器的每一个组件】;如果是web应用创建**AnnotationConfigEmbeddedWebApplicationContext**,否则
    发表于 12-20 06:16

    基于DSP控制的电力线通信模拟前端接口设计

    基于DSP控制的电力线通信模拟前端接口设计
    发表于 10-20 15:51 5次下载
    基于DSP控制的电力线通信模拟前<b class='flag-5'>端接口</b>设计

    数字接口—单端接口与差动接口的对比

    数字接口—单端接口与差动接口的对比
    发表于 11-07 08:07 0次下载
    数字<b class='flag-5'>接口</b>—单<b class='flag-5'>端接口</b>与差动<b class='flag-5'>接口</b>的对比

    为什么建议你替换掉SpringBoot框架的Tomcat?

    SpringBoot框架,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。
    的头像 发表于 01-07 10:26 1035次阅读

    什么是 SpringBoot

    本文从为什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里开始入手,逐步分析了 `SpringBoot` 自动装配的原理,最后手写了一个简单的 `start` 组件,通过实战来体会了 `
    的头像 发表于 04-07 11:28 1248次阅读
    什么是 <b class='flag-5'>SpringBoot</b>?

    如何在SpringBoot解决Redis的缓存穿透等问题

    今天给大家介绍一下如何在SpringBoot解决Redis的缓存穿透、缓存击穿、缓存雪崩的问题。
    的头像 发表于 04-28 11:35 694次阅读

    SpringBoot 后端接口规范(上)

    一个后端接口大致分为四个部分组成: 接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response) 。虽然说后端接口的编写并没有统一
    的头像 发表于 05-05 17:00 727次阅读
    <b class='flag-5'>SpringBoot</b> <b class='flag-5'>后端接口</b><b class='flag-5'>规范</b>(上)

    SpringBoot 后端接口规范(下)

    一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。虽然说后端接口的编写并没有统一
    的头像 发表于 05-05 17:02 586次阅读

    后端分离必备的接口规范

    随着互联网的高速发展,前端页面的展示、交互体验越来越灵活、炫丽,响应体验也要求越来越高,后端服务的高并发、高可用、高性能、高扩展等特性的要求也愈加苛刻,从而导致前后端研发各自专注于自己擅长的领域深耕细作。
    的头像 发表于 05-15 17:16 787次阅读
    前<b class='flag-5'>后端</b>分离必备的<b class='flag-5'>接口</b><b class='flag-5'>规范</b>

    springboot后端交互流程

    Boot 进行开发时,前后端交互是一个非常重要的部分,本文将详细介绍 Spring Boot 前后端交互的流程。 前后端交互的基本原理 在前后端交互的过程
    的头像 发表于 11-22 16:00 1955次阅读