Add unit tests
diff --git a/README.adoc b/README.adoc
index e8df97e..eb361d2 100644
--- a/README.adoc
+++ b/README.adoc
@@ -30,7 +30,7 @@
     while (i < args.length) {
       if ("--logLevel".equals(args[i]) && ++i < args.length) {
         LoggingAdmin admin = LoggingAdmin.getInstance(TOKEN);
-        admin.setLoggerLevel("", args[i]);
+        admin.setLevel("", args[i]);
       }
       i++;
     }
diff --git a/pom.xml b/pom.xml
index ac6eef5..93fd52e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,12 +70,44 @@
     <!-- project version -->
     <revision>0.1.0-SNAPSHOT</revision>
 
+    <!-- Ignore BND Baseline until the first release -->
+    <bnd.baseline.skip>true</bnd.baseline.skip>
+
+    <bnd-module-name>org.apache.logging.admin</bnd-module-name>
+    <bnd-extra-package-options>
+      <!-- Optional dependencies -->
+      ch.qos.logback.*;resolution:=optional,
+      java.util.logging;resolution:=optional,
+      org.slf4j;resolution:=optional,
+      org.apache.logging.log4j.*;resolution:=optional,
+    </bnd-extra-package-options>
+
     <!-- disable `maven-site-plugin`-->
     <maven.site.skip>true</maven.site.skip>
     <maven.site.deploy.skip>true</maven.site.deploy.skip>
 
   </properties>
 
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.assertj</groupId>
+        <artifactId>assertj-bom</artifactId>
+        <version>3.26.3</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>org.junit</groupId>
+        <artifactId>junit-bom</artifactId>
+        <version>5.11.3</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
 
     <dependency>
@@ -100,12 +132,6 @@
     </dependency>
 
     <dependency>
-      <groupId>com.github.spotbugs</groupId>
-      <artifactId>spotbugs-annotations</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
       <groupId>org.jspecify</groupId>
       <artifactId>jspecify</artifactId>
       <version>1.0.0</version>
@@ -125,5 +151,87 @@
       <optional>true</optional>
     </dependency>
 
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
+
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <executions>
+          <!-- Disable default execution to properly name each execution -->
+          <execution>
+            <id>default-test</id>
+            <phase>none</phase>
+          </execution>
+          <execution>
+            <id>jul-test</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <classpathDependencyExcludes>
+                <exclude>ch.qos.logback:*</exclude>
+                <exclude>org.apache.logging.log4j:*</exclude>
+                <exclude>org.slf4j:*</exclude>
+              </classpathDependencyExcludes>
+              <systemPropertyVariables>
+                <admin.implementation>jul</admin.implementation>
+                <java.util.logging.config.file>${project.basedir}/src/test/resources/logging.properties</java.util.logging.config.file>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+          <execution>
+            <id>log4j-core-test</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <classpathDependencyExcludes>
+                <exclude>ch.qos.logback:*</exclude>
+                <exclude>org.slf4j:*</exclude>
+              </classpathDependencyExcludes>
+              <systemPropertyVariables>
+                <admin.implementation>log4j-core</admin.implementation>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+          <execution>
+            <id>logback-test</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <classpathDependencyExcludes>
+                <exclude>org.apache.logging.log4j:*</exclude>
+              </classpathDependencyExcludes>
+              <systemPropertyVariables>
+                <admin.implementation>logback</admin.implementation>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
 </project>
diff --git a/src/main/java/org/apache/logging/admin/LoggingAdmin.java b/src/main/java/org/apache/logging/admin/LoggingAdmin.java
index ce2be20..0954b05 100644
--- a/src/main/java/org/apache/logging/admin/LoggingAdmin.java
+++ b/src/main/java/org/apache/logging/admin/LoggingAdmin.java
@@ -16,11 +16,11 @@
  */
 package org.apache.logging.admin;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
-import org.apache.logging.admin.internal.FactoryHolder;
+import org.apache.logging.admin.internal.FactoryUtil;
 import org.jspecify.annotations.Nullable;
 
 /**
@@ -48,7 +48,7 @@
  *       while (i < args.length) {
  *         if ("--logLevel".equals(args[i]) && ++i < args.length) {
  *           LoggingAdmin admin = LoggingAdmin.getInstance(TOKEN);
- *           admin.setLoggerLevel("", args[i]);
+ *           admin.setLevel("", args[i]);
  *         }
  *         i++;
  *       }
@@ -57,26 +57,23 @@
  * </pre>
  */
 public interface LoggingAdmin {
+    /**
+     * Implementation independent name for the root logger.
+     */
+    String ROOT_LOGGER_NAME = "";
 
     /**
-     * The logger context that this interface will be configuring.
-     *
-     * @return A logger context.
+     * The names of the available log levels in decreasing severity order.
      */
-    Object getLoggerContext();
-
-    /**
-     * The names of the available log levels.
-     */
-    Set<String> getSupportedLevels();
+    List<String> getSupportedLevels();
 
     /**
      * A map associating logger names with the configured log levels.
      * <p>
-     *   Loggers that inherit their configuration from the parent logger will be associated with {@link Optional#empty()}.
+     *   Loggers that inherit their configuration from the parent logger will be associated with {@code null}.
      * </p>
      */
-    Map<String, Optional<String>> getLoggerLevels();
+    Map<String, @Nullable String> getLevels();
 
     /**
      * The configured log level for the given logger.
@@ -85,7 +82,8 @@
      * </p>
      * @param loggerName The name of the logger.
      */
-    Optional<String> getLoggerLevel(String loggerName);
+    @Nullable
+    String getLevel(String loggerName);
 
     /**
      * Sets the level for a logger.
@@ -93,41 +91,21 @@
      * @param loggerName The name of the logger.
      * @param level The level to use or {@code null} to inherit the level of the parent logger.
      */
-    void setLoggerLevel(String loggerName, @Nullable String level);
+    void setLevel(String loggerName, @Nullable String level);
 
     /**
-     * Retrieves the logging configuration admin for the given classloader.
+     * Retrieves the logging configuration admin appropriate for the caller
      * <p>
-     *   If {@code token} is not null, it also sets a security token for the appropriate logger context.
+     *   The {@code token} parameter sets a security token for the appropriate logger context.
      *   All future invocations of this method will need to use the same token.
      *   Tokens are compared using object equality.
      * </p>
-     * @param token The security token.
-     * @param classLoader A class loader.
+     * @param token Any Java object.
      * @return A logging configuration admin.
-     * @throws IllegalStateException If no implementation of the Apache Logging Admin API is present on the classpath.
-     * @throws SecurityException If a security token is set for the associated logger context and the provided token
-     * does not match.
-     */
-    static LoggingAdmin getInstance(Object token, ClassLoader classLoader) {
-        return FactoryHolder.getLoggingConfigurationAdmin(
-                Objects.requireNonNull(token), Objects.requireNonNull(classLoader));
-    }
-
-    /**
-     * Retrieves the logging configuration admin for the classloader that loaded this class.
-     * <p>
-     *   If {@code token} is not null, it also sets a security token for the appropriate logger context.
-     *   All future invocations of this method will need to use the same token.
-     *   Tokens are compared using object equality.
-     * </p>
-     * @return A logging configuration admin.
-     * @throws IllegalStateException If no implementation of the Apache Logging Admin API is present on the classpath.
      * @throws SecurityException If a security token is set for the associated logger context and the provided token
      * does not match.
      */
     static LoggingAdmin getInstance(Object token) {
-        return FactoryHolder.getLoggingConfigurationAdmin(
-                Objects.requireNonNull(token), LoggingAdmin.class.getClassLoader());
+        return FactoryUtil.getLoggingAdmin(Objects.requireNonNull(token));
     }
 }
diff --git a/src/main/java/org/apache/logging/admin/internal/FactoryHolder.java b/src/main/java/org/apache/logging/admin/internal/FactoryHolder.java
deleted file mode 100644
index 08a94c3..0000000
--- a/src/main/java/org/apache/logging/admin/internal/FactoryHolder.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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 org.apache.logging.admin.internal;
-
-import aQute.bnd.annotation.Cardinality;
-import aQute.bnd.annotation.Resolution;
-import aQute.bnd.annotation.spi.ServiceConsumer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Map;
-import java.util.ServiceLoader;
-import java.util.WeakHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-import org.apache.logging.admin.LoggingAdmin;
-import org.apache.logging.admin.spi.LoggingAdminFactory;
-
-@ServiceConsumer(
-        value = LoggingAdminFactory.class,
-        cardinality = Cardinality.MULTIPLE,
-        resolution = Resolution.MANDATORY)
-public final class FactoryHolder {
-
-    private static final Collection<LoggingAdminFactory> factories = new ArrayList<>();
-    private static final ReentrantLock lock = new ReentrantLock();
-    private static final Map<ClassLoader, LoggingAdmin> instances = new WeakHashMap<>();
-    private static final Map<Object, Object> tokensByLoggerContext = new WeakHashMap<>();
-
-    static {
-        ServiceLoader.load(LoggingAdminFactory.class, FactoryHolder.class.getClassLoader())
-                .forEach(factories::add);
-    }
-
-    private static void checkSecurityToken(Object token, Object loggerContext) {
-        Object oldToken = tokensByLoggerContext.get(loggerContext);
-        if (oldToken == null) {
-            tokensByLoggerContext.put(loggerContext, token);
-            return;
-        }
-        if (token != oldToken) {
-            throw new SecurityException("Security token changed from " + oldToken + " to " + token);
-        }
-    }
-
-    public static LoggingAdmin getLoggingConfigurationAdmin(Object token, ClassLoader classLoader) {
-        lock.lock();
-        try {
-            LoggingAdmin instance = instances.get(classLoader);
-            if (instance == null) {
-                instance = factories.stream()
-                        .filter(f -> f.isActive(classLoader))
-                        .findAny()
-                        .map(f -> f.createLoggingConfigurationAdmin(classLoader))
-                        .orElseThrow(() ->
-                                new IllegalStateException("No " + LoggingAdminFactory.class.getName() + " found."));
-                instances.put(classLoader, instance);
-            }
-            checkSecurityToken(token, instance.getLoggerContext());
-            return instance;
-        } finally {
-            lock.unlock();
-        }
-    }
-
-    private FactoryHolder() {}
-}
diff --git a/src/main/java/org/apache/logging/admin/internal/FactoryUtil.java b/src/main/java/org/apache/logging/admin/internal/FactoryUtil.java
new file mode 100644
index 0000000..5719b57
--- /dev/null
+++ b/src/main/java/org/apache/logging/admin/internal/FactoryUtil.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.logging.admin.internal;
+
+import aQute.bnd.annotation.Cardinality;
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceConsumer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.ServiceLoader;
+import org.apache.logging.admin.LoggingAdmin;
+import org.apache.logging.admin.spi.LoggingAdminFactory;
+
+@ServiceConsumer(
+        value = LoggingAdminFactory.class,
+        cardinality = Cardinality.MULTIPLE,
+        resolution = Resolution.MANDATORY)
+public final class FactoryUtil {
+
+    private static final Collection<LoggingAdminFactory> factories = new ArrayList<>();
+
+    static {
+        ServiceLoader.load(LoggingAdminFactory.class, FactoryUtil.class.getClassLoader())
+                .forEach(factories::add);
+    }
+
+    public static LoggingAdmin getLoggingAdmin(Object token) {
+        LoggingAdminFactory factory = factories.stream()
+                .filter(LoggingAdminFactory::isActive)
+                .sorted(Comparator.comparing(LoggingAdminFactory::getPriority))
+                .findAny()
+                .orElseThrow(() ->
+                        new IllegalStateException("No active " + LoggingAdminFactory.class.getName() + " found."));
+        return factory.getLoggingAdmin(token);
+    }
+
+    private FactoryUtil() {}
+}
diff --git a/src/main/java/org/apache/logging/admin/internal/JulAdmin.java b/src/main/java/org/apache/logging/admin/internal/JulAdmin.java
new file mode 100644
index 0000000..5d49388
--- /dev/null
+++ b/src/main/java/org/apache/logging/admin/internal/JulAdmin.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.logging.admin.internal;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.logging.admin.LoggingAdmin;
+import org.jspecify.annotations.Nullable;
+
+class JulAdmin implements LoggingAdmin {
+
+    private static final ReentrantLock lock = new ReentrantLock();
+    private static final Map<Logger, Object> tokensByRootLogger = new WeakHashMap<>();
+
+    private static final List<String> levels = Collections.unmodifiableList(Stream.of(
+                    Level.OFF,
+                    Level.SEVERE,
+                    Level.WARNING,
+                    Level.INFO,
+                    Level.CONFIG,
+                    Level.FINE,
+                    Level.FINER,
+                    Level.FINEST,
+                    Level.ALL)
+            .map(Level::toString)
+            .collect(Collectors.toList()));
+
+    private final LogManager logManager = LogManager.getLogManager();
+    private final Logger rootLogger;
+
+    JulAdmin(Logger rootLogger) {
+        this.rootLogger = rootLogger;
+    }
+
+    @Override
+    public List<String> getSupportedLevels() {
+        return levels;
+    }
+
+    @Override
+    public Map<String, @Nullable String> getLevels() {
+        final Map<String, @Nullable String> loggerLevels = new HashMap<>();
+        Collections.list(logManager.getLoggerNames()).forEach(loggerName -> fillLoggerLevels(loggerName, loggerLevels));
+        return loggerLevels;
+    }
+
+    @Override
+    public @Nullable String getLevel(String loggerName) {
+        return Optional.ofNullable(logManager.getLogger(loggerName))
+                .map(Logger::getLevel)
+                .map(Level::getName)
+                .orElse(null);
+    }
+
+    @Override
+    public void setLevel(String loggerName, @Nullable String level) {
+        Logger logger = logManager.getLogger(loggerName);
+        Logger rootLogger = findRootLogger(logger);
+        // Prevents setting the log level of a different "logger context"
+        if (this.rootLogger.equals(rootLogger)) {
+            logger.setLevel(level != null ? Level.parse(level) : null);
+        }
+    }
+
+    private void fillLoggerLevels(String loggerName, Map<String, @Nullable String> loggerLevels) {
+        String currentName = loggerName;
+        while (loggerLevels.putIfAbsent(currentName, getLevel(currentName)) == null) {
+            if (currentName.isEmpty()) {
+                break;
+            }
+            int idx = currentName.lastIndexOf('.');
+            currentName = idx == -1 ? "" : currentName.substring(0, idx);
+        }
+    }
+
+    private static Logger findRootLogger(Logger logger) {
+        Logger current = logger;
+        while (current != null) {
+            if ("".equals(current.getName()) || current.getParent() == null) {
+                return current;
+            }
+            current = current.getParent();
+        }
+        throw new IllegalStateException("Unable to find root logger for " + logger);
+    }
+
+    /**
+     * Checks if the `java.logging` module is present.
+     * @throws LinkageError If `java.logging` is absent.
+     */
+    static boolean isActive() {
+        return LogManager.getLogManager() != null;
+    }
+
+    static LoggingAdmin newInstance(Object token) {
+        lock.lock();
+        try {
+            Logger rootLogger = Logger.getLogger("");
+            if (tokensByRootLogger.computeIfAbsent(rootLogger, k -> token) != token) {
+                throw new SecurityException("The security token does not match: " + token);
+            }
+            return new JulAdmin(rootLogger);
+        } finally {
+            lock.unlock();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java b/src/main/java/org/apache/logging/admin/internal/JulAdminFactory.java
similarity index 67%
rename from src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java
rename to src/main/java/org/apache/logging/admin/internal/JulAdminFactory.java
index 7c257df..d0d22dc 100644
--- a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java
+++ b/src/main/java/org/apache/logging/admin/internal/JulAdminFactory.java
@@ -14,22 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.admin.internal.jul;
+package org.apache.logging.admin.internal;
 
 import aQute.bnd.annotation.spi.ServiceProvider;
 import org.apache.logging.admin.LoggingAdmin;
 import org.apache.logging.admin.spi.LoggingAdminFactory;
 
+/**
+ * Logging admin factory for `java.util.logging`.
+ */
 @ServiceProvider(LoggingAdminFactory.class)
-public class JulFactory implements LoggingAdminFactory {
-
+public class JulAdminFactory implements LoggingAdminFactory {
     @Override
-    public boolean isActive(ClassLoader classLoader) {
-        return true;
+    public int getPriority() {
+        return Integer.MAX_VALUE;
     }
 
     @Override
-    public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) {
-        return new JulAdmin(classLoader);
+    public boolean isActive() {
+        try {
+            return JulAdmin.isActive();
+        } catch (LinkageError e) {
+            return false;
+        }
+    }
+
+    @Override
+    public LoggingAdmin getLoggingAdmin(Object token) {
+        return JulAdmin.newInstance(token);
     }
 }
diff --git a/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdmin.java b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdmin.java
new file mode 100644
index 0000000..3ed3a99
--- /dev/null
+++ b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdmin.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.logging.admin.internal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.logging.admin.LoggingAdmin;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.util.NameUtil;
+import org.jspecify.annotations.Nullable;
+
+class Log4jCoreAdmin implements LoggingAdmin {
+
+    private static final ReentrantLock lock = new ReentrantLock();
+    private static final Map<LoggerContext, Object> tokensByLoggerContext = new WeakHashMap<>();
+
+    private final LoggerContext loggerContext;
+
+    Log4jCoreAdmin(LoggerContext loggerContext) {
+        this.loggerContext = loggerContext;
+    }
+
+    @Override
+    public List<String> getSupportedLevels() {
+        return Stream.of(Level.values()).sorted().map(Level::name).collect(Collectors.toList());
+    }
+
+    @Override
+    public Map<String, @Nullable String> getLevels() {
+        Map<String, @Nullable String> loggerLevels = new HashMap<>();
+        // Insert the ancestors of all existing loggers
+        loggerContext.getLoggers().forEach(logger -> fillLoggerLevels(logger.getName(), loggerLevels));
+        // Insert the ancestors of all existing logger configurations
+        loggerContext
+                .getConfiguration()
+                .getLoggers()
+                .keySet()
+                .forEach(loggerName -> fillLoggerLevels(loggerName, loggerLevels));
+        return loggerLevels;
+    }
+
+    @Override
+    public @Nullable String getLevel(String loggerName) {
+        Configuration config = loggerContext.getConfiguration();
+        return Optional.of(config.getLoggerConfig(loggerName))
+                .filter(lc -> loggerName.equals(lc.getName()))
+                .map(LoggerConfig::getLevel)
+                .map(Level::name)
+                .orElse(null);
+    }
+
+    @Override
+    public void setLevel(String loggerName, @Nullable String level) {
+        boolean changed;
+        Configuration config = loggerContext.getConfiguration();
+        Level levelObj = level != null ? Level.valueOf(level) : null;
+        LoggerConfig loggerConfig = config.getLoggerConfig(loggerName);
+        if (!loggerName.equals(loggerConfig.getName())) {
+            loggerConfig = new LoggerConfig(loggerName, levelObj, true);
+            config.addLogger(loggerName, loggerConfig);
+            changed = true;
+        } else {
+            changed = Objects.equals(levelObj, loggerConfig.getLevel());
+            loggerConfig.setLevel(levelObj);
+        }
+        if (changed) {
+            loggerContext.updateLoggers();
+        }
+    }
+
+    private void fillLoggerLevels(String loggerName, Map<String, @Nullable String> loggerLevels) {
+        String currentName = loggerName;
+        while (currentName != null && loggerLevels.putIfAbsent(currentName, getLevel(currentName)) == null) {
+            currentName = NameUtil.getSubName(currentName);
+        }
+    }
+
+    static boolean isActive() {
+        org.apache.logging.log4j.spi.LoggerContext loggerContext = PrivateLogManager.getContext();
+        return loggerContext instanceof org.apache.logging.log4j.core.LoggerContext;
+    }
+
+    static LoggingAdmin newInstance(Object token) {
+        lock.lock();
+        try {
+            LoggerContext loggerContext = (LoggerContext) PrivateLogManager.getContext();
+            if (tokensByLoggerContext.computeIfAbsent(loggerContext, k -> token) != token) {
+                throw new SecurityException("The security token does not match: " + token);
+            }
+            return new Log4jCoreAdmin(loggerContext);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private static final class PrivateLogManager extends LogManager {
+        private PrivateLogManager() {}
+
+        public static org.apache.logging.log4j.spi.LoggerContext getContext() {
+            return LogManager.getContext(LoggingAdmin.class.getName(), false);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdminFactory.java
similarity index 66%
copy from src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java
copy to src/main/java/org/apache/logging/admin/internal/Log4jCoreAdminFactory.java
index 7c257df..8f46127 100644
--- a/src/main/java/org/apache/logging/admin/internal/jul/JulFactory.java
+++ b/src/main/java/org/apache/logging/admin/internal/Log4jCoreAdminFactory.java
@@ -14,22 +14,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.admin.internal.jul;
+package org.apache.logging.admin.internal;
 
 import aQute.bnd.annotation.spi.ServiceProvider;
 import org.apache.logging.admin.LoggingAdmin;
 import org.apache.logging.admin.spi.LoggingAdminFactory;
 
-@ServiceProvider(LoggingAdminFactory.class)
-public class JulFactory implements LoggingAdminFactory {
-
+@ServiceProvider(value = LoggingAdminFactory.class)
+public class Log4jCoreAdminFactory implements LoggingAdminFactory {
     @Override
-    public boolean isActive(ClassLoader classLoader) {
-        return true;
+    public int getPriority() {
+        return 512;
     }
 
     @Override
-    public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) {
-        return new JulAdmin(classLoader);
+    public boolean isActive() {
+        try {
+            return Log4jCoreAdmin.isActive();
+        } catch (LinkageError e) {
+            return false;
+        }
+    }
+
+    @Override
+    public LoggingAdmin getLoggingAdmin(Object token) {
+        return Log4jCoreAdmin.newInstance(token);
     }
 }
diff --git a/src/main/java/org/apache/logging/admin/internal/LogbackAdmin.java b/src/main/java/org/apache/logging/admin/internal/LogbackAdmin.java
new file mode 100644
index 0000000..9aa03a6
--- /dev/null
+++ b/src/main/java/org/apache/logging/admin/internal/LogbackAdmin.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.logging.admin.internal;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.logging.admin.LoggingAdmin;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.LoggerFactory;
+
+class LogbackAdmin implements LoggingAdmin {
+
+    private static final ReentrantLock lock = new ReentrantLock();
+    private static final Map<LoggerContext, Object> tokensByLoggerContext = new WeakHashMap<>();
+
+    private static final List<String> levels = Collections.unmodifiableList(
+            Stream.of(Level.OFF, Level.ERROR, Level.WARN, Level.INFO, Level.DEBUG, Level.TRACE)
+                    .map(Level::toString)
+                    .collect(Collectors.toList()));
+
+    private final LoggerContext loggerContext;
+
+    LogbackAdmin(final LoggerContext loggerContext) {
+        this.loggerContext = loggerContext;
+    }
+
+    @Override
+    public List<String> getSupportedLevels() {
+        return levels;
+    }
+
+    @Override
+    public Map<String, @Nullable String> getLevels() {
+        Map<String, @Nullable String> loggerLevels = new HashMap<>();
+        loggerContext
+                .getLoggerList()
+                .forEach(logger -> loggerLevels.put(rootToEmpty(logger.getName()), getLevel(logger)));
+        return loggerLevels;
+    }
+
+    @Override
+    public @Nullable String getLevel(String loggerName) {
+        return getLevel(loggerContext.getLogger(emptyToRoot(loggerName)));
+    }
+
+    private static @Nullable String getLevel(Logger logger) {
+        return Optional.ofNullable(logger.getLevel()).map(Level::toString).orElse(null);
+    }
+
+    @Override
+    public void setLevel(String loggerName, @Nullable String level) {
+        loggerContext.getLogger(emptyToRoot(loggerName)).setLevel(level != null ? Level.valueOf(level) : null);
+    }
+
+    private String rootToEmpty(String loggerName) {
+        return Logger.ROOT_LOGGER_NAME.equals(loggerName) ? "" : loggerName;
+    }
+
+    private String emptyToRoot(String loggerName) {
+        return loggerName.isEmpty() ? Logger.ROOT_LOGGER_NAME : loggerName;
+    }
+
+    static boolean isActive() {
+        return LoggerFactory.getILoggerFactory() instanceof ch.qos.logback.classic.LoggerContext;
+    }
+
+    static LoggingAdmin newInstance(Object token) {
+        lock.lock();
+        try {
+            LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+            if (tokensByLoggerContext.computeIfAbsent(loggerContext, k -> token) != token) {
+                throw new SecurityException("The security token does not match: " + token);
+            }
+            return new LogbackAdmin(loggerContext);
+        } finally {
+            lock.unlock();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdminFactory.java b/src/main/java/org/apache/logging/admin/internal/LogbackAdminFactory.java
similarity index 67%
rename from src/main/java/org/apache/logging/admin/internal/logback/LogbackAdminFactory.java
rename to src/main/java/org/apache/logging/admin/internal/LogbackAdminFactory.java
index 3b3492a..b05a03c 100644
--- a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdminFactory.java
+++ b/src/main/java/org/apache/logging/admin/internal/LogbackAdminFactory.java
@@ -14,21 +14,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.admin.internal.logback;
+package org.apache.logging.admin.internal;
 
-import ch.qos.logback.classic.LoggerContext;
+import aQute.bnd.annotation.spi.ServiceProvider;
 import org.apache.logging.admin.LoggingAdmin;
 import org.apache.logging.admin.spi.LoggingAdminFactory;
-import org.slf4j.LoggerFactory;
 
+@ServiceProvider(value = LoggingAdminFactory.class)
 public class LogbackAdminFactory implements LoggingAdminFactory {
     @Override
-    public boolean isActive(ClassLoader classLoader) {
-        return LoggerFactory.getILoggerFactory() instanceof ch.qos.logback.classic.LoggerContext;
+    public int getPriority() {
+        return 1024;
     }
 
     @Override
-    public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) {
-        return new LogbackAdmin((LoggerContext) LoggerFactory.getILoggerFactory());
+    public boolean isActive() {
+        try {
+            return LogbackAdmin.isActive();
+        } catch (LinkageError e) {
+            return false;
+        }
+    }
+
+    @Override
+    public LoggingAdmin getLoggingAdmin(Object token) {
+        return LogbackAdmin.newInstance(token);
     }
 }
diff --git a/src/main/java/org/apache/logging/admin/internal/jul/JulAdmin.java b/src/main/java/org/apache/logging/admin/internal/jul/JulAdmin.java
deleted file mode 100644
index 54fc6a8..0000000
--- a/src/main/java/org/apache/logging/admin/internal/jul/JulAdmin.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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 org.apache.logging.admin.internal.jul;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.LogManager;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.apache.logging.admin.LoggingAdmin;
-import org.jspecify.annotations.Nullable;
-
-class JulAdmin implements LoggingAdmin {
-
-    private static final Set<String> levels = Stream.of(
-                    Level.ALL,
-                    Level.FINEST,
-                    Level.FINER,
-                    Level.FINE,
-                    Level.CONFIG,
-                    Level.INFO,
-                    Level.WARNING,
-                    Level.SEVERE,
-                    Level.OFF)
-            .map(Level::toString)
-            .collect(Collectors.toSet());
-
-    private final LogManager logManager = LogManager.getLogManager();
-    private final ClassLoader classLoader;
-
-    JulAdmin(final ClassLoader classLoader) {
-        this.classLoader = classLoader;
-    }
-
-    @Override
-    public Object getLoggerContext() {
-        // Implementations that support multiple logger contexts can be recognized by having multiple root loggers.
-        ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader);
-        try {
-            return Logger.getLogger("");
-        } finally {
-            updateThreadContextClassLoader(oldClassLoader);
-        }
-    }
-
-    @Override
-    public Set<String> getSupportedLevels() {
-        return levels;
-    }
-
-    @Override
-    public Map<String, Optional<String>> getLoggerLevels() {
-        ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader);
-        try {
-            final Map<String, Optional<String>> loggerLevels = new HashMap<>();
-            for (final String loggerName : Collections.list(logManager.getLoggerNames())) {
-                loggerLevels.computeIfAbsent(loggerName, this::getLoggerLevel);
-            }
-            return loggerLevels;
-        } finally {
-            updateThreadContextClassLoader(oldClassLoader);
-        }
-    }
-
-    @Override
-    public Optional<String> getLoggerLevel(String loggerName) {
-        ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader);
-        try {
-            return Optional.ofNullable(logManager.getLogger(loggerName))
-                    .map(Logger::getLevel)
-                    .map(Level::getName);
-        } finally {
-            updateThreadContextClassLoader(oldClassLoader);
-        }
-    }
-
-    private Optional<String> doGetLoggerLevel(String loggerName) {
-        return Optional.ofNullable(logManager.getLogger(loggerName))
-                .map(Logger::getLevel)
-                .map(Level::getName);
-    }
-
-    @Override
-    public void setLoggerLevel(String loggerName, @Nullable String level) {
-        ClassLoader oldClassLoader = updateThreadContextClassLoader(classLoader);
-        try {
-            logManager.getLogger(loggerName).setLevel(level != null ? Level.parse(level) : null);
-        } finally {
-            updateThreadContextClassLoader(oldClassLoader);
-        }
-    }
-
-    private static ClassLoader updateThreadContextClassLoader(final ClassLoader classLoader) {
-        ClassLoader old = Thread.currentThread().getContextClassLoader();
-        Thread.currentThread().setContextClassLoader(classLoader);
-        return old;
-    }
-}
diff --git a/src/main/java/org/apache/logging/admin/internal/log4j/CoreAdmin.java b/src/main/java/org/apache/logging/admin/internal/log4j/CoreAdmin.java
deleted file mode 100644
index 7edd685..0000000
--- a/src/main/java/org/apache/logging/admin/internal/log4j/CoreAdmin.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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 org.apache.logging.admin.internal.log4j;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.apache.logging.admin.LoggingAdmin;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.config.LoggerConfig;
-import org.apache.logging.log4j.core.util.NameUtil;
-import org.jspecify.annotations.Nullable;
-
-class CoreAdmin implements LoggingAdmin {
-
-    private final LoggerContext loggerContext;
-
-    CoreAdmin(LoggerContext loggerContext) {
-        this.loggerContext = loggerContext;
-    }
-
-    @Override
-    public Object getLoggerContext() {
-        return loggerContext;
-    }
-
-    @Override
-    public Set<String> getSupportedLevels() {
-        return Stream.of(Level.values()).map(Level::name).collect(Collectors.toSet());
-    }
-
-    @Override
-    public Map<String, Optional<String>> getLoggerLevels() {
-        Map<String, Optional<String>> loggerLevels = new HashMap<>();
-        // Insert the ancestors of all existing loggers
-        loggerContext.getLoggers().forEach(logger -> fillLoggerLevels(logger.getName(), loggerLevels));
-        // Insert the ancestors of all existing logger configurations
-        loggerContext
-                .getConfiguration()
-                .getLoggers()
-                .keySet()
-                .forEach(loggerName -> fillLoggerLevels(loggerName, loggerLevels));
-        return loggerLevels;
-    }
-
-    @Override
-    public Optional<String> getLoggerLevel(String loggerName) {
-        Configuration config = loggerContext.getConfiguration();
-        return Optional.of(config.getLoggerConfig(loggerName))
-                .filter(lc -> loggerName.equals(lc.getName()))
-                .map(LoggerConfig::getLevel)
-                .map(Level::name);
-    }
-
-    @Override
-    public void setLoggerLevel(String loggerName, @Nullable String level) {
-        boolean changed;
-        Configuration config = loggerContext.getConfiguration();
-        Level levelObj = level != null ? Level.valueOf(level) : null;
-        LoggerConfig loggerConfig = config.getLoggerConfig(loggerName);
-        if (!loggerName.equals(loggerConfig.getName())) {
-            loggerConfig = new LoggerConfig(loggerName, levelObj, true);
-            config.addLogger(loggerName, loggerConfig);
-            changed = true;
-        } else {
-            changed = Objects.equals(levelObj, loggerConfig.getLevel());
-            loggerConfig.setLevel(levelObj);
-        }
-        if (changed) {
-            loggerContext.updateLoggers();
-        }
-    }
-
-    private void fillLoggerLevels(String loggerName, Map<String, Optional<String>> loggerLevels) {
-        String currentName = loggerName;
-        while (currentName != null && loggerLevels.computeIfAbsent(currentName, this::getLoggerLevel) == null) {
-            currentName = NameUtil.getSubName(currentName);
-        }
-    }
-}
diff --git a/src/main/java/org/apache/logging/admin/internal/log4j/CoreFactory.java b/src/main/java/org/apache/logging/admin/internal/log4j/CoreFactory.java
deleted file mode 100644
index 9b9c82a..0000000
--- a/src/main/java/org/apache/logging/admin/internal/log4j/CoreFactory.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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 org.apache.logging.admin.internal.log4j;
-
-import aQute.bnd.annotation.spi.ServiceProvider;
-import org.apache.logging.admin.LoggingAdmin;
-import org.apache.logging.admin.spi.LoggingAdminFactory;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.spi.LoggerContext;
-
-@ServiceProvider(value = LoggingAdminFactory.class)
-public class CoreFactory implements LoggingAdminFactory {
-
-    @Override
-    public boolean isActive(ClassLoader classLoader) {
-        LoggerContext loggerContext = LogManager.getContext(classLoader, false);
-        return loggerContext instanceof org.apache.logging.log4j.core.LoggerContext;
-    }
-
-    @Override
-    public LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader) {
-        LoggerContext loggerContext = LogManager.getContext(classLoader, false);
-        return new CoreAdmin((org.apache.logging.log4j.core.LoggerContext) loggerContext);
-    }
-}
diff --git a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdmin.java b/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdmin.java
deleted file mode 100644
index 3dd1fe3..0000000
--- a/src/main/java/org/apache/logging/admin/internal/logback/LogbackAdmin.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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 org.apache.logging.admin.internal.logback;
-
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.apache.logging.admin.LoggingAdmin;
-import org.jspecify.annotations.Nullable;
-
-class LogbackAdmin implements LoggingAdmin {
-
-    private static final Set<String> levels = Stream.of(
-                    Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.OFF)
-            .map(Level::toString)
-            .collect(Collectors.toSet());
-
-    private final LoggerContext loggerContext;
-
-    @Override
-    public Object getLoggerContext() {
-        return loggerContext;
-    }
-
-    LogbackAdmin(final LoggerContext loggerContext) {
-        this.loggerContext = loggerContext;
-    }
-
-    @Override
-    public Set<String> getSupportedLevels() {
-        return levels;
-    }
-
-    @Override
-    public Map<String, Optional<String>> getLoggerLevels() {
-        return loggerContext.getLoggerList().stream()
-                .collect(Collectors.toMap(Logger::getName, LogbackAdmin::getLoggerLevel));
-    }
-
-    @Override
-    public Optional<String> getLoggerLevel(String loggerName) {
-        return getLoggerLevel(loggerContext.getLogger(loggerName));
-    }
-
-    private static Optional<String> getLoggerLevel(ch.qos.logback.classic.Logger logger) {
-        return Optional.ofNullable(logger.getLevel()).map(Level::toString);
-    }
-
-    @Override
-    public void setLoggerLevel(String loggerName, @Nullable String level) {
-        loggerContext.getLogger(loggerName).setLevel(level != null ? Level.valueOf(level) : null);
-    }
-}
diff --git a/src/main/java/org/apache/logging/admin/package-info.java b/src/main/java/org/apache/logging/admin/package-info.java
index be36b44..8c0bc7e 100644
--- a/src/main/java/org/apache/logging/admin/package-info.java
+++ b/src/main/java/org/apache/logging/admin/package-info.java
@@ -14,7 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@Export
+@Version("0.1.0")
 @NullMarked
 package org.apache.logging.admin;
 
 import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java b/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java
index b6a79c4..6d11a8c 100644
--- a/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java
+++ b/src/main/java/org/apache/logging/admin/spi/LoggingAdminFactory.java
@@ -27,17 +27,29 @@
 public interface LoggingAdminFactory {
 
     /**
-     * Determines whether the logging system handled by this factory is used.
-     *
-     * @param classLoader The class loader associated with the logger context.
+     * Provides the order in which this factory should be evaluated.
+     * <p>
+     *   Lower numerical values are evaluated first.
+     * </p>
      */
-    boolean isActive(ClassLoader classLoader);
+    default int getPriority() {
+        return 0;
+    }
+
+    /**
+     * Determines whether the logging system handled by this factory is used.
+     */
+    boolean isActive();
 
     /**
      * Creates a new {@link LoggingAdmin} instance associated with the given classloader.
-     *
-     * @param classLoader The class loader associated with the logger context.
-     * @return A new instance of {@link LoggingAdmin}.
+     * <p>
+     *   The {@code token} parameter sets a security token for the appropriate logger context.
+     *   All future invocations of this method will need to use the same token.
+     *   Tokens are compared using object equality.
+     * </p>
+     * @param token Any Java object.
+     * @return An instance of {@link LoggingAdmin}.
      */
-    LoggingAdmin createLoggingConfigurationAdmin(ClassLoader classLoader);
+    LoggingAdmin getLoggingAdmin(Object token) throws SecurityException;
 }
diff --git a/src/main/java/org/apache/logging/admin/spi/package-info.java b/src/main/java/org/apache/logging/admin/spi/package-info.java
index ab19b30..2751805 100644
--- a/src/main/java/org/apache/logging/admin/spi/package-info.java
+++ b/src/main/java/org/apache/logging/admin/spi/package-info.java
@@ -14,7 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@Export
+@Version("0.1.0")
 @NullMarked
 package org.apache.logging.admin.spi;
 
 import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/logging/admin/LoggingAdminTest.java b/src/test/java/org/apache/logging/admin/LoggingAdminTest.java
new file mode 100644
index 0000000..02612ec
--- /dev/null
+++ b/src/test/java/org/apache/logging/admin/LoggingAdminTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.logging.admin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class LoggingAdminTest {
+
+    private static final Map<String, List<String>> EXPECTED_LEVELS;
+
+    static {
+        final Map<String, List<String>> expectedLevels = new HashMap<>();
+        expectedLevels.put(
+                "jul", Arrays.asList("OFF", "SEVERE", "WARNING", "INFO", "CONFIG", "FINE", "FINER", "FINEST", "ALL"));
+        expectedLevels.put(
+                "log4j-core", Arrays.asList("OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "ALL"));
+        expectedLevels.put("logback", Arrays.asList("OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"));
+        EXPECTED_LEVELS = Collections.unmodifiableMap(expectedLevels);
+    }
+
+    private static final Object TOKEN = new Object();
+    private static LoggingAdmin admin;
+
+    // Log4j Core is the default in the IDE
+    private static final String type = System.getProperty("admin.implementation", "log4j-core");
+
+    private static String debugLevel() {
+        return "jul".equals(type) ? "FINE" : "DEBUG";
+    }
+
+    @BeforeAll
+    static void setup() {
+        admin = LoggingAdmin.getInstance(TOKEN);
+        // JUL creates loggers lazily, so we force the creation of `foo.bar`
+        Logger.getLogger("foo.bar");
+    }
+
+    @Test
+    void should_return_correct_supported_levels() {
+        assertThat(admin.getSupportedLevels()).containsExactlyElementsOf(EXPECTED_LEVELS.get(type));
+    }
+
+    static Stream<Arguments> should_return_correct_configured_levels() {
+        return Stream.of(
+                Arguments.of(LoggingAdmin.ROOT_LOGGER_NAME, "INFO"),
+                Arguments.of("foo", null),
+                Arguments.of("foo.bar", debugLevel()));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void should_return_correct_configured_levels(String loggerName, @Nullable String expectedLevel) {
+        assertThat(admin.getLevel(loggerName))
+                .as("Level of logger `%s`.", loggerName)
+                .isEqualTo(expectedLevel);
+    }
+
+    @Test
+    void should_return_correct_configured_level_map() {
+        assertThat(admin.getLevels())
+                .contains(
+                        entry(LoggingAdmin.ROOT_LOGGER_NAME, "INFO"),
+                        entry("foo", null),
+                        entry("foo.bar", debugLevel()));
+    }
+
+    @Test
+    void should_return_admin_if_token_correct() {
+        assertDoesNotThrow(() -> LoggingAdmin.getInstance(TOKEN));
+    }
+
+    @Test
+    void should_throw_if_token_incorrect() {
+        assertThrows(SecurityException.class, () -> LoggingAdmin.getInstance(new Object()));
+    }
+
+    private static Map.Entry<String, @Nullable String> entry(String loggerName, @Nullable String level) {
+        return new AbstractMap.SimpleImmutableEntry<>(loggerName, level);
+    }
+}
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..23eb4be
--- /dev/null
+++ b/src/test/resources/log4j2.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to you 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.
+  -->
+<Configuration xmlns="https://logging.apache.org/xml/ns"
+               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+               xsi:schemaLocation="
+                   https://logging.apache.org/xml/ns
+                   https://logging.apache.org/xml/ns/log4j-config-2.xsd">
+  <Appenders/>
+  <Loggers>
+    <Root level="INFO"/>
+    <Logger name="foo.bar" level="DEBUG"/>
+  </Loggers>
+</Configuration>
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
new file mode 100644
index 0000000..5e98a3e
--- /dev/null
+++ b/src/test/resources/logback.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to you 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.
+  -->
+<!DOCTYPE configuration>
+<configuration>
+  <root level="INFO"/>
+  <logger name="foo.bar" level="DEBUG"/>
+</configuration>
diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties
new file mode 100644
index 0000000..adcc875
--- /dev/null
+++ b/src/test/resources/logging.properties
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you 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.
+#
+handlers =
+.level = INFO
+foo.bar.level = FINE