org.mule.runtime.module.artifact.classloader.RegionClassLoader Maven / Gradle / Ivy
/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.runtime.module.artifact.classloader;
import static java.lang.Integer.toHexString;
import static java.lang.String.format;
import static java.lang.System.identityHashCode;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang.ClassUtils.getPackageName;
import static org.mule.runtime.api.util.Preconditions.checkArgument;
import org.mule.runtime.module.artifact.classloader.exception.ClassNotFoundInRegionException;
import org.mule.runtime.module.artifact.descriptor.ArtifactDescriptor;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import sun.misc.CompoundEnumeration;
/**
 * Defines a classloader for a Mule artifact composed of other artifacts.
 * 
 * Each artifact in the region can provide classes and resources to other artifacts. What is shared is defined on each artifact by
 * providing a {@link ArtifactClassLoaderFilter}. Lookup policy for each classloader added to the region must be aware of any
 * dependency between members of the region and updated accordingly.
 * 
 * For any member X of the region, if it has a dependency against another region member Y, then X must add all the exported
 * packages from Y as PARENT_FIRST. (to indicate that X wants to load those Y's packages)
 * 
 * For any member X of the region, if there is another region member Y that is not a dependency, then X must add all the exported
 * packages from Y as CHILD_ONLY. (to indicate that X does not want to load those Y's packages)
 * 
 * Only a region member can export a given package, but same resources can be exported by many members. The order in which the
 * resources are found will depend on the order in which the class loaders were added to the region.
 */
public class RegionClassLoader extends MuleDeployableArtifactClassLoader {
  protected static final String REGION_OWNER_CANNOT_BE_REMOVED_ERROR = "Region owner cannot be removed";
  static {
    registerAsParallelCapable();
  }
  private final List registeredClassLoaders = new ArrayList<>();
  private final Map packageMapping = new HashMap<>();
  private final Map> resourceMapping = new HashMap<>();
  private ArtifactClassLoader ownerClassLoader;
  /**
   * Creates a new region.
   *
   * @param artifactId artifact unique ID for the artifact owning the created class loader instance. Non empty.
   * @param artifactDescriptor descriptor for the artifact owning the created class loader instance. Non null.
   * @param parent parent classloader for the region. Non null
   * @param lookupPolicy lookup policy to use on the region
   */
  public RegionClassLoader(String artifactId, ArtifactDescriptor artifactDescriptor, ClassLoader parent,
                           ClassLoaderLookupPolicy lookupPolicy) {
    super(artifactId, artifactDescriptor, new URL[0], parent, lookupPolicy, emptyList());
  }
  @Override
  public List getArtifactPluginClassLoaders() {
    return registeredClassLoaders.stream().map(r -> r.unfilteredClassLoader).collect(toList());
  }
  /**
   * Adds a class loader to the region.
   *
   * @param artifactClassLoader classloader to add. Non null.
   * @param filter filter used to provide access to the added classloader. Non null
   * @throws IllegalArgumentException if the class loader is already a region member.
   */
  public synchronized void addClassLoader(ArtifactClassLoader artifactClassLoader, ArtifactClassLoaderFilter filter) {
    checkArgument(artifactClassLoader != null, "artifactClassLoader cannot be null");
    checkArgument(filter != null, "filter cannot be null");
    RegionMemberClassLoader registeredClassLoader = findRegisteredClassLoader(artifactClassLoader);
    if (artifactClassLoader == ownerClassLoader || registeredClassLoader != null) {
      throw new IllegalArgumentException(createClassLoaderAlreadyInRegionError(artifactClassLoader.getArtifactId()));
    }
    if (ownerClassLoader == null) {
      ownerClassLoader = artifactClassLoader;
    } else {
      registeredClassLoaders.add(new RegionMemberClassLoader(artifactClassLoader, filter));
    }
    filter.getExportedClassPackages().forEach(p -> {
      LookupStrategy packageLookupStrategy = getClassLoaderLookupPolicy().getPackageLookupStrategy(p);
      if (!(packageLookupStrategy instanceof ChildFirstLookupStrategy)) {
        throw new IllegalStateException(illegalPackageMappingError(p, packageLookupStrategy));
      } else if (packageMapping.containsKey(p)) {
        throw new IllegalStateException(duplicatePackageMappingError(p));
      } else {
        packageMapping.put(p, artifactClassLoader);
      }
    });
    for (String exportedResource : filter.getExportedResources()) {
      List classLoaders = resourceMapping.get(exportedResource);
      if (classLoaders == null) {
        classLoaders = new ArrayList<>();
        resourceMapping.put(exportedResource, classLoaders);
      }
      classLoaders.add(artifactClassLoader);
    }
  }
  static String illegalPackageMappingError(String p, LookupStrategy packageLookupStrategy) {
    return format("Attempt to map package '%s' which was already defined on the region lookup policy with '%s'",
                  p, packageLookupStrategy.getClass().getName());
  }
  static String duplicatePackageMappingError(String packageName) {
    return "Attempt to redefine mapping for package: " + packageName;
  }
  private RegionMemberClassLoader findRegisteredClassLoader(ArtifactClassLoader artifactClassLoader) {
    for (RegionMemberClassLoader registeredClassLoader : registeredClassLoaders) {
      if (registeredClassLoader.unfilteredClassLoader == artifactClassLoader) {
        return registeredClassLoader;
      }
    }
    return null;
  }
  /**
   * Removes a class loader member from the region.
   * 
   * Only region members that do not export any package or resoruce can be removed from the region as they are not visible to
   * other members.
   *
   * @param artifactClassLoader class loader to remove. Non null
   * @return true if the class loader is a region member and was removed, false if it is not a region member.
   * @throws IllegalArgumentException if the class loader is the region owner or is a regiion member that exports packages or
   *         resources.
   */
  public synchronized boolean removeClassLoader(ArtifactClassLoader artifactClassLoader) {
    checkArgument(artifactClassLoader != null, "artifactClassLoader cannot be null");
    if (ownerClassLoader == artifactClassLoader) {
      throw new IllegalArgumentException(REGION_OWNER_CANNOT_BE_REMOVED_ERROR);
    }
    RegionMemberClassLoader registeredClassLoader = findRegisteredClassLoader(artifactClassLoader);
    int index = registeredClassLoaders.indexOf(registeredClassLoader);
    if (index < 0) {
      return false;
    }
    if (!registeredClassLoader.filter.getExportedClassPackages().isEmpty()
        || !registeredClassLoader.filter.getExportedResources().isEmpty()) {
      throw new IllegalArgumentException(createCannotRemoveClassLoaderError(artifactClassLoader.getArtifactId()));
    }
    registeredClassLoaders.remove(index);
    return true;
  }
  @Override
  public Class> findLocalClass(String name) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
      final String packageName = getPackageName(name);
      final ArtifactClassLoader artifactClassLoader = packageMapping.get(packageName);
      if (artifactClassLoader != null) {
        try {
          return artifactClassLoader.findLocalClass(name);
        } catch (ClassNotFoundException e) {
          throw new ClassNotFoundInRegionException(name, getArtifactId(), artifactClassLoader.getArtifactId(), e);
        }
      } else {
        throw new ClassNotFoundInRegionException(name, getArtifactId());
      }
    }
  }
  @Override
  public final URL findResource(final String name) {
    URL resource = null;
    final List artifactClassLoaders = resourceMapping.get(name);
    if (artifactClassLoaders != null) {
      for (ArtifactClassLoader artifactClassLoader : artifactClassLoaders) {
        resource = artifactClassLoader.getClassLoader().getResource(name);
        if (resource != null) {
          break;
        }
      }
    }
    return resource;
  }
  @Override
  public final Enumeration findResources(final String name) throws IOException {
    final List artifactClassLoaders = resourceMapping.get(name);
    List> enumerations = new ArrayList<>(registeredClassLoaders.size());
    if (artifactClassLoaders != null) {
      for (ArtifactClassLoader artifactClassLoader : artifactClassLoaders) {
        final Enumeration partialResources = artifactClassLoader.findResources(name);
        if (partialResources.hasMoreElements()) {
          enumerations.add(partialResources);
        }
      }
    }
    return new CompoundEnumeration<>(enumerations.toArray(new Enumeration[0]));
  }
  @Override
  public void dispose() {
    registeredClassLoaders.stream().map(c -> c.unfilteredClassLoader).forEach(classLoader -> {
      disposeClassLoader(classLoader);
    });
    registeredClassLoaders.clear();
    disposeClassLoader(ownerClassLoader);
    super.dispose();
  }
  private void disposeClassLoader(ArtifactClassLoader classLoader) {
    try {
      classLoader.dispose();
    } catch (Exception e) {
      final String message = "Error disposing classloader for '{}'. This can cause a memory leak";
      if (logger.isDebugEnabled()) {
        logger.debug(message, classLoader.getArtifactId(), e);
      } else {
        logger.error(message, classLoader.getArtifactId());
      }
    }
  }
  @Override
  public URL findLocalResource(String resourceName) {
    URL resource = getOwnerClassLoader().findLocalResource(resourceName);
    if (resource == null && getParent() instanceof LocalResourceLocator) {
      resource = ((LocalResourceLocator) getParent()).findLocalResource(resourceName);
    }
    return resource;
  }
  private ArtifactClassLoader getOwnerClassLoader() {
    return ownerClassLoader;
  }
  @Override
  public String toString() {
    return format("%s[%s] -> %s@%s", getClass().getName(), getArtifactId(), packageMapping.toString(),
                  toHexString(identityHashCode(this)));
  }
  static String createCannotRemoveClassLoaderError(String artifactId) {
    return format("Cannot remove classloader '%s' as it exports at least a package or resource", artifactId);
  }
  static String createClassLoaderAlreadyInRegionError(String artifactId) {
    return "Region already contains classloader for artifact:" + artifactId;
  }
  private static class RegionMemberClassLoader {
    final ArtifactClassLoader unfilteredClassLoader;
    final ArtifactClassLoaderFilter filter;
    private RegionMemberClassLoader(ArtifactClassLoader unfilteredClassLoader, ArtifactClassLoaderFilter filter) {
      this.unfilteredClassLoader = unfilteredClassLoader;
      this.filter = filter;
    }
  }
}
              © 2015 - 2025 Weber Informatics LLC | Privacy Policy