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..625b4455458d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassLoaderCache.java @@ -0,0 +1,121 @@ +/* + * 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 classNotFoundExceptionCache; + + private Map> resourceUrlCache; + + private Map>> 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 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> getResourcesCache(String name) { + if (!this.enableCache) { + return null; + } + return this.resourcesUrlCache.get(name); + } + + public Enumeration cacheResourceUrls(String name, Enumeration 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 Map createCache(int maxSize) { + return Collections.synchronizedMap(new LinkedHashMap(maxSize, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry 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..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 @@ -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 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 findResources(String name) throws IOException { + Optional> 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 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) { + this.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 doDefinePackage(DefinePackageCallType type, Supplier 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..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 @@ -108,4 +108,58 @@ 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); + } + }