This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git


The following commit(s) were added to refs/heads/javax by this push:
     new fb40bbe65 TAP5-2779: multiple classloader fixes
fb40bbe65 is described below

commit fb40bbe6585b36b643b92ecff57b3cabbd67b66d
Author: Thiago H. de Paula Figueiredo <[email protected]>
AuthorDate: Fri May 17 09:42:21 2024 -0300

    TAP5-2779: multiple classloader fixes
---
 .../internal/plastic/PlasticClassLoader.java       |  70 +++-
 .../internal/plastic/PlasticClassPool.java         |  20 +-
 .../corelib/pages/ComponentLibraries.java          | 163 +++++++++-
 .../tapestry5/corelib/pages/ExceptionReport.java   |   6 +
 .../tapestry5/corelib/pages/PageCatalog.java       |   1 +
 .../corelib/pages/PageClassLoaderContexts.java     | 104 ++++++
 .../tapestry5/internal/ThrowawayClassLoader.java   | 105 ++++++
 .../ComponentDependencyGraphvizGeneratorImpl.java  |  28 +-
 .../services/ComponentDependencyRegistry.java      |  13 +
 .../services/ComponentDependencyRegistryImpl.java  | 124 ++++++-
 .../services/ComponentInstantiatorSourceImpl.java  |  64 +++-
 .../services/ComponentModelSourceImpl.java         |  52 ++-
 .../internal/services/PageSourceImpl.java          |  89 ++---
 .../apache/tapestry5/modules/DashboardModule.java  |  10 +-
 .../apache/tapestry5/modules/PageLoadModule.java   |  34 +-
 .../services/pageload/PageClassLoaderContext.java  |  52 ++-
 .../pageload/PageClassLoaderContextManager.java    |   6 +
 .../PageClassLoaderContextManagerImpl.java         | 358 ++++++++++++++-------
 .../tapestry5/corelib/pages/ComponentLibraries.tml | 289 ++++++++++-------
 .../apache/tapestry5/corelib/pages/PageCatalog.tml |   2 +-
 .../corelib/pages/PageClassLoaderContexts.tml      |  17 +
 tapestry-core/src/test/app1/AtComponentType.tml    |   6 +
 tapestry-core/src/test/app1/GridDemo.tml           |   2 +-
 .../integration/app1/pages/AtComponentType.java    |  24 ++
 .../tapestry5/integration/app1/pages/GridDemo.java |   6 +
 .../ComponentDependencyRegistryImplTest.java       | 149 +++++++--
 tapestry-core/src/test/resources/log4j.properties  |   1 +
 27 files changed, 1413 insertions(+), 382 deletions(-)

diff --git 
a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java
 
b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java
index 85a263b75..591ba3729 100644
--- 
a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java
+++ 
b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java
@@ -1,4 +1,4 @@
-// Copyright 2011 The Apache Software Foundation
+// Copyright 2011, 2023, 2024 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,6 +14,14 @@
 
 package org.apache.tapestry5.internal.plastic;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -35,6 +43,10 @@ public class PlasticClassLoader extends ClassLoader
     
     private String tag;
     
+    private Map<String, Class<?>> cache;
+    
+    private static List<String> log = new ArrayList<>();
+    
     public PlasticClassLoader(ClassLoader parent, ClassLoaderDelegate 
delegate) 
     {
         super(parent);
@@ -50,24 +62,36 @@ public class PlasticClassLoader extends ClassLoader
 
             if (loadedClass != null)
                 return loadedClass;
-
+            
             if (shouldInterceptClassLoading(name))
             {
-                Class<?> c = null;
-                if ((filter != null && filter.test(name)) || (filter == null 
&& delegate.shouldInterceptClassLoading(name)))
-                {
-                    c = delegate.loadAndTransformClass(name);
-                }
-                else if (alternativeClassloading != null)
+                
+                Class<?> c = getFromCache(name);
+                
+                if (c == null)
                 {
-                    c = alternativeClassloading.apply(name);
+                
+                    if ((filter != null && filter.test(name)) || (filter == 
null && delegate.shouldInterceptClassLoading(name)))
+                    {
+                        c = delegate.loadAndTransformClass(name);
+                    }
+                    else if (alternativeClassloading != null)
+                    {
+                        c = alternativeClassloading.apply(name);
+                    }
+                    
+                    if (cache != null && c != null)
+                    {
+                        cache.put(name, c);
+                    }
+                    
                 }
                 
                 if (c == null)
                 {
                     return super.loadClass(name, resolve);                    
                 }
-                    
+                
                 if (resolve)
                     resolveClass(c);
 
@@ -110,6 +134,10 @@ public class PlasticClassLoader extends ClassLoader
     public void setTag(String tag) 
     {
         this.tag = tag;
+        if (cache == null)
+        {
+            cache = Collections.synchronizedMap(new HashMap<>());
+        }
     }
     
     /**
@@ -126,9 +154,27 @@ public class PlasticClassLoader extends ClassLoader
     @Override
     public String toString()
     {
-        final String superToString = super.toString();
-        final String id = 
superToString.substring(superToString.indexOf('@')).trim();
+        final String id = getClassLoaderId();
         return String.format("PlasticClassLoader[%s, tag=%s, parent=%s]", id, 
tag, getParent());
     }
 
+    public String getClassLoaderId() {
+        final String superToString = super.toString();
+        return superToString.substring(superToString.indexOf('@')).trim();
+    }
+    
+    private Class<?> getFromCache(String name)
+    {
+        Class<?> c = null;
+        if (cache != null && cache.containsKey(name))
+        {
+            c = cache.get(name);
+        }
+        if (c == null && getParent() instanceof PlasticClassLoader)
+        {
+            c = ((PlasticClassLoader) getParent()).getFromCache(name);
+        }
+        return c;
+    }
+    
 }
diff --git 
a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java
 
b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java
index 9daa47d51..0bc3bcb7c 100644
--- 
a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java
+++ 
b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java
@@ -753,7 +753,18 @@ public class PlasticClassPool implements 
ClassLoaderDelegate, Opcodes, PlasticCl
 
     private FieldInstrumentations getFieldInstrumentations(String 
classInternalName)
     {
-        FieldInstrumentations result = instrumentations.get(classInternalName);
+        
+        // Check whether parent pool already has instrumentations for
+        // that class to avoid a duplicated class definition attempt
+        // when running tapestry-core in multiple classloader mode.
+        PlasticClassPool current = this;
+        FieldInstrumentations result;
+        do 
+        {
+            result = current.instrumentations.get(classInternalName);
+            current = current.parent;
+        }
+        while (current != null && result == null);
 
         if (result != null)
         {
@@ -845,6 +856,13 @@ public class PlasticClassPool implements 
ClassLoaderDelegate, Opcodes, PlasticCl
             throw new RuntimeException(e);
         }
     }
+
+    @Override
+    public String toString() 
+    {
+        return "PlasticClassPool [loader=" + loader + "]";
+    }
+    
 }
 
 
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java
index 4ebaad2f1..83e4cfa27 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java
@@ -21,11 +21,20 @@ import java.util.List;
 
 import org.apache.tapestry5.Block;
 import org.apache.tapestry5.annotations.Cached;
+import org.apache.tapestry5.annotations.InjectComponent;
 import org.apache.tapestry5.annotations.OnEvent;
+import org.apache.tapestry5.annotations.Persist;
 import org.apache.tapestry5.annotations.Property;
 import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
 import org.apache.tapestry5.annotations.WhitelistAccessOnly;
+import org.apache.tapestry5.corelib.components.Zone;
 import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+import org.apache.tapestry5.internal.ThrowawayClassLoader;
+import org.apache.tapestry5.internal.plastic.ClassLoaderDelegate;
+import org.apache.tapestry5.internal.plastic.PlasticClassLoader;
+import 
org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGenerator;
+import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
+import 
org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
 import org.apache.tapestry5.ioc.annotations.Description;
 import org.apache.tapestry5.ioc.annotations.Inject;
 import org.apache.tapestry5.ioc.annotations.Symbol;
@@ -59,6 +68,9 @@ public class ComponentLibraries
     };
 
     private static enum Type { PAGE, COMPONENT, MIXIN }
+    
+    @InjectComponent
+    private Zone zone;
 
     @Inject
     private ComponentClassResolver componentClassResolver;
@@ -97,6 +109,39 @@ public class ComponentLibraries
     @Inject
     private ComponentLibraryInfoSource componentLibraryInfoSource;
     
+    @Inject
+    private ComponentDependencyRegistry componentDependencyRegistry;
+    
+    @Inject
+    private ComponentDependencyGraphvizGenerator 
componentDependencyGraphvizGenerator;
+    
+    @Property
+    private String selectedComponent;
+    
+    @Property
+    private String dependency;
+    
+    @Persist
+    @Property
+    private boolean showEverything;
+    
+//    void onActivate(List<String> context)
+//    {
+//        if (context.size() > 0)
+//        {
+//            selectedComponent = String.join("/", context);
+//        }
+//        else
+//        {
+//            selectedComponent = null;
+//        }
+//    }
+//    
+//    Object[] onPassivate()
+//    {
+//        return selectedComponent.split("/");
+//    }
+    
     @Cached
     public List<LibraryMapping> getLibraryMappings()
     {
@@ -105,7 +150,7 @@ public class ComponentLibraries
         // add all the library mappings, except the "" (empty string) one.
         for (LibraryMapping libraryMapping : 
componentClassResolver.getLibraryMappings())
         {
-            if (!"".equals(libraryMapping.libraryName)) {
+            if (showEverything || !"".equals(libraryMapping.libraryName)) {
                 mappings.add(libraryMapping);
             }
         }
@@ -132,6 +177,7 @@ public class ComponentLibraries
     private List<String> filter(final List<String> allNames)
     {
         List<String> logicalNames = new ArrayList<String>();
+        final List<LibraryMapping> libraryMappings = getLibraryMappings();
         for (String name : allNames)
         {
             
@@ -140,6 +186,25 @@ public class ComponentLibraries
             {
                 logicalNames.add(name);
             }
+            else
+            {
+                if (libraryMapping.libraryName.equals(""))
+                {
+                    boolean isWebappLibrary = true;
+                    for (LibraryMapping otherLibraryMapping : libraryMappings) 
{
+                        if (!libraryMapping.equals(otherLibraryMapping) &&
+                                
name.startsWith(otherLibraryMapping.libraryName + "/"))
+                        {
+                            isWebappLibrary = false;
+                            break;
+                        }
+                    }
+                    if (isWebappLibrary)
+                    {
+                        logicalNames.add(name);
+                    }
+                }
+            }
         }
         
         return logicalNames;
@@ -211,7 +276,12 @@ public class ComponentLibraries
     @Cached(watch = "logicalName")
     public Description getDescription() throws ClassNotFoundException
     {
-        return Class.forName(getClassName()).getAnnotation(Description.class);
+        try {
+            return 
Class.forName(getClassName()).getAnnotation(Description.class);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
     }
 
     public boolean isClassHasTags() throws ClassNotFoundException
@@ -325,5 +395,94 @@ public class ComponentLibraries
             object.put(propertyName, value);
         }
     }
+    
+    public String getGraphvizValue()
+    {
+        return componentDependencyGraphvizGenerator.generate(
+                getClassName(selectedComponent));
+    }
+    
+    public String getClassName(String logicalName)
+    {
+        return componentClassResolver.getClassName(logicalName);
+    }
+    
+    public String getComponentClassName()
+    {
+        return getClassName(selectedComponent);
+    }
+    
+    public List<String> getDependencies()
+    {
+        final String className = 
componentClassResolver.getClassName(selectedComponent);
+        final List<String> dependencies = new ArrayList<>();
+        
dependencies.addAll(componentDependencyRegistry.getDependencies(className, 
DependencyType.INJECT_PAGE));
+        
dependencies.addAll(componentDependencyRegistry.getDependencies(className, 
DependencyType.SUPERCLASS));
+        
dependencies.addAll(componentDependencyRegistry.getDependencies(className, 
DependencyType.USAGE));
+        Collections.sort(dependencies);
+        return dependencies;
+    }
+    
+    public List<String> getDependents()
+    {
+        final String className = 
componentClassResolver.getClassName(selectedComponent);
+        List<String> dependents = new ArrayList<>(
+                componentDependencyRegistry.getDependents(className));
+        Collections.sort(dependents);
+        return dependents;
+    }
+    
+    public String getDisplayLogicalName()
+    {
+        return componentClassResolver.getLogicalName(dependency);
+    }
+    
+    public Object onSelectComponent(String selectedComponent)
+    {
+        this.selectedComponent = selectedComponent;
+        final String className = 
componentClassResolver.getClassName(selectedComponent);
+        if (!componentDependencyRegistry.contains(className)) 
+        {
+            
+            final ClassLoader classLoader = new 
ThrowawayClassLoader(getClass().getClassLoader());
+            
+            try 
+            {
+                
componentDependencyRegistry.register(classLoader.loadClass(className));
+            } catch (ClassNotFoundException e) 
+            {
+                throw new RuntimeException(e);
+            }
+        }
+        return zone.getBody();
+    }
+    
+    public Object getContext()
+    {
+        return logicalName;
+    }
+    
+    public Object onReset()
+    {
+        selectedComponent = null;
+        return zone.getBody();
+    }
+    
+    public Object onShowEverything()
+    {
+        showEverything = true;
+        return zone.getBody();
+    }
+    
+    public Object onShowRestricted()
+    {
+        showEverything = false;
+        return zone.getBody();
+    }
+    
+    public String getLibraryName()
+    {
+        return !libraryMapping.libraryName.isEmpty() ? 
libraryMapping.libraryName : "Webapp's own component library";
+    }
 
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
index f11d80548..985b38fa6 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
@@ -41,6 +41,7 @@ import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 import org.apache.tapestry5.services.ExceptionReporter;
 import org.apache.tapestry5.services.PageRenderLinkSource;
 import org.apache.tapestry5.services.URLEncoder;
+import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
 
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -123,6 +124,10 @@ public class ExceptionReport extends AbstractInternalPage 
implements ExceptionRe
     @Inject
     @ComponentClasses 
     private InvalidationEventHub classesInvalidationHub;
+    
+    @Inject
+    @Property
+    private PageClassLoaderContextManager pageClassLoaderContextManager;
 
     private String failurePage;
 
@@ -187,6 +192,7 @@ public class ExceptionReport extends AbstractInternalPage 
implements ExceptionRe
 
     public void reportException(Throwable exception)
     {
+        
System.out.print(pageClassLoaderContextManager.getRoot().toRecursiveString());
         rootException = exception;
 
         rootURL = baseURLSource.getBaseURL(request.isSecure());
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
index 974a4a9e4..ba28b0a98 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
@@ -227,6 +227,7 @@ public class PageCatalog
     void onActionFromPreloadPageClassLoaderContexts()
     {
         pageClassLoaderContextManager.preload();
+        alertManager.warn("Component dependency information and page 
classloader contexts preloaded.");
     }
     
     Object onClearPage(String className)
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.java
new file mode 100644
index 000000000..54a12b256
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.java
@@ -0,0 +1,104 @@
+// Copyright 2024 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.corelib.pages;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.tapestry5.MarkupWriter;
+import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
+import org.apache.tapestry5.annotations.WhitelistAccessOnly;
+import org.apache.tapestry5.internal.plastic.PlasticClassLoader;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.services.pageload.PageClassLoaderContext;
+import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
+
+/**
+ * Shows information about the page classloader contexts.
+ */
+@UnknownActivationContextCheck(false)
+@WhitelistAccessOnly
+public class PageClassLoaderContexts
+{
+
+    @Inject
+    private PageClassLoaderContextManager pageClassLoaderContextManager;
+    
+    void onRender(MarkupWriter writer)
+    {
+        final PageClassLoaderContext root = 
pageClassLoaderContextManager.getRoot();
+        render(root, writer);
+    }
+
+    private void render(PageClassLoaderContext context, MarkupWriter writer) 
+    {
+        
+        final int classes = context.getClassNames().size();
+        writer.element("li");
+        writer.element("details");
+        writer.element("summary");
+        writer.element("span", "class", "glyphicon glyphicon-zoom-in");
+        writer.end(); // span
+        writer.write(context.getName());
+        writer.write(" (");
+        writer.write(((PlasticClassLoader) 
context.getClassLoader()).getClassLoaderId());
+        writer.write(", ");
+        writer.write(String.valueOf(classes));
+        if (classes > 1)
+        {
+            writer.write(" classes)");
+        }
+        else
+        {
+            writer.write(" class)");
+        }
+        writer.end(); // summary
+
+        if (!context.isRoot() && !context.getClassNames().isEmpty())
+        {
+            final List<String> classNames = new 
ArrayList<>(context.getClassNames());
+            Collections.sort(classNames);
+            writer.element("ul");
+            for (String className : classNames) 
+            {
+                writer.element("li").text(className);
+                writer.end(); // li
+            }
+            writer.end(); // ul
+        }
+        
+        writer.end(); // details
+        
+        final List<PageClassLoaderContext> children = new 
ArrayList<>(context.getChildren());
+        
+        if (!children.isEmpty())
+        {
+            
children.sort(Comparator.comparing(PageClassLoaderContext::getName));
+            writer.element("ul");
+            for (PageClassLoaderContext child : children) 
+            {
+                writer.element("li");
+                render(child, writer);
+                writer.end(); // li
+            }
+            writer.end(); // ul
+        }
+        
+        writer.end(); //li
+    }
+    
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/ThrowawayClassLoader.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/ThrowawayClassLoader.java
new file mode 100644
index 000000000..62c133210
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/ThrowawayClassLoader.java
@@ -0,0 +1,105 @@
+// Copyright 2024 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.internal;
+
+import static org.junit.Assert.assertNotEquals;
+
+import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
+
+public class ThrowawayClassLoader extends ClassLoader
+{
+
+    final private ClassLoader parent;
+    
+    public ThrowawayClassLoader(ClassLoader parent) 
+    {
+        super(parent);
+        this.parent = parent;
+    }
+    
+    @Override
+    protected Class<?> loadClass(String name, boolean resolve) throws 
ClassNotFoundException 
+    {
+        synchronized (getClassLoadingLock(name)) 
+        
+        {
+            // First, check if the class has already been loaded
+            Class<?> c = findLoadedClass(name);
+            if (c == null) {
+                if (name.contains(".base.") || name.contains(".pages.") ||
+                        name.contains(".components.") || 
name.contains(".mixins."))
+                {
+                    final byte[] bytes = 
PlasticInternalUtils.readBytecodeForClass(parent, name, true);
+                    c = defineClass(name, bytes, 0, bytes.length);
+                    if (resolve) 
+                    {
+                        resolveClass(c);
+                    }
+                }
+                else
+                {
+                    c = parent.loadClass(name);
+                }
+            }
+            return c;
+        }    
+    }
+
+    public static void main(String[] args) throws Exception 
+    {
+        
+        final String className = 
"org.apache.tapestry5.corelib.components.BeanEditor";
+        
+        final ClassLoader parentClassLoader = 
ThrowawayClassLoader.class.getClassLoader();
+        ClassLoader classLoader1 = create(parentClassLoader);
+        ClassLoader classLoader2 = create(parentClassLoader);
+        
+        System.out.println("Parent class loader 1: " + parentClassLoader);
+        System.out.println("Class loader 1       : " + classLoader1);
+        System.out.println("Class loader 2       : " + classLoader2);
+        
+        Class class1 = classLoader1.loadClass(className);
+        Class class2 = classLoader2.loadClass(className);
+        Class class3 = parentClassLoader.loadClass(className);
+
+        System.out.println("Class 1 : " + class1.getClassLoader());
+        System.out.println("Class 2 : " + class2.getClassLoader());
+        System.out.println("Class 3 : " + class3.getClassLoader());        
+        
+        assertNotEquals(class1, class2);
+        assertNotEquals(class1, class3);
+        assertNotEquals(class2, class3);
+        
+    }
+    
+    public static Class<?> load(final String className)
+    {
+        ThrowawayClassLoader loader = new ThrowawayClassLoader(
+                ThrowawayClassLoader.class.getClassLoader());
+        try 
+        {
+            return loader.loadClass(className);
+        } catch (ClassNotFoundException e) 
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static ClassLoader create(final ClassLoader parentClassLoader) {
+//        return 
TapestryInternalUtils.createThrowawayClassloader(parentClassLoader);
+        return new ThrowawayClassLoader(parentClassLoader);
+    }
+    
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java
index 24671c003..ef9cf1017 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java
@@ -59,12 +59,7 @@ public class ComponentDependencyGraphvizGeneratorImpl 
implements ComponentDepend
         for (String className : classNames) 
         {
             createNode(className, nodeMap);
-            for (DependencyType dependencyType : DependencyType.values()) 
-            {
-                addDependencies(className, allClasses, dependencyType, 
nodeMap);
-            }
-            
-
+            addDependencies(className, allClasses, nodeMap);
         }
         
         final List<Node> nodes = new ArrayList<>(nodeMap.values());
@@ -141,16 +136,19 @@ public class ComponentDependencyGraphvizGeneratorImpl 
implements ComponentDepend
         return label.replace('.', '_').replace('/', '_');
     }
 
-    private void addDependencies(String className, Set<String> allClasses, 
DependencyType type, Map<String, Node> nodeMap) 
+    private void addDependencies(String className, Set<String> allClasses, 
Map<String, Node> nodeMap) 
     {
         if (!allClasses.contains(className))
         {
             createNode(className, nodeMap);
-            for (String dependency : 
componentDependencyRegistry.getDependencies(className, type))
+            allClasses.add(className);
+            for (DependencyType type : DependencyType.values()) 
             {
-                addDependencies(dependency, allClasses, type, nodeMap);
+                for (String dependency : 
componentDependencyRegistry.getDependencies(className, type))
+                {
+                    addDependencies(dependency, allClasses, nodeMap);
+                }
             }
-            allClasses.add(className);
         }
     }
 
@@ -188,7 +186,8 @@ public class ComponentDependencyGraphvizGeneratorImpl 
implements ComponentDepend
         }
     }
 
-    private static final class Node {
+    private static final class Node 
+    {
 
         final private String id;
         final private String className;
@@ -204,5 +203,12 @@ public class ComponentDependencyGraphvizGeneratorImpl 
implements ComponentDepend
             this.dependencies.addAll(dependencies);
         }
 
+        @Override
+        public String toString() 
+        {
+            return "Node [id=" + id + ", className=" + className + ", 
dependencies=" + dependencies + ", label=" + label + "]";
+        }
+
     }
+    
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java
index ad79630f4..314c8e513 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java
@@ -67,6 +67,13 @@ public interface ComponentDependencyRegistry {
      */
     void register(Class<?> clasz);
 
+    /**
+     * Register all the dependencies of a given class and uses a given
+     * classloader to load other classes if needed.
+     * @since 5.8.7
+     */
+    void register(Class<?> clasz, ClassLoader classLoader);
+
     /**
      * Register all the dependencies of a given component.
      */
@@ -105,6 +112,12 @@ public interface ComponentDependencyRegistry {
      */
     Set<String> getDependencies(String className, DependencyType type);
     
+    /**
+     * Returns all dependencies of a given class, direct and indirect.
+     * @param className a class name.
+     */
+    Set<String> getAllNonPageDependencies(String className);
+    
     /**
      * Signs up this registry to invalidation events from a given hub.
      */
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java
index 090d71268..fcaec091b 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java
@@ -46,7 +46,9 @@ import org.apache.tapestry5.annotations.Mixins;
 import org.apache.tapestry5.commons.Resource;
 import org.apache.tapestry5.commons.internal.util.TapestryException;
 import org.apache.tapestry5.commons.services.InvalidationEventHub;
+import org.apache.tapestry5.commons.util.UnknownValueException;
 import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.internal.ThrowawayClassLoader;
 import org.apache.tapestry5.internal.parser.ComponentTemplate;
 import org.apache.tapestry5.internal.parser.StartComponentToken;
 import org.apache.tapestry5.internal.parser.TemplateToken;
@@ -83,6 +85,8 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
     
     private static final String META_ATTRIBUTE_SEPARATOR = ",";
     
+    private static final String NO_DEPENDENCY = "NONE";
+    
     // Key is a component, values are the components that depend on it.
     final private Map<String, Set<Dependency>> map;
     
@@ -150,10 +154,14 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
                     {
                         final JSONObject jsonObject = 
jsonArray.getJSONObject(i);
                         final String className = jsonObject.getString("class");
-                        final DependencyType dependencyType = 
DependencyType.valueOf(jsonObject.getString("type"));
-                        final String dependency = 
jsonObject.getString("dependency");
-                        add(className, dependency, dependencyType);
-                        alreadyProcessed.add(dependency);
+                        final String type = jsonObject.getString("type");
+                        if (!type.equals(NO_DEPENDENCY))
+                        {
+                            final DependencyType dependencyType = 
DependencyType.valueOf(type);
+                            final String dependency = 
jsonObject.getString("dependency");
+                            add(className, dependency, dependencyType);
+                            alreadyProcessed.add(dependency);
+                        }
                         alreadyProcessed.add(className);
                     }
                 } catch (IOException e) 
@@ -179,9 +187,15 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
             INVALIDATIONS_DISABLED.set(0);
         });
     }
-    
+
     @Override
     public void register(Class<?> component) 
+    {
+        register(component, component.getClassLoader());
+    }
+    
+    @Override
+    public void register(Class<?> component, ClassLoader classLoader) 
     {
         
         final String className = component.getName();
@@ -189,7 +203,7 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
         Consumer<Class<?>> processClass = furtherDependencies::add;
         Consumer<String> processClassName = s -> {
             try {
-                
furtherDependencies.add(component.getClassLoader().loadClass(s));
+                furtherDependencies.add(classLoader.loadClass(s));
             } catch (ClassNotFoundException e) {
                 throw new RuntimeException(e);
             }
@@ -216,6 +230,7 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
             {
                 final Class<?> dependency = field.getType();
                 add(component, dependency, DependencyType.INJECT_PAGE);
+                processClass.accept(dependency);
             }
             
             // @Component
@@ -245,7 +260,7 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
             if (!alreadyProcessed.contains(dependencyClassName)
                     && 
plasticManager.shouldInterceptClassLoading(dependency.getName()))
             {
-                register(dependency);
+                register(dependency, classLoader);
             }
         }
         
@@ -261,14 +276,14 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
     private void registerTemplate(Class<?> component, Consumer<String> 
processClassName) 
     {
         // TODO: implement caching of template dependency information, probably
-        // by listening separaterly to ComponentTemplateSource to invalidate 
caches
+        // by listening separately to ComponentTemplateSource to invalidate 
caches
         // just when template changes.
         
         final String className = component.getName();
         ComponentModel mock = new ComponentModelMock(component, 
isPage(className));
         final Resource templateResource = 
componentTemplateLocator.locateTemplate(mock, Locale.getDefault());
         String dependency;
-        if (templateResource != null)
+        if (templateResource != null && templateResource.exists())
         {
             final ComponentTemplate template = 
templateParser.parseTemplate(templateResource);
             final List<TemplateToken> tokens = new LinkedList<>();
@@ -287,15 +302,34 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
                     String logicalName = componentToken.getComponentType();
                     if (logicalName != null)
                     {
-                        dependency = 
resolver.resolveComponentTypeToClassName(logicalName);
-                        add(className, dependency, DependencyType.USAGE);
-                        processClassName.accept(dependency);
+                        try
+                        {
+                            dependency = 
resolver.resolveComponentTypeToClassName(logicalName);
+                            add(className, dependency, DependencyType.USAGE);
+                            processClassName.accept(dependency);
+                        }
+                        catch (UnknownValueException e)
+                        {
+                            // Logical name doesn't match an existing 
component. Ignore
+                        }
                     }
                     for (String mixin : 
TapestryInternalUtils.splitAtCommas(componentToken.getMixins()))
                     {
-                        dependency = 
resolver.resolveMixinTypeToClassName(mixin);
-                        add(className, dependency, DependencyType.USAGE);
-                        processClassName.accept(dependency);
+                        try
+                        {
+                            if (mixin.contains("::"))
+                            {
+                                mixin = mixin.substring(0, 
mixin.indexOf("::"));
+                            }
+                            dependency = 
resolver.resolveMixinTypeToClassName(mixin);
+                            add(className, dependency, DependencyType.USAGE);
+                            processClassName.accept(dependency);
+                        }
+                        catch (UnknownValueException e)
+                        {
+                            // Mixin name doesn't match an existing mixin. 
Ignore
+                        }
+
                     }
                 }
             }
@@ -527,15 +561,21 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
     @Override
     public Set<String> getDependents(String className) 
     {
+        
+        ensureClassIsAlreadyProcessed(className);
+        
         final Set<Dependency> dependents = map.get(className);
         return dependents != null 
                 ? dependents.stream().map(d -> 
d.className).collect(Collectors.toSet()) 
                 : Collections.emptySet();
     }
-    
+
     @Override
     public Set<String> getDependencies(String className, DependencyType type) 
     {
+        
+        ensureClassIsAlreadyProcessed(className);
+        
         Set<String> dependencies = Collections.emptySet();
         if (alreadyProcessed.contains(className))
         {
@@ -548,7 +588,30 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
         return dependencies;
     }
 
+    @Override
+    public Set<String> getAllNonPageDependencies(String className) 
+    {
+        final Set<String> dependencies = new HashSet<>();
+        getAllNonPageDependencies(className, dependencies);
+        // Just in case, since it's possible to have circular dependencies.
+        dependencies.remove(className);
+        return Collections.unmodifiableSet(dependencies);
+    }
+
+    private void getAllNonPageDependencies(String className, Set<String> 
dependencies) 
+    {
+        Set<String> theseDependencies = new HashSet<>();
+        theseDependencies.addAll(getDependencies(className, 
DependencyType.USAGE));
+        theseDependencies.addAll(getDependencies(className, 
DependencyType.SUPERCLASS));
+        theseDependencies.removeAll(dependencies);
+        dependencies.addAll(theseDependencies);
+        for (String dependency : theseDependencies) 
+        {
+            getAllNonPageDependencies(dependency, dependencies);
+        }
+    }
 
+    
     private boolean contains(Set<Dependency> dependencies, String className, 
DependencyType type) 
     {
         boolean contains = false;
@@ -667,6 +730,7 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
                 JSONArray jsonArray = new JSONArray();
                 for (String className : classNames)
                 {
+                    boolean hasDependencies = false;
                     for (DependencyType dependencyType : 
DependencyType.values())
                     {
                         final Set<String> dependencies = 
getDependencies(className, dependencyType);
@@ -677,6 +741,20 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
                             object.put("type", dependencyType.name());
                             object.put("dependency", dependency);
                             jsonArray.add(object);
+                            hasDependencies = true;
+                        }
+                    }
+                    // Add a fake dependency so classes without dependencies
+                    // nor classes depending on it are properly stored and 
+                    // retrieved, thus avoiding these classes getting into the 
+                    // unknown page classloader context.
+                    if (!hasDependencies)
+                    {
+                        if (getDependents(className).isEmpty()) {
+                            JSONObject object = new JSONObject();
+                            object.put("class", className);
+                            object.put("type", NO_DEPENDENCY);
+                            jsonArray.add(object);
                         }
                     }
                 }
@@ -742,6 +820,20 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
         }
     }
 
+    private void ensureClassIsAlreadyProcessed(String className) {
+        if (!contains(className))
+        {
+            ThrowawayClassLoader classLoader = new 
ThrowawayClassLoader(getClass().getClassLoader());
+            try 
+            {
+                register(classLoader.loadClass(className));
+            } catch (ClassNotFoundException e) 
+            {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     /**
      * Only really implemented method is {@link 
ComponentModel#getBaseResource()}
      */
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java
index 8a5344c72..4d6b421d6 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java
@@ -356,7 +356,23 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
 
     public Instantiator getInstantiator(final String className)
     {
-        return classToInstantiator.computeIfAbsent(className, 
this::createInstantiatorForClass);
+        Instantiator instantiator;
+        if (multipleClassLoaders)
+        {
+            instantiator = classToInstantiator.get(className);
+
+            if (instantiator == null)
+            {
+                instantiator = createInstantiatorForClass(className);
+                classToInstantiator.put(className, instantiator);
+            }
+
+        }
+        else 
+        {
+            instantiator = classToInstantiator.computeIfAbsent(className, 
this::createInstantiatorForClass);
+        }
+        return instantiator;
     }
     
     private static final ThreadLocal<Set<String>> OPEN_INSTANTIATORS = 
@@ -376,6 +392,34 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
                         OPEN_INSTANTIATORS.get().add(className);
                         
                         componentDependencyRegistry.disableInvalidations();
+                        
+                        // Make sure the dependencies have been processed in 
case
+                        // there was some invalidation going on and they're 
not there.
+                        
+                        if (multipleClassLoaders)
+                        {
+
+                            final Set<String> dependencies = new HashSet<>();
+                            dependencies.addAll(
+                                    
componentDependencyRegistry.getDependencies(className, DependencyType.USAGE));
+                            dependencies.addAll(
+                                    
componentDependencyRegistry.getDependencies(className, 
DependencyType.SUPERCLASS));
+                            for (String dependency : dependencies)
+                            {
+                                if 
(!OPEN_INSTANTIATORS.get().contains(dependency))
+                                {
+                                    if (multipleClassLoaders)
+                                    {
+                                        getInstantiator(dependency);
+                                    }
+                                    else
+                                    {
+                                        createInstantiatorForClass(dependency);
+                                    }
+                                }
+                            }
+                        }
+                            
                         PageClassLoaderContext context;
                         try
                         {
@@ -386,20 +430,18 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
                             componentDependencyRegistry.enableInvalidations();
                         }
                         
-                        // Make sure the dependencies have been processed in 
case
-                        // there was some invalidation going on and they're 
not there.
-
-                        // TODO: maybe we need superclasses here too?
-                        final Set<String> dependencies = 
componentDependencyRegistry.getDependencies(className, DependencyType.USAGE);
-                        for (String dependency : dependencies)
+                        ClassInstantiator<Component> plasticInstantiator;
+                        try 
                         {
-                            if (!OPEN_INSTANTIATORS.get().contains(dependency))
+                            plasticInstantiator = 
context.getPlasticManager().getClassInstantiator(className);
+                            if (multipleClassLoaders)
                             {
-                                createInstantiatorForClass(dependency);
+                                
context.getPlasticManager().getClassLoader().loadClass(className);
                             }
+                        } catch (Exception e) {
+//                            
System.out.println(pageClassLoaderContextManager.getRoot().toRecursiveString());
+                            throw new RuntimeException(e);
                         }
-                        
-                        ClassInstantiator<Component> plasticInstantiator = 
context.getPlasticManager().getClassInstantiator(className);
                         final ComponentModel model = 
classToModel.get(className);
                         
                         OPEN_INSTANTIATORS.get().remove(className);
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
index 9c01847b3..dcbfbc47e 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java
@@ -14,6 +14,11 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.util.Set;
+
+import org.apache.tapestry5.SymbolConstants;
+import 
org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
+import org.apache.tapestry5.ioc.annotations.Symbol;
 import org.apache.tapestry5.model.ComponentModel;
 import org.apache.tapestry5.services.ComponentClassResolver;
 
@@ -22,15 +27,55 @@ public class ComponentModelSourceImpl implements 
ComponentModelSource
     private final ComponentClassResolver resolver;
 
     private final ComponentInstantiatorSource source;
+    
+    private final ComponentDependencyRegistry componentDependencyRegistry;
+    
+    private final PageSource pageSource;
+    
+    private final boolean multipleClassLoaders;
 
-    public ComponentModelSourceImpl(ComponentClassResolver resolver, 
ComponentInstantiatorSource source)
+    public ComponentModelSourceImpl(ComponentClassResolver resolver, 
ComponentInstantiatorSource source,
+            ComponentDependencyRegistry componentDependencyRegistry,
+            PageSource pageSource,
+            @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
+            @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean 
multipleClassLoaders)
     {
         this.resolver = resolver;
         this.source = source;
+        this.componentDependencyRegistry = componentDependencyRegistry;
+        this.pageSource = pageSource;
+        this.multipleClassLoaders = !productionMode && multipleClassLoaders;
     }
 
     public ComponentModel getModel(String componentClassName)
     {
+        if (multipleClassLoaders && isPage(componentClassName))
+        {
+            
+            final Set<String> superclasses = 
componentDependencyRegistry.getDependencies(
+                    componentClassName, DependencyType.SUPERCLASS);
+            
+            if (!superclasses.isEmpty())
+            {
+                final String superclass = superclasses.iterator().next();
+                if (isPage(superclass))
+                {
+                    getModel(superclass);
+                    try
+                    {
+                        
pageSource.getPage(resolver.getLogicalName(componentClassName));
+                    }
+                    catch (IllegalStateException e)
+                    {
+                        // This can be thrown in PageSourceImpl in case an
+                        // infinite method call recursion is detected. In
+                        // that case, the page instance is already created,
+                        // so the objective of the line above is already
+                        // fulfilled and we can safely ignore the exception
+                    }
+                }
+            }
+        }
         return source.getInstantiator(componentClassName).getModel();
     }
 
@@ -38,4 +83,9 @@ public class ComponentModelSourceImpl implements 
ComponentModelSource
     {
         return getModel(resolver.resolvePageNameToClassName(pageName));
     }
+    
+    private boolean isPage(String componentClassName)
+    {
+        return componentClassName.contains(".pages.");
+    }
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
index 6c67fe802..38ab835fd 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
@@ -1,4 +1,4 @@
-// Copyright 2010, 2011, 2012 The Apache Software Foundation
+// Copyright 2010, 2011, 2012, 2023, 2024 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
 package org.apache.tapestry5.internal.services;
 
 import java.lang.ref.SoftReference;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -29,6 +30,7 @@ import 
org.apache.tapestry5.commons.services.InvalidationEventHub;
 import org.apache.tapestry5.commons.util.CollectionFactory;
 import org.apache.tapestry5.func.F;
 import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.internal.ThrowawayClassLoader;
 import 
org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
 import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 import org.apache.tapestry5.internal.structure.ComponentPageElement;
@@ -106,6 +108,11 @@ public class PageSourceImpl implements PageSource
     }
 
     private final Map<CachedPageKey, Object> pageCache = 
CollectionFactory.newConcurrentMap();
+    
+    private final Map<String, Boolean> abstractClassInfoCache = 
CollectionFactory.newConcurrentMap();
+    
+    private final static ThreadLocal<String> CURRENT_PAGE = 
+            ThreadLocal.withInitial(() -> null);
 
     public PageSourceImpl(PageLoader pageLoader, 
ComponentRequestSelectorAnalyzer selectorAnalyzer,
             ComponentDependencyRegistry componentDependencyRegistry,
@@ -174,19 +181,39 @@ public class PageSourceImpl implements PageSource
             final String className = 
componentClassResolver.resolvePageNameToClassName(canonicalPageName);
             if (multipleClassLoaders)
             {
+                
+                if (canonicalPageName.equals(CURRENT_PAGE.get()))
+                {
+                    throw new IllegalStateException("Infinite method loop 
detected. Bailing out.");
+                }
+                else
+                {
+                    CURRENT_PAGE.set(canonicalPageName);
+                }
             
                 // Avoiding problems in PlasticClassPool.createTransformation()
                 // when the class being loaded has a page superclass
-                final List<String> pageDependencies = 
preprocessPageDependencies(className);
+                final List<String> pageDependencies = 
getPageDependencies(className);
                 
-                for (String pageClassName : pageDependencies)
+                for (String dependencyClassName : pageDependencies)
                 {
                     // Avoiding infinite recursion caused by circular 
dependencies
-                    if (!alreadyProcessed.contains(pageClassName))
+                    if (!alreadyProcessed.contains(dependencyClassName))
                     {
-                        alreadyProcessed.add(pageClassName);
-                        page = 
getPage(componentClassResolver.resolvePageClassNameToPageName(pageClassName), 
-                                invalidateUnknownContext, alreadyProcessed);
+                        alreadyProcessed.add(dependencyClassName);
+                        
+                        // Avoiding infinite recursion when, through component 
overriding,
+                        // a dependency resolves to the same canonical page 
name as the
+                        // one already requested in this call.
+                        final String dependencyPageName = 
componentClassResolver.resolvePageClassNameToPageName(dependencyClassName);
+                        final String resolvedDependencyPageClass = 
componentClassResolver.resolvePageNameToClassName(dependencyPageName);
+                        if (!canonicalPageName.equals(dependencyPageName)
+                                && 
!className.equals(resolvedDependencyPageClass)
+                                && !isAbstract(dependencyClassName))
+                        {
+                            page = getPage(dependencyPageName, 
+                                    invalidateUnknownContext, 
alreadyProcessed);
+                        }
                     }
                 }
                 
@@ -221,7 +248,7 @@ public class PageSourceImpl implements PageSource
                     if (invalidateUnknownContext)
                     {
                         
pageClassLoaderContextManager.invalidateAndFireInvalidationEvents(context);
-                        preprocessPageDependencies(className);
+                        getPageDependencies(className);
                     }
                     context.getClassNames().clear();
                     // Avoiding bad invalidations
@@ -234,7 +261,7 @@ public class PageSourceImpl implements PageSource
         
     }
 
-    private List<String> preprocessPageDependencies(final String className) {
+    private List<String> getPageDependencies(final String className) {
         final List<String> pageDependencies = new ArrayList<>();
         pageDependencies.addAll(
                 new 
ArrayList<String>(componentDependencyRegistry.getDependencies(className, 
DependencyType.INJECT_PAGE)));
@@ -244,48 +271,16 @@ public class PageSourceImpl implements PageSource
         final Iterator<String> iterator = pageDependencies.iterator();
         while (iterator.hasNext())
         {
-            if (!iterator.next().contains(".pages."))
+            final String dependency = iterator.next();
+            if (!dependency.contains(".pages.") && 
!dependency.equals(className))
             {
                 iterator.remove();
             }
         }
         
-        preprocessPageClassLoaderContexts(className, pageDependencies);
         return pageDependencies;
     }
 
-    private void preprocessPageClassLoaderContexts(String className, final 
List<String> pageDependencies) {
-        for (int i = 0; i < 5; i++)
-        {
-            pageClassLoaderContextManager.get(className);
-            for (String pageClassName : pageDependencies)
-            {
-                final PageClassLoaderContext context = 
pageClassLoaderContextManager.get(pageClassName);
-                if (i == 1)
-                {
-                    try 
-                    {
-                        context.getClassLoader().loadClass(pageClassName);
-                    } catch (ClassNotFoundException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-            }
-        }
-        
-        // TODO: remove
-//        for (String pageClassName : pageDependencies)
-//        {
-//            try 
-//            {
-//                
pageClassLoaderContextManager.get(pageClassName).getClassLoader().loadClass(pageClassName);
-//            } catch (ClassNotFoundException e) 
-//            {
-//                throw new RuntimeException(e);
-//            }
-//        }
-    }
-
     @PostInjection
     public void setupInvalidation(@ComponentClasses InvalidationEventHub 
classesHub,
                                   @ComponentTemplates InvalidationEventHub 
templatesHub,
@@ -332,6 +327,7 @@ public class PageSourceImpl implements PageSource
                             iterator.remove();
                         }
                     }
+                    abstractClassInfoCache.remove(className);
                 }
             }
         }
@@ -372,4 +368,11 @@ public class PageSourceImpl implements PageSource
         return page;
     }
     
+    private boolean isAbstract(final String className)
+    {
+        final Boolean computeIfAbsent = 
abstractClassInfoCache.computeIfAbsent(className, 
+                (s) -> 
Modifier.isAbstract(ThrowawayClassLoader.load(className).getModifiers()));
+        return computeIfAbsent;
+    }
+    
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java
index c4985b510..c2938bf1e 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java
@@ -14,12 +14,14 @@
 
 package org.apache.tapestry5.modules;
 
+import org.apache.tapestry5.SymbolConstants;
 import org.apache.tapestry5.commons.OrderedConfiguration;
 import 
org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGenerator;
 import 
org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGeneratorImpl;
 import org.apache.tapestry5.internal.services.dashboard.DashboardManagerImpl;
 import org.apache.tapestry5.ioc.ServiceBinder;
 import org.apache.tapestry5.ioc.annotations.Contribute;
+import org.apache.tapestry5.ioc.annotations.Symbol;
 import org.apache.tapestry5.services.dashboard.DashboardManager;
 import org.apache.tapestry5.services.dashboard.DashboardTab;
 
@@ -32,11 +34,17 @@ public class DashboardModule
     }
 
     @Contribute(DashboardManager.class)
-    public static void defaultTabs(OrderedConfiguration<DashboardTab> 
configuration)
+    public static void defaultTabs(OrderedConfiguration<DashboardTab> 
configuration,
+            @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, 
+            @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean 
multipleClassLoaders)
     {
         configuration.add("Pages", new DashboardTab("Pages", 
"core/PageCatalog"));
         configuration.add("Services", new DashboardTab("Services", 
"core/ServiceStatus"));
         configuration.add("Libraries", new DashboardTab("ComponentLibraries", 
"core/ComponentLibraries"));
         configuration.add("PageDependencyGraph", new 
DashboardTab("PageDependencyGraph", "core/PageDependencyGraph"));
+        if (!productionMode && multipleClassLoaders)
+        {
+            configuration.add("PageClassLoaderContexts", new 
DashboardTab("PageClassLoaderContexts", "core/PageClassLoaderContexts"));
+        }
     }
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java
index ae6e76a77..d97fc677a 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java
@@ -47,6 +47,7 @@ import org.apache.tapestry5.services.pageload.PagePreloader;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
 import org.apache.tapestry5.services.pageload.ReferenceType;
 import org.apache.tapestry5.services.templates.ComponentTemplateLocator;
+import org.slf4j.LoggerFactory;
 
 /**
  * @since 5.3
@@ -97,26 +98,23 @@ public class PageLoadModule
     {
         if (!productionMode && multipleClassLoaders)
         {
-            // Preload the page activation context tree for the already known 
classes
-            for (int i = 0; i < 5; i++)
+            // If we have component dependency information previously stored 
in 
+            // a file, then we just preload the page classloader contexts.
+            // Otherwise, we gather component dependency information then
+            // preload the page classloader contexts.
+            if 
(componentDependencyRegistry.isStoredDependencyInformationPresent())
             {
-                for (String className : 
componentDependencyRegistry.getClassNames()) 
-                {
-                    pageClassLoaderContextManager.get(className);
-                }
+                pageClassLoaderContextManager.preloadContexts();
+            }
+            else 
+            {
+                LoggerFactory.getLogger(PageClassLoaderContextManager.class)
+                    .warn("If the component dependency process is taking too 
long, "
+                            + "consider writing its results to a file using 
the "
+                            + " 'Store dependency information' button "
+                            + "in the /t5dashboard/pages page.");
+                pageClassLoaderContextManager.preload();
             }
-        }
-        // Preload the dependency information for all pages 
-        // when in production mode. Without that, exceptions during
-        // page assembly will occurr. This should add just a few
-        // seconds to page initialization. If it takes too long,
-        // we can create a version of preload() that accepts a boolean
-        // parameter defining whether templates should be parsed or not
-        // (the exception occurrs when a superclass isn't loaded
-        // and transformed before a subclass)
-        else if (productionMode)
-        {
-            pageClassLoaderContextManager.preload();
         }
     }
     
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java
index 8ae71f5f4..8d914be9c 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java
@@ -22,7 +22,6 @@ import java.util.function.Function;
 
 import org.apache.tapestry5.commons.services.PlasticProxyFactory;
 import org.apache.tapestry5.internal.plastic.PlasticClassLoader;
-import org.apache.tapestry5.internal.plastic.PlasticClassPool;
 import org.apache.tapestry5.plastic.PlasticManager;
 import org.apache.tapestry5.plastic.PlasticUtils;
 import org.slf4j.Logger;
@@ -316,35 +315,70 @@ public class PageClassLoaderContext
         PageClassLoaderContext other = (PageClassLoaderContext) obj;
         return Objects.equals(name, other.name);
     }
-
+    
     @Override
     public String toString() 
     {
+        return toString(true);
+    }
+
+    public String toString(boolean includeClassNames) 
+    {
+        final PlasticClassLoader classLoader = (PlasticClassLoader) 
proxyFactory.getClassLoader();
         return "PageClassloaderContext [name=" + name + 
                 ", parent=" + (parent != null ? parent.getName() : "null" ) + 
-                ", classLoader=" + 
afterAt(proxyFactory.getClassLoader().toString()) +
-                (isRoot() ? ""  : ", classNames=" + classNames) + 
+                ", classLoader=" + afterAt(classLoader.getClassLoaderId()) +
+                (isRoot() || !includeClassNames ? ""  : ", classNames=" + 
classNames) + 
                 "]";
     }
     
     public String toRecursiveString()
+    {
+        return toRecursiveString(true);
+    }
+    
+    public String toRecursiveString(boolean outputClasses)
     {
         StringBuilder builder = new StringBuilder();
-        toRecursiveString(builder, "");
+        toRecursiveString(builder, "", outputClasses);
         return builder.toString();
     }
     
-    private void toRecursiveString(StringBuilder builder, String tabs)
+    public final boolean isEqualOrAncestor(PageClassLoaderContext 
dependencyContext) 
+    {
+        boolean equalOrAncestor = this.equals(dependencyContext);
+        if (!equalOrAncestor) 
+        {
+            PageClassLoaderContext parent = this.getParent();
+            while (parent != null && !equalOrAncestor) 
+            {
+                equalOrAncestor = parent.equals(dependencyContext);
+                if (equalOrAncestor) 
+                {
+                    break;
+                }
+                else 
+                {
+                    parent = parent.getParent();
+                }
+            }
+        }
+        return equalOrAncestor;
+    }
+    
+    private void toRecursiveString(StringBuilder builder, String tabs, boolean 
outputClasses)
     {
         builder.append(tabs);
         builder.append(name);
         builder.append(" : ");
         builder.append(afterAt(proxyFactory.getClassLoader().toString()));
-        builder.append(" : ");
-        builder.append(classNames);
+        if (outputClasses) {
+            builder.append(" : ");
+            builder.append(classNames);
+        }
         builder.append("\n");
         for (PageClassLoaderContext child : children) {
-            child.toRecursiveString(builder, tabs + "\t");
+            child.toRecursiveString(builder, tabs + "\t", outputClasses);
         }
     }
 
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java
index b37f0a0fc..fa91b70f3 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java
@@ -95,5 +95,11 @@ public interface PageClassLoaderContextManager
      * page classloader contexts.
      */
     void preload();
+    
+    /**
+     * Preloads the page classloader contexts.
+     * @since 5.8.7
+     */
+    void preloadContexts();
 
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java
index afe8a6746..6b83ad044 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java
@@ -1,4 +1,4 @@
-// Copyright 2023 The Apache Software Foundation
+// Copyright 2023, 2024 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@ package org.apache.tapestry5.services.pageload;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -28,6 +29,7 @@ import org.apache.tapestry5.SymbolConstants;
 import org.apache.tapestry5.commons.internal.util.TapestryException;
 import org.apache.tapestry5.commons.services.InvalidationEventHub;
 import org.apache.tapestry5.commons.services.PlasticProxyFactory;
+import org.apache.tapestry5.internal.ThrowawayClassLoader;
 import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
 import 
org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
 import 
org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub;
@@ -66,10 +68,14 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
     
     private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1);
     
+    private final static ThreadLocal<AtomicInteger> CONTEXTS_CREATED = 
ThreadLocal.withInitial(AtomicInteger::new);
+    
     private Function<ClassLoader, PlasticProxyFactory> 
plasticProxyFactoryProvider;
     
     private PageClassLoaderContext root;
     
+    private boolean preloadingContexts;
+
     public PageClassLoaderContextManagerImpl(
             final ComponentDependencyRegistry componentDependencyRegistry, 
             final ComponentClassResolver componentClassResolver,
@@ -83,7 +89,7 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
         this.componentClassResolver = componentClassResolver;
         this.invalidationHub = invalidationHub;
         this.componentClassesInvalidationEventHub = 
componentClassesInvalidationEventHub;
-        this.multipleClassLoaders = multipleClassLoaders;
+        this.multipleClassLoaders = multipleClassLoaders && !productionMode;
         this.productionMode = productionMode;
         invalidationHub.addInvalidationCallback(this::listen);
         NESTED_MERGE_COUNT.set(0);
@@ -126,28 +132,44 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
     {
         PageClassLoaderContext context;
         
-        final String enclosingClassName = getAdjustedClassName(className);
-        context = root.findByClassName(enclosingClassName);
-        
-        if (context == null)
+        // Class isn't in a controlled package, so it doesn't get transformed
+        // and should go for the root context, which is never thrown out.
+        if (!root.getPlasticManager().shouldInterceptClassLoading(className))
+        {
+            context = root;
+        }
+        else if (productionMode || !multipleClassLoaders)
+        {
+            context = getUnknownContext(root, plasticProxyFactoryProvider);
+        }
+        else
         {
-            Set<String> classesToInvalidate = new HashSet<>();
             
-            context = processUsingDependencies(
-                    enclosingClassName, 
-                    root, 
-                    () -> getUnknownContext(root, plasticProxyFactoryProvider),
-                    plasticProxyFactoryProvider,
-                    classesToInvalidate);
+            // Multiple classloader mode.
+            final String enclosingClassName = getAdjustedClassName(className);
+            context = root.findByClassName(enclosingClassName);
             
-            if (!classesToInvalidate.isEmpty())
+            if (context == null)
             {
-                invalidate(classesToInvalidate);
-            }
-
-            if (!className.equals(enclosingClassName))
-            {
-                loadClass(className, context);
+                Set<String> classesToInvalidate = new HashSet<>();
+                
+                context = processUsingDependencies(
+                        enclosingClassName, 
+                        root, 
+                        () -> getUnknownContext(root, 
plasticProxyFactoryProvider),
+                        plasticProxyFactoryProvider,
+                        classesToInvalidate);
+                
+                if (!classesToInvalidate.isEmpty())
+                {
+                    invalidate(classesToInvalidate);
+                }
+    
+                if (!className.equals(enclosingClassName))
+                {
+                    loadClass(enclosingClassName, context);
+                }
+                
             }
             
         }
@@ -228,125 +250,128 @@ public class PageClassLoaderContextManagerImpl 
implements PageClassLoaderContext
         {
             
             LOGGER.debug("Processing class {}", className);
+            alreadyProcessed.add(className);
+            
+            // Sorting dependencies by type/alphabetically so we have 
consistent 
+            // context trees between runs of the same webapp
+            List<String> allNonPageDependencies = new ArrayList<>(
+                    
componentDependencyRegistry.getAllNonPageDependencies(className));
+            Collections.sort(allNonPageDependencies, 
ClassNameComparator.INSTANCE);
             
-            // Class isn't in a controlled package, so it doesn't get 
transformed
-            // and should go for the root context, which is never thrown out.
-            if 
(!root.getPlasticManager().shouldInterceptClassLoading(className))
+            List<String> dependencies = new 
ArrayList<>(getDependenciesWithoutPages(className));
+            Collections.sort(dependencies, ClassNameComparator.INSTANCE);
+            
+            // Process dependencies depth-first
+            do
             {
-                context = root;
-            } else {
-                if (!productionMode && (
-                        !componentDependencyRegistry.contains(className) ||
-                        !multipleClassLoaders))
-                {
-                    context = unknownContextProvider.get();
-                }
-                else 
+                
+                // Very unlikely to have infinite loops, but lets
+                // avoid them anyway.
+                int passes = 0;
+                
+                int contextsCreatedInThisPass = -1;
+                
+                while (contextsCreatedInThisPass < 
CONTEXTS_CREATED.get().get() && passes < 1000)
                 {
-
-                    alreadyProcessed.add(className);
-                    
-                    // Sorting dependencies alphabetically so we have 
consistent results.
-                    List<String> dependencies = new 
ArrayList<>(getDependenciesWithoutPages(className));
-                    Collections.sort(dependencies);
                     
-                    // Process dependencies depth-first
-                    for (String dependency : dependencies)
+                    contextsCreatedInThisPass = CONTEXTS_CREATED.get().get();
+            
+                    for (String dependency : allNonPageDependencies)
                     {
                         // Avoid infinite recursion loops
-                        if (!alreadyProcessed.contains(dependency))
+                        if (!alreadyProcessed.contains(dependency)  
+                                || root.findByClassName(dependency) == null)
                         {
                             processUsingDependencies(dependency, root, 
unknownContextProvider, 
                                     plasticProxyFactoryProvider, 
classesToInvalidate, alreadyProcessed, false);
                         }
                     }
                     
-                    // Collect context dependencies
-                    Set<PageClassLoaderContext> contextDependencies = new 
HashSet<>();
-                    for (String dependency : dependencies) 
-                    {
-                        PageClassLoaderContext dependencyContext = 
root.findByClassName(dependency);
-                        // Avoid infinite recursion loops
-                        if (!alreadyProcessed.contains(dependency))
-                        {
-                            if (dependencyContext == null)
-                            {
-                                dependencyContext = 
processUsingDependencies(dependency, root, unknownContextProvider,
-                                        plasticProxyFactoryProvider, 
classesToInvalidate, alreadyProcessed);
-    
-                            }
-                            if (!dependencyContext.isRoot())
-                            {
-                                contextDependencies.add(dependencyContext);
-                            }
-                        }
-                    }
-                    
-                    if (!multipleClassLoaders)
-                    {
-                        context = root;
-                    }
-                    else if (contextDependencies.size() == 0)
-                    {
-                        context = new PageClassLoaderContext(
-                                getContextName(className), 
-                                root, 
-                                Collections.singleton(className), 
-                                
plasticProxyFactoryProvider.apply(root.getClassLoader()),
-                                this::get);
-                    }
-                    else 
+                }
+                
+            }
+            while (!allNeededContextsAvailable(allNonPageDependencies));
+            
+            // Collect context dependencies
+            Set<PageClassLoaderContext> contextDependencies = new HashSet<>();
+            for (String dependency : allNonPageDependencies) 
+            {
+                PageClassLoaderContext dependencyContext = 
root.findByClassName(dependency);
+                // Avoid infinite recursion loops
+                if (multipleClassLoaders || 
!alreadyProcessed.contains(dependency))
+                {
+                    if (dependencyContext == null)
                     {
-                        PageClassLoaderContext parentContext;
-                        if (contextDependencies.size() == 1)
-                        {
-                            parentContext = 
contextDependencies.iterator().next();
-                        }
-                        else
-                        {
-                            parentContext = merge(contextDependencies, 
plasticProxyFactoryProvider, root, classesToInvalidate);
-                        }
-                        context = new PageClassLoaderContext(
-                                getContextName(className), 
-                                parentContext, 
-                                Collections.singleton(className), 
-                                
plasticProxyFactoryProvider.apply(parentContext.getClassLoader()),
-                                this::get);
-                    }
+                        dependencyContext = 
processUsingDependencies(dependency, root, unknownContextProvider,
+                                plasticProxyFactoryProvider, 
classesToInvalidate, alreadyProcessed);
 
-                    if (multipleClassLoaders)
-                    {
-                        context.getParent().addChild(context);
                     }
-                    
-                    // Ensure non-page class is initialized in the correct 
context and classloader.
-                    // Pages get their own context and classloader, so this 
initialization
-                    // is both non-needed and a cause for an NPE if it happens.
-                    if (!componentClassResolver.isPage(className)
-                            || 
componentDependencyRegistry.getDependencies(className, 
DependencyType.USAGE).isEmpty())
+                    if (!dependencyContext.isRoot())
                     {
-                        loadClass(className, context);
+                        contextDependencies.add(dependencyContext);
                     }
-
-                    if (multipleClassLoaders)
-                    {
-                        LOGGER.debug("New context: {}", context);
-                    }
-                    
                 }
             }
             
+            if (contextDependencies.size() == 0)
+            {
+                context = new PageClassLoaderContext(
+                        getContextName(className), 
+                        root, 
+                        Collections.singleton(className), 
+                        
plasticProxyFactoryProvider.apply(root.getClassLoader()),
+                        this::get);
+                CONTEXTS_CREATED.get().incrementAndGet();
+            }
+            else 
+            {
+                PageClassLoaderContext parentContext;
+                if (contextDependencies.size() == 1)
+                {
+                    parentContext = contextDependencies.iterator().next();
+                }
+                else
+                {
+                    parentContext = merge(contextDependencies, 
plasticProxyFactoryProvider, root, classesToInvalidate);
+                }
+                context = new PageClassLoaderContext(
+                        getContextName(className), 
+                        parentContext, 
+                        Collections.singleton(className), 
+                        
plasticProxyFactoryProvider.apply(parentContext.getClassLoader()),
+                        this::get);
+                CONTEXTS_CREATED.get().incrementAndGet();
+            }
+
+            LOGGER.debug("New context: {}", context);
+            
         }
         context.addClass(className);
+        context.getParent().addChild(context);
         
         return context;
     }
 
+    private boolean allNeededContextsAvailable(List<String> dependencies) 
+    {
+        boolean available = true;
+        for (String dependency : dependencies)
+        {
+            if (root.findByClassName(dependency) == null)
+            {
+                available = false;
+                break;
+            }
+        }
+        return available;
+    }
+
     private Set<String> getDependenciesWithoutPages(String className) 
     {
         Set<String> dependencies = new HashSet<>();
         
dependencies.addAll(componentDependencyRegistry.getDependencies(className, 
DependencyType.USAGE));
         
dependencies.addAll(componentDependencyRegistry.getDependencies(className, 
DependencyType.SUPERCLASS));
+        dependencies.remove(className); // Just in case
         return Collections.unmodifiableSet(dependencies);
     }
 
@@ -387,12 +412,19 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
             }
             LOGGER.debug(builder.toString().trim());
         }
-        
+
+        final Set<String> classesToReprocess = multipleClassLoaders ? new 
HashSet<>() : Collections.emptySet();
         Set<PageClassLoaderContext> allContextsIncludingDescendents = new 
HashSet<>();
+        
         for (PageClassLoaderContext context : contextDependencies) 
         {
+            final Set<PageClassLoaderContext> descendents = 
context.getDescendents();
             allContextsIncludingDescendents.add(context);
-            allContextsIncludingDescendents.addAll(context.getDescendents());
+            allContextsIncludingDescendents.addAll(descendents);
+            for (PageClassLoaderContext descendent : descendents) 
+            {
+                addClassNames(descendent, classesToReprocess);
+            }
         }
 
         PageClassLoaderContext merged;
@@ -458,8 +490,22 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
             plasticProxyFactoryProvider.apply(parent.getClassLoader()),
             this::get);
         
+        CONTEXTS_CREATED.get().incrementAndGet();
+        
         parent.addChild(merged);
         
+        // Recreating contexts for classes that got invalidated but
+        // aren't part of the new merged context (i.e. the classes
+        // in contexts are are descendent of the merged contexts).
+//        if (!classesToReprocess.isEmpty())
+//        {
+//            final List<String> sorted = new ArrayList<>(classesToReprocess);
+//            for (String className : sorted) 
+//            {
+//                get(className);
+//            }
+//        }
+        
 //        for (String className : classNames) 
 //        {
 //            loadClass(className, merged);
@@ -578,7 +624,7 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
     }
     
     private void invalidate(Set<String> classesToInvalidate) {
-        if (!classesToInvalidate.isEmpty())
+        if (!classesToInvalidate.isEmpty() && !preloadingContexts)
         {
             LOGGER.debug("Invalidating classes {}", classesToInvalidate);
             markAsInvalidatingContext();
@@ -647,10 +693,7 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
     public void preload() 
     {
         
-        final PageClassLoaderContext context = new 
PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, 
-                Collections.emptySet(), 
-                plasticProxyFactoryProvider.apply(root.getClassLoader()),
-                this::get);
+        final ClassLoader classLoader = new 
ThrowawayClassLoader(PageClassLoaderContext.class.getClassLoader());
         
         final List<String> pageNames = componentClassResolver.getPageNames();
         final List<String> classNames = new ArrayList<>(pageNames.size());
@@ -664,7 +707,7 @@ public class PageClassLoaderContextManagerImpl implements 
PageClassLoaderContext
             try 
             {
                 final String className = 
componentClassResolver.resolvePageNameToClassName(page);
-                
componentDependencyRegistry.register(context.getClassLoader().loadClass(className));
+                
componentDependencyRegistry.register(classLoader.loadClass(className));
                 classNames.add(className);
             } catch (ClassNotFoundException e) 
             {
@@ -680,30 +723,101 @@ public class PageClassLoaderContextManagerImpl 
implements PageClassLoaderContext
         
         if (LOGGER.isInfoEnabled())
         {
-            LOGGER.info(String.format("Dependency information gathered in %.3f 
ms", (finish - start) / 1000.0));
+            LOGGER.info(String.format("Dependency information for %d pages 
gathered in %.3f s", 
+                    pageNames.size(),  (finish - start) / 1000.0));
         }
         
-        context.invalidate();
+        preloadContexts();
         
+    }
+    
+    @Override
+    public void preloadContexts() 
+    {
+        long start;
+        long finish;
         LOGGER.info("Starting preloading page classloader contexts");
         
         start = System.currentTimeMillis();
         
-        for (int i = 0; i < 10; i++)
+        final List<String> classNames = new 
ArrayList<>(componentDependencyRegistry.getClassNames());
+        classNames.sort(ClassNameComparator.INSTANCE);
+        
+        int runs = 0;
+        preloadingContexts = true;
+        
+        try 
         {
-            for (String className : classNames) 
+            // The run counter check is to just avoid possible infinite loops,
+            // although that's very unlikely.
+            int contexts = -1;
+            while (runs < 5000 && contexts < CONTEXTS_CREATED.get().get())
             {
-                get(className);
+                runs++;
+                contexts = CONTEXTS_CREATED.get().get();
+                for (String className : classNames) 
+                {
+                    get(className);
+                }
             }
         }
+        finally
+        {
+            preloadingContexts = false;
+        }
         
         finish = System.currentTimeMillis();
 
         if (LOGGER.isInfoEnabled())
         {
-            LOGGER.info(String.format("Preloading of page classloadercontexts 
finished in %.3f ms", (finish - start) / 1000.0));
+            LOGGER.info(String.format("Preloading of page classloader contexts 
finished in %.3f s (%d passes)", (finish - start) / 1000.0, runs));
         }
+    }
+    
+    /**
+     * Sorts base classes before mixins, mixins before components and 
components
+     * before pages. If both classes belong to the same type, order 
alphabetically.
+     */
+    private static final class ClassNameComparator implements 
Comparator<String> 
+    {
+        
+        private static final Comparator<String> INSTANCE = new 
ClassNameComparator();
 
+        @Override
+        public int compare(String o1, String o2) 
+        {
+            int value1 = getValue(o1);
+            int value2 = getValue(o2);
+            int comparison = value1 - value2;
+            if (comparison == 0)
+            {
+                comparison = o1.compareTo(o2);
+            }
+            return comparison;
+        }
+        
+        private int getValue(String className)
+        {
+            int value;
+            if (className.contains(".base."))
+            {
+                value = 0;
+            }
+            else if (className.contains(".mixins."))
+            {
+                value = 1;
+            }
+            else if (className.contains(".components."))
+            {
+                value = 2;
+            }
+            else
+            {
+                value = 3;
+            }
+            return value;
+        }
+        
     }
     
 }
diff --git 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml
 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml
index e5323cb5a..a43b79659 100644
--- 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml
+++ 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml
@@ -3,125 +3,180 @@
 
     <h2 t:type="If" t:test="productionMode">This page is disabled in 
production mode</h2>
     
-    <t:if t:test="!productionMode">
-           <h1><strong>${libraryMappings.size()}</strong> component libraries 
used</h1>
-           
-           
-           <ul id="libraryList" class="list-group">
-               <li t:type="Loop" t:source="libraryMappings" 
t:value="libraryMapping" class="list-group-item">
-                       <a href="#${libraryClientId}">
-                               <code>${libraryMapping.libraryName}</code> 
<t:if test="info">: ${info.name}</t:if>
-                       </a>
-                       <p t:type="If" t:test="info?.description">
-                               ${info.description}
-                       </p>
-                       <p class="tags" t:type="If" t:test="!info?.tags.empty">
-                               Tags: 
-                                       <span t:type="Loop" 
t:source="info.tags" t:value="var:tag" class="badge" 
-                                               style="margin-right: 0.3em; 
font-size: 0.75em">
-                                               ${var:tag}
-                                       </span>
-                               </p>
-               </li>
-           </ul>
-           
-       
-               <div id="libraries">
-               
-                       <div class="libraryInfo" t:type="Loop" 
t:source="libraryMappings" t:value="libraryMapping" id="${libraryClientId}">
-               
-                               <h2><code>${libraryMapping.libraryName}</code> 
<t:if test="info">: ${info.name}</t:if></h2>
-                               
-                               <t:if test="info">
+    <t:If t:test="!productionMode">
+               <t:zone>
                        
-                                   <dl class="dl-horizontal">
-                                   
-                                       <dt>Homepage</dt>
-                                       <dd class="homepage">
-                                               <t:if test="info.homepageUrl" 
else="message:not-informed">
-                                                       <a 
href="${info.homepageUrl}">${info.homepageUrl}</a>
-                                               </t:if>
-                                       </dd>
-                                       
-                                       <dt>Version</dt>
-                                       <dd class="version">
-                                               <t:if test="info.version" 
else="message:not-informed">
-                                                       ${info.version}
-                                               </t:if>
-                                       </dd>
-
-                                       <dt>Tapestry version</dt>
-                                       <dd class="tapestryVersion">
-                                               <t:if 
test="info.tapestryVersion" else="message:not-informed">
-                                                       ${info.tapestryVersion}
-                                               </t:if>
-                                       </dd>
+                       <p>
+                               <t:if test="showEverything">
+                                       <p:then>
+                                               <t:eventLink 
event="showRestricted" zone="^">
+                                                       Don't show webapp 
library
+                                               </t:eventLink>
+                                       </p:then>                               
+                                       <p:else>
+                                               <t:eventLink 
event="showEverything" zone="^">
+                                                       Show webapp library
+                                               </t:eventLink>
+                                       </p:else>       
+                               </t:if>
+                       </p>
+                       
+                       <t:If test="!selectedComponent">
+                       
+                           <h1><strong>${libraryMappings.size()}</strong> 
component libraries used</h1>
+                           
+                           <ul id="libraryList" class="list-group">
+                               <li t:type="Loop" t:source="libraryMappings" 
t:value="libraryMapping" class="list-group-item">
+                                       <a href="#${libraryClientId}">
+                                               <code>${libraryName}</code> 
<t:if test="info">: ${info.name}</t:if>
+                                       </a>
+                                       <p t:type="If" 
t:test="info?.description">
+                                               ${info.description}
+                                       </p>
+                                       <p class="tags" t:type="If" 
t:test="!info?.tags.empty">
+                                               Tags: 
+                                                       <span t:type="Loop" 
t:source="info.tags" t:value="var:tag" class="badge" 
+                                                               
style="margin-right: 0.3em; font-size: 0.75em">
+                                                               ${var:tag}
+                                                       </span>
+                                               </p>
+                               </li>
+                           </ul>
+                           
+                       
+                               <div id="libraries">
                                
-                                       <dt>Documentation URL</dt>
-                                       <dd class="documentationUrl">
-                                               <t:if 
test="info.documentationUrl" else="message:not-informed">
-                                                       <a 
href="${info.documentationUrl}">${info.documentationUrl}</a>
-                                               </t:if>
-                                       </dd>
-                                       
-                                       <dt>JavaDoc URL</dt>
-                                       <dd class="javadocUrl">
-                                               <t:if test="info.javadocUrl" 
else="message:not-informed">
-                                                       <a 
href="${info.javadocUrl}">${info.javadocUrl}</a>
-                                               </t:if>
-                                       </dd>
-                                       
-                                       <t:if 
test="info.dependencyManagementInfoPresent">
-                                               <dt>Dependency information</dt>
-                                               <dd 
class="dependencyInformation">
-                                                       Group id <code 
class="groupId">${info.groupId}</code>,
-                                                       artifact id <code 
class="groupId">${info.artifactId}</code>,
-                                                       version <code 
class="groupId">${info.version}</code>
-                                                       <br/>
-                                                       <a 
href="${info.dependencyManagementInfoUrl}" 
-                                                               target="_blank">
-                                                               <em>More 
information at Maven Central Respository</em>
-                                                       </a>
-                                               </dd>
-                                       </t:if>
-                                       
-                                       <dt>Source browse URL</dt>
-                                       <dd class="sourceBrowseUrl">
-                                               <t:if 
test="info.sourceBrowseUrl" else="message:not-informed">
-                                                       <a 
href="${info.sourceBrowseUrl}">${info.sourceBrowseUrl}</a>
-                                               </t:if>
-                                       </dd>
-                                       
-                                       <dt>Issue tracker URL</dt>
-                                       <dd class="issueTrackerUrl">
-                                               <t:if 
test="info.issueTrackerUrl" else="message:not-informed">
-                                                       <a 
href="${info.issueTrackerUrl}">${info.issueTrackerUrl}</a>
-                                               </t:if>
-                                       </dd>
-                                       
-                                       <dt></dt>
-                                       <dd class="jsonDescription">
-                                               <a t:type="EventLink" 
t:event="json" t:context="libraryMapping.libraryName">Generate JSON 
description</a>
-                                       </dd>
+                                       <div class="libraryInfo" t:type="Loop" 
t:source="libraryMappings" t:value="libraryMapping" id="${libraryClientId}">
                                
-                                   </dl>
-                           
-                               </t:if>
+                                               <h2><code>${libraryName}</code> 
<t:if test="info">: ${info.name}</t:if></h2>
+                                               
+                                               <t:if test="info">
+                                       
+                                                   <dl class="dl-horizontal">
+                                                   
+                                                       <dt>Homepage</dt>
+                                                       <dd class="homepage">
+                                                               <t:if 
test="info.homepageUrl" else="message:not-informed">
+                                                                       <a 
href="${info.homepageUrl}">${info.homepageUrl}</a>
+                                                               </t:if>
+                                                       </dd>
+                                                       
+                                                       <dt>Version</dt>
+                                                       <dd class="version">
+                                                               <t:if 
test="info.version" else="message:not-informed">
+                                                                       
${info.version}
+                                                               </t:if>
+                                                       </dd>
+               
+                                                       <dt>Tapestry 
version</dt>
+                                                       <dd 
class="tapestryVersion">
+                                                               <t:if 
test="info.tapestryVersion" else="message:not-informed">
+                                                                       
${info.tapestryVersion}
+                                                               </t:if>
+                                                       </dd>
+                                               
+                                                       <dt>Documentation 
URL</dt>
+                                                       <dd 
class="documentationUrl">
+                                                               <t:if 
test="info.documentationUrl" else="message:not-informed">
+                                                                       <a 
href="${info.documentationUrl}">${info.documentationUrl}</a>
+                                                               </t:if>
+                                                       </dd>
+                                                       
+                                                       <dt>JavaDoc URL</dt>
+                                                       <dd class="javadocUrl">
+                                                               <t:if 
test="info.javadocUrl" else="message:not-informed">
+                                                                       <a 
href="${info.javadocUrl}">${info.javadocUrl}</a>
+                                                               </t:if>
+                                                       </dd>
+                                                       
+                                                       <t:if 
test="info.dependencyManagementInfoPresent">
+                                                               <dt>Dependency 
information</dt>
+                                                               <dd 
class="dependencyInformation">
+                                                                       Group 
id <code class="groupId">${info.groupId}</code>,
+                                                                       
artifact id <code class="groupId">${info.artifactId}</code>,
+                                                                       version 
<code class="groupId">${info.version}</code>
+                                                                       <br/>
+                                                                       <a 
href="${info.dependencyManagementInfoUrl}" 
+                                                                               
target="_blank">
+                                                                               
<em>More information at Maven Central Respository</em>
+                                                                       </a>
+                                                               </dd>
+                                                       </t:if>
+                                                       
+                                                       <dt>Source browse 
URL</dt>
+                                                       <dd 
class="sourceBrowseUrl">
+                                                               <t:if 
test="info.sourceBrowseUrl" else="message:not-informed">
+                                                                       <a 
href="${info.sourceBrowseUrl}">${info.sourceBrowseUrl}</a>
+                                                               </t:if>
+                                                       </dd>
+                                                       
+                                                       <dt>Issue tracker 
URL</dt>
+                                                       <dd 
class="issueTrackerUrl">
+                                                               <t:if 
test="info.issueTrackerUrl" else="message:not-informed">
+                                                                       <a 
href="${info.issueTrackerUrl}">${info.issueTrackerUrl}</a>
+                                                               </t:if>
+                                                       </dd>
+                                                       
+                                                       <dt></dt>
+                                                       <dd 
class="jsonDescription">
+                                                               <a 
t:type="EventLink" t:event="json" 
t:context="libraryMapping.libraryName">Generate JSON description</a>
+                                                       </dd>
+                                               
+                                                   </dl>
+                                           
+                                               </t:if>
+                       
+                                               <p t:type="If" t:test="!info" 
class="noInformation">No additional information provided for 
<code>${libraryMapping.libraryName}</code>.</p>
+                                               
+                       <!--                    <div t:type="Zone" t:id="pages" 
id="prop:libraryClientZoneClientId"> -->
+                       <!--                    </div> -->
+                       
+                                               <div t:type="Delegate" 
to="componentsTable"></div>
+                                               <div t:type="Delegate" 
to="pagesTable"></div>
+                                               <div t:type="Delegate" 
to="mixinsTable"></div>
+                                               
+                                       </div>
+                                       
+                               </div>
        
-                               <p t:type="If" t:test="!info" 
class="noInformation">No additional information provided for 
<code>${libraryMapping.libraryName}</code>.</p>
+                       </t:If>                 
                                
-       <!--                    <div t:type="Zone" t:id="pages" 
id="prop:libraryClientZoneClientId"> -->
-       <!--                    </div> -->
-       
-                               <div t:type="Delegate" 
to="componentsTable"></div>
-                               <div t:type="Delegate" to="pagesTable"></div>
-                               <div t:type="Delegate" to="mixinsTable"></div>
+                       <t:If test="selectedComponent">
+                               <h1><strong>${selectedComponent}</strong> 
(${componentClassName})</h1>
                                
-                       </div>
-                       
-               </div>
-               
-       </t:if>
+                               <p>
+                                       <t:eventLink event="reset" 
zone="^">Back to component listing</t:eventLink>
+                               </p>
+                               
+                           <div class="panel panel-default vert-offset">
+                               <div class="panel-heading">Component dependency 
information for ${selectedComponent} (just direct dependencies)</div>
+                               <div class="panel-body">
+                                       <ul>
+                                               <li t:type="Loop" 
t:value="dependency" t:source="dependencies">
+                                                       ${displayLogicalName} 
(${dependency})
+                                               </li>
+                                       </ul>
+                               </div>
+                           </div>
+                           <div class="panel panel-default vert-offset">
+                               <div class="panel-heading">Components depending 
on ${selectedComponent} (just direct dependencies)</div>
+                               <div class="panel-body">
+                                       <ul>
+                                               <li t:type="Loop" 
t:value="dependency" t:source="dependents">
+                                                       ${displayLogicalName} 
(${dependency})
+                                               </li>
+                                       </ul>
+                               </div>
+                           </div>
+                           <div class="panel panel-default vert-offset">
+                               <div 
class="panel-heading">${selectedComponent}'s dependency tree</div>
+                               <div class="panel-body">
+                                       <t:graphviz value="graphvizValue" 
showSource="true"/>
+                               </div>
+                           </div>
+                       </t:If>
+               </t:zone>               
+       </t:If>
        
        <t:block id="classesTable">
                <div t:type="If" t:test="!logicalNames.empty">
@@ -138,7 +193,11 @@
                                </thead>
                                <tbody>
                                        <tr t:type="Loop" 
t:source="logicalNames" t:value="logicalName">
-                                               
<td><code>${simpleLogicalName}</code></td>
+                                               <td>
+                                                       <t:eventLink 
event="selectComponent" t:context="context" t:zone="^">
+                                                               
<code>${simpleLogicalName}</code>
+                                                       </t:eventLink>
+                                               </td>
                                                <td>${description?.text()}</td>
                                                <td>
                                                        <ul t:type="If" 
t:test="classHasTags" style="padding: 0; margin: 0;">
@@ -163,5 +222,5 @@
                        </table>
                </div>
        </t:block>
-
+       
 </t:block>
diff --git 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
index bdac0f8b1..2eea91036 100644
--- 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
+++ 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
@@ -77,7 +77,7 @@
                    <div class="panel panel-default vert-offset" t:type="If" 
t:test="selectedPage">
                        <div class="panel-heading">${selectedPage.name}'s 
dependency tree</div>
                        <div class="panel-body">
-                               <t:graphviz value="graphvizValue"/>
+                               <t:graphviz value="graphvizValue" 
showSource="true"/>
                        </div>
                    </div>
            </t:if>
diff --git 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.tml
 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.tml
new file mode 100644
index 000000000..8e39c0f0e
--- /dev/null
+++ 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.tml
@@ -0,0 +1,17 @@
+<t:block id="content" 
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd";
+         xmlns:p="tapestry:parameter">
+       <h1>Page classloader contexts <t:glyphicon name="list" /></h1>
+       <style>
+               #tree li {
+                       display: block;
+                       margin-top: 0.25em;
+                       line-height: 2m;
+               }
+               #tree .glyphicon {
+                       margin-right: 0.25em;
+               }
+       </style>
+       <ul id="tree">          
+               <t:trigger event="render"/>
+       </ul>
+</t:block>
\ No newline at end of file
diff --git a/tapestry-core/src/test/app1/AtComponentType.tml 
b/tapestry-core/src/test/app1/AtComponentType.tml
new file mode 100644
index 000000000..50a2bf92e
--- /dev/null
+++ b/tapestry-core/src/test/app1/AtComponentType.tml
@@ -0,0 +1,6 @@
+<t:border xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd";
+          xmlns:p="tapestry:parameter">
+
+    <input t:id="input"/>
+
+</t:border>
\ No newline at end of file
diff --git a/tapestry-core/src/test/app1/GridDemo.tml 
b/tapestry-core/src/test/app1/GridDemo.tml
index 5a4f12eb7..650ecf163 100644
--- a/tapestry-core/src/test/app1/GridDemo.tml
+++ b/tapestry-core/src/test/app1/GridDemo.tml
@@ -1,6 +1,6 @@
 <html t:type="Border" 
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd";>
 
-    <h1>Grid Demo</h1>
+    <h1>Grid Demo!!!!</h1>
 
     <table t:type="grid" t:id="grid" source="tracks" row="track">
         <t:parameter name="ratingheader">
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/AtComponentType.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/AtComponentType.java
new file mode 100644
index 000000000..de0627822
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/AtComponentType.java
@@ -0,0 +1,24 @@
+// Copyright 2024 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.integration.app1.pages;
+
+import org.apache.tapestry5.Field;
+import org.apache.tapestry5.annotations.Component;
+
+public class AtComponentType
+{
+    @Component(type = "textfield")
+    private Field input;
+}
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java
index 1d064920d..83faa8be5 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java
@@ -14,6 +14,7 @@
 
 package org.apache.tapestry5.integration.app1.pages;
 
+import org.apache.tapestry5.MarkupWriter;
 import org.apache.tapestry5.annotations.InjectComponent;
 import org.apache.tapestry5.annotations.Property;
 import org.apache.tapestry5.corelib.components.Grid;
@@ -48,4 +49,9 @@ public class GridDemo
     {
         grid.getSortModel().updateSort("rating");
     }
+    
+//    public void afterRender(MarkupWriter writer)
+//    {
+//        writer.getDocument().find("html/body").elementAt(1, 
"p").text("bbbbb");
+//    }
 }
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java
index 8b89e7216..9c535be69 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java
@@ -19,17 +19,22 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 import org.apache.tapestry5.commons.MappedConfiguration;
+import org.apache.tapestry5.commons.Resource;
 import org.apache.tapestry5.corelib.base.AbstractComponentEventLink;
 import org.apache.tapestry5.corelib.base.AbstractField;
 import org.apache.tapestry5.corelib.base.AbstractLink;
@@ -52,6 +57,7 @@ import org.apache.tapestry5.corelib.components.Loop;
 import org.apache.tapestry5.corelib.components.Output;
 import org.apache.tapestry5.corelib.components.OutputRaw;
 import org.apache.tapestry5.corelib.components.PageLink;
+import org.apache.tapestry5.corelib.components.PasswordField;
 import org.apache.tapestry5.corelib.components.PropertyDisplay;
 import org.apache.tapestry5.corelib.components.PropertyEditor;
 import org.apache.tapestry5.corelib.components.RenderObject;
@@ -61,9 +67,7 @@ import org.apache.tapestry5.corelib.components.TextOutput;
 import org.apache.tapestry5.corelib.components.Zone;
 import org.apache.tapestry5.corelib.mixins.FormGroup;
 import org.apache.tapestry5.corelib.mixins.RenderDisabled;
-import org.apache.tapestry5.corelib.pages.PropertyEditBlocks;
 import org.apache.tapestry5.integration.app1.base.BaseLayoutPage;
-import org.apache.tapestry5.integration.app1.base.EmptyExtendTemplate;
 import org.apache.tapestry5.integration.app1.components.Border;
 import org.apache.tapestry5.integration.app1.components.ErrorComponent;
 import org.apache.tapestry5.integration.app1.components.OuterAny;
@@ -75,15 +79,17 @@ import 
org.apache.tapestry5.integration.app1.mixins.EchoValue;
 import org.apache.tapestry5.integration.app1.mixins.EchoValue2;
 import org.apache.tapestry5.integration.app1.mixins.TextOnlyOnDisabled;
 import org.apache.tapestry5.integration.app1.pages.AlertsDemo;
+import org.apache.tapestry5.integration.app1.pages.AtComponentType;
 import org.apache.tapestry5.integration.app1.pages.BlockCaller;
 import org.apache.tapestry5.integration.app1.pages.BlockHolder;
-import 
org.apache.tapestry5.integration.app1.pages.EmbeddedComponentTypeConflict;
 import org.apache.tapestry5.integration.app1.pages.InstanceMixinDependencies;
 import org.apache.tapestry5.integration.app1.pages.MixinParameterDefault;
-import org.apache.tapestry5.integration.app1.pages.TemplateOverrideDemo;
 import 
org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
 import org.apache.tapestry5.internal.services.templates.DefaultTemplateLocator;
+import org.apache.tapestry5.internal.services.templates.PageTemplateLocator;
 import org.apache.tapestry5.ioc.internal.QuietOperationTracker;
+import org.apache.tapestry5.ioc.internal.util.AbstractResource;
+import org.apache.tapestry5.model.ComponentModel;
 import org.apache.tapestry5.modules.TapestryModule;
 import org.apache.tapestry5.plastic.PlasticManager;
 import org.apache.tapestry5.services.ComponentClassResolver;
@@ -96,6 +102,7 @@ import org.testng.annotations.Test;
 /**
  * Tests {@link ComponentDependencyRegistryImpl}.
  */
+@SuppressWarnings("deprecation")
 public class ComponentDependencyRegistryImplTest
 {
     
@@ -123,11 +130,37 @@ public class ComponentDependencyRegistryImplTest
         TapestryModule.contributeTemplateParser(templateConfiguration);
         templateParser = new TemplateParserImpl(templateConfiguration.map, 
false, new QuietOperationTracker());
         
-        componentTemplateLocator = new DefaultTemplateLocator();
+        final String rootFolder = "src/test/app1/";
+        final File folderFile = new File(rootFolder);
+        final Resource app1ContextFolderResource = new FileResource("/", 
folderFile);
+        
+        final PageTemplateLocator pageTemplateLocator = new 
PageTemplateLocator(app1ContextFolderResource, resolver, "");
+        final DefaultTemplateLocator defaultTemplateLocator = new 
DefaultTemplateLocator();
+        componentTemplateLocator = new ComponentTemplateLocator() 
+        {
+            @Override
+            public Resource locateTemplate(ComponentModel model, Locale 
locale) 
+            {
+                Resource resource = 
defaultTemplateLocator.locateTemplate(model, locale);
+                if (resource == null)
+                {
+                    resource = pageTemplateLocator.locateTemplate(model, 
locale);
+                }
+                return resource != null && resource.exists() ? resource : null;
+            }
+        };
         
         resolver = EasyMock.createMock(ComponentClassResolver.class);
         
+        
EasyMock.expect(resolver.resolvePageClassNameToPageName(EasyMock.anyString()))
+                .andAnswer(() -> {
+                    String s = ((String) EasyMock.getCurrentArguments()[0]);
+                    final String pageName = s.substring(s.lastIndexOf('.') + 
1);
+                    return pageName;
+                }).anyTimes();
+        
         expectResolveComponent(TextField.class);
+        expectResolveComponent(PasswordField.class);
         expectResolveComponent(Border.class);
         expectResolveComponent(BeanEditForm.class);
         expectResolveComponent(Zone.class);
@@ -163,8 +196,6 @@ public class ComponentDependencyRegistryImplTest
         EasyMock.expect(resolver.resolveMixinTypeToClassName("formgroup"))
             .andReturn(FormGroup.class.getName()).anyTimes();
         
-        // TODO: remove this
-//        
EasyMock.expect(resolver.getLogicalName(EasyMock.anyString())).andAnswer(() -> 
(String) EasyMock.getCurrentArguments()[0]).anyTimes();
         EasyMock.expect(resolver.isPage(EasyMock.anyString())).andAnswer(() -> 
{
             String string = (String) EasyMock.getCurrentArguments()[0];
             return string.contains(".pages.");
@@ -351,7 +382,40 @@ public class ComponentDependencyRegistryImplTest
         
     }
     
-    // Tested code isn't being used at the moment
+    @Test
+    public void get_all_non_page_dependencies()
+    {
+        
+        final String page = "page";
+        final String className = "root";
+        final String superclass = "superclass";
+        final Set<String> expectedAllNonPageDependencies = new HashSet<>();
+        final BiConsumer<String, String> add = (c, d) -> {
+            expectedAllNonPageDependencies.add(d);
+            add(c, d, DependencyType.USAGE);
+        };
+        
+        add(className, page, DependencyType.INJECT_PAGE);
+        
+        add(className, superclass, DependencyType.SUPERCLASS);
+        expectedAllNonPageDependencies.add(superclass);
+        
+        add.accept(className, "child1");
+        add.accept(className, "child2");
+        add.accept("child1", "child1.1");
+        add.accept("child2", "child2.1");
+        add.accept("child2.1", className);
+        add.accept(superclass, "child2.1");
+        add.accept(superclass, "superclassDependency");
+        
+        expectedAllNonPageDependencies.remove(className);
+
+        final Set<String> allNonPageDependencies = 
componentDependencyRegistry.getAllNonPageDependencies(className);
+        assertFalse(allNonPageDependencies.contains(className));
+        assertEquals(expectedAllNonPageDependencies, allNonPageDependencies);
+        
+    }
+    
     @Test
     public void register()
     {
@@ -390,8 +454,8 @@ public class ComponentDependencyRegistryImplTest
         assertDependencies(OuterAny.class, Any.class);
         
         // @Component, type() defined
-        
componentDependencyRegistry.register(EmbeddedComponentTypeConflict.class);
-        assertDependencies(EmbeddedComponentTypeConflict.class, 
TextField.class);
+        componentDependencyRegistry.register(AtComponentType.class);
+        assertDependencies(AtComponentType.class, TextField.class, 
Border.class);
 
         // @Mixin, type() not defined
         componentDependencyRegistry.register(AbstractTextField.class);
@@ -416,8 +480,15 @@ public class ComponentDependencyRegistryImplTest
         // Templates with <t:replace>
         componentDependencyRegistry.register(SubclassWithImport.class);
         assertDependencies(SubclassWithImport.class,
-                OutputRaw.class, SuperclassWithImport.class);
-
+                OutputRaw.class, SuperclassWithImport.class, Loop.class);
+        
+        // Circular dependency: BlockHolder <-> BlockCaller
+        componentDependencyRegistry.register(BlockHolder.class);
+        assertDependencies(BlockHolder.class, 
+                BlockCaller.class, Loop.class, ActionLink.class);
+        assertDependencies(BlockCaller.class, 
+                BlockHolder.class, Border.class, PageLink.class, 
Delegate.class);
+        
     }
     
     private void assertDependencies(Class clasz, Class... dependencies) {
@@ -433,14 +504,14 @@ public class ComponentDependencyRegistryImplTest
             .collect(Collectors.toSet());
     }
 
-//    private static Set<String> setOf(String ... strings)
-//    {
-//        return new HashSet<>(Arrays.asList(strings));
-//    }
-    
     private void add(String component, String dependency)
     {
-        componentDependencyRegistry.add(component, dependency, 
DependencyType.USAGE, true);
+        add(component, dependency, DependencyType.USAGE);
+    }
+    
+    private void add(String component, String dependency, DependencyType type)
+    {
+        componentDependencyRegistry.add(component, dependency, type, true);
     }
 
     private static final class MockMappedConfiguration<String, URL> implements 
MappedConfiguration<String, URL>
@@ -474,4 +545,46 @@ public class ComponentDependencyRegistryImplTest
         
     }
     
+    final private static class FileResource extends AbstractResource
+    {
+        
+        final private File file;
+        
+        final private String path;
+
+        public FileResource(String path, File file) 
+        {
+            super(path);
+            this.file = file;
+            this.path = path;
+        }
+
+        @SuppressWarnings("deprecation")
+        @Override
+        public java.net.URL toURL() 
+        {
+            try 
+            {
+                final File actualFile = new File(file, path);
+                return actualFile.exists() ? actualFile.toURL() : null;
+            } catch (MalformedURLException e) 
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        protected Resource newResource(String path) 
+        {
+            return new FileResource(path, file);
+        }
+
+        @Override
+        public String toString() 
+        {
+            return "FileResource [file=" + file + "/" + path + "]";
+        }
+        
+    }
+    
 }
diff --git a/tapestry-core/src/test/resources/log4j.properties 
b/tapestry-core/src/test/resources/log4j.properties
index febecf129..9dcdaacb2 100644
--- a/tapestry-core/src/test/resources/log4j.properties
+++ b/tapestry-core/src/test/resources/log4j.properties
@@ -21,6 +21,7 @@ 
log4j.category.org.apache.tapestry5.integration.app1.base.InheritBase=debug
 log4j.category.org.apache.tapestry5.integration.app1.pages.inherit=debug
 
 #log4j.category.org.apache.tapestry5.services.pageload=debug
+log4j.category.org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub=off
 
log4j.category.org.apache.tapestry5.integration.app1.services.AppModule.TimingFilter=ERROR
 # 
log4j.category.tapestry.transformer.org.apache.tapestry5.integration.app1.base.InheritBase=debug
 # 
log4j.category.tapestry.transformer.org.apache.tapestry5.integration.app1.pages.inherit=debug

Reply via email to