1. 程式人生 > >Jetty原始碼閱讀---Jetty的類載入器WebAppClassLoader

Jetty原始碼閱讀---Jetty的類載入器WebAppClassLoader

前面介紹了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包中相應的類。