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.FileNotFoundException;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.net.URL;
025import java.security.AccessController;
026import java.security.PrivilegedAction;
027import java.util.Collection;
028import java.util.Enumeration;
029import java.util.HashSet;
030import java.util.concurrent.ConcurrentMap;
031import java.util.concurrent.locks.Lock;
032import java.util.jar.JarEntry;
033import java.util.jar.JarFile;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import org.apache.hadoop.conf.Configuration;
038import org.apache.hadoop.fs.FileStatus;
039import org.apache.hadoop.fs.FileSystem;
040import org.apache.hadoop.fs.FileUtil;
041import org.apache.hadoop.fs.Path;
042import org.apache.hadoop.io.IOUtils;
043import org.apache.yetus.audience.InterfaceAudience;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
048import org.apache.hbase.thirdparty.com.google.common.collect.MapMaker;
049
050/**
051 * ClassLoader used to load classes for Coprocessor instances.
052 * <p>
053 * This ClassLoader always tries to load classes from the specified coprocessor
054 * jar first actually using URLClassLoader logic before delegating to the parent
055 * ClassLoader, thus avoiding dependency conflicts between HBase's classpath and
056 * classes in the coprocessor jar.
057 * <p>
058 * Certain classes are exempt from being loaded by this ClassLoader because it
059 * would prevent them from being cast to the equivalent classes in the region
060 * server.  For example, the Coprocessor interface needs to be loaded by the
061 * region server's ClassLoader to prevent a ClassCastException when casting the
062 * coprocessor implementation.
063 * <p>
064 * A HDFS path can be used to specify the coprocessor jar. In this case, the jar
065 * will be copied to local at first under some folder under ${hbase.local.dir}/jars/tmp/.
066 * The local copy will be removed automatically when the HBase server instance is
067 * stopped.
068 * <p>
069 * This ClassLoader also handles resource loading.  In most cases this
070 * ClassLoader will attempt to load resources from the coprocessor jar first
071 * before delegating to the parent.  However, like in class loading,
072 * some resources need to be handled differently.  For all of the Hadoop
073 * default configurations (e.g. hbase-default.xml) we will check the parent
074 * ClassLoader first to prevent issues such as failing the HBase default
075 * configuration version check.
076 */
077@InterfaceAudience.Private
078public class CoprocessorClassLoader extends ClassLoaderBase {
079  private static final Logger LOG = LoggerFactory.getLogger(CoprocessorClassLoader.class);
080
081  // A temporary place ${hbase.local.dir}/jars/tmp/ to store the local
082  // copy of the jar file and the libraries contained in the jar.
083  private static final String TMP_JARS_DIR = File.separator
084     + "jars" + File.separator + "tmp" + File.separator;
085
086  /**
087   * External class loaders cache keyed by external jar path.
088   * ClassLoader instance is stored as a weak-reference
089   * to allow GC'ing when it is not used
090   * (@see HBASE-7205)
091   */
092  private static final ConcurrentMap<Path, CoprocessorClassLoader> classLoadersCache =
093    new MapMaker().concurrencyLevel(3).weakValues().makeMap();
094
095  /**
096   * If the class being loaded starts with any of these strings, we will skip
097   * trying to load it from the coprocessor jar and instead delegate
098   * directly to the parent ClassLoader.
099   */
100  private static final String[] CLASS_PREFIX_EXEMPTIONS = new String[] {
101    // Java standard library:
102    "com.sun.",
103    "java.",
104    "javax.",
105    "org.ietf",
106    "org.omg",
107    "org.w3c",
108    "org.xml",
109    "sunw.",
110    // logging
111    "org.slf4j",
112    "org.apache.log4j",
113    "com.hadoop",
114    // HBase:
115    "org.apache.hadoop.hbase",
116  };
117
118  /**
119   * If the resource being loaded matches any of these patterns, we will first
120   * attempt to load the resource with the parent ClassLoader.  Only if the
121   * resource is not found by the parent do we attempt to load it from the coprocessor jar.
122   */
123  private static final Pattern[] RESOURCE_LOAD_PARENT_FIRST_PATTERNS =
124      new Pattern[] {
125    Pattern.compile("^[^-]+-default\\.xml$")
126  };
127
128  private static final Pattern libJarPattern = Pattern.compile("[/]?lib/([^/]+\\.jar)");
129
130  /**
131   * A locker used to synchronize class loader initialization per coprocessor jar file
132   */
133  private static final KeyLocker<String> locker = new KeyLocker<>();
134
135  /**
136   * A set used to synchronized parent path clean up.  Generally, there
137   * should be only one parent path, but using a set so that we can support more.
138   */
139  static final HashSet<String> parentDirLockSet = new HashSet<>();
140
141  /**
142   * Creates a JarClassLoader that loads classes from the given paths.
143   */
144  private CoprocessorClassLoader(ClassLoader parent) {
145    super(parent);
146  }
147
148  private void init(Path pathPattern, String pathPrefix,
149      Configuration conf) throws IOException {
150    // Copy the jar to the local filesystem
151    String parentDirStr =
152      conf.get(LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + TMP_JARS_DIR;
153    synchronized (parentDirLockSet) {
154      if (!parentDirLockSet.contains(parentDirStr)) {
155        Path parentDir = new Path(parentDirStr);
156        FileSystem fs = FileSystem.getLocal(conf);
157        fs.delete(parentDir, true); // it's ok if the dir doesn't exist now
158        parentDirLockSet.add(parentDirStr);
159        if (!fs.mkdirs(parentDir) && !fs.getFileStatus(parentDir).isDirectory()) {
160          throw new RuntimeException("Failed to create local dir " + parentDirStr
161            + ", CoprocessorClassLoader failed to init");
162        }
163      }
164    }
165
166    FileSystem fs = pathPattern.getFileSystem(conf);
167    Path pathPattern1 = fs.isDirectory(pathPattern) ?
168      new Path(pathPattern, "*.jar") : pathPattern;  // append "*.jar" if a directory is specified
169    FileStatus[] fileStatuses = fs.globStatus(pathPattern1);  // return all files that match the pattern
170    if (fileStatuses == null || fileStatuses.length == 0) {  // if no one matches
171      throw new FileNotFoundException(pathPattern1.toString());
172    } else {
173      boolean validFileEncountered = false;
174      for (Path path : FileUtil.stat2Paths(fileStatuses)) {  // for each file that match the pattern
175        if (fs.isFile(path)) {  // only process files, skip for directories
176          File dst = new File(parentDirStr, "." + pathPrefix + "."
177            + path.getName() + "." + System.currentTimeMillis() + ".jar");
178          fs.copyToLocalFile(path, new Path(dst.toString()));
179          dst.deleteOnExit();
180
181          addURL(dst.getCanonicalFile().toURI().toURL());
182
183          JarFile jarFile = new JarFile(dst.toString());
184          try {
185            Enumeration<JarEntry> entries = jarFile.entries();  // get entries inside a jar file
186            while (entries.hasMoreElements()) {
187              JarEntry entry = entries.nextElement();
188              Matcher m = libJarPattern.matcher(entry.getName());
189              if (m.matches()) {
190                File file = new File(parentDirStr, "." + pathPrefix + "."
191                  + path.getName() + "." + System.currentTimeMillis() + "." + m.group(1));
192                try (FileOutputStream outStream = new FileOutputStream(file)) {
193                  IOUtils.copyBytes(jarFile.getInputStream(entry),
194                    outStream, conf, true);
195                }
196                file.deleteOnExit();
197                addURL(file.toURI().toURL());
198              }
199            }
200          } finally {
201            jarFile.close();
202          }
203
204          validFileEncountered = true;  // Set to true when encountering a file
205        }
206      }
207      if (validFileEncountered == false) {  // all items returned by globStatus() are directories
208        throw new FileNotFoundException("No file found matching " + pathPattern1.toString());
209      }
210    }
211  }
212
213  // This method is used in unit test
214  public static CoprocessorClassLoader getIfCached(final Path path) {
215    Preconditions.checkNotNull(path, "The jar path is null!");
216    return classLoadersCache.get(path);
217  }
218
219  // This method is used in unit test
220  public static Collection<? extends ClassLoader> getAllCached() {
221    return classLoadersCache.values();
222  }
223
224  // This method is used in unit test
225  public static void clearCache() {
226    classLoadersCache.clear();
227  }
228
229  /**
230   * Get a CoprocessorClassLoader for a coprocessor jar path from cache.
231   * If not in cache, create one.
232   *
233   * @param path the path to the coprocessor jar file to load classes from
234   * @param parent the parent class loader for exempted classes
235   * @param pathPrefix a prefix used in temp path name to store the jar file locally
236   * @param conf the configuration used to create the class loader, if needed
237   * @return a CoprocessorClassLoader for the coprocessor jar path
238   * @throws IOException
239   */
240  public static CoprocessorClassLoader getClassLoader(final Path path,
241      final ClassLoader parent, final String pathPrefix,
242      final Configuration conf) throws IOException {
243    CoprocessorClassLoader cl = getIfCached(path);
244    String pathStr = path.toString();
245    if (cl != null) {
246      LOG.debug("Found classloader "+ cl + " for "+ pathStr);
247      return cl;
248    }
249
250    if (path.getFileSystem(conf).isFile(path) && !pathStr.endsWith(".jar")) {
251      throw new IOException(pathStr + ": not a jar file?");
252    }
253
254    Lock lock = locker.acquireLock(pathStr);
255    try {
256      cl = getIfCached(path);
257      if (cl != null) {
258        LOG.debug("Found classloader "+ cl + " for "+ pathStr);
259        return cl;
260      }
261
262      cl = AccessController.doPrivileged(
263          new PrivilegedAction<CoprocessorClassLoader>() {
264        @Override
265        public CoprocessorClassLoader run() {
266          return new CoprocessorClassLoader(parent);
267        }
268      });
269
270      cl.init(path, pathPrefix, conf);
271
272      // Cache class loader as a weak value, will be GC'ed when no reference left
273      CoprocessorClassLoader prev = classLoadersCache.putIfAbsent(path, cl);
274      if (prev != null) {
275        // Lost update race, use already added class loader
276        LOG.warn("THIS SHOULD NOT HAPPEN, a class loader"
277          +" is already cached for " + pathStr);
278        cl = prev;
279      }
280      return cl;
281    } finally {
282      lock.unlock();
283    }
284  }
285
286  @Override
287  public Class<?> loadClass(String name)
288      throws ClassNotFoundException {
289    return loadClass(name, null);
290  }
291
292  public Class<?> loadClass(String name, String[] includedClassPrefixes)
293      throws ClassNotFoundException {
294    // Delegate to the parent immediately if this class is exempt
295    if (isClassExempt(name, includedClassPrefixes)) {
296      if (LOG.isDebugEnabled()) {
297        LOG.debug("Skipping exempt class " + name +
298            " - delegating directly to parent");
299      }
300      return parent.loadClass(name);
301    }
302
303    synchronized (getClassLoadingLock(name)) {
304      // Check whether the class has already been loaded:
305      Class<?> clasz = findLoadedClass(name);
306      if (clasz != null) {
307        if (LOG.isDebugEnabled()) {
308          LOG.debug("Class " + name + " already loaded");
309        }
310      }
311      else {
312        try {
313          // Try to find this class using the URLs passed to this ClassLoader
314          if (LOG.isDebugEnabled()) {
315            LOG.debug("Finding class: " + name);
316          }
317          clasz = findClass(name);
318        } catch (ClassNotFoundException e) {
319          // Class not found using this ClassLoader, so delegate to parent
320          if (LOG.isDebugEnabled()) {
321            LOG.debug("Class " + name + " not found - delegating to parent");
322          }
323          try {
324            clasz = parent.loadClass(name);
325          } catch (ClassNotFoundException e2) {
326            // Class not found in this ClassLoader or in the parent ClassLoader
327            // Log some debug output before re-throwing ClassNotFoundException
328            if (LOG.isDebugEnabled()) {
329              LOG.debug("Class " + name + " not found in parent loader");
330            }
331            throw e2;
332          }
333        }
334      }
335      return clasz;
336    }
337  }
338
339  @Override
340  public URL getResource(String name) {
341    URL resource = null;
342    boolean parentLoaded = false;
343
344    // Delegate to the parent first if necessary
345    if (loadResourceUsingParentFirst(name)) {
346      if (LOG.isDebugEnabled()) {
347        LOG.debug("Checking parent first for resource " + name);
348      }
349      resource = super.getResource(name);
350      parentLoaded = true;
351    }
352
353    if (resource == null) {
354      synchronized (getClassLoadingLock(name)) {
355        // Try to find the resource in this jar
356        resource = findResource(name);
357        if ((resource == null) && !parentLoaded) {
358          // Not found in this jar and we haven't attempted to load
359          // the resource in the parent yet; fall back to the parent
360          resource = super.getResource(name);
361        }
362      }
363    }
364    return resource;
365  }
366
367  /**
368   * Determines whether the given class should be exempt from being loaded
369   * by this ClassLoader.
370   * @param name the name of the class to test.
371   * @return true if the class should *not* be loaded by this ClassLoader;
372   * false otherwise.
373   */
374  protected boolean isClassExempt(String name, String[] includedClassPrefixes) {
375    if (includedClassPrefixes != null) {
376      for (String clsName : includedClassPrefixes) {
377        if (name.startsWith(clsName)) {
378          return false;
379        }
380      }
381    }
382    for (String exemptPrefix : CLASS_PREFIX_EXEMPTIONS) {
383      if (name.startsWith(exemptPrefix)) {
384        return true;
385      }
386    }
387    return false;
388  }
389
390  /**
391   * Determines whether we should attempt to load the given resource using the
392   * parent first before attempting to load the resource using this ClassLoader.
393   * @param name the name of the resource to test.
394   * @return true if we should attempt to load the resource using the parent
395   * first; false if we should attempt to load the resource using this
396   * ClassLoader first.
397   */
398  protected boolean loadResourceUsingParentFirst(String name) {
399    for (Pattern resourcePattern : RESOURCE_LOAD_PARENT_FIRST_PATTERNS) {
400      if (resourcePattern.matcher(name).matches()) {
401        return true;
402      }
403    }
404    return false;
405  }
406}