0

I am currently working on a spring-library that allows user-defined config-classes (has nothing to to with @Configuration) to be adjusted from another part of the application before they are used:

interface ConfigAdjuster<T extends Config<T>> {
    void adjust(T t);
}

abstract class Config<T extends Config<T>> {
     @Autowired
     Optional<ConfigAdjuster<T>> adjuster;

     @PostConstruct
     private void init() {
         //i know this cast is somewhat unsafe, just ignore it for this question
         adjuster.ifPresent(a -> a.adjust((T)this));
     }
}

This can be used as follows:

class MyConfig extends Config<MyConfig> {
    //imagine many fields of more complex types
    public String myData;
}

@Configuration
class MyConfigDefaults {
    @Profile("dev")
    @Bean 
    public MyConfig devDefaults() {
        //imagine setting defaults values here
        return new MyConfig();
    }
}

Now a consumer of the library that uses MyConfig can do the following somewhere in his application:

@Bean
public ConfigAdjuster<MyConfig> adjustDefaults() {
    return cfg -> {
        cfg.myData = "something_other_than_default";
    }
}

The biggest problem I see with this approach is that the whole "adjust the config"-part is somewhat hidden for the user. You can not easily tell you are able to change the default-configuration by using a ConfigAdjuster. In the worst case the user tries to autowire the config object and tries to modify it that way which results in undefined behaviour because other components could already have been initialized with the defaults.

Is there an easy way to make this approach more "telling" than what it is right now? The whole idea is to not copy&paste the whole default-config + adjustment parts across multiple projects.

One way to make all of this more explicit would be to require the adjuster in the constructor of Config, but this pollutes every constructor and usage of the inherting classes.

Any thoughts on this?

Edit: Do note that this is a simplified version of the library and I do know about the implications of a private @PostConstruct etc. If you have another way of achieving all of this without the @PostConstruct please do share :)

Edit2: Let me outline the main goals of this library again:

  1. Allow the definition of default config-objects for the library-user
  2. Allow the enduser (consuming a depedency using this library) to overwrite certain parts of the default configuration before it is used
  3. Save the library-user from boilerplate (e.g. define 2. on their own)
roookeee
  • 1,710
  • 13
  • 24
  • Relates to https://stackoverflow.com/q/43232021/344480 – Matthias Feb 04 '19 at 17:12
  • This can help you: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Primary.html – David Pérez Cabrera Feb 04 '19 at 22:06
  • `@Primary` and the likes of `@ConditionalOnMissingBean` don't help at all as these don't allow the user to customize some parts of the default configuration object, only all of it which eliminates most benefits of default configuration objects – roookeee Feb 04 '19 at 22:30

1 Answers1

2

There is two solution for your problem:

1- define a generic Customizer something like:

public interface Customizer<T> {

    T customize(T t);

    boolean supports(Class target);
}

in your lib you have a config:

public class MyConfig {

    private String property;

    public MyConfig() {
    }

    public void setProperty(String property) {
        this.property = property;
    }
}

so your Default configuration should look something like this:

@Configuration
public class DefaultConfiguration {


    @Autowired(required = false)
    private List<Customizer> customizers;

    @Bean
    public MyConfig myConfig() {
        MyConfig myConfig = new MyConfig();
        myConfig.setProperty("default value");
        if (customizers != null) {     
          for (Customizer c : customizers) {
            if (c.supports(MyConfig.class)) {
                return (MyConfig) c.customize(myConfig);
            }
          }
        }
        return myConfig;
    }
}

this way, the only thing the user should do whenever he wants to customize you bean is to implement Customizer, and then declare it as a bean.

public class MyConfigCustomizer implements Customizer<MyConfig> {

    @Override
    public MyConfig customize(MyConfig myConfig) {
        //customization here
        return myConfig;
    }

    @Override
    public boolean supports(Class<?> target) {
        return MyConfig.class.isAssignableFrom(target);
    }
}

and he should declare it:

@Bean 
public Customizer<MyConfig> customizer(){
     return new MyConfigCustomizer ();
 }

I think this answers your question, but it's ugly (uncheched warnings and a List ...) not the best, as everything seems to the user customizable even it's not.

2- I suggest you expose interfaces for Beans that can be adjusted by the user, something like:

public interface MyConfigCustomizer{

MyConfig customize(MyConfig config);

}

your Default Configuration:

@Configuration
public class DefaultConfiguration {


    @Autowired(required = false)
    private MyConfigCustomizer customizer;

    @Bean
    public MyConfig myConfig() {
        MyConfig myConfig = new MyConfig();
        myConfig.setProperty("default value");
        if (customizer != null) {
            return customizer.customize(myconfig);
        }
        return myConfig;
    }

}

this way the user knows that MyConfig can be adjusted (and not all the beans).

stacker
  • 4,317
  • 2
  • 10
  • 24
  • Isn't the second variant exactly the same as I posted? The only thing that seems different is that the library user has to autowire the Customizer himself while defining his default configuration, right? The main problem of this approach is that you as the library user can forget to autowire the customizer and apply it - I would love to have the library take care of that. Thanks for the input though! – roookeee Feb 06 '19 at 08:54
  • @naze I don't think you understand the approach, in your library you need to define the configuration ( all required beans (including Default initialization) for your lib) and let the user create the customizer if he needs it, doing so, Autowiring the customizer is inside your lib, the only thing the user needs to do is to declare his customizer as a Bean somewhere in his code – stacker Feb 06 '19 at 13:22
  • it seems the description of my problem is off - that's exactly how my approach works too. I think the misunderstanding lies in the double usage: a library creator uses my library -> someone else uses said library -> the user only has to create a customizer – roookeee Feb 06 '19 at 14:32