
org.eclipse.jetty.webapp.WebAppClassLoader Maven / Gradle / Ivy
Show all versions of jetty-webapp Show documentation
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.webapp;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
/**
* ClassLoader for HttpContext.
*
* Specializes URLClassLoader with some utility and file mapping
* methods.
*
* This loader defaults to the 2.3 servlet spec behavior where non
* system classes are loaded from the classpath in preference to the
* parent loader. Java2 compliant loading, where the parent loader
* always has priority, can be selected with the
* {@link org.eclipse.jetty.webapp.WebAppContext#setParentLoaderPriority(boolean)}
* method and influenced with {@link WebAppContext#isServerClass(String)} and
* {@link WebAppContext#isSystemClass(String)}.
*
* If no parent class loader is provided, then the current thread
* context classloader will be used. If that is null then the
* classloader that loaded this class is used as the parent.
*/
public class WebAppClassLoader extends URLClassLoader
{
static
{
registerAsParallelCapable();
}
private static final Logger LOG = Log.getLogger(WebAppClassLoader.class);
private static final ThreadLocal __loadServerClasses = new ThreadLocal<>();
private final Context _context;
private final ClassLoader _parent;
private final Set _extensions=new HashSet();
private String _name=String.valueOf(hashCode());
private final List _transformers = new CopyOnWriteArrayList<>();
/* ------------------------------------------------------------ */
/** The Context in which the classloader operates.
*/
public interface Context
{
/* ------------------------------------------------------------ */
/** Convert a URL or path to a Resource.
* The default implementation
* is a wrapper for {@link Resource#newResource(String)}.
* @param urlOrPath The URL or path to convert
* @return The Resource for the URL/path
* @throws IOException The Resource could not be created.
*/
Resource newResource(String urlOrPath) throws IOException;
/* ------------------------------------------------------------ */
/**
* @return Returns the permissions.
*/
PermissionCollection getPermissions();
/* ------------------------------------------------------------ */
/** Is the class a System Class.
* A System class is a class that is visible to a webapplication,
* but that cannot be overridden by the contents of WEB-INF/lib or
* WEB-INF/classes
* @param clazz The fully qualified name of the class.
* @return True if the class is a system class.
*/
boolean isSystemClass(Class> clazz);
/* ------------------------------------------------------------ */
/** Is the class a Server Class.
* A Server class is a class that is part of the implementation of
* the server and is NIT visible to a webapplication. The web
* application may provide it's own implementation of the class,
* to be loaded from WEB-INF/lib or WEB-INF/classes
* @param clazz The fully qualified name of the class.
* @return True if the class is a server class.
*/
boolean isServerClass(Class> clazz);
/* ------------------------------------------------------------ */
/**
* @return True if the classloader should delegate first to the parent
* classloader (standard java behaviour) or false if the classloader
* should first try to load from WEB-INF/lib or WEB-INF/classes (servlet
* spec recommendation).
*/
boolean isParentLoaderPriority();
/* ------------------------------------------------------------ */
String getExtraClasspath();
boolean isServerResource(String name, URL parent_url);
boolean isSystemResource(String name, URL webapp_url);
}
/* ------------------------------------------------------------ */
/** Run an action with access to ServerClasses
* Run the passed {@link PrivilegedExceptionAction} with the classloader
* configured so as to allow server classes to be visible
* @param The type returned by the action
* @param action The action to run
* @return The return from the action
* @throws Exception if thrown by the action
*/
public static T runWithServerClassAccess(PrivilegedExceptionAction action) throws Exception
{
Boolean lsc=__loadServerClasses.get();
try
{
__loadServerClasses.set(true);
return action.run();
}
finally
{
if (lsc==null)
__loadServerClasses.remove();
else
__loadServerClasses.set(lsc);
}
}
/* ------------------------------------------------------------ */
/**
* Constructor.
* @param context the context for this classloader
* @throws IOException if unable to initialize from context
*/
public WebAppClassLoader(Context context)
throws IOException
{
this(null,context);
}
/* ------------------------------------------------------------ */
/**
* Constructor.
*
* @param parent the parent classloader
* @param context the context for this classloader
* @throws IOException if unable to initialize classloader
*/
public WebAppClassLoader(ClassLoader parent, Context context)
throws IOException
{
super(new URL[]{},parent!=null?parent
:(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
:(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
:ClassLoader.getSystemClassLoader())));
_parent=getParent();
_context=context;
if (_parent==null)
throw new IllegalArgumentException("no parent classloader!");
_extensions.add(".jar");
_extensions.add(".zip");
// TODO remove this system property
String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
if(extensions!=null)
{
StringTokenizer tokenizer = new StringTokenizer(extensions, ",;");
while(tokenizer.hasMoreTokens())
_extensions.add(tokenizer.nextToken().trim());
}
if (context.getExtraClasspath()!=null)
addClassPath(context.getExtraClasspath());
}
/* ------------------------------------------------------------ */
/**
* @return the name of the classloader
*/
public String getName()
{
return _name;
}
/* ------------------------------------------------------------ */
/**
* @param name the name of the classloader
*/
public void setName(String name)
{
_name=name;
}
/* ------------------------------------------------------------ */
public Context getContext()
{
return _context;
}
/* ------------------------------------------------------------ */
/**
* @param resource Comma or semicolon separated path of filenames or URLs
* pointing to directories or jar files. Directories should end
* with '/'.
* @throws IOException if unable to add classpath from resource
*/
public void addClassPath(Resource resource)
throws IOException
{
if (resource instanceof ResourceCollection)
{
for (Resource r : ((ResourceCollection)resource).getResources())
addClassPath(r);
}
else
{
addClassPath(resource.toString());
}
}
/* ------------------------------------------------------------ */
/**
* @param classPath Comma or semicolon separated path of filenames or URLs
* pointing to directories or jar files. Directories should end
* with '/'.
* @throws IOException if unable to add classpath
*/
public void addClassPath(String classPath)
throws IOException
{
if (classPath == null)
return;
StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");
while (tokenizer.hasMoreTokens())
{
Resource resource= _context.newResource(tokenizer.nextToken().trim());
if (LOG.isDebugEnabled())
LOG.debug("Path resource=" + resource);
// Add the resource
if (resource.isDirectory() && resource instanceof ResourceCollection)
addClassPath(resource);
else
{
// Resolve file path if possible
File file= resource.getFile();
if (file != null)
{
URL url= resource.getURI().toURL();
addURL(url);
}
else if (resource.isDirectory())
{
addURL(resource.getURI().toURL());
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Check file exists and is not nested jar: "+resource);
throw new IllegalArgumentException("File not resolvable or incompatible with URLClassloader: "+resource);
}
}
}
}
/* ------------------------------------------------------------ */
/**
* @param file Checks if this file type can be added to the classpath.
*/
private boolean isFileSupported(String file)
{
int dot = file.lastIndexOf('.');
return dot!=-1 && _extensions.contains(file.substring(dot));
}
/* ------------------------------------------------------------ */
/** Add elements to the class path for the context from the jar and zip files found
* in the specified resource.
* @param lib the resource that contains the jar and/or zip files.
*/
public void addJars(Resource lib)
{
if (lib.exists() && lib.isDirectory())
{
String[] files=lib.list();
if (files != null)
{
Arrays.sort(files);
}
for (int f=0;files!=null && f getResources(String name) throws IOException
{
List from_parent = new ArrayList<>();
List from_webapp = new ArrayList<>();
Enumeration urls = _parent.getResources(name);
while (urls!=null && urls.hasMoreElements())
{
URL url = urls.nextElement();
if (Boolean.TRUE.equals(__loadServerClasses.get()) || !_context.isServerResource(name,url))
from_parent.add(url);
}
urls = this.findResources(name);
while (urls!=null && urls.hasMoreElements())
{
URL url = urls.nextElement();
if (!_context.isSystemResource(name,url) || from_parent.isEmpty())
from_webapp.add(url);
}
List resources;
if (_context.isParentLoaderPriority())
{
from_parent.addAll(from_webapp);
resources = from_parent;
}
else
{
from_webapp.addAll(from_parent);
resources = from_webapp;
}
if (LOG.isDebugEnabled())
LOG.debug("getResources {} {}",name,resources);
return Collections.enumeration(resources);
}
/* ------------------------------------------------------------ */
/**
* Get a resource from the classloader
*
* NOTE: this method provides a convenience of hacking off a leading /
* should one be present. This is non-standard and it is recommended
* to not rely on this behavior
*/
@Override
public URL getResource(String name)
{
URL resource=null;
if (_context.isParentLoaderPriority())
{
URL parent_url=_parent.getResource(name);
// return if we have a url the webapp is allowed to see
if (parent_url!=null
&& (Boolean.TRUE.equals(__loadServerClasses.get())
|| !_context.isServerResource(name,parent_url)))
resource = parent_url;
else
{
URL webapp_url = this.findResource(name);
// If found here then OK to use regardless of system or server classes
// If it is a system resource, we've already tried to load from parent, so
// would have returned it.
// If it is a server resource, doesn't matter as we have loaded it from the
// webapp
if (webapp_url!=null)
resource = webapp_url;
}
}
else
{
URL webapp_url = this.findResource(name);
if (webapp_url!=null && !_context.isSystemResource(name,webapp_url))
resource = webapp_url;
else
{
// Couldn't find or see a webapp resource, so try a parent
URL parent_url=_parent.getResource(name);
if (parent_url!=null
&& (Boolean.TRUE.equals(__loadServerClasses.get())
|| !_context.isServerResource(name,parent_url)))
resource = parent_url;
// We couldn't find a parent resource, so OK to return a webapp one if it exists
// and we just couldn't see it before
else if (webapp_url!=null)
resource = webapp_url;
}
}
// Perhaps this failed due to leading /
if (resource==null && name.startsWith("/"))
resource = getResource(name.substring(1));
if (LOG.isDebugEnabled())
LOG.debug("getResource {} {}",name,resource);
return resource;
}
/* ------------------------------------------------------------ */
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name))
{
ClassNotFoundException ex= null;
Class> parent_class = null;
Class> webapp_class = null;
// Has this loader loaded the class already?
webapp_class = findLoadedClass(name);
if (webapp_class != null)
{
if (LOG.isDebugEnabled())
LOG.debug("found webapp loaded {}",webapp_class);
return webapp_class;
}
// Should we try the parent loader first?
if (_context.isParentLoaderPriority())
{
// Try the parent loader
try
{
parent_class = _parent.loadClass(name);
// If the webapp is allowed to see this class
if (Boolean.TRUE.equals(__loadServerClasses.get()) || !_context.isServerClass(parent_class))
{
if (LOG.isDebugEnabled())
LOG.debug("PLP parent loaded {}",parent_class);
return parent_class;
}
}
catch (ClassNotFoundException e)
{
// Save it for later
ex = e;
}
// Try the webapp loader
try
{
// If found here then OK to use regardless of system or server classes
// If it is a system class, we've already tried to load from parent, so
// would have returned it.
// If it is a server class, doesn't matter as we have loaded it from the
// webapp
webapp_class = this.findClass(name);
resolveClass(webapp_class);
if (LOG.isDebugEnabled())
LOG.debug("PLP webapp loaded {}",webapp_class);
return webapp_class;
}
catch (ClassNotFoundException e)
{
if (ex==null)
ex = e;
else
ex.addSuppressed(e);
}
throw ex;
}
else
{
// Not parent loader priority, so...
// Try the webapp classloader first
// Look in the webapp classloader as a resource, to avoid
// loading a system class.
String path = name.replace('.', '/').concat(".class");
URL webapp_url = findResource(path);
if (webapp_url!=null && !_context.isSystemResource(name,webapp_url))
{
webapp_class = this.foundClass(name,webapp_url);
resolveClass(webapp_class);
if (LOG.isDebugEnabled())
LOG.debug("WAP webapp loaded {}",webapp_class);
return webapp_class;
}
// Try the parent loader
try
{
parent_class = _parent.loadClass(name);
// If the webapp is allowed to see this class
if (Boolean.TRUE.equals(__loadServerClasses.get()) || !_context.isServerClass(parent_class))
{
if (LOG.isDebugEnabled())
LOG.debug("WAP parent loaded {}",parent_class);
return parent_class;
}
}
catch (ClassNotFoundException e)
{
ex=e;
}
// We couldn't find a parent class, so OK to return a webapp one if it exists
// and we just couldn't see it before
if (webapp_url!=null)
{
webapp_class = this.foundClass(name,webapp_url);
resolveClass(webapp_class);
if (LOG.isDebugEnabled())
LOG.debug("WAP !server webapp loaded {}",webapp_class);
return webapp_class;
}
throw ex==null?new ClassNotFoundException(name):ex;
}
}
}
/* ------------------------------------------------------------ */
/**
* @param transformer the transformer to add
* @deprecated {@link #addTransformer(ClassFileTransformer)} instead
*/
@Deprecated
public void addClassFileTransformer(ClassFileTransformer transformer)
{
_transformers.add(transformer);
}
/* ------------------------------------------------------------ */
/**
* @param transformer the transformer to remove
* @return true if transformer was removed
* @deprecated use {@link #removeTransformer(ClassFileTransformer)} instead
*/
@Deprecated
public boolean removeClassFileTransformer(ClassFileTransformer transformer)
{
return _transformers.remove(transformer);
}
/* ------------------------------------------------------------ */
public void addTransformer(ClassFileTransformer transformer)
{
_transformers.add(transformer);
}
/* ------------------------------------------------------------ */
public boolean removeTransformer(ClassFileTransformer transformer)
{
return _transformers.remove(transformer);
}
/* ------------------------------------------------------------ */
@Override
protected Class> findClass(final String name) throws ClassNotFoundException
{
if (_transformers.isEmpty())
return super.findClass(name);
String path = name.replace('.', '/').concat(".class");
URL url = findResource(path);
if (url==null)
throw new ClassNotFoundException(name);
return foundClass(name,url);
}
/* ------------------------------------------------------------ */
protected Class> foundClass(final String name, URL url) throws ClassNotFoundException
{
if (_transformers.isEmpty())
return super.findClass(name);
InputStream content=null;
try
{
content = url.openStream();
byte[] bytes = IO.readBytes(content);
if (LOG.isDebugEnabled())
LOG.debug("foundClass({}) url={} cl={}",name,url,this);
for (ClassFileTransformer transformer : _transformers)
{
byte[] tmp = transformer.transform(this,name,null,null,bytes);
if (tmp != null)
bytes = tmp;
}
return defineClass(name,bytes,0,bytes.length);
}
catch (IOException e)
{
throw new ClassNotFoundException(name,e);
}
catch (IllegalClassFormatException e)
{
throw new ClassNotFoundException(name,e);
}
finally
{
if (content!=null)
{
try
{
content.close();
}
catch (IOException e)
{
throw new ClassNotFoundException(name,e);
}
}
}
}
/* ------------------------------------------------------------ */
@Override
public void close() throws IOException
{
super.close();
}
/* ------------------------------------------------------------ */
@Override
public String toString()
{
return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode());
}
}