[D] Using Custom ClassLoader to Isolate Log42j and Slf4j. Co nsole log: Properties contains an invalid element or attribute "property" [logging-log4j2]

zhanjinhao (via GitHub) <[email protected]>
Newsgroups gmane.comp.jakarta.log4j.devel
Message-ID <[email protected]>
GitHub user zhanjinhao created a discussion: Using Custom ClassLoader to Isolate Log42j and Slf4j. Console log: Properties contains an invalid element or attribute "property"

env:
1. log4j2 version: 2.17.2
2. slf4j version: 1.7.36
3. jdk version: 8
4. os version: win11

### Requirements

I want to develop an Agent to trace service status. To isolate Log42j and Slf4j, I develop a custom classloader which named LogClassLoader. 

The main requirements are:
1. Load jar from specific directory, not the AppClassLoader's classpath
5. Load log4j2.xml from specific direcotry
6. output the msg to the path defined in log4j2.xml.


### Code and Problem

LogClassLoader override loadClass() method. When it works, if the name has prefix about Log4j2 or Slf4j, I will load class from the specific directory. the code about LogClassLoader is copy from apache skywalking AgentClassLoader. The code of LogClassLoader is:

```

/**
 * 用于加载插件和插件的拦截器
 */
public class LogClassLoader extends ClassLoader {

  /**
   * 用于加载日志组件的加载器
   */
  @Getter
  private static LogClassLoader DEFAULT_LOADER;

  /**
   * 自定义类加载器加载类的路径
   */
  private File classpath;
  private List<Jar> allJarList;
  private ReentrantLock jarScanLock = new ReentrantLock();

  public LogClassLoader(ClassLoader parent) {
    super(parent);

    File agentJarDir = AgentPackagePath.getPath();
    System.out.println(String.format("logLibDir: %s", agentJarDir));

    classpath = new File(new File(agentJarDir, "lib"), "log");
    getAllJarList();
  }

  public static void initDefaultLoader() {
    if (DEFAULT_LOADER == null) {
      DEFAULT_LOADER = new LogClassLoader(LogClassLoader.class.getClassLoader());
    }
  }

  private static List<String> logPrefixList = new ArrayList<>();

  static {
    logPrefixList.add("org.slf4j.");
    logPrefixList.add("org.apache.logging.log4j.");
    logPrefixList.add("org.apache.logging.slf4j.");
  }

  @Override
  public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    for (String logPrefix : logPrefixList) {
      if (name.startsWith(logPrefix)) {
        Class<?> aClass = doFindClass(name);
        if (resolve) {
          resolveClass(aClass);
        }
      }
    }

    return super.loadClass(name, resolve);
  }

  /**
   * loadClass --> findClass(自定义自己的类加载逻辑) --> defineClass
   */
  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    return super.findClass(name);
  }

  private Class<?> doFindClass(String name) throws ClassNotFoundException {
    List<Jar> _allJarList = getAllJarList();

    String concat = name.replace(".", "/").concat(".class");
    for (Jar jar : _allJarList) {
      JarEntry jarEntry = jar.jarFile.getJarEntry(concat);
      if (jarEntry == null) {
        continue;
      }
      try {
        URL url = new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + concat);
        byte[] byteArray = IOUtils.toByteArray(url);
        return defineClass(name, byteArray, 0, byteArray.length);
      } catch (Exception e) {
        System.out.println(String.format("find class %s error", name));
      }
    }
    throw new ClassNotFoundException("can not find " + name);
  }

  private List<Jar> getAllJarList() {
    if (allJarList == null) {
      jarScanLock.lock();
      try {
        if (allJarList == null) {
          allJarList = doGetJars();
        }
      } finally {
        jarScanLock.unlock();
      }
    }
    return allJarList;
  }

  private List<Jar> doGetJars() {
    List<Jar> _allJarList = new ArrayList<>();

    if (classpath.exists() && classpath.isDirectory()) {
      String[] list = classpath.list(new FilenameFilter() {
        @Override
        public boolean accept(File dir, String name) {
          return name.endsWith(".jar");
        }
      });
      if (list == null || list.length == 0) {
        return _allJarList;
      }
      for (String s : list) {
        File jarSourceFile = new File(classpath, s);
        try {
          Jar jar = new Jar(new JarFile(jarSourceFile), jarSourceFile);
          _allJarList.add(jar);
          System.out.println(String.format("load jar %s success.", jarSourceFile));
        } catch (Exception e) {
          System.out.println(String.format("jar %s load fail.", jarSourceFile));
          e.printStackTrace();
        }
      }
    }

    return _allJarList;
  }

  private static class Jar {
    /**
     * jar文件对对应的jarFile对象
     */
    private final JarFile jarFile;
    /**
     * jar文件
     */
    private final File sourceFile;

    public Jar(JarFile jarFile, File sourceFile) {
      this.jarFile = jarFile;
      this.sourceFile = sourceFile;
    }
  }

}
```

My log4j2.xml is :

```
<?xml version="1.0" encoding="UTF-8"?>
<configuration monitorInterval="5">
  <!--变量配置-->
  <Properties>
    <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
    <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
    <property name="LOG_PATTERN">external3333 - %date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</property>
    <!-- 定义日志存储的路径 -->
    <property name="FILE_PATH">./agent_logs</property>
    <property name="FILE_NAME">agentLogs</property>
  </Properties>
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <!--设置日志格式及颜色-->
      <PatternLayout
              pattern="external3333 - %date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
              disableAnsi="false" noConsoleNoAnsi="false"/>
    </Console>
    <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
    <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">
      <PatternLayout pattern="${LOG_PATTERN}"/>
    </File>
    <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
    <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log"
                 filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
      <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
      <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="${LOG_PATTERN}"/>
      <Policies>
        <!--interval属性用来指定多久滚动一次,默认是1 hour-->
        <TimeBasedTriggeringPolicy interval="1"/>
        <SizeBasedTriggeringPolicy size="10MB"/>
      </Policies>
      <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
      <DefaultRolloverStrategy max="15"/>
    </RollingFile>
    <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
    <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log"
                 filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
      <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
      <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="${LOG_PATTERN}"/>
      <Policies>
        <!--interval属性用来指定多久滚动一次,默认是1 hour-->
        <TimeBasedTriggeringPolicy interval="1"/>
        <SizeBasedTriggeringPolicy size="10MB"/>
      </Policies>
      <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
      <DefaultRolloverStrategy max="15"/>
    </RollingFile>
    <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
    <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log"
                 filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
      <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
      <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="${LOG_PATTERN}"/>
      <Policies>
        <!--interval属性用来指定多久滚动一次,默认是1 hour-->
        <TimeBasedTriggeringPolicy interval="1"/>
        <SizeBasedTriggeringPolicy size="10MB"/>
      </Policies>
      <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
      <DefaultRolloverStrategy max="15"/>
    </RollingFile>
  </Appenders>
  <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
  <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
  <loggers>
    <root level="info">
      <appender-ref ref="Console"/>
      <appender-ref ref="Filelog"/>
      <appender-ref ref="RollingFileInfo"/>
      <appender-ref ref="RollingFileWarn"/>
      <appender-ref ref="RollingFileError"/>
    </root>
  </loggers>
</configuration>
```

In order to load jar and log4j2.xml from specific directory, I use Class.forName(className, true, LogClassLoader) to load class. And I use reflection api to invoke getContext(ClassLoader.class, boolean.class, URI.class) method to load log4j2.xml, which avoid use the configuration in AppClassLoader's classpath. And the code is: 

```
public class MyLoggerFactory {

  static Class<?> loggerFactoryClass;
  // 添加Logger接口的引用
  private static Class<?> loggerInterface;

  static Method getLoggerString;
  static Method getLoggerClass;

  static Class<?> logManagerClass;
  static Class<?> loggerContextClass;

  static {
    try {
      LogClassLoader.initDefaultLoader();
      LogClassLoader defaultLoader = LogClassLoader.getDEFAULT_LOADER();

      ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
      Thread.currentThread().setContextClassLoader(defaultLoader);

      try {
        loggerFactoryClass = Class.forName("org.slf4j.LoggerFactory", true, defaultLoader);
        loggerInterface = Class.forName("org.slf4j.Logger", true, defaultLoader);
        logManagerClass = Class.forName("org.apache.logging.log4j.LogManager", true, defaultLoader);
        loggerContextClass = Class.forName("org.apache.logging.log4j.core.LoggerContext", true, defaultLoader);

        getLoggerString = loggerFactoryClass.getDeclaredMethod("getLogger", String.class);
        getLoggerClass = loggerFactoryClass.getDeclaredMethod("getLogger", Class.class);

        File agentJarDir = AgentPackagePath.getPath();

        File conFile = new File(new File(new File(AgentPackagePath.getPath(), "lib"), "log"), "log4j2.xml");

//        ConfigurationSource source = new ConfigurationSource(new FileInputStream(conFile), conFile);
//        Configuration configuration = new XmlConfiguration(null, source);
//        Configurator.initialize(configuration);

        // config dist/lib/log/log4j2.xml
        Method getContextMethod = logManagerClass.getDeclaredMethod("getContext", ClassLoader.class, boolean.class, URI.class);
        Object logContext = getContextMethod.invoke(null, null, false, conFile.toURI());
      } finally {
        Thread.currentThread().setContextClassLoader(contextClassLoader);
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static Logger getLogger(Class<?> clazz) {
    try {
      Object invoke = getLoggerClass.invoke(null, clazz);

      // 使用代理模式包装返回的对象,避免类型转换问题
      if (invoke != null) {
        return createLoggerProxy(invoke);
      }
      throw new RuntimeException(String.format("Failed to create logger %s instance", clazz));
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  // 创建代理Logger实例来避免类加载器问题
  private static Logger createLoggerProxy(Object loggerInstance) {
    // 实现代理逻辑,将方法调用转发给实际的logger实例
    // 这样可以绕过类加载器类型检查问题
    return (Logger) Proxy.newProxyInstance(
            MyLoggerFactory.class.getClassLoader(),
            new Class[]{Logger.class},
            new InvocationHandler() {
              @Override
              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 在目标对象上查找匹配的方法
                Method targetMethod = loggerInstance.getClass().getMethod(
                        method.getName(), method.getParameterTypes());
                return targetMethod.invoke(loggerInstance, args);
              }
            }
    );
  }

}

```

When it works, I find it can load jar and log4j2.xml from the specific directory. 
**BUT, it can not output the msg to the path defined in property of configuration!**

 The result is it cannot replace the variables of Appenders , although the variables  are defined in properties element.

<img width="1899" height="805" alt="image" src="https://github.com/user-attachments/assets/28bb8f45-2a7d-493b-b50a-dfbc0bb52bbd" />


The config can work as expected in springboot's project. 

The full demo project is: https://github.com/[zhanjinhao/log-learn](https://github.com/zhanjinhao/log-learn) and it can reproduce the problem. **Run Application in springboot way, the console will output "Properties contains an invalid element or attribute "property"" and the file will be created in ${FILE_PATH} not in "agent_logs"**


### DEBUG

I try to debug the code to find some reason.

in org.apache.logging.log4j.core.config.AbstractConfiguration#doConfigure, the children of Properties is empty.

<img width="2182" height="1459" alt="image" src="https://github.com/user-attachments/assets/c2b201c3-5565-4f59-98bb-afbee984083f" />

In springboot project, the children is an List which contains three children.

<img width="2346" height="1405" alt="image" src="https://github.com/user-attachments/assets/3fd81366-32d5-4fcf-9158-ca81fa4456a7" />

But i don't know why the children are different. I don't have knowledge about xml parsing. 

Can somebody give me some advice! Thank a lot.



GitHub link: https://github.com/apache/logging-log4j2/discussions/3960

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.