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