V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
binbinyouliiii
V2EX  ›  Java

如何优雅的在 Java 方法同时返回 状态码 和 结果

  •  
  •   binbinyouliiii · 2019-07-23 16:24:35 +08:00 · 7366 次点击
    这是一个创建于 1984 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写 Spring Web 的各位估计都写过返回 JSON 的项目,一般的结构都是这样的

    {
        "status":200,
        "msg":"正常"
        "data":{
            ......
        }
    }
    
    {
        "status":201,
        "msg":"非正常"
    }
    

    按理说 Controller 层不参与业务的处理,顶多校验一下合法性之类的,所以大多数状态码都是交给 Service 层来返回的,可是 Java 的方法只能返回一个结果,想要返回多个结果只能包装成一个对象。但是包装成这样总感觉不舒服,照理说不应该由 service 层包装。

    public Package<User> getUser(){
        User user = new User();
        
        if(user.getAge!=10){
            //返回非正常
            return new Package(201,null);
        }
        //返回正常
        return new Package(200,user);
    }
    

    我想到的还有一种方式就是用 Exception 来做,直接把状态码放到 Exception 中,非 200 状态的就让 Controller 层 catch 后处理,好处就语法上就会有强制性 catch,但是这样做就会出现遇到 如果有多个正常状态码,Controller 层无法处理的问题:

    public User getUser() throws CustomException {
        User user = new User();
        
        if(user.getAge!=10){
            //返回异常
            throw new CustomException(201);
        }
        //返回正常
        return user;
    }
    

    请教大家平常用什么方法比较优雅。

    chendy
        1
    chendy  
       2019-07-23 16:31:53 +08:00   ❤️ 1
    多种正常错误码的场景是什么样的?
    个人观点是不给 service 层做这个包装,全部 controller 做,或者自己拿 ControllerAdvice 或者自己写 MessageConverter 啥的
    顺便吐槽一下国内就这么不喜欢 http 状态码区分成功失败么
    icebow
        2
    icebow  
       2019-07-23 16:32:12 +08:00
    ResponseEntity?
    beryl
        3
    beryl  
       2019-07-23 16:32:38 +08:00
    抛异常,返回错误码问题!,感觉这是个引战帖
    misaka19000
        4
    misaka19000  
       2019-07-23 16:33:59 +08:00
    切面
    EastLord
        5
    EastLord  
       2019-07-23 16:38:51 +08:00
    problem-spring-web 这个行吗
    qwerthhusn
        6
    qwerthhusn  
       2019-07-23 16:39:25 +08:00   ❤️ 1
    我很早之前做过,就是写一个 ControllerAdvice。将所有的 Controller 返回的数据再包裹一层{"code": "success", "data": Controller 返回的}。如果业务想要返回非 success 的响应,通过抛出一个指定的异常,然后再在 ExceptionHandler 里面捕获。

    但是后来发现对于 REST 接口,为什么要将所有的业务响应再包裹一层呢?
    而且我感觉不少公司都是这么搞的。客户端是根据 HTTP 错误码还是根据 body 中自定义的错误码判断业务正常呢????

    我之前发的一个帖子也顺便提到过这个东西。https://www.v2ex.com/t/558315#reply18
    我反正是比较讨厌 REST。


    这个是相关逻辑的代码片段
    ```
    @RestControllerAdvice
    @Slf4j
    public class ControllerResponseWrapper implements ResponseBodyAdvice<Object> {
    private static final List<Class<? extends HttpMessageConverter>> PASSED_CONVERTER_TYPES =
    ImmutableList.of(ResourceHttpMessageConverter.class);

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    return !PASSED_CONVERTER_TYPES.contains(converterType);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
    MediaType selectedContentType,
    Class<? extends HttpMessageConverter<?>> selectedConverterType,
    ServerHttpRequest request, ServerHttpResponse response) {
    // 在 Controller 的某个接口方法返回 String 时,
    // 会由 StringHttpMessageConverter 进行 response 写入,而不再是 MappingJackson2HttpMessageConverter
    // 所以预先转好 JSON 返回
    if (body instanceof String) {
    return JsonUtils.writeValueAsString(new WrappedResponse<>(CommonResultCode.SUCCESS.getCode(), null, body));
    }
    // 如果已经包装成了 ResponseWrapper,例如 ExceptionHandler 处理的,则不再处理
    else if (body instanceof ResultCode) {
    return body;
    } else {
    return new WrappedResponse<>(CommonResultCode.SUCCESS.getCode(), null, body);
    }
    }
    }
    ```
    lululau
        7
    lululau  
       2019-07-23 16:47:54 +08:00
    参照 Rails, 这两种方法都是可取的,Rails 是同时支持这两种风格的 API
    Ravenddd
        8
    Ravenddd  
       2019-07-23 16:49:16 +08:00
    接过一次用 http 状态码区分业务的接口, 还是比较恶心的, 如:用户不存在直接 404, 我还以为我 url 错了
    lululau
        9
    lululau  
       2019-07-23 16:50:50 +08:00
    @Ravenddd 这样有问题吗,资源找不到不是 404 吗
    binbinyouliiii
        10
    binbinyouliiii  
    OP
       2019-07-23 16:51:53 +08:00
    @chendy #1 不排除只把状态码只当状态来用,比如 我返回的数据格式可能有两种,就必须用两个状态码来区分是哪个。
    话说我觉得,http 只是个协议,可能用来做 web 服务,也可能做微服务,如果做微服务,我哪天改通讯协议了,岂不是跟 HTTP 耦合了,改起来不也烦。

    @beryl #3 这种帖子能引起战就有鬼了。

    @EastLord #5 处理方法尽量和框架无关比较好。
    TheBestSivir
        11
    TheBestSivir  
       2019-07-23 16:54:28 +08:00
    @chendy 恩,国内互联网尤其不喜欢 http 状态码区分成功失败,因为几乎没有大厂的 C 端业务完全适合 restful,而假设不玩 restful 的完全范式,其所谓的统一接口( Uniform Interface )也就没有意义了,自然就是放飞自我,各自团队各自规范,协议层也没啥特别统一的必要了。
    还有最重要的一点,劫持太严重了,走 http 状态码实在是不行。。。
    binbinyouliiii
        12
    binbinyouliiii  
    OP
       2019-07-23 16:55:32 +08:00
    @chendy #1 其实还有,国内网络环境很复杂,很多网络设备都有防火墙之类的装置,他们会拦截你的非 200 结果,返回其他结果
    vmskipper
        13
    vmskipper  
       2019-07-23 17:00:46 +08:00
    自定义状态码 参考各司开放平台文档
    Takamine
        14
    Takamine  
       2019-07-23 17:01:14 +08:00
    常规的做法就是统一封装响应对象到比如 ServiceResponse<T>中,不同系统和业务之间的状态码也具体描述在枚举字段中统一整理。
    至于是不是就遵从用 http 状态码 restful 的那一套见仁见智吧。
    个人觉得可能 http 的状态码 200 表示这次请求是成功的,但是返回的对象里面的 code 是 50010 表示密码错误,也完全可以接受。
    binbinyouliiii
        15
    binbinyouliiii  
    OP
       2019-07-23 17:09:23 +08:00
    @Takamine #14 就是感觉由 Service 封装不舒服
    Aresxue
        16
    Aresxue  
       2019-07-23 17:16:19 +08:00
    自定义状态码,比较友好的方式是 HTTP 状态码+四位自定义编码作为整个状态码,嫌长的话就屏蔽掉 Http 状态码,只使用自定义四位或六位编码。对系统异常和业务异常做严格区分,最好设置个切面统一捕获处理。
    binbinyouliiii
        17
    binbinyouliiii  
    OP
       2019-07-23 17:19:10 +08:00
    @Aresxue #16
    @vmskipper #13
    我做的就是自定义状态码,文中用 200 表示是为了方便理解这是一个正常的状态码。
    nikandaoleshenme
        18
    nikandaoleshenme  
       2019-07-23 17:45:47 +08:00
    @chendy 最后一句话表示同理,最烦 code=200,200 从哪冒出来的,莫名其妙,http 也为 200,到底按哪个值为准,应该是很久很久很久以前,有个程序员写的,后面的都是从他这复制来的,无论请求成功,失败,异常,错误,都特么的 http 200,
    zhybb2010
        19
    zhybb2010  
       2019-07-23 17:51:58 +08:00
    我使用 异常 /拦截器 去做处理,http 状态码一半只会使用 200/403, 其它的都是自己的状态码控制
    Takamine
        20
    Takamine  
       2019-07-23 18:07:50 +08:00 via Android
    @binbinyouliiii 我们的开发规范是 controller 不包含任何业务逻辑(除开部分简单验签),相当于整个业务全是 service 包场,controller 就只是一个 return,其他啥都不用干。不管怎么样,最后总得有个地方把响应封装,实现方式多种多样,按照开发规范来_(:з」∠)_。
    ke1e
        21
    ke1e  
       2019-07-23 18:15:30 +08:00 via Android
    一个简单的方法,所有方法都返回同一个类,假设是 TopContext,有状态码和 data 属性,再提供静态方法,例如 TopContext.ok(data),这样就可以实现。但是还是有点复杂
    liuxey
        22
    liuxey  
       2019-07-23 19:54:37 +08:00
    我见过一个国内知名公司使用四层结构,在 controller 和 service 层增加 facade 层,然后 controller 就真只做接收校验和返回

    虽然有人可能不喜欢,但使用异常来结束业务代码真的很精简,写起来及其飘逸

    当然还有 http 状态码设计等东西,我觉的没有孰优孰劣,用好了用统一了都是值得肯定的
    shipy
        23
    shipy  
       2019-07-23 19:56:31 +08:00
    用元组呀
    palmers
        24
    palmers  
       2019-07-23 20:12:21 +08:00
    异常和状态码的作用目的是不一样的,你这么用是不对的, 再者 java 是面向对象语言,应该站在对象的角度来考虑 如果按照对象角度返回状态对象就是合理的, 再者 底层抛出异常代表这个是无法容忍的错误, 不应该继续后续逻辑,而不是应该用来设计不同的状态
    springmarker
        25
    springmarker  
       2019-07-23 20:28:49 +08:00
    @Takamine #20 状态码的包装也由 service 层来处理的话,那碰到 service 之间的调用难道还得 get 出来 data 才行?
    顺便问一下,你们的状态码 code 的枚举类是怎么维护的,是每个项目一个?还是所有项目共用?共用的话是怎么维护的,谢谢。

    @ke1e #21 我之前就是这么做的,但是还是属于我说的第一种类型,就是包装,感觉不够清真。

    @liuxey #22 写 4 层是真的要死了。

    @shipy #23 元组在 Java 中并不原生,某种程度上来说跟包装差不多。

    @palmers #24 绝大多数业务就是 一个正常状态+多个异常状态 ,我觉得 业务的抛出非正常状态代码也可以理解为“异常”,catch 的话,只需 catch 住我们自定义的异常就可以了,其他无法容忍的错误该怎么还是怎么样
    lihongjie0209
        26
    lihongjie0209  
       2019-07-23 20:34:58 +08:00
    业务层直接抛异常, controller 只处理正常情况, 异常情况同意用拦截器翻译异常类型以及异常信息发送给前端
    lwd369
        27
    lwd369  
       2019-07-23 20:41:31 +08:00
    正好今天白天一直在纠结这个问题,在 service 层处理非正常响应时,直接抛出一个 exception,然后用 ExceptionHandler 里统一返回,不知道这样使用异常是否合理。如果使用普通的 ifelse 流程控制的话,要把错误状态返回给 controller 层似乎又有点麻烦。
    Takamine
        28
    Takamine  
       2019-07-24 00:15:07 +08:00 via Android
    @springmarker 是的,只要是暴露出去的都做封装,不管是业务之间还是应用之间。
    我们是不同系统用不同状态码,一般一个系统会分别领取分别代表业务异常、系统异常等的不同数字编号开头的 500-2000 个状态码。后面新增的系统再接着申请领取状态码,以此类推。所有系统的状态码有做统一维护。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2051 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 42ms · UTC 00:41 · PVG 08:41 · LAX 16:41 · JFK 19:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.