Browse Source

Optional plugin dependencies (#270)

tags/release-2.6.0
Andreas Rudolph 5 years ago
parent
commit
153c7b3326

+ 6
- 0
pf4j/pom.xml View File

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

+ 121
- 0
pf4j/src/main/java/org/pf4j/AbstractExtensionFinder.java View File

@@ -15,12 +15,14 @@
*/
package org.pf4j;

import org.pf4j.asm.ExtensionInfo;
import org.pf4j.util.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -35,6 +37,8 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin

protected PluginManager pluginManager;
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) {
this.pluginManager = pluginManager;
@@ -97,6 +101,41 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin

for (String className : classNames) {
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);
Class<?> extensionClass = classLoader.loadClass(className);

@@ -186,6 +225,60 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin
// TODO optimize (do only for some transitions)
// clear cache
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) {
@@ -218,6 +311,34 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin
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) {
int ordinal = 0;
if (extensionClass.isAnnotationPresent(Extension.class)) {

+ 16
- 2
pf4j/src/main/java/org/pf4j/DependencyResolver.java View File

@@ -143,9 +143,23 @@ public class DependencyResolver {
dependenciesGraph.addVertex(pluginId);
dependentsGraph.addVertex(pluginId);
} else {
boolean edgeAdded = false;
for (PluginDependency dependency : dependencies) {
dependenciesGraph.addEdge(pluginId, dependency.getPluginId());
dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
// Don't register optional plugins in the dependency graph
// 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
- 0
pf4j/src/main/java/org/pf4j/Extension.java View File

@@ -47,4 +47,16 @@ public @interface Extension {
*/
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
- 0
pf4j/src/main/java/org/pf4j/PluginClassLoader.java View File

@@ -194,6 +194,12 @@ public class PluginClassLoader extends URLClassLoader {
List<PluginDependency> dependencies = pluginDescriptor.getDependencies();
for (PluginDependency dependency : dependencies) {
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 {
return classLoader.loadClass(className);
} catch (ClassNotFoundException e) {

+ 15
- 1
pf4j/src/main/java/org/pf4j/PluginDependency.java View File

@@ -22,6 +22,7 @@ public class PluginDependency {

private String pluginId;
private String pluginVersionSupport = "*";
private boolean optional;

public PluginDependency(String dependency) {
int index = dependency.indexOf('@');
@@ -33,6 +34,13 @@ public class PluginDependency {
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() {
@@ -43,9 +51,15 @@ public class PluginDependency {
return pluginVersionSupport;
}

public boolean isOptional() {
return optional;
}

@Override
public String toString() {
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport=" + pluginVersionSupport + "]";
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport="
+ pluginVersionSupport + ", optional="
+ optional + "]";
}

}

+ 102
- 0
pf4j/src/main/java/org/pf4j/asm/ExtensionInfo.java View File

@@ -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
- 0
pf4j/src/main/java/org/pf4j/asm/ExtensionVisitor.java View File

@@ -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);
}
};
}
}

+ 20
- 1
pf4j/src/test/java/org/pf4j/PluginDependencyTest.java View File

@@ -32,15 +32,34 @@ public class PluginDependencyTest {
PluginDependency instance = new PluginDependency("test");
assertEquals("test", instance.getPluginId());
assertEquals("*", instance.getPluginVersionSupport());
assertEquals(false, instance.isOptional());

instance = new PluginDependency("test@");
assertEquals("test", instance.getPluginId());
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");
assertEquals("test", instance.getPluginId());
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
- 0
pom.xml View File

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

<junit.version>4.12</junit.version>
<mockito.version>2.0.28-beta</mockito.version>

Loading…
Cancel
Save