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}