From 49146f93ff5a9e306df0d4c5b236b1a33c0caec6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=9A=E4=B9=8B?= <bingjie.lbj@antgroup.com>
Date: Tue, 1 Mar 2022 16:43:28 +0800
Subject: [PATCH 1/3] Add a cache to LaunchedURLClassloader to improve startup
 performance

This commit adds a ClassLoaderCache to LaunchedURLClassLoader  to improve startup performance. URLClassLoader shows awful performance when find Classes/Resources which do not exist.
---
 .../boot/loader/ClassLoaderCache.java         | 120 ++++++++++++++++++
 .../boot/loader/LaunchedURLClassLoader.java   |  36 +++++-
 .../loader/LaunchedURLClassLoaderTests.java   |  50 ++++++++
 3 files changed, 202 insertions(+), 4 deletions(-)
 create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java

diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java
new file mode 100644
index 000000000000..11943de1324d
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2012-2020 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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.springframework.boot.loader;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A class/resource cache used by {@link LaunchedURLClassLoader} .
+ *
+ * @author Bingjie Lv
+ * @since 2.7.0
+ *
+ */
+public class ClassLoaderCache {
+
+	private boolean enableCache = Boolean.getBoolean("loader.cache.enable");
+
+	private final int cacheSize = Integer.getInteger("loader.cache.size", 3000);
+
+	private Map<String, ClassNotFoundException> classNotFoundExceptionCache;
+
+	private Map<String, Optional<URL>> resourceUrlCache;
+
+	private Map<String, Optional<Enumeration<URL>>> resourcesUrlCache;
+
+	public ClassLoaderCache() {
+		this.classNotFoundExceptionCache = createCache(this.cacheSize);
+		this.resourceUrlCache = createCache(this.cacheSize);
+		this.resourcesUrlCache = createCache(this.cacheSize);
+	}
+
+	public void fastClassNotFoundException(String name) throws ClassNotFoundException {
+		if (!this.enableCache) {
+			return;
+		}
+		ClassNotFoundException classNotFoundException = this.classNotFoundExceptionCache.get(name);
+		if (classNotFoundException != null) {
+			throw classNotFoundException;
+		}
+	}
+
+	public void cacheClassNotFoundException(String name, ClassNotFoundException exception) {
+		if (!this.enableCache) {
+			return;
+		}
+		this.classNotFoundExceptionCache.put(name, exception);
+	}
+
+	public Optional<URL> getResourceCache(String name) {
+		if (!this.enableCache) {
+			return null;
+		}
+		return this.resourceUrlCache.get(name);
+	}
+
+	public URL cacheResourceUrl(String name, URL url) {
+		if (!this.enableCache) {
+			return url;
+		}
+		this.resourceUrlCache.put(name, (url != null) ? Optional.of(url) : Optional.empty());
+		return url;
+	}
+
+	public Optional<Enumeration<URL>> getResourcesCache(String name) {
+		if (!this.enableCache) {
+			return null;
+		}
+		return this.resourcesUrlCache.get(name);
+	}
+
+	public Enumeration<URL> cacheResourceUrls(String name, Enumeration<URL> urlEnumeration) {
+		if (!this.enableCache) {
+			return urlEnumeration;
+		}
+		if (!urlEnumeration.hasMoreElements()) {
+			this.resourcesUrlCache.put(name, Optional.of(urlEnumeration));
+		}
+		return urlEnumeration;
+	}
+
+	public void clearCache() {
+		if (this.enableCache) {
+			this.classNotFoundExceptionCache.clear();
+			this.resourceUrlCache.clear();
+			this.resourcesUrlCache.clear();
+		}
+	}
+
+	public void setEnableCache(boolean enableCache) {
+		this.enableCache = enableCache;
+	}
+
+	protected <K, V> Map<K, V> createCache(int maxSize) {
+		return Collections.synchronizedMap(new LinkedHashMap<K, V>(maxSize, 0.75f, true) {
+			@Override
+			protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+				return size() >= maxSize;
+			}
+		});
+	}
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
index 75ac50815094..3e9d33dbb098 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
@@ -26,6 +26,7 @@
 import java.security.AccessController;
 import java.security.PrivilegedExceptionAction;
 import java.util.Enumeration;
+import java.util.Optional;
 import java.util.function.Supplier;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
@@ -55,6 +56,8 @@ public class LaunchedURLClassLoader extends URLClassLoader {
 
 	private final Object packageLock = new Object();
 
+	private final ClassLoaderCache loaderCache = new ClassLoaderCache();
+
 	private volatile DefinePackageCallType definePackageCallType;
 
 	/**
@@ -92,12 +95,16 @@ public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls,
 
 	@Override
 	public URL findResource(String name) {
+		Optional<URL> optional = this.loaderCache.getResourceCache(name);
+		if (optional != null) {
+			return optional.orElse(null);
+		}
 		if (this.exploded) {
-			return super.findResource(name);
+			return this.loaderCache.cacheResourceUrl(name, super.findResource(name));
 		}
 		Handler.setUseFastConnectionExceptions(true);
 		try {
-			return super.findResource(name);
+			return this.loaderCache.cacheResourceUrl(name, super.findResource(name));
 		}
 		finally {
 			Handler.setUseFastConnectionExceptions(false);
@@ -106,12 +113,17 @@ public URL findResource(String name) {
 
 	@Override
 	public Enumeration<URL> findResources(String name) throws IOException {
+		Optional<Enumeration<URL>> optional = this.loaderCache.getResourcesCache(name);
+		if (optional != null) {
+			return optional.orElse(null);
+		}
 		if (this.exploded) {
-			return super.findResources(name);
+			return this.loaderCache.cacheResourceUrls(name, super.findResources(name));
 		}
 		Handler.setUseFastConnectionExceptions(true);
 		try {
-			return new UseFastConnectionExceptionsEnumeration(super.findResources(name));
+			return this.loaderCache.cacheResourceUrls(name,
+					new UseFastConnectionExceptionsEnumeration(super.findResources(name)));
 		}
 		finally {
 			Handler.setUseFastConnectionExceptions(false);
@@ -120,6 +132,21 @@ public Enumeration<URL> findResources(String name) throws IOException {
 
 	@Override
 	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+		this.loaderCache.fastClassNotFoundException(name);
+		try {
+			return loadClassInternal(name, resolve);
+		}
+		catch (ClassNotFoundException ex) {
+			this.loaderCache.cacheClassNotFoundException(name, ex);
+			throw ex;
+		}
+	}
+
+	public void setEnableCache(boolean enableCache){
+		loaderCache.setEnableCache(enableCache);
+	}
+
+	private Class<?> loadClassInternal(String name, boolean resolve) throws ClassNotFoundException {
 		if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
 			try {
 				Class<?> result = loadClassInLaunchedClassLoader(name);
@@ -297,6 +324,7 @@ private <T> T doDefinePackage(DefinePackageCallType type, Supplier<T> call) {
 	 * Clear URL caches.
 	 */
 	public void clearCache() {
+		this.loaderCache.clearCache();
 		if (this.exploded) {
 			return;
 		}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
index c37e01de61c3..226aea054aa1 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
@@ -108,4 +108,54 @@ void resolveFromNestedWhileThreadIsInterrupted() throws Exception {
 		}
 	}
 
+	@Test
+	void enableLoaderCache() throws Exception {
+		resolveResourceFromArchive();
+		resolveResourcesFromArchive();
+		resolveRootPathFromArchive();
+		resolveRootResourcesFromArchive();
+		resolveFromNested();
+		resolveFromNestedWhileThreadIsInterrupted();
+
+		LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
+				new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
+		loader.setEnableCache(true);
+		assertThat(loader.getResource("demo/Application.java")).isEqualTo(loader.getResource("demo/Application.java"));
+		assertThat(loader.loadClass("demo.Application")).isEqualTo(loader.loadClass("demo.Application"));
+		assertThat(loader.getResource("demo/ApplicationNotExist.java")).isNull();
+		assertThat(loader.getResources("demo/ApplicationNotExist.java").hasMoreElements()).isNotEqualTo(true);
+		assertThat(loader.getResources("demo/ApplicationNotExist.java").hasMoreElements()).isNotEqualTo(true);
+		ClassNotFoundException ex = null;
+		ClassNotFoundException ex1 = null;
+		ClassNotFoundException ex2 = null;
+		ClassNotFoundException ex3 = null;
+		try {
+			loader.loadClass("demo.ApplicationNotExist");
+		} catch (ClassNotFoundException exception){
+			ex = exception;
+		}
+		try {
+			loader.loadClass("demo.ApplicationNotExist");
+		} catch (ClassNotFoundException exception){
+			ex1 = exception;
+		}
+		try {
+			loader.setEnableCache(false);
+			loader.loadClass("demo.ApplicationNotExist");
+		} catch (ClassNotFoundException exception){
+			ex2 = exception;
+			loader.setEnableCache(true);
+		}
+		try {
+			loader.clearCache();
+			loader.loadClass("demo.ApplicationNotExist");
+		} catch (ClassNotFoundException exception){
+			ex3 = exception;
+		}
+		assertThat(ex).isNotNull();
+		assertThat(ex1).isNotNull().isEqualTo(ex);
+		assertThat(ex2).isNotNull().isNotEqualTo(ex).isNotEqualTo(ex1);
+		assertThat(ex3).isNotNull().isNotEqualTo(ex2).isNotEqualTo(ex1).isNotEqualTo(ex);
+	}
+
 }

From 22d2d2917b86958dfd75873adcc8d2ec060f285f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=9A=E4=B9=8B?= <bingjie.lbj@antgroup.com>
Date: Tue, 1 Mar 2022 19:23:59 +0800
Subject: [PATCH 2/3] Add a cache to LaunchedURLClassloader to improve startup
 performance

This commit adds a ClassLoaderCache to LaunchedURLClassLoader  to improve startup performance. URLClassLoader shows awful performance when find Classes/Resources which do not exist.
---
 .../boot/loader/LaunchedURLClassLoader.java          |  4 ++--
 .../boot/loader/LaunchedURLClassLoaderTests.java     | 12 ++++++++----
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
index 3e9d33dbb098..06084906c304 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
@@ -142,8 +142,8 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
 		}
 	}
 
-	public void setEnableCache(boolean enableCache){
-		loaderCache.setEnableCache(enableCache);
+	public void setEnableCache(boolean enableCache) {
+		this.loaderCache.setEnableCache(enableCache);
 	}
 
 	private Class<?> loadClassInternal(String name, boolean resolve) throws ClassNotFoundException {
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
index 226aea054aa1..a40956a1683f 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
@@ -131,25 +131,29 @@ void enableLoaderCache() throws Exception {
 		ClassNotFoundException ex3 = null;
 		try {
 			loader.loadClass("demo.ApplicationNotExist");
-		} catch (ClassNotFoundException exception){
+		}
+		catch (ClassNotFoundException exception) {
 			ex = exception;
 		}
 		try {
 			loader.loadClass("demo.ApplicationNotExist");
-		} catch (ClassNotFoundException exception){
+		}
+		catch (ClassNotFoundException exception) {
 			ex1 = exception;
 		}
 		try {
 			loader.setEnableCache(false);
 			loader.loadClass("demo.ApplicationNotExist");
-		} catch (ClassNotFoundException exception){
+		}
+		catch (ClassNotFoundException exception) {
 			ex2 = exception;
 			loader.setEnableCache(true);
 		}
 		try {
 			loader.clearCache();
 			loader.loadClass("demo.ApplicationNotExist");
-		} catch (ClassNotFoundException exception){
+		}
+		catch (ClassNotFoundException exception) {
 			ex3 = exception;
 		}
 		assertThat(ex).isNotNull();

From d3a871738ad69d336d5a1d81f52402d7bae72c4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=9A=E4=B9=8B?= <bingjie.lbj@antgroup.com>
Date: Tue, 1 Mar 2022 20:33:45 +0800
Subject: [PATCH 3/3] Add a cache to LaunchedURLClassloader to improve startup
 performance

This commit adds a ClassLoaderCache to LaunchedURLClassLoader  to improve startup performance. URLClassLoader shows awful performance when find Classes/Resources which do not exist.
---
 .../java/org/springframework/boot/loader/ClassLoaderCache.java   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java
index 11943de1324d..625b4455458d 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java
@@ -117,4 +117,5 @@ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
 			}
 		});
 	}
+
 }