Motivation
I've been looking at the problem of dynamic properties in Java. Typically properties are injected into a program at start and never change. Common examples include timeouts, host names, etc. Changing these requires and edit and restart (edit can also mean an update to a remote data source).
What I want is runtime updates to properties, and the program uses the new values. Hence "dynamic properties".
There are several schemes for this built around frameworks or external data sources. I want something using only the JDK.
Interfaces
My interfaces became one for tracking key-value pairs, one for updating them (javadoc munged to display in post):
/** *Tracking
tracks string key-value pairs, tracking external updates * to the boxed values. Optionally tracks them as a type converted from * string. Example use:* Tracking dynafig = ...; * Optional<AtomicReference<String>> prop = dynafig.track("prop"); * boolean propDefined = prop.isPresent(); * AtomicReference<String> propRef = prop.get(); * String propValue = propRef.get(); * // External source updates key-value pair for "prop" * String newPropValue = propRef.get();* * In an injection context:* class Wheel { * private final AtomicInteger rapidity; * * @Inject * public Wheel(final Tracking dynafig) { * rapidity = dynafig.track("rapidity"). * orElseThrow(() -> new IllegalStateException( * "Missing 'rapidity' property)); * } * * public void spin() { * spinAtRate(rapidity.get()); * } * }* * @author Brian Oxley * @see Updating Updating key-value pairs * @see Default Reference implementation */ public interface Tracking { /** * Tracks the given key value as a string. Returns empty if * key is undefined. If key is defined, may stil * return anull
boxed value. * * @param key the key, never missing * * @return the optional atomic value string, never missing * * @todo Some way to work out type from convert */ @Nonnull Optional<AtomicReference<String>> track(@Nonnull final String key); /** * Tracks the given key value as a boolean. Returns empty if * key is undefined. * * @param key the key, never missing * * @return the optional atomic value boolean, never missing */ @Nonnull Optional<AtomicBoolean> trackBool(@Nonnull final String key); /** * Tracks the given key value as an integer. Returns empty if * key is undefined. * * @param key the key, never missing * * @return the optional atomic value integer, never missing */ @Nonnull Optional<AtomicInteger> trackInt(@Nonnull final String key); /** * Tracks the given key value as type. Returns * empty if key is undefined. If key is defined, * may stil return anull
boxed value. * * @param key the key, never missing * @param type the value type token, never missing * @param convert the value converter, never missing * @param <T> the value type * * @return the optional atomic value reference, never missing * * @todo Some way to work out type from convert */ @Nonnull <T> Optional<AtomicReference<T>> trackAs(@Nonnull final String key, @Nonnull final Class<T> type, // TODO: Can this be worked out? @Nonnull final Function<String, T> convert); }
/** * {@code Updating} updates key-value pairs. * * @author Brian Oxley * @see Tracking Tracking key-value pairs * @see Default Reference implementation */ public interface Updating { /** * Updates a key-value pair with a new value. * * @param key the key, never missing * @param value the value, possiblynull
* * @returntrue
if updated elsefalse
if created */ boolean update(@Nonnull final String key, @Nullable final String value); /** * Updates a key-value pair as a map entry for convenience. * * @param entry the entry, never missing * * @returntrue
if updated elsefalse
if created * @see #update(String, String) */ default boolean update(@Nonnull final Map.Entry<String, String> entry) { return update(entry.getKey(), entry.getValue()); } /** * Updates a set of key-value pairs for convenience. Each key is * invidually updated in entry-set order. * * @param values the key-value set, never missing * * @see #update(String, String) */ default void updateAll(@Nonnull final Map<String, String> values) { values.entrySet().stream(). forEach(this::update); } }
Team lead
I built a reference implementation to demonstrate these were plausible. To my team I explained like this:
- Coded very much in Java 8 functional style
- Trivial API - 4 tracking calls (variants on return type), 1 updating call (plus 2 convenience)
- Minimal dependencies - JDK + javax.annotations (@Nonnull)
- Thread-safe, not concurrency-safe (lost updates, etc). Fine if properties are not updated on top of each other (i.e., several in the same microsecond)
- I expect it to be straight-forward to integrate into JSR107, Cassandra, etc, but I've not tried this from home
And offered some advice to my juniors:
- Be functional where sensible, the benefits are almost beyond enumeration
- Corollary: Avoid state - in Java that means fields. Temp variables (locals) are debatable and come down to individual taste. I lean to avoiding them, but there's nothing wrong with creating locals that clarify the code for others (including yourself 6 mos. later)
- Corollary: Push fields down as low as possible, avoid globals and "local globals" (static fields)
- Complicate your data structure, not your calls. People reason about complex data structures much better than they do about complex logic
- Avoid null. Really. Treat all uses of null as code smell - think of it as "machine level" programming. You should do high-level programming
Implementation
All tests pass. The reference implementation:
public final class Default implements Tracking, Updating { private final Map<String, Value> keys = new ConcurrentHashMap<>(); private final Map<String, Value> values = new ConcurrentHashMap<>(); public Default() { } public Default(@Nonnull final Map<String, String> keys) { updateAll(keys); } @Nonnull @Override public Optional<AtomicReference<String>> track( @Nonnull final String key) { return Optional.ofNullable(keys.get(key)). map(Value::get); } @Nonnull @Override public Optional<AtomicBoolean> trackBool(@Nonnull final String key) { return Optional.ofNullable(keys.get(key)). map(Value::getBool); } @Nonnull @Override public Optional<AtomicInteger> trackInt(@Nonnull final String key) { return Optional.ofNullable(keys.get(key)). map(Value::getInt); } @Nonnull @Override public <T> Optional<AtomicReference<T>> trackAs(@Nonnull final String key, @Nonnull final Class<T> type, @Nonnull final Function<String, T> convert) { return Optional.ofNullable(keys.get(key)). map(v -> v.getAs(type, convert)); } @Override public boolean update(@Nonnull final String key, @Nullable final String value) { return null != keys.put(key, values.compute(key, (k, v) -> null == v ? new Value(value) : v.update(value))); } private static final class Atomic<T> { private final T atomic; private final Consumer<String> update; private static Atomic<AtomicReference<String>> of( final String value) { final AtomicReference<String> atomic = new AtomicReference<>( value); return new Atomic<>(atomic, atomic::set); } private static Atomic<AtomicBoolean> boolOf(final String value) { final AtomicBoolean atomic = new AtomicBoolean( null == value ? false : Boolean.valueOf(value)); return new Atomic<>(atomic, v -> atomic.set(null == v ? false : Boolean.valueOf(v))); } private static Atomic<AtomicInteger> intOf(final String value) { final AtomicInteger atomic = new AtomicInteger( null == value ? 0 : Integer.valueOf(value)); return new Atomic<>(atomic, v -> atomic.set(null == v ? 0 : Integer.valueOf(v))); } private static <T> Atomic<AtomicReference<T>> asOf(final String value, final Function<String, T> convert) { final AtomicReference<T> atomic = new AtomicReference<>( convert.apply(value)); return new Atomic<>(atomic, v -> atomic.set(convert.apply(v))); } private Atomic(final T atomic, final Consumer<String> update) { this.atomic = atomic; this.update = update; } private void update(final String value) { update.accept(value); } } private final class Value { @Nullable private final String value; private final Map<Class<?>, Atomic<?>> values; private Value(final String value) { this(value, new ConcurrentHashMap<>(3)); } private Value(@Nullable final String value, final Map<Class<?>, Atomic<?>> values) { this.value = value; this.values = values; } private Value update(final String value) { final Value newValue = new Value(value, values); newValue.values.values().stream(). forEach(a -> a.update(value)); return newValue; } @SuppressWarnings("unchecked") private AtomicReference<String> get() { return (AtomicReference<String>) values. computeIfAbsent(String.class, k -> Atomic.of(value)).atomic; } private AtomicBoolean getBool() { return (AtomicBoolean) values. computeIfAbsent(Boolean.class, k -> Atomic.boolOf(value)).atomic; } private AtomicInteger getInt() { return (AtomicInteger) values. computeIfAbsent(Integer.class, k -> Atomic.intOf(value)).atomic; } @SuppressWarnings("unchecked") private <T> AtomicReference<T> getAs(final Class<T> type, final Function<String, T> convert) { return (AtomicReference<T>) values. computeIfAbsent(type, k -> Atomic.asOf(value, convert)).atomic; } @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final Value that = (Value) o; return Objects.equals(value, that.value); } @Override public int hashCode() { return Objects.hash(value); } } }