log4j2 如何识别自定义的日志组件

在 Log4j2 中,要识别和使用自定义的日志组件,需要进行以下步骤:

  1. 创建自定义的日志组件:首先,你需要创建一个实现 org.apache.logging.log4j.core.Appender 接口的类来定义自定义的日志组件。

该接口定义了日志记录事件的处理方法。你可以根据自己的需求自定义日志组件的行为和功能。

  1. 配置 Log4j2:接下来,你需要在 Log4j2 的配置文件中添加对自定义日志组件的配置。配置文件通常是一个 XML 文件,其中定义了日志记录器(Logger)、日志级别(Level)、Appender(日志组件)等元素。在配置文件中,你需要添加一个对自定义日志组件的配置。

    <Configuration>
      <!-- 其他配置 -->
       
      <Appenders>
        <!-- 添加自定义日志组件的配置 -->
        <CustomAppender name="custom" class="com.example.CustomAppender">
          <!-- 自定义配置项 -->
        </CustomAppender>
           
        <!-- 其他 Appenders 的配置 -->
      </Appenders>
       
      <!-- 其他配置 -->
         
      <Loggers>
        <!-- 添加日志记录器的配置 -->
        <Root level="info">
          <!-- 关联自定义日志组件 -->
          <AppenderRef ref="custom" />
        </Root>
           
        <!-- 其他 Loggers 的配置 -->
      </Loggers>
         
      <!-- 其他配置 -->
    </Configuration>
    

    在上面的示例中,CustomAppender 是自定义日志组件的类名,custom 是自定义日志组件的名称(用于引用),你需要将这些值替换为你自己的类名和名称。

  2. 使用自定义日志组件:完成上述配置后,你就可以在代码中使用 Log4j2 记录日志了。通过获取 Logger 对象并使用相应的日志级别调用适当的方法,你可以记录日志,并且该日志将由自定义的日志组件处理。

    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
       
    public class MyClass {
      private static final Logger logger = LogManager.getLogger(MyClass.class);
       
      public void doSomething() {
        logger.info("This is a log message.");
      }
    }
    

在上面的示例中,LogManager.getLogger(MyClass.class) 用于获取名为 “MyClass” 的 Logger 对象。你可以根据需要在代码的不同位置获取不同的 Logger 对象。

通过以上步骤,你应该能够成功识别和使用自定义的日志组件。请确保在 Log4j2 的配置文件中正确地配置和引用自定义日志组件,并在代码中使用适当的 Logger 对象记录日志。

@Plugin 自定义的插件如何自定义实现呢?

抱歉,我误解了你的问题。如果你要识别和使用基于 @Plugin 注解自定义的插件,以下是一些步骤:

  1. 创建自定义插件:首先,你需要创建一个类来实现你的自定义插件逻辑。这个类需要使用 @Plugin 注解进行标记,同时实现 org.apache.logging.log4j.core.Appender 接口或其他相关接口,具体取决于你要实现的插件类型。你还可以使用其他注解,如 @PluginFactory@PluginAttribute 等来配置和自定义你的插件。

    import org.apache.logging.log4j.core.Appender;
    import org.apache.logging.log4j.core.Layout;
    import org.apache.logging.log4j.core.config.plugins.Plugin;
    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
       
    @Plugin(name = "CustomAppender", category = "Core", elementType = "appender", printObject = true)
    public class CustomAppender implements Appender {
       
      // 插件实现代码
       
      @PluginFactory
      public static CustomAppender createAppender(@PluginAttribute("name") String name) {
        // 插件工厂方法代码
      }
       
      // 其他插件方法和逻辑
    }
    

    在上面的示例中,@Plugin 注解用于定义插件的名称、类别和元素类型等信息。@PluginFactory 注解用于定义插件的工厂方法,它会在配置文件中使用。你可以根据自己的需求和插件类型来自定义插件的方法和逻辑。

  2. 配置 Log4j2:在 Log4j2 的配置文件中,你需要添加对自定义插件的配置。配置文件可以是 XML、JSON 或其他格式,具体取决于你的配置偏好。在配置文件中,你需要使用 @Plugin 注解中定义的名称来引用自定义插件。

    <Configuration>
      <!-- 其他配置 -->
       
      <Appenders>
        <!-- 添加自定义插件的配置 -->
        <CustomAppender name="custom" otherAttributes="value">
          <!-- 自定义配置项 -->
        </CustomAppender>
           
        <!-- 其他 Appenders 的配置 -->
      </Appenders>
       
      <!-- 其他配置 -->
         
      <Loggers>
        <!-- 添加日志记录器的配置 -->
        <Root level="info">
          <!-- 关联自定义插件 -->
          <AppenderRef ref="custom" />
        </Root>
           
        <!-- 其他 Loggers 的配置 -->
      </Loggers>
         
      <!-- 其他配置 -->
    </Configuration>
    

    在上面的示例中,CustomAppender 是自定义插件的名称,custom 是自定义插件的引用名称。你需要将这些值替换为你自己的名称和引用。

  3. 使用自定义插件:完成上述配置后,你就可以在代码中使用 Log4j2 记录日志了。通过获取 Logger 对象并使用相应的日志级别调用适当的方法,你可以记录日志,并且该日志将由自定义插件处理。

    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
       
    public class MyClass {
      private static final Logger logger = LogManager.getLogger(MyClass.class);
       
      public void doSomething() {
        logger.info("This is a log message.");
      }
    }
    

在上面的示例中,LogManager.getLogger(MyClass.class) 用于获取名为 “MyClass” 的 Logger 对象。你可以根据需要在代码的不同位置获取不同的 Logger 对象。

通过以上步骤,你应该能够成功识别和使用基于 @Plugin 注解自定义的插件。请确保在 Log4j2 的配置文件中正确地配置和引用自定义插件,并在代码中使用适当的 Logger 对象记录日志。

log4j2 是如何扫描解析这些 @plugin 组件的?

在 Log4j2 中,扫描和解析 @Plugin 组件是通过插件管理器(PluginManager)来完成的。

插件管理器负责加载和管理所有 Log4j2 插件,包括通过 @Plugin 注解定义的自定义组件。

当 Log4j2 启动时,它会自动扫描类路径上的插件,并将它们注册到插件管理器中。 插件管理器会根据类路径中的 META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat 文件来定位和加载插件。这个文件列出了所有可用插件的类名。

对于自定义的 @Plugin 组件,你需要确保以下几点:

  1. 插件类的包名:自定义的插件类必须位于 org.apache.logging.log4j.core.config.plugins 包或其子包下。

  2. 插件类的 @Plugin 注解:确保自定义的插件类上有正确的 @Plugin 注解。@Plugin 注解的参数指定了插件的名称、类别和元素类型等信息。

  3. 插件类的注册:插件类必须在 META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat 文件中注册。这可以通过 Maven 插件或手动操作来完成。这个文件是一个文本文件,每一行包含一个插件类的完整类名。

插件管理器会在 Log4j2 启动时读取 Log4j2Plugins.dat 文件并加载所有的插件类。一旦插件被加载,它们就可以在 Log4j2 配置文件中使用了。

总结起来,Log4j2 通过插件管理器扫描和解析 @Plugin 组件。插件管理器会读取类路径上的 Log4j2Plugins.dat 文件,并加载其中列出的所有插件类。自定义的插件类必须符合要求,并在 Log4j2Plugins.dat 文件中注册,才能被 Log4j2 识别和使用。

可是我自定义 @Plugin 时,并没有在 Log4j2Plugins.dat 文件中注册

如果你的自定义 @Plugin 没有在 Log4j2Plugins.dat 文件中注册,Log4j2 将无法自动扫描和加载该插件。在这种情况下,你需要通过其他方式手动注册你的自定义插件。

有两种常用的手动注册插件的方法:

  1. 使用 packages 属性:在 Log4j2 的配置文件中,你可以使用 packages 属性指定要扫描的包名,使 Log4j2 可以扫描到自定义插件。在 Configuration 元素中添加 packages 属性,并指定包名列表,用逗号或分号分隔。

    <Configuration packages="com.example.plugins">
      <!-- 配置项 -->
    </Configuration>
    

    在上面的示例中,Log4j2 将扫描 com.example.plugins 包及其子包中的插件类。

  2. 使用 PluginManager 手动注册:在代码中,你可以使用 PluginManager 手动注册自定义插件。通过获取 PluginManager 对象,并调用 addPackageaddPlugin 方法来注册插件。

    import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
       
    PluginManager pluginManager = new PluginManager();
    pluginManager.addPackage("com.example.plugins");
    // 或者使用 pluginManager.addPlugin(Class<? extends Plugin> pluginClass) 方法
       
    // 其他配置和初始化步骤
       
    pluginManager.collectPlugins();
    

    在上面的示例中,addPackage 方法用于注册指定包及其子包中的插件,addPlugin 方法用于注册单个插件类。最后,调用 collectPlugins 方法来收集和初始化插件。

通过以上方法之一手动注册自定义插件,Log4j2 将能够识别和使用你的插件,即使它没有在 Log4j2Plugins.dat 文件中注册。请确保在配置文件中配置正确的包名或在代码中正确注册插件。

详细解释下 log4j2 中的 ResolverUtil 类

ResolverUtil 类是 Log4j2 中的一个实用工具类,用于在类路径上扫描和查找类。

它提供了一种方便的方式来自动发现和加载类,特别适用于插件系统和动态类加载的场景。

以下是对 ResolverUtil 类的详细解释:

  1. 扫描指定包及其子包:ResolverUtil 可以递归地扫描指定包及其子包中的类。通过调用 ResolverUtilfindInPackage 方法并传递包名作为参数,可以扫描并查找符合条件的类。

    ResolverUtil<MyClass> resolver = new ResolverUtil<>();
    resolver.findInPackage("com.example.plugins");
    Set<Class<? extends MyClass>> classes = resolver.getClasses();
    

    在上面的示例中,findInPackage 方法将扫描 com.example.plugins 包及其子包中的类,并将符合条件的类保存在 classes 集合中。

  2. 自定义条件过滤器:ResolverUtil 允许你自定义条件过滤器来过滤扫描到的类。你可以通过实现 ResolverUtil.Test 接口并将其传递给 ResolverUtiladdIfMatching 方法,来定义过滤条件。

    ResolverUtil<MyClass> resolver = new ResolverUtil<>();
    resolver.addIfMatching(new ResolverUtil.Test() {
      @Override
      public boolean matches(Class<?> type) {
        // 自定义条件过滤逻辑
        return type.isAnnotationPresent(MyAnnotation.class);
      }
    });
    resolver.findInPackage("com.example.plugins");
    Set<Class<? extends MyClass>> classes = resolver.getClasses();
    

    在上面的示例中,通过实现 ResolverUtil.Test 接口并在 matches 方法中定义条件过滤逻辑,只有带有 MyAnnotation 注解的类才会被匹配。

  3. 支持自定义类加载器:ResolverUtil 支持指定自定义的类加载器来加载类。通过调用 ResolverUtilsetClassLoader 方法并传递类加载器作为参数,可以指定特定的类加载器。

    ResolverUtil<MyClass> resolver = new ResolverUtil<>();
    resolver.setClassLoader(customClassLoader);
    resolver.findInPackage("com.example.plugins");
    Set<Class<? extends MyClass>> classes = resolver.getClasses();
    

    在上面的示例中,setClassLoader 方法用于设置使用 customClassLoader 加载类。

通过 ResolverUtil 类,你可以方便地扫描指定包及其子包中的类,并根据自定义的条件过滤器过滤匹配的类。这在 Log4j2 的插件系统中非常有用,可以用于自动发现和加载插件类。

log4j2 是如何实现 gc free 的

Log4j 2.x 在设计上考虑了减少垃圾回收(GC)的影响,并采取了一些策略来实现 GC-free。

下面是 Log4j 2.x 实现 GC-free 的一些关键方法:

  1. 使用可重用的数据结构:Log4j 2.x 尽量使用可重用的数据结构,避免频繁创建和销毁对象。例如,LogEvent 对象被设计为可重用的,通过 ThreadLocal 方式存储和获取,避免了创建大量临时对象。

  2. 使用池化技术:Log4j 2.x 使用了对象池化技术,将常用的对象进行预先分配和缓存,以便在需要时进行重复使用。这减少了对象的创建和垃圾回收的频率。

  3. 优化字符串拼接:Log4j 2.x 使用了一种名为 ParameterizedMessage 的消息格式化机制,它使用参数占位符和参数数组,避免了字符串拼接的开销。这样,只有在需要实际输出日志时,才进行字符串拼接操作。

  4. 减少对象分配:Log4j 2.x 在实现中尽量减少不必要的对象分配。它使用了一些技术,如复用对象、使用局部变量等,来避免创建不必要的临时对象。

  5. 使用低GC的数据结构:Log4j 2.x 使用了一些低GC的数据结构,如基于数组的环形缓冲区和无锁队列,以提高性能并减少垃圾回收的影响。

这些方法的结合使用使 Log4j 2.x 在运行时尽量减少垃圾回收的频率和开销,从而提高了性能和效率。

尽管 Log4j 2.x 在设计上考虑了 GC-free,但在实际使用中,仍然需要根据具体情况进行配置和优化,以获得最佳性能和资源利用。

log4j2 如果在使用中追求极致的性能,有哪些最佳实践的建议?

如果你追求 Log4j 2.x 在使用中的极致性能,以下是一些最佳实践的建议:

  1. 使用异步日志记录器(Async Loggers):Async Loggers 是 Log4j 2.x 引入的一种机制,可以在日志记录过程中异步地执行一些操作,如日志写入磁盘。通过使用 Async Loggers,可以将日志记录过程与应用程序的主线程解耦,提高性能和吞吐量。

    配置 Async Loggers 需要使用 <AsyncLogger> 元素来替代 <Logger> 元素,并使用适当的属性来配置线程池、队列大小等参数。

  2. 避免频繁的日志级别判断:在日志记录的代码中,避免频繁地进行日志级别判断。尽量将日志级别判断放在关键路径的外部,以避免不必要的开销。

  3. 配置合适的 Appender:选择合适的 Appender 可以对性能产生影响。对于高吞吐量的场景,可以考虑使用性能较好的 Appender,如 AsyncAppender、RollingFileAppender 等。

  4. 禁用不必要的功能和输出:仔细检查 Log4j 2.x 的配置,禁用不必要的功能和输出,以减少额外的开销。例如,禁用不必要的日志记录器、过滤器、布局等。

  5. 优化日志消息的构建:构建日志消息时,尽量减少字符串拼接的开销。使用 ParameterizedMessageLogger.printf 等方式,避免频繁的字符串拼接操作。

  6. 配置适当的日志级别:在生产环境中,使用适当的日志级别来减少日志的输出量。过多的低级别日志可能会影响性能,因此只记录必要的关键信息。

  7. 定期审查和优化配置:定期审查 Log4j 2.x 的配置,查找和消除性能瓶颈。尝试使用不同的配置选项,并进行基准测试和性能评估,以确定最佳配置。

  8. 使用适当的日志库版本:确保使用最新版本的 Log4j 2.x,其中包含了性能改进和 bug 修复。随着 Log4j 的不断发展,新版本通常会带来更好的性能和稳定性。

请注意,这些最佳实践的适用性可能会因应用程序的特定需求和环境而有所不同。最佳实践的选择和调整应该根据实际情况进行。

对于极致性能的追求,还可以结合实际的基准测试和性能评估,以确定最佳的配置和优化策略。

在实现日志脱敏时,自定义实现 PatternLayout 和 Rewrite 都可以。二者在性能上有什么区别吗?

在实现日志脱敏时,自定义实现 PatternLayout 和 Rewrite 都可以用作日志输出格式化和修改的手段。它们在性能上有一些区别。

  1. PatternLayout:PatternLayout 是 Log4j 2 提供的一种日志输出格式化工具,通过指定模式字符串来定义日志消息的格式。在自定义 PatternLayout 实现日志脱敏时,你可以编写自定义的转换符来对敏感数据进行脱敏处理。

    性能方面,PatternLayout 的性能通常是比较高的。一旦模式字符串被解析,它可以高效地格式化日志消息,因为它直接将消息转换为字符串,并应用相应的转换符。

  2. Rewrite:Rewrite 是 Log4j 2 提供的一种日志重写工具,它可以在日志记录过程中修改、替换或删除日志事件的某些属性。你可以编写自定义的 RewritePolicy 来实现日志脱敏。

    性能方面,Rewrite 的性能取决于所使用的 RewritePolicy 的实现。一些复杂的 RewritePolicy 可能会导致性能下降,尤其是在处理大量日志事件时。因此,对于性能要求较高的场景,需要注意 RewritePolicy 的实现方式,避免引入不必要的性能开销。

综合来看,PatternLayout 的性能通常较好,因为它直接在日志输出过程中进行格式化

而 Rewrite 的性能则取决于所使用的 RewritePolicy 的实现复杂性和效率。在实际使用中,建议根据具体需求和性能要求选择合适的方式来实现日志脱敏,并进行性能测试和评估,以确保所选方案在特定场景下具备足够的性能。

ResolverUtil 源码解析

核心方法

/**
 * Scans for classes starting at the package provided and descending into subpackages. Each class is offered up to
 * the Test as it is discovered, and if the Test returns true the class is retained. Accumulated classes can be
 * fetched by calling {@link #getClasses()}.
 *
 * @param test
 *        an instance of {@link Test} that will be used to filter classes
 * @param packageName
 *        the name of the package from which to start scanning for classes, e.g. {@code net.sourceforge.stripes}
 */
public void findInPackage(final Test test, String packageName) {

其中的 Test 接口:

/**
 * A simple interface that specifies how to test classes to determine if they are to be included in the results
 * produced by the ResolverUtil.
 */
public interface Test {
    /**
     * Will be called repeatedly with candidate classes. Must return True if a class is to be included in the
     * results, false otherwise.
     *
     * @param type
     *        The Class to match against.
     * @return true if the Class matches.
     */
    boolean matches(Class<?> type);
    /**
     * Test for a resource.
     *
     * @param resource
     *        The URI to the resource.
     * @return true if the resource matches.
     */
    boolean matches(URI resource);

    boolean doesMatchClass();

    boolean doesMatchResource();
}

工具方法

public void findInPackage(final Test test, String packageName) {
        // 包名替换为路径
        packageName = packageName.replace('.', '/');

        // 获取类加载器
        final ClassLoader loader = getClassLoader();
        Enumeration<URL> urls;

        try {
          // 加载资源
            urls = loader.getResources(packageName);
        } catch (final IOException ioe) {
            LOGGER.warn("Could not read package: {}", packageName, ioe);
            return;
        }

        while (urls.hasMoreElements()) {
            try {
                final URL url = urls.nextElement();
                final String urlPath = extractPath(url);

                LOGGER.info("Scanning for classes in '{}' matching criteria {}", urlPath , test);
                // Check for a jar in a war in JBoss
                if (VFSZIP.equals(url.getProtocol())) {
                    final String path = urlPath.substring(0, urlPath.length() - packageName.length() - 2);
                    final URL newURL = new URL(url.getProtocol(), url.getHost(), path);
                    @SuppressWarnings("resource")
                    final JarInputStream stream = new JarInputStream(newURL.openStream());
                    try {
                        loadImplementationsInJar(test, packageName, path, stream);
                    } finally {
                        close(stream, newURL);
                    }
                } else if (VFS.equals(url.getProtocol())) {
                    final String containerPath = urlPath.substring(1, urlPath.length() - packageName.length() - 2);
                    final File containerFile = new File(containerPath);
                    if (containerFile.exists()) {
                        if (containerFile.isDirectory()) {
                            loadImplementationsInDirectory(test, packageName, new File(containerFile, packageName));
                        } else {
                            loadImplementationsInJar(test, packageName, containerFile);
                        }
                    } else {
                        // fallback code for Jboss/Wildfly, if the file couldn't be found
                        // by loading the path as a file, try to read the jar as a stream
                        final String path = urlPath.substring(0, urlPath.length() - packageName.length() - 2);
                        final URL newURL = new URL(url.getProtocol(), url.getHost(), path);

                        try (final InputStream is = newURL.openStream()) {
                            final JarInputStream jarStream;
                            if (is instanceof JarInputStream) {
                                jarStream = (JarInputStream) is;
                            } else {
                                jarStream = new JarInputStream(is);
                            }
                            loadImplementationsInJar(test, packageName, path, jarStream);
                        }
                    }
                } else if (BUNDLE_RESOURCE.equals(url.getProtocol())) {
                    loadImplementationsInBundle(test, packageName);
                } else if (JAR.equals(url.getProtocol())) {
                    loadImplementationsInJar(test, packageName, url);
                } else {
                    final File file = new File(urlPath);
                    if (file.isDirectory()) {
                        loadImplementationsInDirectory(test, packageName, file);
                    } else {
                        loadImplementationsInJar(test, packageName, file);
                    }
                }
            } catch (final IOException | URISyntaxException ioe) {
                LOGGER.warn("Could not read entries", ioe);
            }
        }
    }

extractPath-提取路径

String extractPath(final URL url) throws UnsupportedEncodingException, URISyntaxException {
        String urlPath = url.getPath(); // same as getFile but without the Query portion
        // System.out.println(url.getProtocol() + "->" + urlPath);

        // I would be surprised if URL.getPath() ever starts with "jar:" but no harm in checking
        if (urlPath.startsWith("jar:")) {
            urlPath = urlPath.substring(4);
        }
        // For jar: URLs, the path part starts with "file:"
        if (urlPath.startsWith("file:")) {
            urlPath = urlPath.substring(5);
        }
        // If it was in a JAR, grab the path to the jar
        final int bangIndex = urlPath.indexOf('!');
        if (bangIndex > 0) {
            urlPath = urlPath.substring(0, bangIndex);
        }

        // LOG4J2-445
        // Finally, decide whether to URL-decode the file name or not...
        final String protocol = url.getProtocol();
        final List<String> neverDecode = Arrays.asList(VFS, VFSZIP, BUNDLE_RESOURCE);
        if (neverDecode.contains(protocol)) {
            return urlPath;
        }
        final String cleanPath = new URI(urlPath).getPath();
        if (new File(cleanPath).exists()) {
            // if URL-encoded file exists, don't decode it
            return cleanPath;
        }
        return URLDecoder.decode(urlPath, StandardCharsets.UTF_8.name());
    }

loadImplementationsInDirectory

  /**
     * Finds matches in a physical directory on a file system. Examines all files within a directory - if the File object
     * is not a directory, and ends with <i>.class</i> the file is loaded and tested to see if it is acceptable
     * according to the Test. Operates recursively to find classes within a folder structure matching the package
     * structure.
     *
     * @param test
     *        a Test used to filter the classes that are discovered
     * @param parent
     *        the package name up to this directory in the package hierarchy. E.g. if /classes is in the classpath and
     *        we wish to examine files in /classes/org/apache then the values of <i>parent</i> would be
     *        <i>org/apache</i>
     * @param location
     *        a File object representing a directory
     */
    private void loadImplementationsInDirectory(final Test test, final String parent, final File location) {
        final File[] files = location.listFiles();
        if (files == null) {
            return;
        }

        StringBuilder builder;
        for (final File file : files) {
            builder = new StringBuilder();
            builder.append(parent).append('/').append(file.getName());
            final String packageOrClass = parent == null ? file.getName() : builder.toString();

            if (file.isDirectory()) {
                loadImplementationsInDirectory(test, packageOrClass, file);
            } else if (isTestApplicable(test, file.getName())) {
                addIfMatching(test, packageOrClass);
            }
        }
    }

loadImplementationsInJar

  /**
     * Finds matching classes within a jar files that contains a folder structure matching the package structure. If the
     * File is not a JarFile or does not exist a warning will be logged, but no error will be raised.
     *
     * @param test
     *        a Test used to filter the classes that are discovered
     * @param parent
     *        the parent package under which classes must be in order to be considered
     * @param url
     *        the url that identifies the jar containing the resource.
     */
    private void loadImplementationsInJar(final Test test, final String parent, final URL url) {
        JarURLConnection connection = null;
        try {
            connection = (JarURLConnection) url.openConnection();
            if (connection != null) {
                // A "jar:" URL file remains open after the stream is closed, so do not cache it.
                connection.setUseCaches(false);
                try (final JarFile jarFile = connection.getJarFile()) {
                    final Enumeration<JarEntry> entries = jarFile.entries();
                    while (entries.hasMoreElements()) {
                        final JarEntry entry = entries.nextElement();
                        final String name = entry.getName();
                        if (!entry.isDirectory() && name.startsWith(parent) && isTestApplicable(test, name)) {
                            addIfMatching(test, name);
                        }
                    }
                }
            } else {
                LOGGER.error("Could not establish connection to {}", url.toString());
            }
        } catch (final IOException ex) {
            LOGGER.error("Could not search JAR file '{}' for classes matching criteria {}, file not found",
                url.toString(), test, ex);
        }
    }

参考资料

https://blog.csdn.net/blue_driver/article/details/125007794