有个问题不知道有没有困扰过大家,比如下面这段代码:
@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
,你值得拥有。
不过作为技术专家,我们是不会强制依赖这些工具的,优秀的代码应该出自良好的习惯。




