Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support constructor injection in extensions #50

Merged
merged 3 commits into from
Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ protected ApplicationContext createApplicationContext() {
@Extension(ordinal=1)
public static class HelloGreeting implements Greeting {

private final MessageProvider messageProvider;

@Autowired
private MessageProvider messageProvider;
public HelloGreeting(final MessageProvider messageProvider) {
this.messageProvider = messageProvider;
}

@Override
public String getGreeting() {
Expand Down
241 changes: 197 additions & 44 deletions pf4j-spring/src/main/java/org/pf4j/spring/SpringExtensionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,82 +15,235 @@
*/
package org.pf4j.spring;

import java.util.Map;

import org.pf4j.Extension;
import org.pf4j.ExtensionFactory;
import org.pf4j.Plugin;
import org.pf4j.PluginManager;
import org.pf4j.PluginWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;

/**
* Basic implementation of a extension factory that uses Java reflection to
* instantiate an object.
* Create a new extension instance every time a request is done.
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Stream;

import static java.util.Objects.nonNull;

/**
* Basic implementation of an extension factory.
* <p><p>
* Uses Springs {@link AutowireCapableBeanFactory} to instantiate a given extension class. All kinds of
* {@link Autowired} are supported (see example below). If no {@link ApplicationContext} is available (this is the case
* if either the related plugin is not a {@link SpringPlugin} or the given plugin manager is not a
* {@link SpringPluginManager}), standard Java reflection will be used to instantiate an extension.
* <p><p>
* Creates a new extension instance every time a request is done.
* <p><p>
* Example of supported autowire modes:
* <pre>{@code
* @Extension
* public class Foo implements ExtensionPoint {
*
* private final Bar bar; // Constructor injection
* private Baz baz; // Setter injection
* @Autowired
* private Qux qux; // Field injection
*
* @Autowired
* public Foo(final Bar bar) {
* this.bar = bar;
* }
*
* @Autowired
* public void setBaz(final Baz baz) {
* this.baz = baz;
* }
* }
* }</pre>
*
* @author Decebal Suiu
* @author m-schroeer
*/
public class SpringExtensionFactory implements ExtensionFactory {

private static final Logger log = LoggerFactory.getLogger(SpringExtensionFactory.class);
public static final boolean AUTOWIRE_BY_DEFAULT = true;

private static final int AUTOWIRE_CONSTRUCTOR = AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR;

private PluginManager pluginManager;
private boolean autowire;
/**
* The plugin manager is used for retrieving a plugin from a given extension class
* and as a fallback supplier of an application context.
*/
protected final PluginManager pluginManager;
/**
* Indicates if springs autowiring possibilities should be used.
*/
protected final boolean autowire;

public SpringExtensionFactory(PluginManager pluginManager) {
this(pluginManager, true);
public SpringExtensionFactory(final PluginManager pluginManager) {
this(pluginManager, AUTOWIRE_BY_DEFAULT);
}

public SpringExtensionFactory(PluginManager pluginManager, boolean autowire) {
public SpringExtensionFactory(final PluginManager pluginManager, final boolean autowire) {
this.pluginManager = pluginManager;
this.autowire = autowire;
if (!autowire) {
log.warn("Autowiring is disabled although the only reason for existence of this special factory is" +
" supporting spring and its application context.");
}
}

/**
* Creates an instance of the given {@code extensionClass}. If {@link #autowire} is set to {@code true} this method
* will try to use springs autowiring possibilities.
*
* @param extensionClass The class annotated with {@code @}{@link Extension}.
* @param <T> The type for that an instance should be created.
* @return an instance of the the requested {@code extensionClass}.
* @see #getApplicationContextBy(Class)
*/
@Override
public <T> T create(Class<T> extensionClass) {
T extension = createWithoutSpring(extensionClass);
if (autowire && extension != null) {
ApplicationContext applicationContext = this.getApplicationContext(extensionClass);
if (applicationContext != null) {
Map<String, T> extensionBeanMap = applicationContext.getBeansOfType(extensionClass);
if (!extensionBeanMap.isEmpty()) {
if (extensionBeanMap.size() > 1) {
log.error("There are more than 1 extension bean '{}' defined!", extensionClass.getName());
}
extension = extensionBeanMap.values().iterator().next();
}
applicationContext.getAutowireCapableBeanFactory().autowireBean(extension);
}
public <T> T create(final Class<T> extensionClass) {
if (!this.autowire) {
log.warn("Create instance of '" + nameOf(extensionClass) + "' without using springs possibilities as" +
" autowiring is disabled.");
return createWithoutSpring(extensionClass);
}

return extension;
return getApplicationContextBy(extensionClass)
.map(applicationContext -> createWithSpring(extensionClass, applicationContext))
.orElseGet(() -> createWithoutSpring(extensionClass));
}

protected <T> T createWithoutSpring(Class<T> extensionClass) {
try {
return extensionClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
log.error(e.getMessage(), e);
/**
* Creates an instance of the given {@code extensionClass} by using the {@link AutowireCapableBeanFactory} of the given
* {@code applicationContext}. All kinds of autowiring are applied:
* <ol>
* <li>Constructor injection</li>
* <li>Setter injection</li>
* <li>Field injection</li>
* </ol>
*
* @param extensionClass The class annotated with {@code @}{@link Extension}.
* @param <T> The type for that an instance should be created.
* @param applicationContext The context to use for autowiring.
* @return an autowired extension instance.
*/
@SuppressWarnings("unchecked")
protected <T> T createWithSpring(final Class<T> extensionClass, final ApplicationContext applicationContext) {
final AutowireCapableBeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory();

log.debug("Instantiate extension class '" + nameOf(extensionClass) + "' by using constructor autowiring.");
// Autowire by constructor. This does not include the other types of injection (setters and/or fields).
final Object autowiredExtension = beanFactory.autowire(extensionClass, AUTOWIRE_CONSTRUCTOR,
// The value of the 'dependencyCheck' parameter is actually irrelevant as the using constructor of 'RootBeanDefinition'
// skips action when the autowire mode is set to 'AUTOWIRE_CONSTRUCTOR'. Although the default value in
// 'AbstractBeanDefinition' is 'DEPENDENCY_CHECK_NONE', so it is set to false here as well.
false);
log.trace("Created extension instance by constructor injection: " + autowiredExtension);

log.debug("Completing autowiring of extension: " + autowiredExtension);
// Autowire by using remaining kinds of injection (e. g. setters and/or fields).
beanFactory.autowireBean(autowiredExtension);
log.trace("Autowiring has been completed for extension: " + autowiredExtension);

return (T) autowiredExtension;
}

/**
* Retrieves springs {@link ApplicationContext} from the extensions plugin or the {@link #pluginManager}.
* <p>
* The ordering of checks is:
* <ol>
* <li>If the given {@code extensionClass} belongs to a plugin that is a {@link SpringPlugin} the plugins context will be returned.</li>
* <li>Otherwise, if the given {@link #pluginManager} of this instance is a {@link SpringPluginManager} the managers context will be returned.</li>
* <li>If none of these checks fits, {@code null} is returned.</li>
* </ol>
*
* @param extensionClass The class annotated with {@code @}{@link Extension}.
* @param <T> The Type of extension for that an {@link ApplicationContext} is requested.
* @return the best fitting context, or {@code null}.
*/
protected <T> Optional<ApplicationContext> getApplicationContextBy(final Class<T> extensionClass) {
final Plugin plugin = Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass))
.map(PluginWrapper::getPlugin)
.orElse(null);

final ApplicationContext applicationContext;

if (plugin instanceof SpringPlugin) {
log.debug(" Extension class ' " + nameOf(extensionClass) + "' belongs to spring-plugin '" + nameOf(plugin)
+ "' and will be autowired by using its application context.");
applicationContext = ((SpringPlugin) plugin).getApplicationContext();
} else if (this.pluginManager instanceof SpringPluginManager) {
log.debug(" Extension class ' " + nameOf(extensionClass) + "' belongs to a non spring-plugin (or main application)" +
" '" + nameOf(plugin) + ", but the used PF4J plugin-manager is a spring-plugin-manager. Therefore" +
" the extension class will be autowired by using the managers application contexts");
applicationContext = ((SpringPluginManager) this.pluginManager).getApplicationContext();
} else {
log.warn(" No application contexts can be used for instantiating extension class '" + nameOf(extensionClass) + "'."
+ " This extension neither belongs to a PF4J spring-plugin (id: '" + nameOf(plugin) + "') nor is the used" +
" plugin manager a spring-plugin-manager (used manager: '" + nameOf(this.pluginManager.getClass()) + "')." +
" At perspective of PF4J this seems highly uncommon in combination with a factory which only reason for existence" +
" is using spring (and its application context) and should at least be reviewed. In fact no autowiring can be" +
" applied although autowire flag was set to 'true'. Instantiating will fallback to standard Java reflection.");
applicationContext = null;
}

return null;
return Optional.ofNullable(applicationContext);
}

private <T> ApplicationContext getApplicationContext(Class<T> extensionClass) {
ApplicationContext applicationContext = null;
PluginWrapper pluginWrapper = this.pluginManager.whichPlugin(extensionClass);
if (pluginWrapper != null) { // is plugin extension
Plugin plugin = pluginWrapper.getPlugin();
if (plugin instanceof SpringPlugin) {
applicationContext = ((SpringPlugin) plugin).getApplicationContext();
}
} else if (this.pluginManager instanceof SpringPluginManager) { // is system extension and plugin manager is SpringPluginManager
SpringPluginManager springPluginManager = (SpringPluginManager) this.pluginManager;
applicationContext = springPluginManager.getApplicationContext();
/**
* Creates an instance of the given class object by using standard Java reflection.
*
* @param extensionClass The class annotated with {@code @}{@link Extension}.
* @param <T> The type for that an instance should be created.
* @return an instantiated extension.
* @throws IllegalArgumentException if the given class object has no public constructor.
* @throws RuntimeException if the called constructor cannot be instantiated with {@code null}-parameters.
*/
@SuppressWarnings("unchecked")
protected <T> T createWithoutSpring(final Class<T> extensionClass) throws IllegalArgumentException {
final Constructor<?> constructor = getPublicConstructorWithShortestParameterList(extensionClass)
// An extension class is required to have at least one public constructor.
.orElseThrow(() -> new IllegalArgumentException("Extension class '" + nameOf(extensionClass)
+ "' must have at least one public constructor."));
try {
log.debug("Instantiate '" + nameOf(extensionClass) + "' by calling '" + constructor + "'with standard Java reflection.");
// Creating the instance by calling the constructor with null-parameters (if there are any).
return (T) constructor.newInstance(nullParameters(constructor));
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException ex) {
// If one of these exceptions is thrown it it most likely because of NPE inside the called constructor and
// not the reflective call itself as we precisely searched for a fitting constructor.
log.error(ex.getMessage(), ex);
throw new RuntimeException("Most likely this exception is thrown because the called constructor (" + constructor + ")" +
" cannot handle 'null' parameters. Original message was: "
+ ex.getMessage(), ex);
}
return applicationContext;
}

private Optional<Constructor<?>> getPublicConstructorWithShortestParameterList(final Class<?> extensionClass) {
return Stream.of(extensionClass.getConstructors())
.min(Comparator.comparing(Constructor::getParameterCount));
}

private Object[] nullParameters(final Constructor<?> constructor) {
return new Object[constructor.getParameterCount()];
}

private String nameOf(final Plugin plugin) {
return nonNull(plugin)
? plugin.getWrapper().getPluginId()
: "system";
}

private <T> String nameOf(final Class<T> clazz) {
return clazz.getName();
}
}