UdfContractClassLoader.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.udf;
import java.util.Objects;
/**
* The parent classloader of every user function, exposing the JDK plus exactly two things out of
* this plugin.
*
* <h2>Why a user function does not simply see the plugin</h2>
*
* <p>A user function is code Doris did not write, and it should depend on nothing Doris happens to
* ship. Before plugins, it was loaded against BE's whole system classpath and could reach guava,
* jackson, netty, hadoop and everything else two fat jars dragged along - not by design, but
* because they were there. Every one of those became a compatibility surface nobody chose, and the
* versions on it changed whenever an unrelated dependency did.
*
* <p>So the rule is that a user function ships what it uses. What it cannot ship is the handful of
* classes that have to be <em>the same class object</em> on both sides:
*
* <ul>
* <li>{@code org.joda.time.LocalDate}, {@code DateTime} and {@code LocalDateTime}, because
* {@code Type.DATE_SUPPORTED_JAVA_TYPE} and its siblings are sets of {@code Class} objects
* and a function's declared parameter type is matched against them by identity. A second
* copy of joda does not match, and the function is rejected as having no usable
* {@code evaluate}.</li>
* <li>{@code org.apache.hadoop.hive.ql.exec.UDF} and the classes that load with it, which a user
* function may extend. Nothing in this plugin refers to them; they are here so that such a
* function links.</li>
* </ul>
*
* <p>Everything else resolves against the JDK or against the user's own jar, and a name that is in
* neither fails here rather than binding to whatever BE happens to have.
*/
public final class UdfContractClassLoader extends ClassLoader {
/**
* The only names delegated into the plugin. Prefixes rather than exact names because the Hive
* UDF base class drags a small closure with it and joda's date classes reference the rest of
* joda; listing the leaves would break on the first library upgrade.
*/
private static final String[] CONTRACT_PREFIXES = {
"org.apache.hadoop.hive.ql.exec.",
"org.apache.hadoop.hive.ql.metadata.",
"org.joda.time.",
};
static {
registerAsParallelCapable();
}
private final ClassLoader pluginClassLoader;
/**
* @param pluginClassLoader this plugin's own loader, the only place contract classes come from
*/
public UdfContractClassLoader(ClassLoader pluginClassLoader) {
// The plugin's loader has the platform loader as its parent - DorisPluginClassLoader is
// built that way so that no plugin can reach BE's system classpath. Taking it from there
// rather than naming it keeps this module compilable at Java 8 and keeps the two loaders
// from disagreeing about what "the JDK" means.
super(parentOf(pluginClassLoader));
this.pluginClassLoader = pluginClassLoader;
}
private static ClassLoader parentOf(ClassLoader pluginClassLoader) {
Objects.requireNonNull(pluginClassLoader, "pluginClassLoader");
ClassLoader platform = pluginClassLoader.getParent();
if (platform == null) {
// A null parent is the bootstrap loader, which would leave user functions unable to
// resolve java.sql and the rest of the platform modules. It can only happen if this
// plugin was loaded by something other than DorisPluginClassLoader.
throw new IllegalStateException("The java-udf plugin was loaded by a classloader with no"
+ " parent (" + pluginClassLoader + "), so there is no platform classloader to put"
+ " behind user functions.");
}
return platform;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (isContractClass(name)) {
Class<?> contract = pluginClassLoader.loadClass(name);
if (resolve) {
resolveClass(contract);
}
return contract;
}
// Platform only: this loader owns no classes of its own, so anything that is not in
// the JDK and not in the user's jar ends here. The failure is left bare because the
// caller is always a user function's own loader, which discards whatever its parent
// threw and reports its own miss; the explanation lives there, in
// UserFunctionClassLoader.
return super.loadClass(name, resolve);
}
}
static boolean isContractClass(String name) {
for (String prefix : CONTRACT_PREFIXES) {
if (name.startsWith(prefix)) {
return true;
}
}
return false;
}
}