/* * Copyright (C) 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 * * 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 vip.justlive.oxygen.web.tomcat; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; import javax.servlet.ServletContext; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.Jar; import org.apache.tomcat.JarScanFilter; import org.apache.tomcat.JarScanType; import org.apache.tomcat.JarScanner; import org.apache.tomcat.JarScannerCallback; import org.apache.tomcat.util.buf.UriUtil; import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.scan.JarFactory; import org.apache.tomcat.util.scan.StandardJarScanFilter; import vip.justlive.oxygen.core.util.base.Strings; import vip.justlive.oxygen.core.util.base.Urls; /** * fat-jar scanner * <br> * 使用ide时,WebappClassLoader.getParent() 是 AppClassLoader,只需要扫描 WebappClassLoader 和 AppClassLoader * <br> * 使用springboot插件打包的fat-jar时,WebappClassLoader.getParent() 是 LaunchedURLClassLoader,扫描WebappClassLoader * 和 LaunchedURLClassLoader * * @author wubo */ public class FatJarScanner implements JarScanner { /** * The string resources for this package. */ private static final StringManager SM = StringManager.getManager("org.apache.tomcat.util.scan"); private static final Set<ClassLoader> CLASSLOADER_HIERARCHY; static { Set<ClassLoader> cls = new HashSet<>(); ClassLoader cl = FatJarScanner.class.getClassLoader(); while (cl != null) { cls.add(cl); cl = cl.getParent(); } CLASSLOADER_HIERARCHY = Collections.unmodifiableSet(cls); } private final Log log = LogFactory.getLog(FatJarScanner.class); /** * Controls the filtering of the results from the scan for JARs */ private JarScanFilter jarScanFilter = new StandardJarScanFilter(); /** * Since class loader hierarchies can get complicated, this method attempts to apply the following * rule: A class loader is a web application class loader unless it loaded this class * (StandardJarScanner) or is a parent of the class loader that loaded this class. * * This should mean: the webapp class loader is an application class loader the shared class * loader is an application class loader the server class loader is not an application class * loader the common class loader is not an application class loader the system class loader is * not an application class loader the bootstrap class loader is not an application class loader */ private static boolean isWebappClassLoader(ClassLoader classLoader) { return !CLASSLOADER_HIERARCHY.contains(classLoader); } @Override public JarScanFilter getJarScanFilter() { return jarScanFilter; } @Override public void setJarScanFilter(JarScanFilter jarScanFilter) { this.jarScanFilter = jarScanFilter; } /** * Scan the provided ServletContext and class loade r for JAR files. Each JAR file found will be * passed to the callback handle to be processed. * * @param scanType The type of JAR scan to perform. This is passed to the filter which uses it to * determine how to filter the results * @param context The ServletContext - used to locate and access WEB-INF/lib * @param callback The handle to process any JARs found */ @Override public void scan(JarScanType scanType, ServletContext context, JarScannerCallback callback) { if (log.isTraceEnabled()) { log.trace(SM.getString("jarScan.webinflibStart")); } Set<URL> processedURLs = new HashSet<>(); // Scan WEB-INF/lib doScanWebInfLib(scanType, context, callback, processedURLs); // Scan WEB-INF/classes doScanWebInf(context, callback, processedURLs); // Scan the classpath doScanClassPath(scanType, context, callback, processedURLs); } private void doScanWebInfLib(JarScanType scanType, ServletContext context, JarScannerCallback callback, Set<URL> processedURLs) { Set<String> dirList = context.getResourcePaths(TomcatConf.WEB_INF_LIB); if (dirList == null) { return; } for (String path : dirList) { if (path.endsWith(TomcatConf.JAR_EXT) && getJarScanFilter() .check(scanType, path.substring(path.lastIndexOf(Strings.SLASH) + 1))) { // Need to scan this JAR if (log.isDebugEnabled()) { log.debug(SM.getString("jarScan.webinflibJarScan", path)); } URL url = null; try { url = context.getResource(path); processedURLs.add(url); process(scanType, callback, url, path, true, null); } catch (IOException e) { log.warn(SM.getString("jarScan.webinflibFail", url), e); } } else { if (log.isTraceEnabled()) { log.trace(SM.getString("jarScan.webinflibJarNoScan", path)); } } } } private void doScanWebInf(ServletContext context, JarScannerCallback callback, Set<URL> processedURLs) { try { URL webInfURL = context.getResource(TomcatConf.WEB_INF_CLASSES); if (webInfURL != null) { // WEB-INF/classes will also be included in the URLs returned // by the web application class loader so ensure the class path // scanning below does not re-scan this location. processedURLs.add(webInfURL); URL url = context.getResource(TomcatConf.WEB_INF_CLASSES + TomcatConf.META_INF_PATH); if (url != null) { callback.scanWebInfClasses(); } } } catch (IOException e) { log.warn(SM.getString("jarScan.webinfclassesFail"), e); } } private void doScanClassPath(JarScanType scanType, ServletContext context, JarScannerCallback callback, Set<URL> processedURLs) { if (log.isTraceEnabled()) { log.trace(SM.getString("jarScan.classloaderStart")); } ClassLoader classLoader = context.getClassLoader(); ClassLoader stopLoader = null; if (classLoader.getParent() != null) { // Stop when we reach the bootstrap class loader stopLoader = classLoader.getParent().getParent(); } // JARs are treated as application provided until the common class // loader is reached. boolean isWebapp = true; // Use a Deque so URLs can be removed as they are processed // and new URLs can be added as they are discovered during // processing. Deque<URL> classPathUrlsToProcess = new LinkedList<>(); while (classLoader != null && classLoader != stopLoader) { if (classLoader instanceof URLClassLoader) { if (isWebapp) { isWebapp = isWebappClassLoader(classLoader); } classPathUrlsToProcess.addAll(Arrays.asList(((URLClassLoader) classLoader).getURLs())); processURLs(scanType, callback, processedURLs, isWebapp, classPathUrlsToProcess); } classLoader = classLoader.getParent(); } if (JreCompat.isJre9Available()) { // The application and platform class loaders are not // instances of URLClassLoader. Use the class path in this // case. addClassPath(classPathUrlsToProcess); // Also add any modules JreCompat.getInstance().addBootModulePath(classPathUrlsToProcess); processURLs(scanType, callback, processedURLs, false, classPathUrlsToProcess); } } private void processURLs(JarScanType scanType, JarScannerCallback callback, Set<URL> processedURLs, boolean isWebapp, Deque<URL> classPathUrlsToProcess) { while (!classPathUrlsToProcess.isEmpty()) { URL url = classPathUrlsToProcess.pop(); if (processedURLs.contains(url)) { // Skip this URL it has already been processed continue; } if (log.isDebugEnabled()) { log.debug(SM.getString("jarScan.classloaderJarScan", url)); } try { processedURLs.add(url); process(scanType, callback, url, null, isWebapp, classPathUrlsToProcess); } catch (IOException ioe) { log.warn(SM.getString("jarScan.classloaderFail", url), ioe); } } } private void addClassPath(Deque<URL> classPathUrlsToProcess) { String classPath = System.getProperty("java.class.path"); if (classPath == null || classPath.length() == 0) { return; } String[] classPathEntries = classPath.split(File.pathSeparator); for (String classPathEntry : classPathEntries) { File f = new File(classPathEntry); try { classPathUrlsToProcess.add(f.toURI().toURL()); } catch (MalformedURLException e) { log.warn(SM.getString("jarScan.classPath.badEntry", classPathEntry), e); } } } /** * Scan a URL for JARs with the optional extensions to look at all files and all directories. */ private void process(JarScanType scanType, JarScannerCallback callback, URL url, String webappPath, boolean isWebapp, Deque<URL> classPathUrlsToProcess) throws IOException { if (log.isTraceEnabled()) { log.trace(SM.getString("jarScan.jarUrlStart", url)); } if (Urls.URL_PROTOCOL_JAR.equals(url.getProtocol()) || url.getPath() .endsWith(TomcatConf.JAR_EXT)) { try (Jar jar = JarFactory.newInstance(url)) { processManifest(jar, isWebapp, classPathUrlsToProcess); callback.scan(jar, webappPath, isWebapp); } } else if (Urls.URL_PROTOCOL_FILE.equals(url.getProtocol())) { processFile(scanType, callback, url, webappPath, isWebapp, classPathUrlsToProcess); } } private void processFile(JarScanType scanType, JarScannerCallback callback, URL url, String webappPath, boolean isWebapp, Deque<URL> classPathUrlsToProcess) throws IOException { try { File f = new File(url.toURI()); if (f.isFile()) { // Treat this file as a JAR try (Jar jar = JarFactory.newInstance(UriUtil.buildJarUrl(f))) { processManifest(jar, isWebapp, classPathUrlsToProcess); callback.scan(jar, webappPath, isWebapp); } } else if (f.isDirectory()) { if (scanType == JarScanType.PLUGGABILITY) { callback.scan(f, webappPath, isWebapp); } else { if (new File(f.getAbsoluteFile(), TomcatConf.META_INF).isDirectory()) { callback.scan(f, webappPath, isWebapp); } } } } catch (URISyntaxException e) { throw new IOException(e); } } private void processManifest(Jar jar, boolean isWebapp, Deque<URL> classPathUrlsToProcess) throws IOException { // Not processed for web application JARs nor if the caller did not provide a Deque of URLs to append to. if (isWebapp || classPathUrlsToProcess == null) { return; } Manifest manifest = jar.getManifest(); if (manifest == null) { return; } Attributes attributes = manifest.getMainAttributes(); String classPathAttribute = attributes.getValue("Class-Path"); if (classPathAttribute == null) { return; } String[] classPathEntries = classPathAttribute.split(" "); for (String classPathEntry : classPathEntries) { classPathEntry = classPathEntry.trim(); if (classPathEntry.length() == 0) { continue; } URL jarURL = jar.getJarFileURL(); try { /* * Note: Resolving the relative URLs from the manifest has the * potential to introduce security concerns. However, since * only JARs provided by the container and NOT those provided * by web applications are processed, there should be no * issues. * If this feature is ever extended to include JARs provided * by web applications, checks should be added to ensure that * any relative URL does not step outside the web application. */ URI classPathEntryUri = jar.getJarFileURL().toURI().resolve(classPathEntry); classPathUrlsToProcess.add(classPathEntryUri.toURL()); } catch (Exception e) { if (log.isDebugEnabled()) { log.debug(SM.getString("jarScan.invalidUri", jarURL), e); } } } } }