From f307f0fb793741a745db395cbb98d76492643b76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=20Schr=C3=B6er?=
<16744580+m-schroeer@users.noreply.github.com>
Date: Sun, 19 Jul 2020 19:19:08 +0200
Subject: [PATCH 1/2] Support spring constructor autowiring (injection) in
extension implementations
The implementation follows the former one in getting the best fitting spring applciation context.
After that (if present) the application context is used to create an autowired bean by using mode 'AUTOWIRE_CONSTRUCTOR' and autowire the rest (setters, fields) right after having the instance.
If no context could be received (non spring plugin and non SpringPluginManager (unusual with SpringExtensionFactory but possible)) default Java reflection is used like before except that it public support constructors that have a non empty parameter list as well.
---
.../pf4j/spring/SpringExtensionFactory.java | 230 +++++++++++++++---
1 file changed, 198 insertions(+), 32 deletions(-)
diff --git a/pf4j-spring/src/main/java/org/pf4j/spring/SpringExtensionFactory.java b/pf4j-spring/src/main/java/org/pf4j/spring/SpringExtensionFactory.java
index 480f70b..3c6efcf 100644
--- a/pf4j-spring/src/main/java/org/pf4j/spring/SpringExtensionFactory.java
+++ b/pf4j-spring/src/main/java/org/pf4j/spring/SpringExtensionFactory.java
@@ -15,69 +15,235 @@
*/
package org.pf4j.spring;
+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.
+ *
+ * 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.
+ *
+ * Creates a new extension instance every time a request is done.
+ *
+ * Example of supported autowire modes:
+ *
{@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;
+ * }
+ * }
+ * }
+ *
* @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 PluginManager pluginManager;
- private boolean autowire;
+ private static final int AUTOWIRE_CONSTRUCTOR = AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR;
- public SpringExtensionFactory(PluginManager pluginManager) {
- this(pluginManager, true);
+ /**
+ * 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(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 The type for that an instance should be created.
+ * @return an instance of the the requested {@code extensionClass}.
+ * @see #getApplicationContextBy(Class)
+ */
@Override
- public T create(Class extensionClass) {
- T extension = createWithoutSpring(extensionClass);
- if (autowire && extension != null) {
- // test for SpringBean
- PluginWrapper pluginWrapper = pluginManager.whichPlugin(extensionClass);
- if (pluginWrapper != null) { // is plugin extension
- Plugin plugin = pluginWrapper.getPlugin();
- if (plugin instanceof SpringPlugin) {
- // autowire
- ApplicationContext pluginContext = ((SpringPlugin) plugin).getApplicationContext();
- pluginContext.getAutowireCapableBeanFactory().autowireBean(extension);
- } else if (this.pluginManager instanceof SpringPluginManager) { // is system extension and plugin manager is SpringPluginManager
- SpringPluginManager springPluginManager = (SpringPluginManager) this.pluginManager;
- ApplicationContext pluginContext = springPluginManager.getApplicationContext();
- pluginContext.getAutowireCapableBeanFactory().autowireBean(extension);
- }
- }
+ public T create(final Class 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));
}
+ /**
+ * Creates an instance of the given {@code extensionClass} by using the {@link AutowireCapableBeanFactory} of the given
+ * {@code applicationContext}. All kinds of autowiring are applied:
+ *
+ * - Constructor injection
+ * - Setter injection
+ * - Field injection
+ *
+ *
+ * @param extensionClass The class annotated with {@code @}{@link Extension}.
+ * @param 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 createWithoutSpring(Class> extensionClass) {
+ protected T createWithSpring(final Class 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}.
+ *
+ * The ordering of checks is:
+ *
+ * - If the given {@code extensionClass} belongs to a plugin that is a {@link SpringPlugin} the plugins context will be returned.
+ * - Otherwise, if the given {@link #pluginManager} of this instance is a {@link SpringPluginManager} the managers context will be returned.
+ * - If none of these checks fits, {@code null} is returned.
+ *
+ *
+ * @param extensionClass The class annotated with {@code @}{@link Extension}.
+ * @param The Type of extension for that an {@link ApplicationContext} is requested.
+ * @return the best fitting context, or {@code null}.
+ */
+ protected Optional getApplicationContextBy(final Class 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 Optional.ofNullable(applicationContext);
+ }
+
+ /**
+ * Creates an instance of the given class object by using standard Java reflection.
+ *
+ * @param extensionClass The class annotated with {@code @}{@link Extension}.
+ * @param 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 createWithoutSpring(final Class 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 {
- return (T) extensionClass.newInstance();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
+ 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);
}
+ }
+
+ private Optional> getPublicConstructorWithShortestParameterList(final Class> extensionClass) {
+ return Stream.of(extensionClass.getConstructors())
+ .min(Comparator.comparing(Constructor::getParameterCount));
+ }
- return null;
+ 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 String nameOf(final Class clazz) {
+ return clazz.getName();
+ }
}
From 79bf9bac91099255aeeddca47fbe8f7a1385b380 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=20Schr=C3=B6er?=
<16744580+m-schroeer@users.noreply.github.com>
Date: Sun, 19 Jul 2020 19:21:19 +0200
Subject: [PATCH 2/2] use constructor injection in demo plugin2
---
.../src/main/java/org/pf4j/demo/hello/HelloPlugin.java | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/demo/plugins/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java b/demo/plugins/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java
index 2dc9e14..3761a36 100644
--- a/demo/plugins/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java
+++ b/demo/plugins/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java
@@ -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() {