visit
There are many options for implementing the task of distributing the configuration, I want to share one of the options I implemented - using MongoDB and local caching.
public interface ConfigProvider<T> {
T getConfig();
}
public static <E, T> Function<E, T> provideGetter(String fieldName, Class<T> targetType, Class<E> sourceType) {
String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(targetType);
MethodHandle virtual = lookup.findVirtual(sourceType, getterName, type);
CallSite callSite = LambdaMetafactory.metafactory(lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
virtual, MethodType.methodType(targetType, sourceType)
);
return (Function<E, T>) callSite.getTarget().invokeExact();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public class ConfigProviderImpl<T, E extends ConfigEntity>
implements ConfigProvider<T> {
private final ConfigService<E> configService;
private final String fieldName;
private final Function<E, T> getter;
private final T defaultValue;
@SuppressWarnings("unchecked")
public ConfigProviderImpl(ConfigService<E> configService,
String fieldName, T defaultValue) {
this.configService = configService;
this.fieldName = fieldName;
this.defaultValue = defaultValue;
this.getter = provideGetter(fieldName,
(Class<T>) defaultValue.getClass(),
configService.getType());
}
public T getConfig() {
return Optional.ofNullable(configService.getConfig())
.map(getter)
.orElse(defaultValue);
}
void ensureCreated() {
configService.ensureCreated(fieldName, defaultValue);
}
}
If, for some reason, the expected field is not set, the getConfig
returns the default value.
The ensureCreated
should be called at the application startup, so we can use a BeanPostProcessor for it.
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof ConfigProviderImpl) {
((ConfigProviderImpl<?, ?>) bean).ensureCreated();
}
return bean;
}
The only thing left is the implementation of the ConfigService#.ensureCreated(fieldName, defaultValue)
, so let’s jump to the implementation first.
public class ConfigService<E extends ConfigEntity> {
public static final String COLLECTION_NAME = "app_config";
private final MongoOperations mongoOperations;
private final E initial;
...
public void ensureCreated(String fieldName, Object value) {
mongoOperations.upsert(
Query.query(Criteria.where("_id").is(initial.getId())),
Update.update("_id", initial.getId()),
getType(), COLLECTION_NAME);
Query query = Query.query(Criteria
.where("_id").is(initial.getId())
.and(fieldName).isNull());
Update update = new Update();
update.set(fieldName, value);
mongoOperations.findAndModify(query, update, getType(), COLLECTION_NAME);
}
@SuppressWarnings("unchecked")
public Class<E> getType() {
return (Class<E>) initial.getClass();
}
}
As we need to have the type of the entity and its identifier for fetching and creating the configs, we just pass the empty entity with a hardcoded id in it - initial
. The rest is trivial: upsert is used to ensure the entity exists and then we set the field if it’s null, so we don’t override existing values.
Configs are often read and rarely changed so it’s worth caching them. I prefer Guava’s memoizeWithExpiration
here as a handy API and with no overhead costs.
private final Supplier<E> configEntitySupplier =
memoizeWithExpiration(this::fetchConfig, 60, TimeUnit.SECONDS);
...
public E getConfig() {
return configEntitySupplier.get();
}
private E fetchConfig() {
Query query = Query.query(Criteria.where("_id").is(initial.getId()));
return mongoOperations.findOne(query, getType(), COLLECTION_NAME);
}
The entity should implement interface ConfigEntity
, which provides getId
for ConfigService and has a hardcoded id.
@Data
public class AppConfigEntity implements ConfigEntity {
private String id = "app_config";
...
@Bean
public ConfigProvider<ConfigA> appConfigAProvider(ConfigService<AppConfigEntity> appConfigService) {
ConfigA defaultValue = new ConfigA(20, .7);
return new ConfigProviderImpl<>(appConfigService, "configA", defaultValue);
}
@Bean
public ConfigService<AppConfigEntity> appConfigService(MongoOperations mongoOperations) {
return new ConfigService<>(mongoOperations, new AppConfigEntity());
}
... // and so on for each config
Now you can inject ConfigProvider<ConfigA> appConfigAProvider
and access the config!