玩转SpringBoot项目第一篇--统一返回结构和异常处理

技术栈

SpringBoot 3.0.2

SpringCloudAlibaba 2022.0.0.0-RC2

JDK 17

项目结构

父项目--learning

依赖版本相关的pom

xml 复制代码
<modules>  
    <module>user-center</module>  
    <module>common</module>  
    <module>spring-boot-starter-security</module>  
</modules>  
<properties>  
    <java.version>17</java.version>  
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>  
    <spring-boot.version>3.0.2</spring-boot.version>  
    <lombok.version>1.18.24</lombok.version>  
    <spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>  
    <mysql-connection-version>8.0.30</mysql-connection-version>  
    <mybatis-plus.version>3.4.1</mybatis-plus.version>  
    <fastjson.version>2.0.43</fastjson.version>  
</properties>

大家可以自行调整

子项目--common

这里面将会存储整个项目的所有公共的配置,拦截器,数据模型等。所以这里也将统一返回结构和异常处理的部分放了进去。主要依赖如下,版本跟随父级pom

xml 复制代码
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
</dependency>  
<dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>fastjson</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.projectlombok</groupId>  
    <artifactId>lombok</artifactId>  
</dependency>

核心代码

返回结构--ResponseResult

java 复制代码
/**  
* 请求统一返回结果  
* @author yulbo  
* @date 2023/12/24 15:07  
*/  
@Data  
@ToString  
@AllArgsConstructor  
@NoArgsConstructor  
public class ResponseResult<T> implements Serializable {  
  
    /**  
    * 状态码  
    */  
    private Integer code;  

    /**  
    * 返回成功标识  
    */  
    private Boolean success;  

    /**  
    * 返回信息(异常信息)  
    */  
    private String message;  

    /**  
    * 返回结果  
    */  
    private T data;  

    /**  
    * 全参数方法  
    *  
    * @param code 状态码  
    * @param status 状态  
    * @param message 返回信息  
    * @param data 返回数据  
    * @param  
    * @return {@link ResponseResult}  
    */  
    private static ResponseResult response(Integer code, Boolean status, String message, Object data) {  
        ResponseResult responseResult = new ResponseResult();  
        responseResult.setCode(code);  
        responseResult.setSuccess(status);  
        responseResult.setMessage(message);  
        responseResult.setData(data);  
        return responseResult;  
    }  

    /**  
    * 全参数方法  
    *  
    * @param code 状态码  
    * @param status 状态  
    * @param message 返回信息  
    * @param  
    * @return {@link ResponseResult}  
    */  
    private static ResponseResult response(Integer code, Boolean status, String message) {  
        ResponseResult responseResult = new ResponseResult();  
        responseResult.setCode(code);  
        responseResult.setSuccess(status);  
        responseResult.setMessage(message);  
        return responseResult;  
    }  

    /**  
    * 成功返回(数据)  
    *  
    * @param data 数据  
    * @param  
    * @return {@link ResponseResult}  
    */  
    public static ResponseResult success(Object data) {  
        return response(HttpStatusEnum.SUCCESS.getCode(), true, null, data);  
    }  


    /**  
    * 失败返回(状态码+返回信息)  
    *  
    * @param code 状态码  
    * @param message 返回信息  
    * @param  
    * @return {@link ResponseResult}  
    */  
    public static ResponseResult fail(Integer code, String message) {  
        return response(code, false, message);  
    }  


    /**  
    * 失败返回(返回信息)  
    *  
    * @param message 返回信息  
    * @param  
    * @return {@link ResponseResult}  
    */  
    public static ResponseResult fail(String message) {  
        return response(HttpStatusEnum.ERROR.getCode(), false, message, null);    
    }  
}

这个类就是返回结构,同时对外提供了成功和返回的方法。对应的状态码枚举如下

java 复制代码
@Getter  
public enum HttpStatusEnum {  
    /**  
    * 操作成功  
    */  
    SUCCESS(200, "操作成功"),  
    /**  
    * 对象创建成功  
    */  
    CREATED(201, "对象创建成功"),  
    /**  
    * 请求已经被接受  
    */  
    ACCEPTED(202, "请求已经被接受"),  
    /**  
    * 操作已经执行成功,但是没有返回数据  
    */  
    NO_CONTENT(204, "操作已经执行成功,但是没有返回数据"),  
    /**  
    * 资源已被移除  
    */  
    MOVED_PERM(301, "资源已被移除"),  
    /**  
    * 重定向  
    */  
    SEE_OTHER(303, "重定向"),  
    /**  
    * 资源没有被修改  
    */  
    NOT_MODIFIED(304, "资源没有被修改"),  
    /**  
    * 参数列表错误(缺少,格式不匹配)  
    */  
    BAD_REQUEST(400, "参数列表错误(缺少,格式不匹配)"),  
    /**  
    * 未授权  
    */  
    UNAUTHORIZED(401, "未授权"),  
    /**  
    * 访问受限,授权过期  
    */  
    FORBIDDEN(403, "访问受限,授权过期"),  
    /**  
    * 资源,服务未找到  
    */  
    NOT_FOUND(404, "资源,服务未找!"),  
    /**  
    * 不允许的http方法  
    */  
    BAD_METHOD(405, "不允许的http方法"),  
    /**  
    * 资源冲突,或者资源被锁  
    */  
    CONFLICT(409, "资源冲突,或者资源被锁"),  
    /**  
    * 不支持的数据,媒体类型  
    */  
    UNSUPPORTED_TYPE(415, "不支持的数据,媒体类型"),  
    /**  
    * 系统内部错误  
    */  
    ERROR(500, "系统内部错误"),  
    /**  
    * 接口未实现  
    */  
    NOT_IMPLEMENTED(501, "接口未实现"),  
    /**  
    * 系统警告消息  
    */  
    WARN(601,"系统警告消息");  

    private final Integer code;  
    private final String message;  

    HttpStatusEnum(Integer code, String message) {  
        this.code = code;  
        this.message = message;  
    }  
}

可以根据异常指定不同的异常类型指定不同的状态码,比如如果是权限问题,涉及到前端页面的跳转,那么就需要和其他的异常code区分出来。

正常结果返回拦截器--ResponseBodyHandler

java 复制代码
@RestControllerAdvice  
public class ResponseBodyHandler implements ResponseBodyAdvice<Object> {  
    @Override  
    public boolean supports(MethodParameter returnType
    , Class<? extends HttpMessageConverter<?>> converterType) {  
        return true;  
    }  

    @Override  
    public Object beforeBodyWrite(Object body, MethodParameter returnType
    , MediaType selectedContentType
    , Class<? extends HttpMessageConverter<?>> selectedConverterType
    , ServerHttpRequest request, ServerHttpResponse response) {  
        if(body instanceof ResponseResult){  
            return body;  
        }  
        if(body instanceof String){  
            return JSON.toJSONString(ResponseResult.success(body));  
        }  
        return ResponseResult.success(body);  
    }  
}

主要方法就是beforeBodyWrite,这里面有个特殊的就是需要对String类型的特殊处理,大家可以试一下去掉的效果,有一个类型转换的异常。

异常拦截器--

java 复制代码
@RestControllerAdvice  
@Slf4j  
public class GlobalExceptionHandler {  
  
    @ExceptionHandler(value = BusinessException.class)  
    public ResponseResult handleBusinessException(BusinessException bx) {  
        log.info("接口调用业务异常信息{}", bx.getMessage());  
        // 返回统一处理类  
        return ResponseResult.fail( bx.getMessage());  
    }  
    /**  
    * 捕获 {@code ParamException} 异常  
    */  
    @ExceptionHandler(value = ParamException.class)  
        public ResponseResult paramExceptionHandler(ParamException ex) {  
        log.info("接口调用异常:{},异常信息{}", ex.getMessage(), ex.getMessage());  
        // 返回统一处理类  
        return ResponseResult.fail(ex.getMessage());  
    }  

    /**  
    * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用  
    */  
    @ExceptionHandler(value = Exception.class)  
        public ResponseResult handle(Exception ex) {  
        log.info("接口调用异常:{},异常信息{}", ex.getMessage(), ex.getMessage());  
        // 返回统一处理类  
        return ResponseResult.fail(ex.getMessage());  
    }  
}

根据异常类型选择不同的返回信息或者处理情况等等。如果引入了Swagger等接口文档工具,需要在注解@RestControllerAdvice指定扫描包的路径,也就是Controller的路径。异常类示例如下

java 复制代码
public class AccessDenialException extends RuntimeException{  
  
    private Integer code;  

    private String message;  

    /**  
    * 默认信息:未授权,状态码:401  
    * @author yulbo  
    * @date 2023/12/24 21:34  
    */  
    public AccessDenialException(){  
        super("未授权");  
        this.code = HttpStatusEnum.UNAUTHORIZED.getCode();  
    }  

    /**  
    * 默认状态码:401  
    * @author yulbo  
    * @date 2023/12/24 21:35  
    */  
    public AccessDenialException(String message) {  
        super(message);  
        this.message = message;  
        this.code = HttpStatusEnum.UNAUTHORIZED.getCode();  
    }  

    public AccessDenialException(Integer code,String message){  
        super("未授权");  
        this.code = code;  
        this.message = message;  
    }  
  
}

配置类--CommonConfiguration

对外提供拦截器的Bean

java 复制代码
@Configuration  
public class CommonConfiguration {  
  
    @ConditionalOnMissingBean  
    @Bean  
    public ResponseBodyHandler responseBodyHandler() {  
        return new ResponseBodyHandler();  
    }  

    @ConditionalOnMissingBean  
    @Bean  
    public GlobalExceptionHandler globalExceptionHandler() {  
        return new GlobalExceptionHandler();  
    }  
}

可以选择两种方式导入配置类,一种是在启动类上面@Import导入配置,一种是依赖于SpringBoot的扫描配置类的机制,在SpringBoot3的版本改变了扫描的结构,扫描的是resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件里面的配置,在SpringBoot2的版本中是resources/META-INF/spirng.factories,这是个变化。我选择的是扫描注入的方式。

测试

新包userCenter下测试,创建一个Controller,然后发起调用,正常结果如下

java 复制代码
@RestController  
@RequestMapping("userCenter/rest/v1/user")  
public class UserController {  
  
    @GetMapping("/testSuccess")  
    public Object testSuccess(){  
        return "123213";  
    }  
  
    @GetMapping("/testFail")  
    public Object testFail(){  
        throw new AccessDenialException();  
    } 
}

返回结果

异常情况返回结果

相关推荐
命中的缘分27 分钟前
SpringCloud原理和机制
后端·spring·spring cloud
ErizJ27 分钟前
Golang|分布式索引架构
开发语言·分布式·后端·架构·golang
.生产的驴27 分钟前
SpringBoot 接口国际化i18n 多语言返回 中英文切换 全球化 语言切换
java·开发语言·spring boot·后端·前端框架
Howard_Stark31 分钟前
Spring的BeanFactory和FactoryBean的区别
java·后端·spring
-曾牛40 分钟前
Spring Boot中@RequestParam、@RequestBody、@PathVariable的区别与使用
java·spring boot·后端·intellij-idea·注解·spring boot 注解·混淆用法
软件20544 分钟前
【UserDetailsService】
spring boot
极客智谷1 小时前
Spring AI应用系列——基于Alibaba DashScope的聊天记忆功能实现
人工智能·后端
极客智谷1 小时前
Spring AI应用系列——基于Alibaba DashScope实现功能调用的聊天应用
人工智能·后端
RJiazhen1 小时前
5分钟让你的服务接入AI——用 API Auto MCP Server 实现大模型与后端系统的无缝对话
后端·开源·mcp
前端付豪1 小时前
2、ArkTS 是什么?鸿蒙最强开发语言语法全讲解(附实操案例)
前端·后端·harmonyos