001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.util;
019
020import java.io.File;
021import java.io.IOException;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.util.HashMap;
025
026import org.apache.hadoop.conf.Configuration;
027import org.apache.hadoop.fs.FileStatus;
028import org.apache.hadoop.fs.FileSystem;
029import org.apache.hadoop.fs.Path;
030import org.apache.yetus.audience.InterfaceAudience;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * This is a class loader that can load classes dynamically from new
036 * jar files under a configured folder. The paths to the jar files are
037 * converted to URLs, and URLClassLoader logic is actually used to load
038 * classes. This class loader always uses its parent class loader
039 * to load a class at first. Only if its parent class loader
040 * can not load a class, we will try to load it using the logic here.
041 * <p>
042 * The configured folder can be a HDFS path. In this case, the jar files
043 * under that folder will be copied to local at first under ${hbase.local.dir}/jars/.
044 * The local copy will be updated if the remote copy is updated, according to its
045 * last modified timestamp.
046 * <p>
047 * We can't unload a class already loaded. So we will use the existing
048 * jar files we already know to load any class which can't be loaded
049 * using the parent class loader. If we still can't load the class from
050 * the existing jar files, we will check if any new jar file is added,
051 * if so, we will load the new jar file and try to load the class again.
052 * If still failed, a class not found exception will be thrown.
053 * <p>
054 * Be careful in uploading new jar files and make sure all classes
055 * are consistent, otherwise, we may not be able to load your
056 * classes properly.
057 */
058@InterfaceAudience.Private
059public class DynamicClassLoader extends ClassLoaderBase {
060  private static final Logger LOG =
061      LoggerFactory.getLogger(DynamicClassLoader.class);
062
063  // Dynamic jars are put under ${hbase.local.dir}/jars/
064  private static final String DYNAMIC_JARS_DIR = File.separator
065    + "jars" + File.separator;
066
067  private static final String DYNAMIC_JARS_DIR_KEY = "hbase.dynamic.jars.dir";
068
069  private static final String DYNAMIC_JARS_OPTIONAL_CONF_KEY = "hbase.use.dynamic.jars";
070  private static final boolean DYNAMIC_JARS_OPTIONAL_DEFAULT = true;
071
072  private boolean useDynamicJars;
073
074  private File localDir;
075
076  // FileSystem of the remote path, set only if remoteDir != null
077  private FileSystem remoteDirFs;
078  private Path remoteDir;
079
080  // Last modified time of local jars
081  private HashMap<String, Long> jarModifiedTime;
082
083  /**
084   * Creates a DynamicClassLoader that can load classes dynamically
085   * from jar files under a specific folder.
086   *
087   * @param conf the configuration for the cluster.
088   * @param parent the parent ClassLoader to set.
089   */
090  public DynamicClassLoader(
091      final Configuration conf, final ClassLoader parent) {
092    super(parent);
093
094    useDynamicJars = conf.getBoolean(
095        DYNAMIC_JARS_OPTIONAL_CONF_KEY, DYNAMIC_JARS_OPTIONAL_DEFAULT);
096
097    if (useDynamicJars) {
098      initTempDir(conf);
099    }
100  }
101
102  // FindBugs: Making synchronized to avoid IS2_INCONSISTENT_SYNC complaints about
103  // remoteDirFs and jarModifiedTime being part synchronized protected.
104  private synchronized void initTempDir(final Configuration conf) {
105    jarModifiedTime = new HashMap<>();
106    String localDirPath = conf.get(
107      LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + DYNAMIC_JARS_DIR;
108    localDir = new File(localDirPath);
109    if (!localDir.mkdirs() && !localDir.isDirectory()) {
110      throw new RuntimeException("Failed to create local dir " + localDir.getPath()
111        + ", DynamicClassLoader failed to init");
112    }
113
114    String remotePath = conf.get(DYNAMIC_JARS_DIR_KEY);
115    if (remotePath == null || remotePath.equals(localDirPath)) {
116      remoteDir = null;  // ignore if it is the same as the local path
117    } else {
118      remoteDir = new Path(remotePath);
119      try {
120        remoteDirFs = remoteDir.getFileSystem(conf);
121      } catch (IOException ioe) {
122        LOG.warn("Failed to identify the fs of dir "
123          + remoteDir + ", ignored", ioe);
124        remoteDir = null;
125      }
126    }
127  }
128
129  @Override
130  public Class<?> loadClass(String name)
131      throws ClassNotFoundException {
132    try {
133      return parent.loadClass(name);
134    } catch (ClassNotFoundException e) {
135      if (LOG.isDebugEnabled()) {
136        LOG.debug("Class " + name + " not found - using dynamical class loader");
137      }
138
139      if (useDynamicJars) {
140        return tryRefreshClass(name);
141      }
142      throw e;
143    }
144  }
145
146
147  private Class<?> tryRefreshClass(String name)
148      throws ClassNotFoundException {
149    synchronized (getClassLoadingLock(name)) {
150        // Check whether the class has already been loaded:
151        Class<?> clasz = findLoadedClass(name);
152        if (clasz != null) {
153          if (LOG.isDebugEnabled()) {
154            LOG.debug("Class " + name + " already loaded");
155          }
156        }
157        else {
158          try {
159            if (LOG.isDebugEnabled()) {
160              LOG.debug("Finding class: " + name);
161            }
162            clasz = findClass(name);
163          } catch (ClassNotFoundException cnfe) {
164            // Load new jar files if any
165            if (LOG.isDebugEnabled()) {
166              LOG.debug("Loading new jar files, if any");
167            }
168            loadNewJars();
169
170            if (LOG.isDebugEnabled()) {
171              LOG.debug("Finding class again: " + name);
172            }
173            clasz = findClass(name);
174          }
175        }
176        return clasz;
177      }
178  }
179
180  private synchronized void loadNewJars() {
181    // Refresh local jar file lists
182    File[] files = localDir == null ? null : localDir.listFiles();
183    if (files != null) {
184      for (File file : files) {
185        String fileName = file.getName();
186        if (jarModifiedTime.containsKey(fileName)) {
187          continue;
188        }
189        if (file.isFile() && fileName.endsWith(".jar")) {
190          jarModifiedTime.put(fileName, Long.valueOf(file.lastModified()));
191          try {
192            URL url = file.toURI().toURL();
193            addURL(url);
194          } catch (MalformedURLException mue) {
195            // This should not happen, just log it
196            LOG.warn("Failed to load new jar " + fileName, mue);
197          }
198        }
199      }
200    }
201
202    // Check remote files
203    FileStatus[] statuses = null;
204    if (remoteDir != null) {
205      try {
206        statuses = remoteDirFs.listStatus(remoteDir);
207      } catch (IOException ioe) {
208        LOG.warn("Failed to check remote dir status " + remoteDir, ioe);
209      }
210    }
211    if (statuses == null || statuses.length == 0) {
212      return; // no remote files at all
213    }
214
215    for (FileStatus status: statuses) {
216      if (status.isDirectory()) continue; // No recursive lookup
217      Path path = status.getPath();
218      String fileName = path.getName();
219      if (!fileName.endsWith(".jar")) {
220        if (LOG.isDebugEnabled()) {
221          LOG.debug("Ignored non-jar file " + fileName);
222        }
223        continue; // Ignore non-jar files
224      }
225      Long cachedLastModificationTime = jarModifiedTime.get(fileName);
226      if (cachedLastModificationTime != null) {
227        long lastModified = status.getModificationTime();
228        if (lastModified < cachedLastModificationTime.longValue()) {
229          // There could be some race, for example, someone uploads
230          // a new one right in the middle the old one is copied to
231          // local. We can check the size as well. But it is still
232          // not guaranteed. This should be rare. Most likely,
233          // we already have the latest one.
234          // If you are unlucky to hit this race issue, you have
235          // to touch the remote jar to update its last modified time
236          continue;
237        }
238      }
239      try {
240        // Copy it to local
241        File dst = new File(localDir, fileName);
242        remoteDirFs.copyToLocalFile(path, new Path(dst.getPath()));
243        jarModifiedTime.put(fileName, Long.valueOf(dst.lastModified()));
244        URL url = dst.toURI().toURL();
245        addURL(url);
246      } catch (IOException ioe) {
247        LOG.warn("Failed to load new jar " + fileName, ioe);
248      }
249    }
250  }
251}