Presto加载插件时的白名单机制

背景

遇到一个业务需求,需要将各个查询框架的udf整合在一起,即只用一个jar包且这个jar包包含了hive、presto、gp各个查询框架的udf的实现。

某个函数(称其函数A)需要有hive实现和presto实现,而且它要使用json序列化的功能,选择了com.fasterxml.jackson,由于hive没有这个依赖,因此添加依赖到项目里:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.5</version>
</dependency>

函数A使用jackson的部分代码如下:

public class JsonUtil {

    private static final ThreadLocal<ObjectMapper> objectMapper = ThreadLocal.withInitial(() -> {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false);
        return mapper;
    });

    // ..................

    public static <T> T jsonFrom(String str, Class<T> clazz) throws IOException {
        return objectMapper.get().readValue(str, clazz);
    }

    // ..................
}

在打包时将这个依赖打包到jar中,打好的jar包能够在hive中正常使用

但是,这个jar包在presto中使用的时候抛出了这样的异常:(presto版本为0.214)

java.lang.NoClassDefFoundError: com/fasterxml/jackson/annotation/JsonIncludeProperties

问题排查

是否是依赖缺失?

刚看到这个问题的时候我以为是我打包的时候遗漏了一些依赖,导致找不到这个类,于是我将jar包反编译出来,但是发现jar包里面确实是存在这个类的:

在这里插入图片描述

也就是说这个jar包是完整的,不存在依赖缺失的问题

会不会是因为presto本身有这个依赖?

(事后看来这个排查方向是不对的,但当时实在想不明白)我查看了presto项目是否有依赖jackson,发现lib目录下存在jackson的jar,于是我想当然地将项目里的依赖改成provided

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.5</version>
    <scope>provided</scope>
</dependency>

结果抛出了如下异常:

java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper

也就是说在开发插件的时候,确实是需要提供jackson-databind的依赖的

会不会是因为presto的jackson依赖与我的版本不同?

再仔细查看presto的jackson版本,是2.9.7,与我的2.12.5不同,于是将依赖修改为:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.7</version>
</dependency>

这一次修改后,udf成功在presto中运行了。

思考

虽然问题是解决了,但是我依然不明白:既然presto要求提供jackson的依赖,且我明明提供了完整的2.12.5版本的jackson,但是为什么依然报错呢?

这时候再看看抛出的异常

java.lang.NoClassDefFoundError: com/fasterxml/jackson/annotation/JsonIncludeProperties
	at com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector.findPropertyInclusionByName(JacksonAnnotationIntrospector.java:321)
	at com.fasterxml.jackson.databind.cfg.MapperConfigBase.getDefaultPropertyInclusions(MapperConfigBase.java:685)
	...
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)
    at {打个码}.exec.JsonUtil.jsonFrom(JsonUtil.java:45)
    ...

可以发现,运行过程中是进入了我提供的2.12.5版本的ObjectMapper类的方法,但是最后却无法无法找到2.12.5版本的JsonIncludeProperties类,也就是说presto只加载了我的插件jar包里的部分类

这使我十分困惑,为什么会有这样的现象?带着问题我去阅读了presto源码,想找到presto在加载插件时的ClassLoader的实现类,于是找到了这个类com.facebook.presto.server.PluginClassLoader

class PluginClassLoader
        extends URLClassLoader {
            
    // ..................

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        // grab the magic lock
        synchronized (getClassLoadingLock(name)) {
            // Check if class is in the loaded classes cache
            Class<?> cachedClass = findLoadedClass(name);
            if (cachedClass != null) {
                return resolveClass(cachedClass, resolve);
            }

            // If this is an SPI class, only check SPI class loader
            if (isSpiClass(name)) {
                return resolveClass(spiClassLoader.loadClass(name), resolve);
            }

            // Look for class locally
            return super.loadClass(name, resolve);
        }
    }

    // ..................

    private boolean isSpiClass(String name)
    {
        // todo maybe make this more precise and only match base package
        return spiPackages.stream().anyMatch(name::startsWith);
    }
    
    // ..................

}

可以看到,PluginClassLoader在加载类的时候,会判断是否是SPI class(isSpiClass(String name)),如果是的话,只会使用SPI的类加载器(spiClassLoader)去加载。这其中的spiClassLoader以及spiPackages都存在于这个类com.facebook.presto.server.PluginManager中:

public class PluginManager
{
    private static final ImmutableList<String> SPI_PACKAGES = ImmutableList.<String>builder()
            .add("com.facebook.presto.spi.")
            .add("com.fasterxml.jackson.annotation.")
            .add("io.airlift.slice.")
            .add("io.airlift.units.")
            .add("org.openjdk.jol.")
            .build();

    // ..................

    private URLClassLoader createClassLoader(List<URL> urls)
    {
        ClassLoader parent = getClass().getClassLoader();
        return new PluginClassLoader(urls, parent, SPI_PACKAGES);
    }
    
    // ..................

也就是说PluginManager指定了一堆类路径SPI_PACKAGES,如果需要加载的包是以这些类路径开头的,那么就会从ClassLoader parent = getClass().getClassLoader()也就是presto自己的lib目录下加载,类似一种白名单机制。

presto这样做的目的其实是保证用户开发的插件只能加载到Presto SPI的核心类,而不会(有意或无意)依赖到Presto的内部实现细节。

PluginManager指定的类路径SPI_PACKAGES中,恰巧就有com.fasterxml.jackson.annotation

所以,当加载我的udf的时候,ObjectMappercom.fasterxml.jackson.databind包下的,因此会加载plugin目录下jar包中我提供的高版本的类文件,而不会加载com.fasterxml.jackson.annotation包下类。当这个ObjectMapper使用到高版本才有的类JsonIncludeProperties时,由于之前没有加载到,因此才会抛出异常。

总结

由于presto加载插件的时候,插件提供的类并不会全部加载,对于部分指定的包下的类,presto会选择加载自带的类,因此,在未来开发插件的时候,在添加项目依赖时要多注意一下presto是否将其加入了“白名单”,如果是的话最好选择和presto一样的版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值