Is there a better approach to install registry pattern modules? #459
-
|
I tend to write my library code around vanilla guice modules. A common pattern I use for dynamic bindings is to require installers to construct/build the module with a registry of types that the module needs to act on. This is particularly important for dynamic binding scenarios where the registration of one type may trigger the dynamic binding of other types with various Qualifiers/BindingAnnotations. Here's how it looks: public class MyMod extends AbstractModule {
private final Set<Class<?> registry;
public MyMod(Class<?>... registeredTypes) {
this.registry = Collections.synchronizedSet(Arrays.stream(registeredTypes).collect(Collectors.toSet()));
}
@Override
protected void configure() {
for (var type : registry) {
if (!isValid(type)) { // runtime checks on parameterized types etc
log.warn("Class {} is not a subtype of ExpectedType", type.getName());
continue;
}
// Do dynamic bindings
}
}
}This pattern makes it easy for library modules to be used with or without guicey. Installation can be done a few ways: class MyAppModule extends AbstractModule() {
@Override
protected void configure() {
final var module = new MyMod(Target.class, Target2.class);
install(module);
}
}Via the Bundle: GuiceBundle.builder()
.printDiagnosticInfo() // prints a long diagnotic
.enableAutoConfig(getClass().getPackage().getName()) // Auto Register stuff in our app package
.modules(new MyAppModule(), new MyMod(Target.class, Target2.class);)
.build()Or directly during injector creation. For these reasons, we like to keep our binding logic in a traditional module. Example for the above module: public class MyBundle extends UniqueGuiceyBundle {
@Override
public void initialize(final GuiceyBootstrap bootstrap) throws Exception {
bootstrap.installers(MyBundle.MyInstaller.class);
}
@Override
public void run(final GuiceyEnvironment environment) throws Exception {
// I don't like cheating with the static here -- seems brittle
environment.modules(new MyMod(MyInstaller.REGISTRY.toArray(new Class[0])));
}
public static class MyInstaller implements FeatureInstaller {
private final Reporter reporter = new Reporter(MyInstaller.class, "my types =");
static final Set<Class<? extends MyType>> REGISTRY = Collections.synchronizedSet(new HashSet<>()); // Has to be static?
@Override
@SuppressWarnings("unchecked")
public boolean matches(final Class<?> candidate) {
if (MyMod.isValid(candidate)) {
REGISTRY.add((Class<? extends Consumer<Message>>) candidate);
return true;
}
return false;
}
@Override
public void report() {
for (Class<?> clazz : REGISTRY) {
reporter.line(clazz.getName());
}
reporter.report();
}
}
}None of the other installer interfaces seem to have access to the GuiceyEnvironment, to install the module at the end, and I couldn't find an api to get the Installer instance from the GuiceyEnvironment to get the matched types without the use of a static. I don't think the BindingInstaller is really what I want either because I already have the binding logic encapsulated in my module. While the basic approach works, and makes it really easy to hook into the classpath scan, are there any thoughts on avoiding the static registry? Also, the usual pattern includes a few more parts to add extra classes to the Bundle in case there are types outside the default scan path. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
|
You can use shared state for this: shared state is a special per application stortage for various tricky cases (for components interconnection, to avoid additional static state usage). During startup it is available with thread local We will need to create a value object to store in shared state (supplier used for case 2, see below): public static class Collector implements Supplier<Class<? extends MyType>[]> {
private List<Class<? extends MyType>> extensions = new ArrayList<>();
public void add(Class<? extends MyType> extension) {
extensions.add(extension);
}
@Override
public Class<? extends MyType>[] get() {
return extensions.toArray(new Class[0]);
}
}Our bundle would initialize and use state object: public class MyBundle extends UniqueGuiceyBundle {
@Override
public void initialize(final GuiceyBootstrap bootstrap) throws Exception {
// register state object instance
bootstrap.shareState(Collector.class, new Collector())
.installers(MyBundle.MyInstaller.class);
}
@Override
public void run(final GuiceyEnvironment environment) throws Exception {
// access shared state value
environment.modules(new MyMod(environment
.sharedStateOrFail(Collector.class, "No collector registered").get()));
}
}public class MyInstaller implements FeatureInstaller, TypeInstaller {
@Override
@SuppressWarnings("unchecked")
public boolean matches(final Class<?> candidate) {
if (MyMod.isValid(candidate)) {
// use thread local instance (available during startup)
SharedConfigurationState.getStartupInstance()
.getOrFail(Collector.class, "No collector registered")
.add((Class<? extends MyType)type);
return true;
}
return false;
}
}But, if your module could accept a suplier instead an array, it could be registered in initialization stage and overall bundle could be simpler: public class MyBundle extends UniqueGuiceyBundle {
@Override
public void initialize(final GuiceyBootstrap bootstrap) throws Exception {
Collector collector = new Collector();
// register state object instance
bootstrap.shareState(Collector.class, collector)
// installer would populate it
.installers(MyBundle.MyInstaller.class)
// module accepting supplier would call supplier when values would be ready
.module(new MyModule(collector);
}
}p.s. it's not a good thing that installer interfaces did not provide direct access for various objects, but all required objects are accessible through the shared state |
Beta Was this translation helpful? Give feedback.
You can use shared state for this: shared state is a special per application stortage for various tricky cases (for components interconnection, to avoid additional static state usage).
During startup it is available with thread local
SharedConfigurationState.getStartupInstance()and later values could be accessed withApplicationorEnvironmentinstance:SharedConfigurationState.lookup(application, MyClass.class).We will need to create a value object to store in shared state (supplier used for case 2, see below):