Support a DynamicOptions.ClassNameProvider

The new DynamicOptions.ClassNameProvider class allows the registering of
DynamicOptions by class name.  By using the class name, class loading
can be deferred until classes are needed, allowing classes to be only
loaded via the classloader of the running command.  This classloading
delay is important to help ensure that there is only one version of the
class ever loaded when using cross plugin DynamicOptions.

Direct registering of DynamicBeans is still supported (it is easier to
use for extending core, or same plugin commands, when not crossing
classloader boundaries).  To ensure mutual exclusion of a command's
bindings per plugin, the new  DynamicOptions.ClassNameProvider extends
the DynamicBean interface.  When using the
DynamicOptions.ClassNameProvider, any Options defined directly on it
will be ignored since it is expected that the classname returned by the
getClassName() will instead provide the options.

Change-Id: I19a3de83dec40ea8eff2df8f9977c44729616581
This commit is contained in:
Martin Fick
2017-05-17 18:26:34 -06:00
committed by Martin Fick
parent 104e707c8e
commit c00f64e24b

View File

@@ -49,6 +49,33 @@ public class DynamicOptions {
*/
public interface DynamicBean {}
/**
* To provide additional options to a command in another classloader, bind a ClassNameProvider
* which provides the name of your DynamicBean in the other classLoader.
*
* <p>Do this by binding to just the name of the command you are going to bind to so that your
* classLoader does not load the command's class which likely is not in your classpath. To ensure
* that the command's class is not in your classpath, you can exclude it during your build.
*
* <p>For example:
*
* <pre>
* bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
* .annotatedWith(Exports.named( "com.google.gerrit.plugins.otherplugin.command"))
* .to(MyOptionsClassNameProvider.class);
*
* static class MyOptionsClassNameProvider implements DynamicOptions.ClassNameProvider {
* @Override
* public String getClassName() {
* return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
* }
* }
* </pre>
*/
public interface ClassNameProvider extends DynamicBean {
String getClassName();
}
/**
* Implement this if your DynamicBean needs an opportunity to act on the Bean directly before or
* after argument parsing.
@@ -118,21 +145,32 @@ public class DynamicOptions {
public DynamicBean getDynamicBean(Object bean, DynamicBean dynamicBean) {
ClassLoader coreCl = getClass().getClassLoader();
ClassLoader beanCl = bean.getClass().getClassLoader();
ClassLoader loader = beanCl;
if (beanCl != coreCl) { // bean from a plugin?
ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader();
if (beanCl != dynamicBeanCl) { // in a different plugin?
ClassLoader mergedCL = new DelegatingClassLoader(beanCl, dynamicBeanCl);
try {
return injector
.createChildInjector()
.getInstance(
(Class<DynamicOptions.DynamicBean>)
mergedCL.loadClass(dynamicBean.getClass().getCanonicalName()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
loader = new DelegatingClassLoader(beanCl, dynamicBeanCl);
}
}
String className = null;
if (dynamicBean instanceof ClassNameProvider) {
className = ((ClassNameProvider) dynamicBean).getClassName();
} else if (loader != beanCl) { // in a different plugin?
className = dynamicBean.getClass().getCanonicalName();
}
if (className != null) {
try {
return injector
.createChildInjector()
.getInstance((Class<DynamicOptions.DynamicBean>) loader.loadClass(className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return dynamicBean;
}