roookeee
roookeee

Reputation: 1818

Improve explicitness of spring library for extendable config-objects

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)

Upvotes: 0

Views: 132

Answers (1)

stacker
stacker

Reputation: 4475

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).

Upvotes: 2

Related Questions