栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

web应用中一种统一返回体的设计

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

web应用中一种统一返回体的设计

我的个人网站:http://riun.xyz


JDK1.8,SpringBoot2.6.0

1、依赖


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.6.0
         
    
    com.example
    demo
    0.0.1-SNAPSHOT
    demo
    Demo project for Spring Boot
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    


2、统一返回体类
package com.example.demo.entity.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import javax.servlet.http.HttpServletResponse;


@Data
@AllArgsConstructor
@Builder
public class baseResponse  {
    private int code;
    private String msg;
    private T data;

    public baseResponse(T data) {
        code = HttpServletResponse.SC_OK;
        msg = "success";
        this.data = data;
    }

    public baseResponse(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

}

工具类:

package com.example.demo.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;


@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class JacksonUtil {

    private final static ObjectMapper objectMapper;

    static {
        objectMapper = initWrapperObjectMapper(new ObjectMapper());
    }

    
    public static String toJson(Object object) {
        if (isCharSequence(object)) {
            return (String) object;
        }
        try {
            return getObjectMapper().writevalueAsString(object);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    
    public static ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    
    public static ObjectMapper initWrapperObjectMapper(ObjectMapper objectMapper) {
        if (Objects.isNull(objectMapper)) {
            objectMapper = new ObjectMapper();
        }
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        //不显示为null的字段
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 忽略不能转移的字符
        objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
        // 过滤对象的null属性.
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //忽略transient
        objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        simpleModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        simpleModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
//        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }

    
    public static Boolean isCharSequence(Object object) {
        return !Objects.isNull(object) && isCharSequence(object.getClass());
    }

    
    public static boolean isCharSequence(Class clazz) {
        return clazz != null && CharSequence.class.isAssignableFrom(clazz);
    }

    
    public static Object parse(String json) {
        Object object = null;
        try {
            object = getObjectMapper().readValue(json, Object.class);
        } catch (Exception ignored) {
        }
        return object;
    }

    
    public static  T parseObject(String json, Class clazz) {
        T t = null;
        try {
            t = getObjectMapper().readValue(json, clazz);
        } catch (Exception ignored) {
        }
        return t;
    }

}
3、自定义注解

自定义注解@ResultUnite,该注解标注的类或方法,都能被包装成统一的返回。

wrapper包/annotation包下新建此类

package com.example.demo.wrapper.annotation;

import java.lang.annotation.documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target({ ElementType.TYPE, ElementType.METHOD }) //作用范围
@Retention(RetentionPolicy.RUNTIME)
@documented
@Inherited
public @interface ResultUnite {
}

然后使用@ControllerAdvice注解,来拦截所有 @RequestMapping 等注解标注的方法。这些方法返回时,不立刻返回,而是先经过 supports方法判断是否需要处理,返回true时,则执行beforeBodyWrite方法。

wrapper包/advice包下新建此类

package com.example.demo.wrapper.advice;

import com.example.demo.entity.vo.baseResponse;
import com.example.demo.utils.JacksonUtil;
import com.example.demo.wrapper.annotation.ResultUnite;
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.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.reflect.Method;


@ControllerAdvice
public class baseResponseBodyAdvice implements ResponseBodyAdvice {

    private static final String CONVERT_NAME = "org.springframework.http.converter.StringHttpMessageConverter";


    private boolean isResultUnite(MethodParameter methodParameter, Class aClass) {
        Method method = methodParameter.getMethod();
        return aClass.isAnnotationPresent(ResultUnite.class) ||method.isAnnotationPresent(ResultUnite.class);
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return isResultUnite(methodParameter, aClass);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //处理String类型的返回值
        if (CONVERT_NAME.equalsIgnoreCase(selectedConverterType.getName())) {
            return JacksonUtil.toJson(new baseResponse(body));
        }
        return new baseResponse(body);
    }
}
 

在supports方法中,我们的处理是:通过反射获取到执行方法/类上是否有@ResultUnite注解,有就返回true;无则返回false。

在beforeBodyWrite方法中,我们将方法的返回作为data传入baseResponse构造方法中,重新new了一个baseResponse对象,所以就达到了将返回数据包装成baseResponse的目的。

以上两个类就给我们提供了一个具有能够包装返回数据的注解@ResultUnite。

3.1、阶段测试

新建controller类测试一下

package com.example.demo.controller;

import com.example.demo.wrapper.annotation.ResultUnite;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@Slf4j
@RestController
public class TestController {

    @ResultUnite
    @RequestMapping("/test1")
    public String test1() {
        log.info("---------------test1---------------");
        return "111";
    }
}

上述test1方法被本应该返回“111”的,但是被@ResultUnite注解了,所以执行观察,返回包装后的结果:

{
    "code": 200,
    "msg": "success",
    "data": "111"
}

再试一下:

@ResultUnite
@RequestMapping("/test2")
public List test2() {
    log.info("---------------test2---------------");
    List list = new ArrayList(){
        {
            add("123");
            add("456");
        }
    };
    return list;
}

执行返回:

{
	"code": 200,
	"msg": "success",
	"data": [
		"123",
		"456"
	]
}

这样我们就在任何成功的情况下,统一了对外部的返回(code,msg,data)。且实现非常清爽,在业务代码中只需要一个注解即可。

可是异常情况呢?异常情况因为不会再从controller中的return出去,所以就不受控制了。接下来处理异常情况。

4、统一异常处理 4.1、自定义异常类

这个类看起来和baseResponse结构一样,那为什么不能使用baseResponse代替呢?

因为此类需要继承RuntimeException,将来在代码中可以catch住此异常信息,然后做处理。况且两个类分别是不同功能的,即使属性一样,也不能复用。

package com.example.demo.entity.vo;

import com.example.demo.entity.enums.ExceptionCodeEnum;
import lombok.Data;


@Data
public class CustomerExceptionVO extends RuntimeException {

    private int code;
    private String msg;
    private Object data;

    public CustomerExceptionVO(ExceptionCodeEnum exceptionCodeEnum) {
        code = exceptionCodeEnum.getCode();
        msg = exceptionCodeEnum.getMsg();
    }


    public CustomerExceptionVO(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public CustomerExceptionVO(int code, String msg, Object data) {
        super(msg);
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

枚举异常:

package com.example.demo.entity.enums;

import javax.servlet.http.HttpServletResponse;


public enum ExceptionCodeEnum {

    //通用异常
    
    Bad_Request(HttpServletResponse.SC_BAD_REQUEST, "请求信息错误,请检查参数"),
    
    UnAuth(HttpServletResponse.SC_UNAUTHORIZED, "请先登陆"),
    
    Forbidden(HttpServletResponse.SC_FORBIDDEN, "无权查看"),
    
    Not_Fount(HttpServletResponse.SC_NOT_FOUND, "未找到该路径或资源"),
    
    Method_Not_Allowed(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "请求方式不支持"),

    
    Internal_Server_Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常"),
    
    Service_Unavailable(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "请求超时"),

    //自定义扩展其他业务异常
    
    Username_Or_PassWord_Error(HttpServletResponse.SC_BAD_REQUEST, "用户名密码错误"),
    
    Username_Exist(HttpServletResponse.SC_BAD_REQUEST, "用户名已存在"),
    
    ValidCode_Error(HttpServletResponse.SC_BAD_REQUEST, "验证码不正确"),


    ;
    private int code;
    private String msg;

    ExceptionCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }


    public static ExceptionCodeEnum getByCode(int code) {
        ExceptionCodeEnum[] enums = ExceptionCodeEnum.values();
        for (ExceptionCodeEnum exceptionCodeEnum : enums) {
            if (exceptionCodeEnum.code == code) {
                return exceptionCodeEnum;
            }
        }
        return null;
    }
}

工具类:

package com.example.demo.utils;

import java.text.SimpleDateFormat;
import java.util.Date;


public class CommonUtil {

    public static Object getNotNull(Object original, Object backup) {
        if (backup == null) {
            throw new RuntimeException("备用参数不能为空");
        }
        return original != null ? original : backup;
    }


    public static Object getOrDefault(Object original, Object defaultValue) {
        return original != null ? original : defaultValue;
    }


    public static String getNonce() {
        return getNonce(8);
    }


    
    public static String getNonce(int n) {
        String nonce = "";
        //一个十六进制的值的数组
        String[] array = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"};
        //得到6个随机数
        for (int i = 0; i < n; i++) {
            int num = (int) (Math.random() * 16);
            nonce += array[num];
        }
        return nonce;
    }


    
    public static String getNowDateFomart(){
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        Date date = new Date();
        return format.format(date);
    }
}
4.2、统一异常处理

使用@ControllerAdvice加@ExceptionHandler注解,拦截代码中抛出的异常。拦截到属于CustomerExceptionVO的异常就优先handleCustomerException方法处理;否则就放行,看是否属于Exception异常(显然剩下的都是),然后由handleLeftException处理。

package com.example.demo.exception;

import com.example.demo.entity.vo.baseResponse;
import com.example.demo.entity.vo.CustomerExceptionVO;
import com.example.demo.utils.CommonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandle {


    
    @ExceptionHandler(CustomerExceptionVO.class)
    public baseResponse handleCustomerException(CustomerExceptionVO e, HttpServletRequest request) {
        log.error("异常request uri: {}", request.getRequestURI());
        log.error(e.getMessage(), e);
        return new baseResponse((Integer) CommonUtil.getNotNull(e.getCode(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR), (String) CommonUtil.getOrDefault(e.getMsg(), e.getMessage()), e.getData());
    }


    
    @ExceptionHandler(Exception.class)
    public baseResponse handleLeftException(Exception e, HttpServletRequest request) {
        log.error("异常request uri: {}", request.getRequestURI());
        log.error(e.getMessage(), e);
        return new baseResponse(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常");
    }
}
4.3、意料之外错误的处理

当不是我们的代码抛出的异常时,GlobalExceptionHandle拦截不到,需要使用@RequestMapping(“error”)加ErrorController处理。

package com.example.demo.exception;

import com.example.demo.entity.enums.ExceptionCodeEnum;
import com.example.demo.entity.vo.baseResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;


@Slf4j
@Controller
@RequestMapping("error")
public class GlobalErrorController implements ErrorController {

    @RequestMapping
    @ResponseBody
    public baseResponse error(HttpServletResponse response) {
        int statusCode = response.getStatus();
        ExceptionCodeEnum exceptionCodeEnum = ExceptionCodeEnum.getByCode(statusCode);
        if (exceptionCodeEnum == null) {
            exceptionCodeEnum = ExceptionCodeEnum.Internal_Server_Error;
        }
        log.error("error: {}", exceptionCodeEnum.getMsg());
        baseResponse baseResult = new baseResponse(exceptionCodeEnum.getCode(), exceptionCodeEnum.getMsg());;
        return baseResult;
    }
}
4.4、阶段测试

①、我们故意抛出一个异常:

    @ResultUnite
    @RequestMapping("/test3")
    public String test3() {
        log.info("---------------test3---------------");
        try {
            int i = 5 / 0;
        } catch (Exception e) {
            throw e;
        }
        return "111";
    }

执行测试:

{
	"code": 500,
	"msg": "服务器异常",
	"data": null
}

这个throw e;就是被GlobalExceptionHandle中的handleLeftException拦截到了。

②、让程序内部(非代码)抛出一个异常

    @ResultUnite
    @RequestMapping("/test4")
    public String test4() {
        log.info("---------------test4---------------");
        int i = 5 / 0;
        return "111";
    }

执行测试:

{
	"code": 500,
	"msg": "服务器异常",
	"data": null
}

也能被GlobalExceptionHandle中的handleLeftException拦截到。

③、某些情况需要抛出自定义异常

    @ResultUnite
    @RequestMapping("/test5")
    public String test5() {
        log.info("---------------test5---------------");
        //假装i是入参
        int i = 1;
        if (1 == i) {
            throw new CustomerExceptionVO(ExceptionCodeEnum.Forbidden);
        }
        return "111";
    }

执行测试:

{
	"code": 403,
	"msg": "无权查看",
	"data": null
}

被GlobalExceptionHandle中的handleCustomerException拦截到。

5、异常结束工具

在4.4的③中,这种情况应该是经常由的,我们需要对业务情况判断,然后众多不符合队则的case都异常返回了,这样每次都throw new CustomerExceptionVO是很麻烦的事情,所以可以搞一个工具来使用。

package com.example.demo.exception;

import com.example.demo.entity.enums.ExceptionCodeEnum;
import com.example.demo.entity.vo.CustomerExceptionVO;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;


@Component
public class ApiAssert {

    public static void notNull(Object a) {
        if (a == null) {
            failure(ExceptionCodeEnum.Bad_Request);
        }
        if (a instanceof String) {
            if (((String) a).length() == 0) {
                failure(ExceptionCodeEnum.Bad_Request);
            }
        }
    }

    public static void notNull(Object a, String msg) {
        if (a == null) {
            badRequest(msg);
        }
        if (a instanceof String) {
            if (((String) a).length() == 0) {
                badRequest(msg);
            }
        }
    }

    //-------------------------400-----------------------

    public static void badRequest(String msg) {
        throw new CustomerExceptionVO(HttpServletResponse.SC_BAD_REQUEST, msg);
    }

    //--------------------------500-----------------------
    public static void failure(ExceptionCodeEnum exceptionCodeEnum) {
        throw new CustomerExceptionVO(exceptionCodeEnum);
    }

    public static void failure(String msg, Object data) {
        throw new CustomerExceptionVO(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, data);
    }

    public static void failure(int code, String msg, Object data) {
        throw new CustomerExceptionVO(code, msg, data);
    }

    public static void failure(Object data) {
        throw new CustomerExceptionVO(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error", data);
    }

    //--------------------------业务工具----------------------

    public static void forbidden() {
        throw new CustomerExceptionVO(ExceptionCodeEnum.Forbidden);
    }

    public static void internal() {
        throw new CustomerExceptionVO(ExceptionCodeEnum.Internal_Server_Error);
    }

}

然后就可以在业务代码中使用:

    @ResultUnite
    @RequestMapping("/test6")
    public String test6() {
        log.info("---------------test6---------------");
        //假装i是入参
        int i = 1;
        if (1 == i) {
            ApiAssert.badRequest("请求不合法");
        }
        return "111";
    }

执行测试:

{
	"code": 400,
	"msg": "请求不合法",
	"data": null
}

这样就可以在任何需要的地方使用ApiAssert来结束并返回。

6、总结

上述统一返回体的处理只是一种处理方式,包含了正常结束,异常返回,业务代码自定义中断返回的处理。也许并不适合所有。但能包容大多数情况。

其实仍有许多改进的地方,比如抛出的异常错误码,完全可以自定义一个自己系统统一的错误码表,然后使用这个码表,就不用每次都使用HttpServletResponse的了。

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号