浏览代码

Optional plugin dependencies (#270)

tags/release-2.6.0
Andreas Rudolph 5 年前
父节点
当前提交
153c7b3326

+ 6
- 0
pf4j/pom.xml 查看文件

<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
- 0
pf4j/src/main/java/org/pf4j/AbstractExtensionFinder.java 查看文件

*/ */
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;


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;


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


// 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) {
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)) {

+ 16
- 2
pf4j/src/main/java/org/pf4j/DependencyResolver.java 查看文件

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());
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 查看文件

*/ */
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
- 0
pf4j/src/main/java/org/pf4j/PluginClassLoader.java 查看文件

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) {

+ 15
- 1
pf4j/src/main/java/org/pf4j/PluginDependency.java 查看文件



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('@');
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() {
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
- 0
pf4j/src/main/java/org/pf4j/asm/ExtensionInfo.java 查看文件

/*
* 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 查看文件

/*
* 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 查看文件

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
- 0
pom.xml 查看文件

<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>

正在加载...
取消
保存