UserFunctionLoaders.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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Builds the classloader a user function is loaded from. There are two kinds and they differ only
* in where the jar comes from; both hang off {@link UdfContractClassLoader}, which is what keeps a
* user function from reaching into Doris.
*
* <ul>
* <li><b>Per function</b> - CREATE FUNCTION named a jar, BE downloaded it, and this function gets
* a loader of its own over that one file. It is closed when the function is dropped.</li>
* <li><b>Pre-installed</b> - CREATE FUNCTION named no jar, so the class is expected in
* {@code plugins/java_extensions} or the deprecated {@code custom_lib}. Those directories are
* read once, as they were when they were part of BE's start-up classpath: adding a jar to
* them still requires restarting BE.</li>
* </ul>
*/
final class UserFunctionLoaders {
private static final Logger LOG = LoggerFactory.getLogger(UserFunctionLoaders.class);
/** Where a function with no jar of its own is looked for, in search order. */
private static final String[] PREINSTALLED_DIRS = {"plugins/java_extensions", "custom_lib"};
private static volatile ClassLoader contract;
private static volatile ClassLoader preinstalled;
private UserFunctionLoaders() {
}
/** A loader over one downloaded jar. The caller owns it and closes it with the function. */
static URLClassLoader forJar(String jarPath) throws MalformedURLException, FileNotFoundException {
File file = new File(jarPath);
if (!file.exists()) {
throw new FileNotFoundException("Can not find local file: " + jarPath);
}
return new UserFunctionClassLoader(new URL[] {file.toURI().toURL()}, contract());
}
/**
* The shared loader over the pre-installed directories. Never closed - several functions are
* loaded from it and there is no moment at which the last one is known to be gone.
*/
static ClassLoader preinstalled() {
ClassLoader current = preinstalled;
if (current == null) {
synchronized (UserFunctionLoaders.class) {
if (preinstalled == null) {
List<URL> jars = preinstalledJars();
LOG.info("Java functions with no jar of their own are loaded from {} jar(s) in {}",
jars.size(), Arrays.toString(PREINSTALLED_DIRS));
preinstalled = new UserFunctionClassLoader(jars.toArray(new URL[0]), contract());
}
current = preinstalled;
}
}
return current;
}
/** Where {@link #preinstalled} looks, in search order. */
static String[] preinstalledDirs() {
return PREINSTALLED_DIRS.clone();
}
private static ClassLoader contract() {
ClassLoader current = contract;
if (current == null) {
synchronized (UserFunctionLoaders.class) {
if (contract == null) {
contract = new UdfContractClassLoader(UserFunctionLoaders.class.getClassLoader());
}
current = contract;
}
}
return current;
}
private static List<URL> preinstalledJars() {
List<URL> jars = new ArrayList<>();
String dorisHome = System.getenv("DORIS_HOME");
if (dorisHome == null || dorisHome.trim().isEmpty()) {
LOG.warn("DORIS_HOME is not set, so {} cannot be found; a Java function created without a"
+ " jar will not resolve", Arrays.toString(PREINSTALLED_DIRS));
return jars;
}
for (String dir : PREINSTALLED_DIRS) {
File[] files = new File(dorisHome.trim(), dir).listFiles(
(unused, name) -> name.endsWith(".jar"));
if (files == null) {
continue;
}
// Sorted so that a duplicate class resolves the same way on every node, the same
// reason the plugin directories themselves are sorted.
Arrays.sort(files);
for (File file : files) {
try {
jars.add(file.toURI().toURL());
} catch (MalformedURLException e) {
LOG.warn("Skipping {}: {}", file, e.toString());
}
}
}
return jars;
}
}