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 = 061 LoggerFactory.getLogger(DynamicClassLoader.class); 062 063 // Dynamic jars are put under ${hbase.local.dir}/jars/ 064 private static final String DYNAMIC_JARS_DIR = File.separator 065 + "jars" + File.separator; 066 067 private static final String DYNAMIC_JARS_DIR_KEY = "hbase.dynamic.jars.dir"; 068 069 private static final String DYNAMIC_JARS_OPTIONAL_CONF_KEY = "hbase.use.dynamic.jars"; 070 private static final boolean DYNAMIC_JARS_OPTIONAL_DEFAULT = true; 071 072 private boolean useDynamicJars; 073 074 private File localDir; 075 076 // FileSystem of the remote path, set only if remoteDir != null 077 private FileSystem remoteDirFs; 078 private Path remoteDir; 079 080 // Last modified time of local jars 081 private HashMap<String, Long> jarModifiedTime; 082 083 /** 084 * Creates a DynamicClassLoader that can load classes dynamically 085 * from jar files under a specific folder. 086 * 087 * @param conf the configuration for the cluster. 088 * @param parent the parent ClassLoader to set. 089 */ 090 public DynamicClassLoader( 091 final Configuration conf, final ClassLoader parent) { 092 super(parent); 093 094 useDynamicJars = conf.getBoolean( 095 DYNAMIC_JARS_OPTIONAL_CONF_KEY, DYNAMIC_JARS_OPTIONAL_DEFAULT); 096 097 if (useDynamicJars) { 098 initTempDir(conf); 099 } 100 } 101 102 // FindBugs: Making synchronized to avoid IS2_INCONSISTENT_SYNC complaints about 103 // remoteDirFs and jarModifiedTime being part synchronized protected. 104 private synchronized void initTempDir(final Configuration conf) { 105 jarModifiedTime = new HashMap<>(); 106 String localDirPath = conf.get( 107 LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + DYNAMIC_JARS_DIR; 108 localDir = new File(localDirPath); 109 if (!localDir.mkdirs() && !localDir.isDirectory()) { 110 throw new RuntimeException("Failed to create local dir " + localDir.getPath() 111 + ", DynamicClassLoader failed to init"); 112 } 113 114 String remotePath = conf.get(DYNAMIC_JARS_DIR_KEY); 115 if (remotePath == null || remotePath.equals(localDirPath)) { 116 remoteDir = null; // ignore if it is the same as the local path 117 } else { 118 remoteDir = new Path(remotePath); 119 try { 120 remoteDirFs = remoteDir.getFileSystem(conf); 121 } catch (IOException ioe) { 122 LOG.warn("Failed to identify the fs of dir " 123 + remoteDir + ", ignored", ioe); 124 remoteDir = null; 125 } 126 } 127 } 128 129 @Override 130 public Class<?> loadClass(String name) 131 throws ClassNotFoundException { 132 try { 133 return parent.loadClass(name); 134 } catch (ClassNotFoundException e) { 135 if (LOG.isDebugEnabled()) { 136 LOG.debug("Class " + name + " not found - using dynamical class loader"); 137 } 138 139 if (useDynamicJars) { 140 return tryRefreshClass(name); 141 } 142 throw e; 143 } 144 } 145 146 147 private Class<?> tryRefreshClass(String name) 148 throws ClassNotFoundException { 149 synchronized (getClassLoadingLock(name)) { 150 // Check whether the class has already been loaded: 151 Class<?> clasz = findLoadedClass(name); 152 if (clasz != null) { 153 if (LOG.isDebugEnabled()) { 154 LOG.debug("Class " + name + " already loaded"); 155 } 156 } 157 else { 158 try { 159 if (LOG.isDebugEnabled()) { 160 LOG.debug("Finding class: " + name); 161 } 162 clasz = findClass(name); 163 } catch (ClassNotFoundException cnfe) { 164 // Load new jar files if any 165 if (LOG.isDebugEnabled()) { 166 LOG.debug("Loading new jar files, if any"); 167 } 168 loadNewJars(); 169 170 if (LOG.isDebugEnabled()) { 171 LOG.debug("Finding class again: " + name); 172 } 173 clasz = findClass(name); 174 } 175 } 176 return clasz; 177 } 178 } 179 180 private synchronized void loadNewJars() { 181 // Refresh local jar file lists 182 File[] files = localDir == null ? null : localDir.listFiles(); 183 if (files != null) { 184 for (File file : files) { 185 String fileName = file.getName(); 186 if (jarModifiedTime.containsKey(fileName)) { 187 continue; 188 } 189 if (file.isFile() && fileName.endsWith(".jar")) { 190 jarModifiedTime.put(fileName, Long.valueOf(file.lastModified())); 191 try { 192 URL url = file.toURI().toURL(); 193 addURL(url); 194 } catch (MalformedURLException mue) { 195 // This should not happen, just log it 196 LOG.warn("Failed to load new jar " + fileName, mue); 197 } 198 } 199 } 200 } 201 202 // Check remote files 203 FileStatus[] statuses = null; 204 if (remoteDir != null) { 205 try { 206 statuses = remoteDirFs.listStatus(remoteDir); 207 } catch (IOException ioe) { 208 LOG.warn("Failed to check remote dir status " + remoteDir, ioe); 209 } 210 } 211 if (statuses == null || statuses.length == 0) { 212 return; // no remote files at all 213 } 214 215 for (FileStatus status: statuses) { 216 if (status.isDirectory()) continue; // No recursive lookup 217 Path path = status.getPath(); 218 String fileName = path.getName(); 219 if (!fileName.endsWith(".jar")) { 220 if (LOG.isDebugEnabled()) { 221 LOG.debug("Ignored non-jar file " + fileName); 222 } 223 continue; // Ignore non-jar files 224 } 225 Long cachedLastModificationTime = jarModifiedTime.get(fileName); 226 if (cachedLastModificationTime != null) { 227 long lastModified = status.getModificationTime(); 228 if (lastModified < cachedLastModificationTime.longValue()) { 229 // There could be some race, for example, someone uploads 230 // a new one right in the middle the old one is copied to 231 // local. We can check the size as well. But it is still 232 // not guaranteed. This should be rare. Most likely, 233 // we already have the latest one. 234 // If you are unlucky to hit this race issue, you have 235 // to touch the remote jar to update its last modified time 236 continue; 237 } 238 } 239 try { 240 // Copy it to local 241 File dst = new File(localDir, fileName); 242 remoteDirFs.copyToLocalFile(path, new Path(dst.getPath())); 243 jarModifiedTime.put(fileName, Long.valueOf(dst.lastModified())); 244 URL url = dst.toURI().toURL(); 245 addURL(url); 246 } catch (IOException ioe) { 247 LOG.warn("Failed to load new jar " + fileName, ioe); 248 } 249 } 250 } 251}