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.IOException; 022import java.net.MalformedURLException; 023import java.net.URL; 024import java.util.HashMap; 025 026import org.apache.hadoop.conf.Configuration; 027import org.apache.hadoop.fs.FileStatus; 028import org.apache.hadoop.fs.FileSystem; 029import org.apache.hadoop.fs.Path; 030import org.apache.yetus.audience.InterfaceAudience; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034/** 035 * This is a class loader that can load classes dynamically from new 036 * jar files under a configured folder. The paths to the jar files are 037 * converted to URLs, and URLClassLoader logic is actually used to load 038 * classes. This class loader always uses its parent class loader 039 * to load a class at first. Only if its parent class loader 040 * can not load a class, we will try to load it using the logic here. 041 * <p> 042 * The configured folder can be a HDFS path. In this case, the jar files 043 * under that folder will be copied to local at first under ${hbase.local.dir}/jars/. 044 * The local copy will be updated if the remote copy is updated, according to its 045 * last modified timestamp. 046 * <p> 047 * We can't unload a class already loaded. So we will use the existing 048 * jar files we already know to load any class which can't be loaded 049 * using the parent class loader. If we still can't load the class from 050 * the existing jar files, we will check if any new jar file is added, 051 * if so, we will load the new jar file and try to load the class again. 052 * If still failed, a class not found exception will be thrown. 053 * <p> 054 * Be careful in uploading new jar files and make sure all classes 055 * are consistent, otherwise, we may not be able to load your 056 * classes properly. 057 */ 058@InterfaceAudience.Private 059public class DynamicClassLoader extends ClassLoaderBase { 060 private static final Logger LOG = LoggerFactory.getLogger(DynamicClassLoader.class); 061 062 // Dynamic jars are put under ${hbase.local.dir}/jars/ 063 private static final String DYNAMIC_JARS_DIR = File.separator 064 + "jars" + File.separator; 065 066 private static final String DYNAMIC_JARS_DIR_KEY = "hbase.dynamic.jars.dir"; 067 068 private static final String DYNAMIC_JARS_OPTIONAL_CONF_KEY = "hbase.use.dynamic.jars"; 069 private static final boolean DYNAMIC_JARS_OPTIONAL_DEFAULT = true; 070 071 // The user-provided value for using the DynamicClassLoader 072 private final boolean userConfigUseDynamicJars; 073 // The current state of whether to use the DynamicClassLoader 074 private final boolean useDynamicJars; 075 076 private File localDir; 077 078 // FileSystem of the remote path, set only if remoteDir != null 079 private FileSystem remoteDirFs; 080 private Path remoteDir; 081 082 // Last modified time of local jars 083 private HashMap<String, Long> jarModifiedTime; 084 085 /** 086 * Creates a DynamicClassLoader that can load classes dynamically 087 * from jar files under a specific folder. 088 * 089 * @param conf the configuration for the cluster. 090 * @param parent the parent ClassLoader to set. 091 */ 092 public DynamicClassLoader(final Configuration conf, final ClassLoader parent) { 093 super(parent); 094 095 // Save off the user's original configuration value for the DynamicClassLoader 096 userConfigUseDynamicJars = conf.getBoolean( 097 DYNAMIC_JARS_OPTIONAL_CONF_KEY, DYNAMIC_JARS_OPTIONAL_DEFAULT); 098 099 boolean dynamicJarsEnabled = userConfigUseDynamicJars; 100 if (dynamicJarsEnabled) { 101 try { 102 initTempDir(conf); 103 dynamicJarsEnabled = true; 104 } catch (Exception e) { 105 LOG.error("Disabling the DynamicClassLoader as it failed to initialize its temp directory." 106 + " Check your configuration and filesystem permissions. Custom coprocessor code may" 107 + " not be loaded as a result of this failure.", e); 108 dynamicJarsEnabled = false; 109 } 110 } 111 useDynamicJars = dynamicJarsEnabled; 112 } 113 114 // FindBugs: Making synchronized to avoid IS2_INCONSISTENT_SYNC complaints about 115 // remoteDirFs and jarModifiedTime being part synchronized protected. 116 private synchronized void initTempDir(final Configuration conf) { 117 jarModifiedTime = new HashMap<>(); 118 String localDirPath = conf.get( 119 LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + DYNAMIC_JARS_DIR; 120 localDir = new File(localDirPath); 121 if (!localDir.mkdirs() && !localDir.isDirectory()) { 122 throw new RuntimeException("Failed to create local dir " + localDir.getPath() 123 + ", DynamicClassLoader failed to init"); 124 } 125 126 String remotePath = conf.get(DYNAMIC_JARS_DIR_KEY); 127 if (remotePath == null || remotePath.equals(localDirPath)) { 128 remoteDir = null; // ignore if it is the same as the local path 129 } else { 130 remoteDir = new Path(remotePath); 131 try { 132 remoteDirFs = remoteDir.getFileSystem(conf); 133 } catch (IOException ioe) { 134 LOG.warn("Failed to identify the fs of dir " 135 + remoteDir + ", ignored", ioe); 136 remoteDir = null; 137 } 138 } 139 } 140 141 @Override 142 public Class<?> loadClass(String name) 143 throws ClassNotFoundException { 144 try { 145 return parent.loadClass(name); 146 } catch (ClassNotFoundException e) { 147 if (useDynamicJars) { 148 LOG.debug("Class {} not found - using dynamical class loader", name); 149 return tryRefreshClass(name); 150 } else if (userConfigUseDynamicJars) { 151 // If the user tried to enable the DCL, then warn again. 152 LOG.debug("Not checking DynamicClassLoader for missing class because it is disabled." 153 + " See the log for previous errors."); 154 } 155 throw e; 156 } 157 } 158 159 private Class<?> tryRefreshClass(String name) throws ClassNotFoundException { 160 synchronized (getClassLoadingLock(name)) { 161 // Check whether the class has already been loaded: 162 Class<?> clasz = findLoadedClass(name); 163 164 if (clasz != null) { 165 if (LOG.isDebugEnabled()) { 166 LOG.debug("Class {} already loaded", name); 167 } 168 } else { 169 try { 170 if (LOG.isDebugEnabled()) { 171 LOG.debug("Finding class: {}", name); 172 } 173 174 clasz = findClass(name); 175 } catch (ClassNotFoundException cnfe) { 176 // Load new jar files if any 177 if (LOG.isDebugEnabled()) { 178 LOG.debug("Loading new jar files, if any"); 179 } 180 181 loadNewJars(); 182 183 if (LOG.isDebugEnabled()) { 184 LOG.debug("Finding class again: {}", name); 185 } 186 187 clasz = findClass(name); 188 } 189 } 190 191 return clasz; 192 } 193 } 194 195 private synchronized void loadNewJars() { 196 // Refresh local jar file lists 197 File[] files = localDir == null ? null : localDir.listFiles(); 198 if (files != null) { 199 for (File file : files) { 200 String fileName = file.getName(); 201 if (jarModifiedTime.containsKey(fileName)) { 202 continue; 203 } 204 if (file.isFile() && fileName.endsWith(".jar")) { 205 jarModifiedTime.put(fileName, file.lastModified()); 206 try { 207 URL url = file.toURI().toURL(); 208 addURL(url); 209 } catch (MalformedURLException mue) { 210 // This should not happen, just log it 211 LOG.warn("Failed to load new jar " + fileName, mue); 212 } 213 } 214 } 215 } 216 217 // Check remote files 218 FileStatus[] statuses = null; 219 if (remoteDir != null) { 220 try { 221 statuses = remoteDirFs.listStatus(remoteDir); 222 } catch (IOException ioe) { 223 LOG.warn("Failed to check remote dir status " + remoteDir, ioe); 224 } 225 } 226 if (statuses == null || statuses.length == 0) { 227 return; // no remote files at all 228 } 229 230 for (FileStatus status: statuses) { 231 if (status.isDirectory()) { 232 continue; // No recursive lookup 233 } 234 235 Path path = status.getPath(); 236 String fileName = path.getName(); 237 if (!fileName.endsWith(".jar")) { 238 if (LOG.isDebugEnabled()) { 239 LOG.debug("Ignored non-jar file {}", fileName); 240 } 241 continue; // Ignore non-jar files 242 } 243 Long cachedLastModificationTime = jarModifiedTime.get(fileName); 244 if (cachedLastModificationTime != null) { 245 long lastModified = status.getModificationTime(); 246 if (lastModified < cachedLastModificationTime) { 247 // There could be some race, for example, someone uploads 248 // a new one right in the middle the old one is copied to 249 // local. We can check the size as well. But it is still 250 // not guaranteed. This should be rare. Most likely, 251 // we already have the latest one. 252 // If you are unlucky to hit this race issue, you have 253 // to touch the remote jar to update its last modified time 254 continue; 255 } 256 } 257 try { 258 // Copy it to local 259 File dst = new File(localDir, fileName); 260 remoteDirFs.copyToLocalFile(path, new Path(dst.getPath())); 261 jarModifiedTime.put(fileName, dst.lastModified()); 262 URL url = dst.toURI().toURL(); 263 addURL(url); 264 } catch (IOException ioe) { 265 LOG.warn("Failed to load new jar " + fileName, ioe); 266 } 267 } 268 } 269}