0

We've been using Guice for DI in AWS Lambdas, but now are moving to Spring Boot and long running services.

We've got feature toggles working as dynamic proxies in Guice, but need to implement in Spring.

Say we have a SomeFeature interface and two implementations DisabledImplementation and EnabledImplementation.

I can get really close by tagging DisabledImplementation with @Component("some.feature.disabled") and EnabledImplementation with @Component("some.feature.enabled") and then writing an implementation like this:

@Primary
@Component
public class FlippingFeature implements SomeFeature {

    private final SomeFeature enabled;
    private final SomeFeature disabled;
    private final FeatureFlip featureFlip;

    @Inject
    public FlippingFeature(@Named("some.feature.enabled") SomeFeature enabled,
                           @Named("some.feature.disabled") SomeFeature disabled,
                           FeatureFlip featureFlip) {
        this.enabled = enabled;
        this.disabled = disabled;
        this.featureFlip = featureFlip;
    }

    @Override
    public String foo() {
        return featureFlip.isEnabled("some.feature") ? enabled.foo() : disabled.foo();
    }
}

But I'd prefer to not write the FlippingFeature class at all and do it w/ a dynamic proxy hidden away. Can I do this with a custom BeanFactoryPostProcessor or something else?

Eric
  • 215
  • 3
  • 11
  • I think you should read this: https://stackoverflow.com/help/mcve. There is *way* too much code in your post. – Adrien Brunelat Apr 10 '18 at 13:38
  • I think it is necessary to show what we've done and the goals. – Eric Apr 10 '18 at 13:43
  • Your call, but the more code you post, the more time a potential answer will take to appear (if at all). People just don't want to swallow a wall of code before knowing if they can help or not. – Adrien Brunelat Apr 10 '18 at 13:45
  • By the way, showing what you've done and the goal **and** posting a mcve is not incompatible. – Adrien Brunelat Apr 10 '18 at 13:46
  • I can delete the Guice stuff, I guess... – Eric Apr 10 '18 at 13:47
  • This seems a bit on the crazy side, but I like it. – chrylis -cautiouslyoptimistic- Apr 10 '18 at 13:57
  • @chrylis it works well for Guice, which is why I want to do something similar in Spring. Guice code: https://gist.github.com/efenderbosch/96eb3310b08494911b7c290a14ede2e2 – Eric Apr 10 '18 at 14:05
  • https://www.togglz.org/documentation/spring-boot-starter.html might be helpful – ootero Apr 10 '18 at 14:52
  • I'm trying to avoid if-feature-enabled-else logic in code and just inject an implementation that simply does the right thing. – Eric Apr 10 '18 at 15:13
  • If you actually have separate implementations, then using Spring Boot autoconfiguration classes is probably the way to go. They do very similar stuff with runtime properties all the time. – chrylis -cautiouslyoptimistic- Apr 10 '18 at 17:08
  • We're using LaunchDarkly for the feature flags, so the flag can change at any time. I don't want this in application.yml. I want to be able to change it in one location and have all servers instantly pickup the change w/o a re-deploy or rebuilding the entire Spring context. – Eric Apr 10 '18 at 18:01

1 Answers1

-1

I've got a pretty decent solution now.

@Qualifier
@Retention(RUNTIME)
// tag the disabled feature implementation w/ this annotation
public @interface Disabled {}

@Qualifier
@Retention(RUNTIME)
// tag the enabled feature implementation w/ this annotation
public @interface Enabled {}

@Target(TYPE)
@Retention(RUNTIME)
// tag the feature interface w/ this annotation
public @interface Feature {
    String value();
}

// create a concrete implementation of this class for each feature interface and annotate w/ @Primary
// note the use of @Enabled and @Disabled injection qualifiers
public abstract class FeatureProxyFactoryBean<T> implements FactoryBean<T> {

    private final Class<T> type;
    private FeatureFlag featureFlag;
    protected T enabled;
    protected T disabled;

    protected FeatureProxyFactoryBean(Class<T> type) {
        this.type = type;
    }

    @Autowired
    public void setFeatureFlag(FeatureFlag featureFlag) {
        this.featureFlag = featureFlag;
    }

    @Autowired
    public void setEnabled(@Enabled T enabled) {
        this.enabled = enabled;
    }

    @Autowired
    public void setDisabled(@Disabled T disabled) {
        this.disabled = disabled;
    }

    @Override
    public T getObject() {
        Feature feature = type.getAnnotation(Feature.class);
        if (feature == null) {
            throw new IllegalArgumentException(type.getName() + " must be annotated with @Feature");
        }
        String key = feature.value();

        ClassLoader classLoader = FeatureProxyFactoryBean.class.getClassLoader();
        Class<?>[] interfaces = {type};
        return (T) Proxy.newProxyInstance(classLoader, interfaces,
                (proxy1, method, args) -> featureFlag.isEnabled(key) ?
                        method.invoke(enabled, args) :
                        method.invoke(disabled, args));
    }

    @Override
    public Class<T> getObjectType() {
        return type;
    }
}

// test classes

@Feature("test_key")
public interface SomeFeature {
    String foo();
}

@Disabled
@Component
public class DisabledFeature implements SomeFeature {
    @Override
    public String foo() {
        return "disabled";
    }
}

@Enabled
@Component
public class EnabledFeature implements SomeFeature {
    @Override
    public String foo() {
        return "enabled";
    }
}

@Primary
@Component
public class SomeFeatureProxyFactoryBean extends FeatureProxyFactoryBean<SomeFeature> {
    public SomeFeatureProxyFactoryBean() {
        super(SomeFeature.class);
    }
}

Then inject @Inject SomeFeature someFeature where needed and it will get the proxy instance due to the @Primary annotation.

Now we can toggle the feature on and off in Launchdarkly and it (nearly) instantly gets reflected in all running instances without a restart or re-initializing the Spring context.

Eric
  • 215
  • 3
  • 11
  • Note that delegating `hashCode` and `equals` to the implementations does not work. You need to implement them in the dynamic proxy, otherwise e.g. collections containing proxy objects may break. See https://stackoverflow.com/a/39469786/5519485 – Erik Hofer Apr 11 '18 at 15:00
  • Thanks, but these are service type interfaces and will likely never be put in a collection. Maybe some sort of service registry, I suppose, but not likely. – Eric Apr 11 '18 at 15:02
  • There can also be other things that rely on `equals` and `hashCode` working correctly. This could become a hard to find bug in the future... – Erik Hofer Apr 11 '18 at 15:05