Java tutorial
/** * Copyright 2014 Otto (GmbH & Co KG) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ottogroup.bi.asap.repository; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import com.ottogroup.bi.asap.component.Component; import com.ottogroup.bi.asap.component.annotation.AsapComponent; import com.ottogroup.bi.asap.exception.ComponentInstantiationFailedException; import com.ottogroup.bi.asap.exception.RequiredInputMissingException; import com.ottogroup.bi.asap.exception.UnknownComponentException; /** * Reads the contents of a provided jar (array of bytes), extracts all {@link AsapComponent asap components} and allows * to access them by providing the component name as well as its version * @author mnxfst * @since Oct 29, 2014 * TODO how to manage multiple resources having the same name but being located in differen JAR files? * TODO add performance tracking while loading classes * TODO more testing on initialize and newInstance */ public class ComponentClassloader extends ClassLoader { private static final Logger logger = Logger.getLogger(ComponentClassloader.class); /** list of jar files which contain pipeline components and thus must be scanned for deployment descriptors */ private Set<String> componentJarFiles = new HashSet<String>(); /** mapping from class file to JAR to load it from */ private Map<String, String> classesJarMapping = new HashMap<>(); /** mapping from resources to JAR to load it from */ private Map<String, String> resourcesJarMapping = new HashMap<>(); /** already resolved classes */ private Map<String, Class<?>> cachedClasses = new HashMap<>(); /** managed pipeline components */ private Map<String, ComponentDescriptor> managedComponents = new HashMap<>(); /** * Initializes the class using the provided input * @param parentClassLoader */ public ComponentClassloader(final ClassLoader parentClassLoader) { super(parentClassLoader); } /** * Loads referenced class: * <ul> * <li>find in already loaded classes</li> * <li>look it up in previously loaded and thus cached classes</li> * <li>find it in jar files</li> * <li>ask parent class loader</li> * </ul> * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean) */ public Class<?> loadClass(String name) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // check if class has already been loaded Class<?> clazz = findLoadedClass(name); if (clazz != null) return clazz; // check internal cache for already loaded classes clazz = cachedClasses.get(name); if (clazz != null) { return clazz; } // check if the managed jars contain the class if (classesJarMapping.containsKey(name)) clazz = findClass(name); if (clazz != null) { return clazz; } // otherwise hand over the request to the parent class loader clazz = super.loadClass(name); if (clazz == null) throw new ClassNotFoundException("Class '" + name + "' not found"); return clazz; } } /** * Find class inside managed jars or hand over the parent * @see java.lang.ClassLoader#findClass(java.lang.String) */ protected Class<?> findClass(String name) throws ClassNotFoundException { // find in already loaded classes to make things shorter Class<?> clazz = findLoadedClass(name); if (clazz != null) return clazz; // check if the class searched for is contained inside managed jars, // otherwise hand over the request to the parent class loader String jarFileName = this.classesJarMapping.get(name); if (StringUtils.isBlank(jarFileName)) { super.findClass(name); } // try to find class inside jar the class name is associated with JarInputStream jarInput = null; try { // open a stream on jar which contains the class jarInput = new JarInputStream(new FileInputStream(jarFileName)); // ... and iterate through all entries JarEntry jarEntry = null; while ((jarEntry = jarInput.getNextJarEntry()) != null) { // extract the name of the jar entry and check if it has suffix '.class' and thus contains // the search for implementation String entryFileName = jarEntry.getName(); if (entryFileName.endsWith(".class")) { // replace slashes by dots, remove suffix and compare the result with the searched for class name entryFileName = entryFileName.substring(0, entryFileName.length() - 6).replace('/', '.'); if (name.equalsIgnoreCase(entryFileName)) { // load bytes from jar entry and define a class over it byte[] data = loadBytes(jarInput); if (data != null) { return defineClass(name, data, 0, data.length); } // if the jar entry does not contain any data, throw an exception throw new ClassNotFoundException("Class '" + name + "' not found"); } } } } catch (IOException e) { // if any io error occurs: throw an exception throw new ClassNotFoundException("Class '" + name + "' not found"); } finally { try { jarInput.close(); } catch (IOException e) { logger.error("Failed to close open JAR file '" + jarFileName + "'. Error: " + e.getMessage()); } } // if no such class exists, throw an exception throw new ClassNotFoundException("Class [" + name + "] not found"); } /** * Handle resource lookups by first checking the managed JARs and hand * it over to the parent if no entry exits * @see java.lang.ClassLoader#getResourceAsStream(java.lang.String) */ public InputStream getResourceAsStream(String name) { // lookup the name of the JAR file holding the resource String jarFileName = this.resourcesJarMapping.get(name); // if there is no such file, hand over the request to the parent class loader if (StringUtils.isBlank(jarFileName)) return super.getResourceAsStream(name); // try to find the resource inside the jar it is associated with JarInputStream jarInput = null; try { // open a stream on jar which contains the class jarInput = new JarInputStream(new FileInputStream(jarFileName)); // ... and iterate through all entries JarEntry jarEntry = null; while ((jarEntry = jarInput.getNextJarEntry()) != null) { // extract the name of the jar entry and check if it is equal to the provided name String entryFileName = jarEntry.getName(); if (StringUtils.equals(name, entryFileName)) { // load bytes from jar entry and return it as stream byte[] data = loadBytes(jarInput); if (data != null) return new ByteArrayInputStream(data); } } } catch (IOException e) { logger.error("Failed to read resource '" + name + "' from JAR file '" + jarFileName + "'. Error: " + e.getMessage()); } finally { try { jarInput.close(); } catch (IOException e) { logger.error("Failed to close open JAR file '" + jarFileName + "'. Error: " + e.getMessage()); } } return null; } /** * Initializes the class loader by pointing it to folder holding managed JAR files * @param componentFolder * @param componentJarIdentifier file to search for in component JAR which identifies it as component JAR * @throws IOException * @throws RequiredInputMissingException */ public void initialize(final String componentFolder, final String componentJarIdentifier) throws IOException, RequiredInputMissingException { /////////////////////////////////////////////////////////////////// // validate input if (StringUtils.isBlank(componentFolder)) throw new RequiredInputMissingException("Missing required value for parameter 'componentFolder'"); File folder = new File(componentFolder); if (!folder.isDirectory()) throw new IOException("Provided input '" + componentFolder + "' does not reference a valid folder"); File[] jarFiles = folder.listFiles(); if (jarFiles == null || jarFiles.length < 1) throw new RequiredInputMissingException("No JAR files found in folder '" + componentFolder + "'"); // /////////////////////////////////////////////////////////////////// logger.info("Initializing component classloader [componentJarIdentifier=" + componentJarIdentifier + ", folder=" + componentFolder + "]"); // step through jar files, ensure it is a file and iterate through its contents for (File jarFile : jarFiles) { if (jarFile.isFile()) { JarInputStream jarInputStream = null; try { jarInputStream = new JarInputStream(new FileInputStream(jarFile)); JarEntry jarEntry = null; while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { String jarEntryName = jarEntry.getName(); // if the current file references a class implementation, replace slashes by dots, strip // away the class suffix and add a reference to the classes-2-jar mapping if (StringUtils.endsWith(jarEntryName, ".class")) { jarEntryName = jarEntryName.substring(0, jarEntryName.length() - 6).replace('/', '.'); this.classesJarMapping.put(jarEntryName, jarFile.getAbsolutePath()); } else { // if the current file references a resource, check if it is the identifier file which // marks this jar to contain component implementation if (StringUtils.equalsIgnoreCase(jarEntryName, componentJarIdentifier)) this.componentJarFiles.add(jarFile.getAbsolutePath()); // ...and add a mapping for resource to jar file as well this.resourcesJarMapping.put(jarEntryName, jarFile.getAbsolutePath()); } } } catch (Exception e) { logger.error("Failed to read from JAR file '" + jarFile.getAbsolutePath() + "'. Error: " + e.getMessage()); } finally { try { jarInputStream.close(); } catch (Exception e) { logger.error("Failed to close open JAR file '" + jarFile.getAbsolutePath() + "'. Error: " + e.getMessage()); } } } } // load classes from jars marked component files and extract the deployment descriptors for (String cjf : this.componentJarFiles) { logger.info("Attempting to load pipeline components located in '" + cjf + "'"); // open JAR file and iterate through it's contents JarInputStream jarInputStream = null; try { jarInputStream = new JarInputStream(new FileInputStream(cjf)); JarEntry jarEntry = null; while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { // fetch name of current entry and ensure it is a class file String jarEntryName = jarEntry.getName(); if (jarEntryName.endsWith(".class")) { // replace slashes by dots and strip away '.class' suffix jarEntryName = jarEntryName.substring(0, jarEntryName.length() - 6).replace('/', '.'); Class<?> c = loadClass(jarEntryName); AsapComponent pc = c.getAnnotation(AsapComponent.class); if (pc != null) { this.managedComponents.put(getManagedComponentKey(pc.name(), pc.version()), new ComponentDescriptor(c.getName(), pc.type(), pc.name(), pc.version(), pc.description())); logger.info("pipeline component found [type=" + pc.type() + ", name=" + pc.name() + ", version=" + pc.version() + "]"); ; } } } } catch (Exception e) { logger.error("Failed to read from JAR file '" + cjf + "'. Error: " + e.getMessage()); } finally { try { jarInputStream.close(); } catch (Exception e) { logger.error("Failed to close open JAR file '" + cjf + "'. Error: " + e.getMessage()); } } } } /** * Looks up the reference {@link DataComponent}, instantiates it and passes over the provided {@link Properties} * @param name name of component to instantiate (required) * @param version version of component to instantiate (required) * @param properties properties to use for instantiation * @return * @throws RequiredInputMissingException thrown in case a required parameter value is missing * @throws ComponentInstantiationFailedException thrown in case the instantiation failed for any reason * @throws UnknownComponentException thrown in case the name and version combination does not reference a managed component */ public Component newInstance(final String name, final String version, final Properties properties) throws RequiredInputMissingException, ComponentInstantiationFailedException, UnknownComponentException { ////////////////////////////////////////////////////////////////////////////////////////// // validate provided input if (StringUtils.isBlank(name)) { throw new RequiredInputMissingException("Missing required component name"); } if (StringUtils.isBlank(version)) { throw new RequiredInputMissingException("Missing required component version"); } // ////////////////////////////////////////////////////////////////////////////////////////// // generate component key and retrieve the associated descriptor ... if available String componentKey = getManagedComponentKey(name, version); ComponentDescriptor descriptor = this.managedComponents.get(componentKey); if (descriptor == null) throw new UnknownComponentException("Unknown component [name=" + name + ", version=" + version + "]"); // if the descriptor exists, load the referenced component class, create an instance and hand over the properties try { Class<?> messageHandlerClass = loadClass(descriptor.getComponentClass()); Component instance = (Component) messageHandlerClass.newInstance(); instance.init(properties); return instance; } catch (IllegalAccessException e) { throw new ComponentInstantiationFailedException("Failed to instantiate component [name=" + name + ", version=" + version + ", reason=" + e.getMessage()); } catch (InstantiationException e) { throw new ComponentInstantiationFailedException("Failed to instantiate component [name=" + name + ", version=" + version + ", reason=" + e.getMessage()); } catch (ClassNotFoundException e) { throw new ComponentInstantiationFailedException("Failed to instantiate component [name=" + name + ", version=" + version + ", reason=" + e.getMessage()); } } /** * Returns true in case the referenced component managed by this class loader * @param name * @param version * @return */ public boolean isManagedComponent(final String name, final String version) { return this.managedComponents.containsKey(getManagedComponentKey(name, version)); } /** * Generates a key to be used for looking up the component+version specific class * @param name * @param version * @return */ public String getManagedComponentKey(final String name, final String version) { StringBuffer buf = new StringBuffer(name.trim().toLowerCase()).append("~") .append(version.trim().toLowerCase()); return buf.toString(); } /** * Reads jar file contents from stream * @param is jar input stream * @throws IOException */ protected byte[] loadBytes(InputStream is) throws IOException { BufferedInputStream bufInputStream = new BufferedInputStream(is, 8192); ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); int c; int bufferSize = 8192; byte[] buffer = new byte[bufferSize]; while ((c = bufInputStream.read(buffer)) != -1) { byteOutStream.write(buffer, 0, c); } return byteOutStream.toByteArray(); } /** * @return the managedComponents */ public Map<String, ComponentDescriptor> getManagedComponents() { return managedComponents; } }