Implement DynamicSet<T>, DynamicMap<T> to provide bindings in Guice
The core server can now declare that it needs a DynamicSet supplied by Guice at runtime: DynamicSet.setOf(binder(), SomeBaseType.class); and then receive this as an injection: DynamicSet<SomeBaseType> theThings Core server code can register static implementations into this set: DynamicSet.bind(binder(), SomeBaseType.class).to(AnImpl.class); Plugins may use the same syntax in their own Guice modules to register their own implementations. When a plugin starts, its registrations will be added to the DynamicSet, and when it stops, the references get cleaned up automatically. During a hot reload of a plugin references from the old plugin and the new plugin are matched up by collection member type and Guice annotation information, and atomically swapped. Plugins can use automatic registration if interfaces are annotated with @ExtensionPoint and the plugin implementation is annoated with @Listen and also implements the interface, directly or indirectly through its base classes or interfaces: (gerrit-extension-api) @ExtensionPoint public interface NewChangeListener { (gerrit-server) DynamicSet.setOf(binder(), NewChangeListener.class); (plugin or extension code) @Listen class OnNewChange implements NewChangeListener { Automatic registration binds the listeners into the system module, which may prevent plugins or extensions from automatically connecting with extension points inside of the HTTP or SSH servers. This shouldn't generally be a problem as the majority of interfaces plugins or extensions care about will be defined in the core server, and thus be in the system module. Change-Id: Ic8f371d97f8f0ddb6cad97fef3b58e1c3d32381f
This commit is contained in:
@@ -16,8 +16,16 @@ package com.google.gerrit.server.plugins;
|
||||
|
||||
import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gerrit.extensions.annotations.Export;
|
||||
import com.google.gerrit.extensions.annotations.ExtensionPoint;
|
||||
import com.google.gerrit.extensions.annotations.Listen;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.Scopes;
|
||||
import com.google.inject.TypeLiteral;
|
||||
import com.google.inject.internal.UniqueAnnotations;
|
||||
|
||||
import org.eclipse.jgit.util.IO;
|
||||
import org.objectweb.asm.AnnotationVisitor;
|
||||
@@ -31,7 +39,11 @@ import org.objectweb.asm.Type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
@@ -39,11 +51,15 @@ class AutoRegisterModules {
|
||||
private static final int SKIP_ALL = ClassReader.SKIP_CODE
|
||||
| ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
|
||||
private final String pluginName;
|
||||
private final PluginGuiceEnvironment env;
|
||||
private final JarFile jarFile;
|
||||
private final ClassLoader classLoader;
|
||||
private final ModuleGenerator sshGen;
|
||||
private final ModuleGenerator httpGen;
|
||||
|
||||
private Set<Class<?>> sysSingletons;
|
||||
private Map<TypeLiteral<?>, Class<?>> sysListen;
|
||||
|
||||
Module sysModule;
|
||||
Module sshModule;
|
||||
Module httpModule;
|
||||
@@ -53,6 +69,7 @@ class AutoRegisterModules {
|
||||
JarFile jarFile,
|
||||
ClassLoader classLoader) {
|
||||
this.pluginName = pluginName;
|
||||
this.env = env;
|
||||
this.jarFile = jarFile;
|
||||
this.classLoader = classLoader;
|
||||
this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
|
||||
@@ -60,6 +77,9 @@ class AutoRegisterModules {
|
||||
}
|
||||
|
||||
AutoRegisterModules discover() throws InvalidPluginException {
|
||||
sysSingletons = Sets.newHashSet();
|
||||
sysListen = Maps.newHashMap();
|
||||
|
||||
if (sshGen != null) {
|
||||
sshGen.setPluginName(pluginName);
|
||||
}
|
||||
@@ -69,6 +89,9 @@ class AutoRegisterModules {
|
||||
|
||||
scan();
|
||||
|
||||
if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
|
||||
sysModule = makeSystemModule();
|
||||
}
|
||||
if (sshGen != null) {
|
||||
sshModule = sshGen.create();
|
||||
}
|
||||
@@ -78,6 +101,36 @@ class AutoRegisterModules {
|
||||
return this;
|
||||
}
|
||||
|
||||
private Module makeSystemModule() {
|
||||
return new AbstractModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
for (Class<?> clazz : sysSingletons) {
|
||||
bind(clazz).in(Scopes.SINGLETON);
|
||||
}
|
||||
for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entrySet()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<Object> impl = (Class<Object>) e.getValue();
|
||||
|
||||
Annotation n = impl.getAnnotation(Export.class);
|
||||
if (n == null) {
|
||||
n = impl.getAnnotation(javax.inject.Named.class);
|
||||
}
|
||||
if (n == null) {
|
||||
n = impl.getAnnotation(com.google.inject.name.Named.class);
|
||||
}
|
||||
if (n == null) {
|
||||
n = UniqueAnnotations.create();
|
||||
}
|
||||
bind(type).annotatedWith(n).to(impl);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void scan() throws InvalidPluginException {
|
||||
Enumeration<JarEntry> e = jarFile.entries();
|
||||
while (e.hasMoreElements()) {
|
||||
@@ -103,7 +156,15 @@ class AutoRegisterModules {
|
||||
export(def);
|
||||
} else {
|
||||
PluginLoader.log.warn(String.format(
|
||||
"Plugin %s tries to export abstract class %s",
|
||||
"Plugin %s tries to @Export(\"%s\") abstract class %s",
|
||||
pluginName, def.exportedAsName, def.className));
|
||||
}
|
||||
} else if (def.listen) {
|
||||
if (def.isConcrete()) {
|
||||
listen(def);
|
||||
} else {
|
||||
PluginLoader.log.warn(String.format(
|
||||
"Plugin %s tries to @Listen abstract class %s",
|
||||
pluginName, def.className));
|
||||
}
|
||||
}
|
||||
@@ -135,11 +196,81 @@ class AutoRegisterModules {
|
||||
} else if (is("javax.servlet.http.HttpServlet", clazz)) {
|
||||
if (httpGen != null) {
|
||||
httpGen.export(export, clazz);
|
||||
listen(clazz, clazz);
|
||||
}
|
||||
} else {
|
||||
int cnt = sysListen.size();
|
||||
listen(clazz, clazz);
|
||||
if (cnt == sysListen.size()) {
|
||||
// If no bindings were recorded, the extension isn't recognized.
|
||||
throw new InvalidPluginException(String.format(
|
||||
"Class %s with @Export(\"%s\") not supported",
|
||||
clazz.getName(), export.value()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void listen(ClassData def) throws InvalidPluginException {
|
||||
Class<?> clazz;
|
||||
try {
|
||||
clazz = Class.forName(def.className, false, classLoader);
|
||||
} catch (ClassNotFoundException err) {
|
||||
throw new InvalidPluginException(String.format(
|
||||
"Class %s with @Export(\"%s\") not supported",
|
||||
clazz.getName(), export.value()));
|
||||
"Cannot load %s with @Listen",
|
||||
def.className), err);
|
||||
}
|
||||
|
||||
Listen listen = clazz.getAnnotation(Listen.class);
|
||||
if (listen != null) {
|
||||
listen(clazz, clazz);
|
||||
} else {
|
||||
PluginLoader.log.warn(String.format(
|
||||
"In plugin %s asm incorrectly parsed %s with @Listen",
|
||||
pluginName, clazz.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
private void listen(java.lang.reflect.Type type, Class<?> clazz)
|
||||
throws InvalidPluginException {
|
||||
while (type != null) {
|
||||
Class<?> rawType;
|
||||
if (type instanceof ParameterizedType) {
|
||||
rawType = (Class<?>) ((ParameterizedType) type).getRawType();
|
||||
} else if (type instanceof Class) {
|
||||
rawType = (Class<?>) type;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawType.getAnnotation(ExtensionPoint.class) != null) {
|
||||
TypeLiteral<?> tl = TypeLiteral.get(type);
|
||||
if (env.hasDynamicSet(tl)) {
|
||||
sysSingletons.add(clazz);
|
||||
sysListen.put(tl, clazz);
|
||||
} else if (env.hasDynamicMap(tl)) {
|
||||
if (clazz.getAnnotation(Export.class) == null) {
|
||||
throw new InvalidPluginException(String.format(
|
||||
"Class %s requires @Export(\"name\") annotation for %s",
|
||||
clazz.getName(), rawType.getName()));
|
||||
}
|
||||
sysSingletons.add(clazz);
|
||||
sysListen.put(tl, clazz);
|
||||
} else {
|
||||
throw new InvalidPluginException(String.format(
|
||||
"Cannot register %s, server does not accept %s",
|
||||
clazz.getName(), rawType.getName()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
java.lang.reflect.Type[] interfaces = rawType.getGenericInterfaces();
|
||||
if (interfaces != null) {
|
||||
for (java.lang.reflect.Type i : interfaces) {
|
||||
listen(i, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
type = rawType.getGenericSuperclass();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,9 +300,12 @@ class AutoRegisterModules {
|
||||
|
||||
private static class ClassData implements ClassVisitor {
|
||||
private static final String EXPORT = Type.getType(Export.class).getDescriptor();
|
||||
private static final String LISTEN = Type.getType(Listen.class).getDescriptor();
|
||||
|
||||
String className;
|
||||
int access;
|
||||
String exportedAsName;
|
||||
boolean listen;
|
||||
|
||||
boolean isConcrete() {
|
||||
return (access & Opcodes.ACC_ABSTRACT) == 0
|
||||
@@ -195,6 +329,10 @@ class AutoRegisterModules {
|
||||
}
|
||||
};
|
||||
}
|
||||
if (visible && LISTEN.equals(desc)) {
|
||||
listen = true;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user