Jetty源码阅读---Jetty的类加载器WebAppClassLoader

时间:2022-12-22 19:37:30

前面介绍了java中类加载的一般模型:双亲委派模型,这个模型适用于大多数类加载的场景,但对于web容器却是不适用的;这是因为servlet规范对web容器的类加载做了一些规定,简单的来说有以下几条:

1.WEB-INF/classes和WEB-INF/lib路径下的类会优先于父容器中的类加载,比如WEB-INF/classes下有个ABC类,CLASSPATH下也有个ABC类,jetty会优先加载WEB-INF/classes下的,这与双亲委托模型下的加载行为相反。
2.java.lang.Object等系统类不遵循第一条, WEB-INF/classes或WEB-INF/lib中的类不能替换系统类。对于哪些是系统类,其实没有做出具体规定,jetty中是通过枚举了一些类来进行判断的。
3.Server容器的实现类不被应用中的类引用,即Server的实现类不能被任何应用类加载器加载。对于哪些是系统类,也是通过枚举类的全限定名来实现的。

为了实现上面的三个要求并实现不同部署应用间依赖的隔离,jetty定义了自己的类加载器WebAppClassLoader,通过对这个类加载器使用线程上下文加载模式来达到目的。

1.WebAppClassLoader

WebAppClassLoader是URLClassLoader的子类,重写了其loadClass方法。下面就从这个方法的源码入手:

 @Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
//先检查是否已经加载
Class<?> c= findLoadedClass(name);
ClassNotFoundException ex= null;
//是否父加载器优先,这里默认是false
boolean tried_parent= false;
//要加载的类是否是系统类
boolean system_class=_context.isSystemClass(name);
//要加载的类是否是Server类
boolean server_class=_context.isServerClass(name);

//如果即是系统类又是Server类,则不进行任何加载操作,直接返回
if (system_class && server_class)
{
return null;
}

//如果类还没加载过并且父加载器不为空并且是系统类或父加载器优先并且不是server类,则用父加载器加载
if (c == null && _parent!=null && (_context.isParentLoaderPriority() || system_class) && !server_class)
{
tried_parent= true;
try
{
c= _parent.loadClass(name);
if (LOG.isDebugEnabled())
LOG.debug("loaded " + c);
}
catch (ClassNotFoundException e)
{
ex= e;
}
}

//优先使用当前WebAppClassLoader的findClass()方法进行加载
if (c == null)
{
try
{
c= this.findClass(name);
}
catch (ClassNotFoundException e)
{
ex= e;
}
}

//如果当前类加载器加载不到这个类,并且这个类不是server类也没有被父加载器加载过,则使用父加载器器进行加载
if (c == null && _parent!=null && !tried_parent && !server_class )
c= _parent.loadClass(name);

if (c == null)
throw ex;

//对当前类进行解析
if (resolve)
resolveClass(c);

if (LOG.isDebugEnabled())
LOG.debug("loaded " + c+ " from "+c.getClassLoader());

return c;
}

首先来看下WebAppClassLoader的父加载器的指定逻辑,是在其构造器中指定的:

  super(new URL[]{},parent!=null?parent
:(Thread.currentThread().getContextClassLoader()!=null?Thread.currentThread().getContextClassLoader()
:(WebAppClassLoader.class.getClassLoader()!=null?WebAppClassLoader.class.getClassLoader()
:ClassLoader.getSystemClassLoader())));

总的来说逻辑是如果为当前线程设置了contextClassLoader,这parent就为当前线程的contextClassLoader,否则则是WebAppClassLoader这个类的加载器,如果这个加载器为null(一般不可能),则为系统类加载器。当调用这个构造函数的时候,一般还没有设置线程的contextClassLoader(WebAppContext会将WebAppClassLoader对象设置为线程的ContextClassLoader),所以这个parent一般就是系统类加载器。
然后的问题是如何判断一个类是系统类或者是Server类,可以深入_context.isSystemClass(name)方法来看下:

 public boolean isSystemClass(String name)
{
if (_systemClasses == null)
loadSystemClasses();

return _systemClasses.match(name);
}

看下loadSystemClasses()代码:

protected void loadSystemClasses()
{
if (_systemClasses != null)
return;

//look for a Server attribute with the list of System classes
//to apply to every web application. If not present, use our defaults.
Server server = getServer();
if (server != null)
{
Object systemClasses = server.getAttribute(SERVER_SYS_CLASSES);
if (systemClasses != null && systemClasses instanceof String[])
_systemClasses = new ClasspathPattern((String[])systemClasses);
}

if (_systemClasses == null)
_systemClasses = new ClasspathPattern(__dftSystemClasses);
}

可以如果设置了public final static String SERVER_SYS_CLASSES = "org.eclipse.jetty.webapp.systemClasses";属性那么就会将其作为系统类,一般不会去设置这个属性,那么就会使用默认的__dftSystemClasses数组中指定的类作为系统类,下面来看下这些系统类的定义:

 // System classes are classes that cannot be replaced by
// the web application, and they are *always* loaded via
// system classloader.
public final static String[] __dftSystemClasses =
{
"java.", // Java SE classes (per servlet spec v2.5 / SRV.9.7.2)
"javax.", // Java SE classes (per servlet spec v2.5 / SRV.9.7.2)
"org.xml.", // needed by javax.xml
"org.w3c.", // needed by javax.xml
"org.apache.commons.logging.", // TODO: review if special case still needed
"org.eclipse.jetty.continuation.", // webapp cannot change continuation classes
"org.eclipse.jetty.jndi.", // webapp cannot change naming classes
"org.eclipse.jetty.plus.jaas.", // webapp cannot change jaas classes
"org.eclipse.jetty.websocket.WebSocket", // WebSocket is a jetty extension
"org.eclipse.jetty.websocket.WebSocketFactory", // WebSocket is a jetty extension
"org.eclipse.jetty.servlet.DefaultServlet" // webapp cannot change default servlets
} ;

可以看到其实就是一系列类的枚举,包括java中的系统类和一些jetty中的实现类。对于这些类,肯定不能由应用程序自己来覆盖,必须使用系统加载器来进行加载。上面说的是system class的判断,其实server class的判定也是类似的,下面是server class的枚举:

 // Server classes are classes that are hidden from being
// loaded by the web application using system classloader,
// so if web application needs to load any of such classes,
// it has to include them in its distribution.
public final static String[] __dftServerClasses =
{
"-org.eclipse.jetty.continuation.", // don't hide continuation classes
"-org.eclipse.jetty.jndi.", // don't hide naming classes
"-org.eclipse.jetty.plus.jaas.", // don't hide jaas classes
"-org.eclipse.jetty.websocket.WebSocket", // WebSocket is a jetty extension
"-org.eclipse.jetty.websocket.WebSocketFactory", // WebSocket is a jetty extension
"-org.eclipse.jetty.servlet.DefaultServlet", // don't hide default servlet
"-org.eclipse.jetty.servlet.listener.", // don't hide useful listeners
"org.eclipse.jetty." // hide other jetty classes
} ;

2.WebAppClassLoader的URLClassPath

从上面的loadclass()方法可以看到,如果不是系统类,那么就会走findClass()方法来进行类的加载,WebAppClassLoader本身没有实现findClass()方法,而是走的其父类的URLClassLoader的findClass()方法:

protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<Class>() {
public Class run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
throw new ClassNotFoundException(name);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
}

从源码来看,关键的地方其实就是将传入name中的”.”替换为”/”并且加上”.class”的后缀,然后在/* The search path for classes and resources */ private final URLClassPath ucp;中进行搜索,如果能搜索到,则交给defineClass()方法进行加载。关键点在于ucp的构建。
jetty中对于ucp的构建是在WebInfConfiguration中实现的,WebInfConfiguration代表web容器的WEB-INF目录的配置类,其中的configure()方法会将WEB-INF下classes目录和lib目录下的资源加入到到WebContext的classpath中(也就是上面的ucp),下面是具体的代码:

 @Override
public void configure(WebAppContext context) throws Exception
{
//cannot configure if the context is already started
if (context.isStarted())
{
if (LOG.isDebugEnabled())
LOG.debug("Cannot configure webapp "+context+" after it is started");
return;
}

Resource web_inf = context.getWebInf();

// Add WEB-INF classes and lib classpaths
if (web_inf != null && web_inf.isDirectory() && context.getClassLoader() instanceof WebAppClassLoader)
{
// Look for classes directory
Resource classes= web_inf.addPath("classes/");
if (classes.exists())
((WebAppClassLoader)context.getClassLoader()).addClassPath(classes);

// Look for jars
Resource lib= web_inf.addPath("lib/");
if (lib.exists() || lib.isDirectory())
((WebAppClassLoader)context.getClassLoader()).addJars(lib);
}

// Look for extra resource
@SuppressWarnings("unchecked")
List<Resource> resources = (List<Resource>)context.getAttribute(RESOURCE_URLS);
if (resources!=null)
{
Resource[] collection=new Resource[resources.size()+1];
int i=0;
collection[i++]=context.getBaseResource();
for (Resource resource : resources)
collection[i++]=resource;
context.setBaseResource(new ResourceCollection(collection));
}
}

这里添加classpath的逻辑是先添加classes中的资源,然后再添加lib目录下的资源。classses目录下一般包含项目的代码以及定义的资源文件,lib目录下一般包含项目依赖的第三方jar包。这种加载classpath的顺序保证了两个事情:首先是可以通过在项目的资源文件中添加jar中同名的配置文件覆盖掉jar包的配置;然后可以通过在项目中定义与jar包中完全相同的类(全限定名相同),来替换掉ja包中相应的类。