View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.util;
19  
20  import java.io.File;
21  import java.io.FileNotFoundException;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.net.URL;
25  import java.security.AccessController;
26  import java.security.PrivilegedAction;
27  import java.util.Collection;
28  import java.util.Enumeration;
29  import java.util.HashSet;
30  import java.util.concurrent.ConcurrentMap;
31  import java.util.concurrent.locks.Lock;
32  import java.util.jar.JarEntry;
33  import java.util.jar.JarFile;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.hadoop.hbase.classification.InterfaceAudience;
40  import org.apache.hadoop.conf.Configuration;
41  import org.apache.hadoop.fs.FileSystem;
42  import org.apache.hadoop.fs.Path;
43  import org.apache.hadoop.fs.FileStatus;
44  import org.apache.hadoop.fs.FileUtil;
45  import org.apache.hadoop.hbase.classification.InterfaceAudience;
46  import org.apache.hadoop.io.IOUtils;
47  
48  import com.google.common.base.Preconditions;
49  import com.google.common.collect.MapMaker;
50  
51  /**
52   * ClassLoader used to load classes for Coprocessor instances.
53   * <p>
54   * This ClassLoader always tries to load classes from the specified coprocessor
55   * jar first actually using URLClassLoader logic before delegating to the parent
56   * ClassLoader, thus avoiding dependency conflicts between HBase's classpath and
57   * classes in the coprocessor jar.
58   * <p>
59   * Certain classes are exempt from being loaded by this ClassLoader because it
60   * would prevent them from being cast to the equivalent classes in the region
61   * server.  For example, the Coprocessor interface needs to be loaded by the
62   * region server's ClassLoader to prevent a ClassCastException when casting the
63   * coprocessor implementation.
64   * <p>
65   * A HDFS path can be used to specify the coprocessor jar. In this case, the jar
66   * will be copied to local at first under some folder under ${hbase.local.dir}/jars/tmp/.
67   * The local copy will be removed automatically when the HBase server instance is
68   * stopped.
69   * <p>
70   * This ClassLoader also handles resource loading.  In most cases this
71   * ClassLoader will attempt to load resources from the coprocessor jar first
72   * before delegating to the parent.  However, like in class loading,
73   * some resources need to be handled differently.  For all of the Hadoop
74   * default configurations (e.g. hbase-default.xml) we will check the parent
75   * ClassLoader first to prevent issues such as failing the HBase default
76   * configuration version check.
77   */
78  @InterfaceAudience.Private
79  public class CoprocessorClassLoader extends ClassLoaderBase {
80    private static final Log LOG = LogFactory.getLog(CoprocessorClassLoader.class);
81  
82    // A temporary place ${hbase.local.dir}/jars/tmp/ to store the local
83    // copy of the jar file and the libraries contained in the jar.
84    private static final String TMP_JARS_DIR = File.separator
85       + "jars" + File.separator + "tmp" + File.separator;
86  
87    /**
88     * External class loaders cache keyed by external jar path.
89     * ClassLoader instance is stored as a weak-reference
90     * to allow GC'ing when it is not used
91     * (@see HBASE-7205)
92     */
93    private static final ConcurrentMap<Path, CoprocessorClassLoader> classLoadersCache =
94      new MapMaker().concurrencyLevel(3).weakValues().makeMap();
95  
96    /**
97     * If the class being loaded starts with any of these strings, we will skip
98     * trying to load it from the coprocessor jar and instead delegate
99     * directly to the parent ClassLoader.
100    */
101   private static final String[] CLASS_PREFIX_EXEMPTIONS = new String[] {
102     // Java standard library:
103     "com.sun.",
104     "java.",
105     "javax.",
106     "org.ietf",
107     "org.omg",
108     "org.w3c",
109     "org.xml",
110     "sunw.",
111     // logging
112     "org.apache.commons.logging",
113     "org.apache.log4j",
114     "com.hadoop",
115     // Hadoop/HBase/ZK:
116     "org.apache.hadoop.security",
117     "org.apache.hadoop.HadoopIllegalArgumentException",
118     "org.apache.hadoop.conf",
119     "org.apache.hadoop.fs",
120     "org.apache.hadoop.http",
121     "org.apache.hadoop.io",
122     "org.apache.hadoop.ipc",
123     "org.apache.hadoop.metrics",
124     "org.apache.hadoop.metrics2",
125     "org.apache.hadoop.net",
126     "org.apache.hadoop.util",
127     "org.apache.hadoop.hdfs",
128     "org.apache.hadoop.hbase",
129     "org.apache.zookeeper",
130   };
131 
132   /**
133    * If the resource being loaded matches any of these patterns, we will first
134    * attempt to load the resource with the parent ClassLoader.  Only if the
135    * resource is not found by the parent do we attempt to load it from the coprocessor jar.
136    */
137   private static final Pattern[] RESOURCE_LOAD_PARENT_FIRST_PATTERNS =
138       new Pattern[] {
139     Pattern.compile("^[^-]+-default\\.xml$")
140   };
141 
142   private static final Pattern libJarPattern = Pattern.compile("[/]?lib/([^/]+\\.jar)");
143 
144   /**
145    * A locker used to synchronize class loader initialization per coprocessor jar file
146    */
147   private static final KeyLocker<String> locker = new KeyLocker<String>();
148 
149   /**
150    * A set used to synchronized parent path clean up.  Generally, there
151    * should be only one parent path, but using a set so that we can support more.
152    */
153   static final HashSet<String> parentDirLockSet = new HashSet<String>();
154 
155   /**
156    * Creates a JarClassLoader that loads classes from the given paths.
157    */
158   private CoprocessorClassLoader(ClassLoader parent) {
159     super(parent);
160   }
161 
162   private void init(Path pathPattern, String pathPrefix,
163       Configuration conf) throws IOException {
164     // Copy the jar to the local filesystem
165     String parentDirStr =
166       conf.get(LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + TMP_JARS_DIR;
167     synchronized (parentDirLockSet) {
168       if (!parentDirLockSet.contains(parentDirStr)) {
169         Path parentDir = new Path(parentDirStr);
170         FileSystem fs = FileSystem.getLocal(conf);
171         fs.delete(parentDir, true); // it's ok if the dir doesn't exist now
172         parentDirLockSet.add(parentDirStr);
173         if (!fs.mkdirs(parentDir) && !fs.getFileStatus(parentDir).isDirectory()) {
174           throw new RuntimeException("Failed to create local dir " + parentDirStr
175             + ", CoprocessorClassLoader failed to init");
176         }
177       }
178     }
179 
180     FileSystem fs = pathPattern.getFileSystem(conf);
181     Path pathPattern1 = fs.isDirectory(pathPattern) ?
182       new Path(pathPattern, "*.jar") : pathPattern;  // append "*.jar" if a directory is specified
183     FileStatus[] fileStatuses = fs.globStatus(pathPattern1);  // return all files that match the pattern
184     if (fileStatuses == null || fileStatuses.length == 0) {  // if no one matches
185       throw new FileNotFoundException(pathPattern1.toString());
186     } else {
187       boolean validFileEncountered = false;
188       for (Path path : FileUtil.stat2Paths(fileStatuses)) {  // for each file that match the pattern
189         if (fs.isFile(path)) {  // only process files, skip for directories
190           File dst = new File(parentDirStr, "." + pathPrefix + "."
191             + path.getName() + "." + System.currentTimeMillis() + ".jar");
192           fs.copyToLocalFile(path, new Path(dst.toString()));
193           dst.deleteOnExit();
194 
195           addURL(dst.getCanonicalFile().toURI().toURL());
196 
197           JarFile jarFile = new JarFile(dst.toString());
198           try {
199             Enumeration<JarEntry> entries = jarFile.entries();  // get entries inside a jar file
200             while (entries.hasMoreElements()) {
201               JarEntry entry = entries.nextElement();
202               Matcher m = libJarPattern.matcher(entry.getName());
203               if (m.matches()) {
204                 File file = new File(parentDirStr, "." + pathPrefix + "."
205                   + path.getName() + "." + System.currentTimeMillis() + "." + m.group(1));
206                 try (FileOutputStream outStream = new FileOutputStream(file)) {
207                   IOUtils.copyBytes(jarFile.getInputStream(entry),
208                     outStream, conf, true);
209                 }
210                 file.deleteOnExit();
211                 addURL(file.toURI().toURL());
212               }
213             }
214           } finally {
215             jarFile.close();
216           }
217 
218           validFileEncountered = true;  // Set to true when encountering a file
219         }
220       }
221       if (validFileEncountered == false) {  // all items returned by globStatus() are directories
222         throw new FileNotFoundException("No file found matching " + pathPattern1.toString());
223       }
224     }
225   }
226 
227   // This method is used in unit test
228   public static CoprocessorClassLoader getIfCached(final Path path) {
229     Preconditions.checkNotNull(path, "The jar path is null!");
230     return classLoadersCache.get(path);
231   }
232 
233   // This method is used in unit test
234   public static Collection<? extends ClassLoader> getAllCached() {
235     return classLoadersCache.values();
236   }
237 
238   // This method is used in unit test
239   public static void clearCache() {
240     classLoadersCache.clear();
241   }
242 
243   /**
244    * Get a CoprocessorClassLoader for a coprocessor jar path from cache.
245    * If not in cache, create one.
246    *
247    * @param path the path to the coprocessor jar file to load classes from
248    * @param parent the parent class loader for exempted classes
249    * @param pathPrefix a prefix used in temp path name to store the jar file locally
250    * @param conf the configuration used to create the class loader, if needed
251    * @return a CoprocessorClassLoader for the coprocessor jar path
252    * @throws IOException
253    */
254   public static CoprocessorClassLoader getClassLoader(final Path path,
255       final ClassLoader parent, final String pathPrefix,
256       final Configuration conf) throws IOException {
257     CoprocessorClassLoader cl = getIfCached(path);
258     String pathStr = path.toString();
259     if (cl != null) {
260       LOG.debug("Found classloader "+ cl + " for "+ pathStr);
261       return cl;
262     }
263 
264     if (path.getFileSystem(conf).isFile(path) && !pathStr.endsWith(".jar")) {
265       throw new IOException(pathStr + ": not a jar file?");
266     }
267 
268     Lock lock = locker.acquireLock(pathStr);
269     try {
270       cl = getIfCached(path);
271       if (cl != null) {
272         LOG.debug("Found classloader "+ cl + " for "+ pathStr);
273         return cl;
274       }
275 
276       cl = AccessController.doPrivileged(
277           new PrivilegedAction<CoprocessorClassLoader>() {
278         @Override
279         public CoprocessorClassLoader run() {
280           return new CoprocessorClassLoader(parent);
281         }
282       });
283 
284       cl.init(path, pathPrefix, conf);
285 
286       // Cache class loader as a weak value, will be GC'ed when no reference left
287       CoprocessorClassLoader prev = classLoadersCache.putIfAbsent(path, cl);
288       if (prev != null) {
289         // Lost update race, use already added class loader
290         LOG.warn("THIS SHOULD NOT HAPPEN, a class loader"
291           +" is already cached for " + pathStr);
292         cl = prev;
293       }
294       return cl;
295     } finally {
296       lock.unlock();
297     }
298   }
299 
300   @Override
301   public Class<?> loadClass(String name)
302       throws ClassNotFoundException {
303     return loadClass(name, null);
304   }
305 
306   public Class<?> loadClass(String name, String[] includedClassPrefixes)
307       throws ClassNotFoundException {
308     // Delegate to the parent immediately if this class is exempt
309     if (isClassExempt(name, includedClassPrefixes)) {
310       if (LOG.isDebugEnabled()) {
311         LOG.debug("Skipping exempt class " + name +
312             " - delegating directly to parent");
313       }
314       return parent.loadClass(name);
315     }
316 
317     synchronized (getClassLoadingLock(name)) {
318       // Check whether the class has already been loaded:
319       Class<?> clasz = findLoadedClass(name);
320       if (clasz != null) {
321         if (LOG.isDebugEnabled()) {
322           LOG.debug("Class " + name + " already loaded");
323         }
324       }
325       else {
326         try {
327           // Try to find this class using the URLs passed to this ClassLoader
328           if (LOG.isDebugEnabled()) {
329             LOG.debug("Finding class: " + name);
330           }
331           clasz = findClass(name);
332         } catch (ClassNotFoundException e) {
333           // Class not found using this ClassLoader, so delegate to parent
334           if (LOG.isDebugEnabled()) {
335             LOG.debug("Class " + name + " not found - delegating to parent");
336           }
337           try {
338             clasz = parent.loadClass(name);
339           } catch (ClassNotFoundException e2) {
340             // Class not found in this ClassLoader or in the parent ClassLoader
341             // Log some debug output before re-throwing ClassNotFoundException
342             if (LOG.isDebugEnabled()) {
343               LOG.debug("Class " + name + " not found in parent loader");
344             }
345             throw e2;
346           }
347         }
348       }
349       return clasz;
350     }
351   }
352 
353   @Override
354   public URL getResource(String name) {
355     URL resource = null;
356     boolean parentLoaded = false;
357 
358     // Delegate to the parent first if necessary
359     if (loadResourceUsingParentFirst(name)) {
360       if (LOG.isDebugEnabled()) {
361         LOG.debug("Checking parent first for resource " + name);
362       }
363       resource = super.getResource(name);
364       parentLoaded = true;
365     }
366 
367     if (resource == null) {
368       synchronized (getClassLoadingLock(name)) {
369         // Try to find the resource in this jar
370         resource = findResource(name);
371         if ((resource == null) && !parentLoaded) {
372           // Not found in this jar and we haven't attempted to load
373           // the resource in the parent yet; fall back to the parent
374           resource = super.getResource(name);
375         }
376       }
377     }
378     return resource;
379   }
380 
381   /**
382    * Determines whether the given class should be exempt from being loaded
383    * by this ClassLoader.
384    * @param name the name of the class to test.
385    * @return true if the class should *not* be loaded by this ClassLoader;
386    * false otherwise.
387    */
388   protected boolean isClassExempt(String name, String[] includedClassPrefixes) {
389     if (includedClassPrefixes != null) {
390       for (String clsName : includedClassPrefixes) {
391         if (name.startsWith(clsName)) {
392           return false;
393         }
394       }
395     }
396     for (String exemptPrefix : CLASS_PREFIX_EXEMPTIONS) {
397       if (name.startsWith(exemptPrefix)) {
398         return true;
399       }
400     }
401     return false;
402   }
403 
404   /**
405    * Determines whether we should attempt to load the given resource using the
406    * parent first before attempting to load the resource using this ClassLoader.
407    * @param name the name of the resource to test.
408    * @return true if we should attempt to load the resource using the parent
409    * first; false if we should attempt to load the resource using this
410    * ClassLoader first.
411    */
412   protected boolean loadResourceUsingParentFirst(String name) {
413     for (Pattern resourcePattern : RESOURCE_LOAD_PARENT_FIRST_PATTERNS) {
414       if (resourcePattern.matcher(name).matches()) {
415         return true;
416       }
417     }
418     return false;
419   }
420 }