networknt::json-schema-validator 源码赏析

Json 是一种自解释语言,广泛应用于请求协议、配置文件、格式规范等场景。为了约束 Json 数据格式,需要用到另外一种特殊的 Json 数据 -- JsonSchema 规范。

官网 https://json-schema.org/ 推荐了snow、vert.x、everit-org、networknt等几种 Java 实现,其中 networknt 以优异的性能获得广泛的应用,今天我们一起来分析一下 networknt 的 Java 版本实现。


代码仓库: https://github.com/networknt/json-schema-validator 版本(1.0.64)

核心摘要

各种预制的 validator 都继承自 BaseJsonValidator, 其中有两个重要方法 walk(...) 和 validate(...) 。 两者功能和返回值相似,walk 方法支持在 validate 方法前后调用注册过的 PreWalkListeners/PostWalkListeners 切面方法,可以在里面实现一些自定义功能,比如打印日志。

各种预制的 validator 都需要在ValidatorTypeCode 里进行注册,我们来看一下 ValidatorTypeCode 声明的部分核心代码:

public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
    ...
    MAXIMUM("maximum", "1011", new MessageFormat(I18nSupport.getString("maximum")), MaximumValidator.class, 15),
    ...
    IF_THEN_ELSE("if", "1037", null, IfValidator.class, 12),
    ...
    private ValidatorTypeCode(String value, String errorCode, MessageFormat messageFormat, Class validator, long versionCode) {
        this.value = value;
        this.errorCode = errorCode;
        this.messageFormat = messageFormat;
        this.errorCodeKey = value + "ErrorCode";
        this.validator = validator;
        this.versionCode = versionCode;
        this.customMessage = null;
    }
    
    ...
    public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws Exception {
        if (validator == null) {
            throw new UnsupportedOperationException("No suitable validator for " + getValue());
        }
        // if the config version is not match the validator
        @SuppressWarnings("unchecked")
        Constructor<JsonValidator> c = ((Class<JsonValidator>) validator).getConstructor(
                new Class[]{String.class, JsonNode.class, JsonSchema.class, ValidationContext.class});
        return c.newInstance(schemaPath + "/" + getValue(), schemaNode, parentSchema, validationContext);
    }
    
    ...    
}

有没有发现什么问题?

虽然每种检查元素都是 validator,但在注册到 Factory 的时候实际是被 ValidatorTypeCode (一种Keyword 实现)进行了逐一包装。当需要展开成 validator 时,通过注册的 class 类型进行反射找到固定签名的构造函数并实例化。

性能倒不用担心,validators 是被 JsonSchema 懒加载并持有的,只会被初始化一次且会伴随整个 JsonSchema 实例的整个生命周期不再变更。

那是怎么确定需要加载哪些 validators 呢? 这里就需要提到 Json-Schema 的语法版本 (见 https://json-schema.org/specification-links.html)。networknt 目前支持v4、v6、v7、v2019-09 版本,每个版本都会规定具体检查关键字。

ValidatorTypeCode 中有一个 versionCode 属性,每个 validator 在注册时都指明了自己的版本。为了便于存储 versionCode 通过二进制掩码标记适用版本的。例如 MAXIMUM:15 = 8 + 4 + 2 + 1 (适用所有版本),IF_THEN_ELSE:12 = 8 + 4 (适用 v2019-09 和 v7 )。

versionCode掩码

每个语法版本的注册信息都在 JsonMetaSchema 中,以 v6 为例:

     private static class V6 {
        private static String URI = "https://json-schema.org/draft-06/schema";
        // Draft 6 uses "$id"
        private static final String ID = "$id";

        public static final List<Format> BUILTIN_FORMATS = new ArrayList<Format>(JsonMetaSchema.COMMON_BUILTIN_FORMATS);

        static {
            // add version specific formats here.
            //BUILTIN_FORMATS.add(pattern("phone", "^\\\\+(?:[0-9] ?){6,14}[0-9]$"));
        }

        public static JsonMetaSchema getInstance() {
            return new Builder(URI)
                    .idKeyword(ID)
                    .addFormats(BUILTIN_FORMATS)
                    .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V6))
                    // keywords that may validly exist, but have no validation aspect to them
                    .addKeywords(Arrays.asList(
                            new NonValidationKeyword("$schema"),
                            new NonValidationKeyword("$id"),
                            new NonValidationKeyword("title"),
                            new NonValidationKeyword("description"),
                            new NonValidationKeyword("default"),
                            new NonValidationKeyword("definitions")
                    ))
                    .build();
        }
    }        

主要由两部分组成:ValidatorTypeCode(版本对应的 validators),NonValidationKeyword(版本对应的系统关键字)。两种类型都实现自 Keyword 关键字,用户也可以通过 Keyword 实现自定义方言。

开篇讲过 Json-Schema 是一种特殊的 Json 数据,所以 validators 的全部构建过程就是对 json-schema tree 的解析过程。关键代码:

public JsonSchema extends BaseJsonValidator {
    ...
    /**
     * Please note that the key in {@link #validators} map is a schema path. It is
     * used in {@link com.networknt.schema.walk.DefaultKeywordWalkListenerRunner} to derive the keyword.
     */
    private Map<String, JsonValidator> read(JsonNode schemaNode) {
        Map<String, JsonValidator> validators = new TreeMap<>(VALIDATOR_SORT);
        if (schemaNode.isBoolean()) {
            if (schemaNode.booleanValue()) {
                final String customMessage = getCustomMessage(schemaNode, "true");
                JsonValidator validator = validationContext.newValidator(getSchemaPath(), "true", schemaNode, this, customMessage);
                validators.put(getSchemaPath() + "/true", validator);
            } else {
                final String customMessage = getCustomMessage(schemaNode, "false");
                JsonValidator validator = validationContext.newValidator(getSchemaPath(), "false", schemaNode, this, customMessage);
                validators.put(getSchemaPath() + "/false", validator);
            }
        } else {
            Iterator<String> pnames = schemaNode.fieldNames();
            while (pnames.hasNext()) {
                String pname = pnames.next();
                JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname);
                String customMessage = getCustomMessage(schemaNode, pname);
                JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this, customMessage);
                if (validator != null) {
                    validators.put(getSchemaPath() + "/" + pname, validator);

                    if (pname.equals("required")) {
                        requiredValidator = validator;
                    }
                }

            }
        }
        return validators;
    }
    ...
}

看似只在第一层 schema 做了 validators 的生成,实际上对 properties 等嵌套字段,其内部各自持有子结构的 validators (详见 PropertiesValidator)。

以上就是各种语法关键字定义和 validators 注册过程,下面以一个原子检查器 MaxItemsValidator 为例,具体分析检查过程。

// schema 配置样式: "maxItems": 5

public class MaxItemsValidator extends BaseJsonValidator implements JsonValidator {

    private static final Logger logger = LoggerFactory.getLogger(MaxItemsValidator.class);

    private final ValidationContext validationContext;

    private int max = 0;

    public MaxItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
        super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_ITEMS, validationContext);
        if (schemaNode.canConvertToExactIntegral()) {
            max = schemaNode.intValue();
        }
        this.validationContext = validationContext;
        parseErrorCode(getValidatorType().getErrorCodeKey());
    }

    public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
        debug(logger, node, rootNode, at);

        if (node.isArray()) {
            if (node.size() > max) {
                return Collections.singleton(buildValidationMessage(at, "" + max));
            }
        } else if (this.validationContext.getConfig().isTypeLoose()) {
            if (1 > max) {
                return Collections.singleton(buildValidationMessage(at, "" + max));
            }
        }

        return Collections.emptySet();
    }

}

生成 validator 实例时调用构造函数传入的 schemaPath 和 schemaNode ,取得配置的最大元素个数保存在max属性中。

当遍历数据到对应 node 节点时,会检查对应的 validators, 找到 maxitems 的检查器实例并调用 validate 方法,该方法先判断当前 node 是否为 array 类型,true 则继续判断数组长度是否超过最大限制,否则丢出类型错误。

当命中错误时会调用公共的 buildValidationMessage (String at, String... arguments) 方法。at 用于表示当前错误发生在 json tree 的具体层级位置, arguments 则用于填充在 ValidatorTypeCode 中声明的 MessageFormat 的参数占位符。

maxItems = {0}: there must be a maximum of {1} items in the array

填充后则为: {路径} :下的元素数量不能超过最多 {max} 个 。

需要注意的是错误是以集合 Set 的形式返回,ValidationMessage 的 equals 方法也有做过特殊处理:

public class ValidationMessage {
    ...
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        ValidationMessage that = (ValidationMessage) o;

        if (type != null ? !type.equals(that.type) : that.type != null) return false;
        if (code != null ? !code.equals(that.code) : that.code != null) return false;
        if (path != null ? !path.equals(that.path) : that.path != null) return false;
        if (details != null ? !details.equals(that.details) : that.details != null) return false;
        if (!Arrays.equals(arguments, that.arguments)) return false;
        return !(message != null ? !message.equals(that.message) : that.message != null);

    }
    ...
}

注:从 1.0.58 版本开始增加了多国语言(中/英文)的报错支持,并且增加了自定义错误的功能,这里不再展开详述。


以上即是对 networknt 的 json-schema-validator 核心源码的分析。

总结起来两个要点:

1、检查器的原子化,后期可以通过配置组合的方式进行深层次嵌套。

2、树遍历,Schema 初始化阶段递归生成 validators ,Data 递归触发 validators。

设计方面有很多值得学习的地方比如原子组件的嵌套、切面化 listener 埋点。

但代码中也有很多硬编码,用反射进行 keyword 到 validator 映射等不是特别优雅的地方。

随着版本迭代,逐步添加了i18n、customMessage等更多新功能,笔者会继续跟进一些新的特性,欢迎一起交流学习。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
networknt::json-schema-validator 源码赏析
Json 是一种自解释语言,广泛应用于请求协议、配置文件、格式规范等场景。为了约束 Json 数据格式,需要用到另外一种特殊的 Json 数据 -- JsonS...
<<上一篇
下一篇>>