Browse Source

Optional plugin dependencies (#270)

pull/276/head
Andreas Rudolph 6 years ago committed by Decebal Suiu
parent
commit
153c7b3326
  1. 6
      pf4j/pom.xml
  2. 121
      pf4j/src/main/java/org/pf4j/AbstractExtensionFinder.java
  3. 18
      pf4j/src/main/java/org/pf4j/DependencyResolver.java
  4. 12
      pf4j/src/main/java/org/pf4j/Extension.java
  5. 6
      pf4j/src/main/java/org/pf4j/PluginClassLoader.java
  6. 16
      pf4j/src/main/java/org/pf4j/PluginDependency.java
  7. 102
      pf4j/src/main/java/org/pf4j/asm/ExtensionInfo.java
  8. 96
      pf4j/src/main/java/org/pf4j/asm/ExtensionVisitor.java
  9. 21
      pf4j/src/test/java/org/pf4j/PluginDependencyTest.java
  10. 1
      pom.xml

6
pf4j/pom.xml

@ -43,6 +43,12 @@
<artifactId>java-semver</artifactId> <artifactId>java-semver</artifactId>
<version>0.9.0</version> <version>0.9.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>

121
pf4j/src/main/java/org/pf4j/AbstractExtensionFinder.java

@ -15,12 +15,14 @@
*/ */
package org.pf4j; package org.pf4j;
import org.pf4j.asm.ExtensionInfo;
import org.pf4j.util.ClassUtils; import org.pf4j.util.ClassUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -35,6 +37,8 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin
protected PluginManager pluginManager; protected PluginManager pluginManager;
protected volatile Map<String, Set<String>> entries; // cache by pluginId protected volatile Map<String, Set<String>> entries; // cache by pluginId
protected volatile Map<String, ExtensionInfo> extensionInfos; // cache extension infos by class name
protected Boolean checkForExtensionDependencies = null;
public AbstractExtensionFinder(PluginManager pluginManager) { public AbstractExtensionFinder(PluginManager pluginManager) {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
@ -97,6 +101,41 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin
for (String className : classNames) { for (String className : classNames) {
try { try {
if (isCheckForExtensionDependencies()) {
// Load extension annotation without initializing the class itself.
//
// If optional dependencies are used, the class loader might not be able
// to load the extension class because of missing optional dependencies.
//
// Therefore we're extracting the extension annotation via asm, in order
// to extract the required plugins for an extension. Only if all required
// plugins are currently available and started, the corresponding
// extension is loaded through the class loader.
ExtensionInfo extensionInfo = getExtensionInfo(className, classLoader);
if (extensionInfo == null) {
log.error("No extension annotation was found for '{}'", className);
continue;
}
// Make sure, that all plugins required by this extension are available.
List<String> missingPluginIds = new ArrayList<>();
for (String requiredPluginId : extensionInfo.getPlugins()) {
PluginWrapper requiredPlugin = pluginManager.getPlugin(requiredPluginId);
if (requiredPlugin == null || !PluginState.STARTED.equals(requiredPlugin.getPluginState())) {
missingPluginIds.add(requiredPluginId);
}
}
if (!missingPluginIds.isEmpty()) {
StringBuilder missing = new StringBuilder();
for (String missingPluginId : missingPluginIds) {
if (missing.length() > 0) missing.append(", ");
missing.append(missingPluginId);
}
log.trace("Extension '{}' is ignored due to missing plugins: {}", className, missing);
continue;
}
}
log.debug("Loading class '{}' using class loader '{}'", className, classLoader); log.debug("Loading class '{}' using class loader '{}'", className, classLoader);
Class<?> extensionClass = classLoader.loadClass(className); Class<?> extensionClass = classLoader.loadClass(className);
@ -186,6 +225,60 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin
// TODO optimize (do only for some transitions) // TODO optimize (do only for some transitions)
// clear cache // clear cache
entries = null; entries = null;
// By default we're assuming, that no checks for extension dependencies are necessary.
//
// A plugin, that has an optional dependency to other plugins, might lead to unloadable
// Java classes (NoClassDefFoundError) at application runtime due to possibly missing
// dependencies. Therefore we're enabling the check for optional extensions, if the
// started plugin contains at least one optional plugin dependency.
if (checkForExtensionDependencies == null && PluginState.STARTED.equals(event.getPluginState())) {
for (PluginDependency dependency : event.getPlugin().getDescriptor().getDependencies()) {
if (dependency.isOptional()) {
log.debug("Enable check for extension dependencies via ASM.");
checkForExtensionDependencies = true;
break;
}
}
}
}
/**
* Returns true, if the extension finder checks extensions for its required plugins.
* This feature has to be enabled, in order check the availability of
* {@link Extension#plugins()} configured by an extension.
* <p>
* This feature is enabled by default, if at least one available plugin makes use of
* optional plugin dependencies. Those optional plugins might not be available at runtime.
* Therefore any extension is checked by default against available plugins before its
* instantiation.
* <p>
* Notice: This feature requires the optional <a href="https://asm.ow2.io/">ASM library</a>
* to be available on the applications classpath.
*
* @return true, if the extension finder checks extensions for its required plugins
*/
public final boolean isCheckForExtensionDependencies() {
return Boolean.TRUE.equals(checkForExtensionDependencies);
}
/**
* Plugin developers may enable / disable checks for required plugins of an extension.
* This feature has to be enabled, in order check the availability of
* {@link Extension#plugins()} configured by an extension.
* <p>
* This feature is enabled by default, if at least one available plugin makes use of
* optional plugin dependencies. Those optional plugins might not be available at runtime.
* Therefore any extension is checked by default against available plugins before its
* instantiation.
* <p>
* Notice: This feature requires the optional <a href="https://asm.ow2.io/">ASM library</a>
* to be available on the applications classpath.
*
* @param checkForExtensionDependencies true to enable checks for optional extensions, otherwise false
*/
public void setCheckForExtensionDependencies(boolean checkForExtensionDependencies) {
this.checkForExtensionDependencies = checkForExtensionDependencies;
} }
protected void debugExtensions(Set<String> extensions) { protected void debugExtensions(Set<String> extensions) {
@ -218,6 +311,34 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin
return entries; return entries;
} }
/**
* Returns the parameters of an {@link Extension} annotation without loading
* the corresponding class into the class loader.
*
* @param className name of the class, that holds the requested {@link Extension} annotation
* @param classLoader class loader to access the class
* @return the contents of the {@link Extension} annotation or null, if the class does not
* have an {@link Extension} annotation
*/
private ExtensionInfo getExtensionInfo(String className, ClassLoader classLoader) {
if (extensionInfos == null) {
extensionInfos = new HashMap<>();
}
if (!extensionInfos.containsKey(className)) {
log.trace("Load annotation for '{}' using asm", className);
ExtensionInfo info = ExtensionInfo.load(className, classLoader);
if (info == null) {
log.warn("No extension annotation was found for '{}'", className);
extensionInfos.put(className, null);
} else {
extensionInfos.put(className, info);
}
}
return extensionInfos.get(className);
}
private ExtensionWrapper createExtensionWrapper(Class<?> extensionClass) { private ExtensionWrapper createExtensionWrapper(Class<?> extensionClass) {
int ordinal = 0; int ordinal = 0;
if (extensionClass.isAnnotationPresent(Extension.class)) { if (extensionClass.isAnnotationPresent(Extension.class)) {

18
pf4j/src/main/java/org/pf4j/DependencyResolver.java

@ -143,9 +143,23 @@ public class DependencyResolver {
dependenciesGraph.addVertex(pluginId); dependenciesGraph.addVertex(pluginId);
dependentsGraph.addVertex(pluginId); dependentsGraph.addVertex(pluginId);
} else { } else {
boolean edgeAdded = false;
for (PluginDependency dependency : dependencies) { for (PluginDependency dependency : dependencies) {
dependenciesGraph.addEdge(pluginId, dependency.getPluginId()); // Don't register optional plugins in the dependency graph
dependentsGraph.addEdge(dependency.getPluginId(), pluginId); // to avoid automatic disabling of the plugin,
// if an optional dependency is missing.
if (!dependency.isOptional()) {
edgeAdded = true;
dependenciesGraph.addEdge(pluginId, dependency.getPluginId());
dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
}
}
// Register the plugin without dependencies,
// if all of its dependencies are optional.
if (!edgeAdded) {
dependenciesGraph.addVertex(pluginId);
dependentsGraph.addVertex(pluginId);
} }
} }
} }

12
pf4j/src/main/java/org/pf4j/Extension.java

@ -47,4 +47,16 @@ public @interface Extension {
*/ */
Class<? extends ExtensionPoint>[] points() default {}; Class<? extends ExtensionPoint>[] points() default {};
/**
* An array of plugin IDs, that have to be available in order to load this extension.
* The {@link AbstractExtensionFinder} won't load this extension, if these plugins are not
* available / started at runtime.
* <p>
* Notice: This feature requires the optional <a href="https://asm.ow2.io/">ASM library</a>
* to be available on the applications classpath and has to be explicitly enabled via
* {@link AbstractExtensionFinder#setCheckForExtensionDependencies(boolean)}.
*
* @return plugin IDs, that have to be available in order to load this extension
*/
String[] plugins() default {};
} }

6
pf4j/src/main/java/org/pf4j/PluginClassLoader.java

@ -194,6 +194,12 @@ public class PluginClassLoader extends URLClassLoader {
List<PluginDependency> dependencies = pluginDescriptor.getDependencies(); List<PluginDependency> dependencies = pluginDescriptor.getDependencies();
for (PluginDependency dependency : dependencies) { for (PluginDependency dependency : dependencies) {
ClassLoader classLoader = pluginManager.getPluginClassLoader(dependency.getPluginId()); ClassLoader classLoader = pluginManager.getPluginClassLoader(dependency.getPluginId());
// If the dependency is marked as optional, its class loader might not be available.
if (classLoader == null && dependency.isOptional()) {
continue;
}
try { try {
return classLoader.loadClass(className); return classLoader.loadClass(className);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {

16
pf4j/src/main/java/org/pf4j/PluginDependency.java

@ -22,6 +22,7 @@ public class PluginDependency {
private String pluginId; private String pluginId;
private String pluginVersionSupport = "*"; private String pluginVersionSupport = "*";
private boolean optional;
public PluginDependency(String dependency) { public PluginDependency(String dependency) {
int index = dependency.indexOf('@'); int index = dependency.indexOf('@');
@ -33,6 +34,13 @@ public class PluginDependency {
this.pluginVersionSupport = dependency.substring(index + 1); this.pluginVersionSupport = dependency.substring(index + 1);
} }
} }
// A dependency is considered as optional,
// if the plugin id ends with a question mark.
this.optional = this.pluginId.endsWith("?");
if (this.optional) {
this.pluginId = this.pluginId.substring(0, this.pluginId.length() - 1);
}
} }
public String getPluginId() { public String getPluginId() {
@ -43,9 +51,15 @@ public class PluginDependency {
return pluginVersionSupport; return pluginVersionSupport;
} }
public boolean isOptional() {
return optional;
}
@Override @Override
public String toString() { public String toString() {
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport=" + pluginVersionSupport + "]"; return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport="
+ pluginVersionSupport + ", optional="
+ optional + "]";
} }
} }

102
pf4j/src/main/java/org/pf4j/asm/ExtensionInfo.java

@ -0,0 +1,102 @@
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j.asm;
import org.objectweb.asm.ClassReader;
import org.pf4j.Extension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This class holds the parameters of an {@link org.pf4j.Extension}
* annotation defined for a certain class.
*
* @author Andreas Rudolph
* @author Decebal Suiu
*/
public final class ExtensionInfo {
private static final Logger log = LoggerFactory.getLogger(ExtensionInfo.class);
private final String className;
int ordinal = 0;
List<String> plugins = new ArrayList<>();
List<String> points = new ArrayList<>();
private ExtensionInfo(String className) {
super();
this.className = className;
}
/**
* Get the name of the class, for which extension info was created.
*
* @return absolute class name
*/
public String getClassName() {
return className;
}
/**
* Get the {@link Extension#ordinal()} value, that was assigned to the extension.
*
* @return ordinal value
*/
public int getOrdinal() {
return ordinal;
}
/**
* Get the {@link Extension#plugins()} value, that was assigned to the extension.
*
* @return ordinal value
*/
public List<String> getPlugins() {
return Collections.unmodifiableList(plugins);
}
/**
* Get the {@link Extension#points()} value, that was assigned to the extension.
*
* @return ordinal value
*/
public List<String> getPoints() {
return Collections.unmodifiableList(points);
}
/**
* Load an {@link ExtensionInfo} for a certain class.
*
* @param className absolute class name
* @param classLoader class loader to access the class
* @return the {@link ExtensionInfo}, if the class was annotated with an {@link Extension}, otherwise null
*/
public static ExtensionInfo load(String className, ClassLoader classLoader) {
try (InputStream input = classLoader.getResourceAsStream(className.replace('.', '/') + ".class")) {
ExtensionInfo info = new ExtensionInfo(className);
new ClassReader(input).accept(new ExtensionVisitor(info), ClassReader.SKIP_DEBUG);
return info;
} catch (IOException e) {
log.error(e.getMessage(), e);
return null;
}
}
}

96
pf4j/src/main/java/org/pf4j/asm/ExtensionVisitor.java

@ -0,0 +1,96 @@
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j.asm;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.pf4j.Extension;
import java.util.Arrays;
/**
* This visitor extracts an {@link ExtensionInfo} from any class,
* that holds an {@link Extension} annotation.
* <p>
* The annotation parameters are extracted from byte code by using the
* <a href="https://asm.ow2.io/">ASM library</a>. This makes it possible to
* access the {@link Extension} parameters without loading the class into
* the class loader. This avoids possible {@link NoClassDefFoundError}'s
* for extensions, that can't be loaded due to missing dependencies.
*
* @author Andreas Rudolph
* @author Decebal Suiu
*/
class ExtensionVisitor extends ClassVisitor {
//private static final Logger log = LoggerFactory.getLogger(ExtensionVisitor.class);
private static final int ASM_VERSION = Opcodes.ASM7;
private final ExtensionInfo extensionInfo;
ExtensionVisitor(ExtensionInfo extensionInfo) {
super(ASM_VERSION);
this.extensionInfo = extensionInfo;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
//if (!descriptor.equals("Lorg/pf4j/Extension;")) {
if (!Type.getType(descriptor).getClassName().equals(Extension.class.getName())) {
return super.visitAnnotation(descriptor, visible);
}
return new AnnotationVisitor(ASM_VERSION) {
@Override
public AnnotationVisitor visitArray(final String name) {
if ("ordinal".equals(name) || "plugins".equals(name) || "points".equals(name)) {
return new AnnotationVisitor(ASM_VERSION, super.visitArray(name)) {
@Override
public void visit(String key, Object value) {
//log.debug("Load annotation attribute {} = {} ({})", name, value, value.getClass().getName());
if ("ordinal".equals(name)) {
extensionInfo.ordinal = Integer.parseInt(value.toString());
} else if ("plugins".equals(name)) {
if (value instanceof String) {
//log.debug("found plugin " + value);
extensionInfo.plugins.add((String) value);
} else if (value instanceof String[]) {
//log.debug("found plugins " + Arrays.toString((String[]) value));
extensionInfo.plugins.addAll(Arrays.asList((String[]) value));
} else {
//log.debug("found plugin " + value.toString());
extensionInfo.plugins.add(value.toString());
}
} else if ("points".equals(name)) {
String pointClassName = ((Type) value).getClassName();
//log.debug("found point " + pointClassName);
extensionInfo.points.add(pointClassName);
}
super.visit(key, value);
}
};
}
return super.visitArray(name);
}
};
}
}

21
pf4j/src/test/java/org/pf4j/PluginDependencyTest.java

@ -32,15 +32,34 @@ public class PluginDependencyTest {
PluginDependency instance = new PluginDependency("test"); PluginDependency instance = new PluginDependency("test");
assertEquals("test", instance.getPluginId()); assertEquals("test", instance.getPluginId());
assertEquals("*", instance.getPluginVersionSupport()); assertEquals("*", instance.getPluginVersionSupport());
assertEquals(false, instance.isOptional());
instance = new PluginDependency("test@"); instance = new PluginDependency("test@");
assertEquals("test", instance.getPluginId()); assertEquals("test", instance.getPluginId());
assertEquals("*", instance.getPluginVersionSupport()); assertEquals("*", instance.getPluginVersionSupport());
assertEquals(false, instance.isOptional());
instance = new PluginDependency("test?");
assertEquals("test", instance.getPluginId());
assertEquals("*", instance.getPluginVersionSupport());
assertEquals(true, instance.isOptional());
instance = new PluginDependency("test?@");
assertEquals("test", instance.getPluginId());
assertEquals("*", instance.getPluginVersionSupport());
assertEquals(true, instance.isOptional());
instance = new PluginDependency("test@1.0"); instance = new PluginDependency("test@1.0");
assertEquals("test", instance.getPluginId()); assertEquals("test", instance.getPluginId());
assertEquals("1.0", instance.getPluginVersionSupport()); assertEquals("1.0", instance.getPluginVersionSupport());
assertEquals("PluginDependency [pluginId=test, pluginVersionSupport=1.0]", instance.toString()); assertEquals(false, instance.isOptional());
assertEquals("PluginDependency [pluginId=test, pluginVersionSupport=1.0, optional=false]", instance.toString());
instance = new PluginDependency("test?@1.0");
assertEquals("test", instance.getPluginId());
assertEquals("1.0", instance.getPluginVersionSupport());
assertEquals(true, instance.isOptional());
assertEquals("PluginDependency [pluginId=test, pluginVersionSupport=1.0, optional=true]", instance.toString());
} }
} }

1
pom.xml

@ -45,6 +45,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version> <java.version>1.7</java.version>
<slf4j.version>1.7.25</slf4j.version> <slf4j.version>1.7.25</slf4j.version>
<asm.version>7.0</asm.version>
<junit.version>4.12</junit.version> <junit.version>4.12</junit.version>
<mockito.version>2.0.28-beta</mockito.version> <mockito.version>2.0.28-beta</mockito.version>

Loading…
Cancel
Save