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