暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

拯救在抛出NPE和null值判断间犹豫不前的你

解析与重构 2021-01-19
479

有个问题不知道有没有困扰过大家,比如下面这段代码:

@Override
public UserPropertyEntity convert(String message) throws MessageConversionException {
    try {
        Map<String, Object> data = DefaultJsonUtils.deserialize(message);

        if (data == null) {
            throw new MessageConversionException("Deserialize kafka message[" + message + "] error");
        }

        Long userId = (Long)data.get("user_id");
        if (userId == null) {
            throw new MessageConversionException("The message does not contain the key[user_id],it is illegal.");
        }

        String msgType = (String)data.get("msg_type");
        if (msgType == null) {
            throw new MessageConversionException("The message does not contain the key[msg_type], it is illegal.");
        }
        return converterFactory.getConverter(msgType.trim()).convert(userId, data);
    } catch (JsonDeserializeException e) {
        throw new MessageConversionException("Deserialize kafka message[" + message + "] failed.", e);
    } catch (ConversionException e) {
        throw new MessageConversionException("The user_id in message[" + message + "] is illegal, cann`t cast into long.", e);
    }
}

这段代码的主要作用其实就是将传入进来的String
类型的参数转换成一个Entity
对象。

但是,除了异常处理的部分之外,其他大部分的代码都是在判断数据是否为null。

优秀的Java程序员写出来的代码,不应该在不该抛出NullPointException
的地方抛出NullPointException

我们常常会听到这样的原则:

  • 所有public
    类型的方法入参在使用之前都应该做一次非空判断
  • 所有函数的返回参数,在使用之前都应该做一次非空判断

可是,这样写出来的Java代码,实在是太累赘了啊!

我最近一直在反思,这样的原则一定是必要的吗?难道调用public
方法的调用方一定是不可信任的,随时都有可能传入null值作为函数入参吗?程序中调用的其他方法,也随时都可能使用null值作为返回值吗?

最好在Java语言中,可以增加一部分限制和约束,能够在特定的结构中,保证函数的入参或出参都不可能是null。如果能做到这点,Java写出来的代码将会简洁很多!

因此,可以将应用程序分分类:

  • 接受外部数据作为程序执行的入口的模块,比如Spring WebMvc
    中的@Controller
    类,以及各种PRC
    接口处理类;

  • 调用外部接口作为程序执行出口的模块,比如JDBC
    调用,外部接口调用;

  • 其他程序内部的分层模块,比如Spring WebMvc
    应用中的@Service
    层、@Repository
    层等;

由于接受外部系统调用请求和调用外部系统服务,这两种类型的模块都依赖于外部服务的入参和出参,因此在这些模块中增加非空校验一定是必须的。这块哪怕代码显得累赘,或稍微影响性能都不应该去除非空判断。调用或被调用的部分不加保护,无异于在大街上裸奔……

但是为了逻辑清晰,在系统内部采取的应用逻辑分层的各个模块,完全可以在遵循一定约束规则的前提下,取消掉非空判断。

Spring
框架的某些包中,存在如下的package-info.java
文件:

@NonNullApi
@NonNullFields
package com.xiaohongshu.risk.platform.insight.schema.support;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

package-info.java
是Java中的包描述文件,每个package
下面只能有一个,且名字唯一。在stack overflow
上有这么一段回答:

Another good reason to use package-info.java is to add default annotations for use by FindBugs. For instance, if you put this in your package-info file:

@DefaultAnnotation(NonNull.class)
package com.my.package
;

then when findbugs runs on the code in that package, all methods and fields are assumed to be non-null unless you annotate them with @CheckForNull. This is much nicer and more foolproof than requiring developers to add @NonNull annotations to each method and field.

于是,我有了一个大胆的想法:

/**
 * @author jingxuan
 * @date 2021/1/18 8:40 下午
 */

@Nonnull
@ParametersAreNonnullByDefault
package com.xiaohongshu.risk.platform.insight.schema.support;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;

在每一个内部package
(指不调用外部服务,也不被外部服务调用的package
)中,增加一个默认的package-info.java
文件,文件内容如上。

然后保证,这个package
中的所有方法的返回值都是非null的值,且保证调用这个package
中方法时传入的参数一定没有null值。

之后,在每个方法的内部处理逻辑上,就再也不写任何的null值判断逻辑了。

如果有例外,可以在可能是null值的位置(方法的返回值,或者方法的入参),使用@Nullable
标注,比如:

private void init(String cur, @Nullable String tail) {
    this.cur = cur;
    this.tail = tail;
}

再比如:

public @Nullable String getTail() {
    return this.tail;
}

这样做有什么用呢,好处如下:

  • 自己写代码的时候,会明确知道哪些方法会返回null,哪些不会;也会知道调用哪些方法的时候不应该传入null值;

  • 对于有@ParametersAreNonnullByDefault
    声明的方法,再也不用写对入参的null值判断逻辑了

  • 别人阅读你的代码的时候,更便于理解,也更便于修改(做个好人,别再制造祖传代码了)

那有没有自动化工具可以帮助检测哪些代码违反了约束呢?有!findbugs
,你值得拥有。

不过作为技术专家,我们是不会强制依赖这些工具的,优秀的代码应该出自良好的习惯。


文章转载自解析与重构,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论