private void checkForValueChanges(final PropertyPath propertyPath,
                                      final Prop prop,
                                      final Prop parentProp,
                                      final Object newValue,
                                      final Object existingValue,
                                      final boolean logChanges,
                                      final AtomicInteger changeCounter) {
        // Get one of them, doesn't matter which
        final Optional<Object> optConfig = NullSafe.coalesce(existingValue, newValue);
        final Optional<Class<?>> optClass = optConfig.map(Object::getClass);

        if (optClass.filter(AbstractConfig.class::isAssignableFrom).isPresent()) {
            // Config object so recurse into each prop
            final Object config = optConfig.get();

            final Map<String, Prop> propMap = PropertyUtil.getProperties(config);
            propMap.forEach((childPropName, childProp) -> {
                // Both of these could be null if their parent is null
                final Object existingChildVal = NullSafe.get(existingValue, childProp::getValueFromConfigObject);
                final Object newChildVal = NullSafe.get(newValue, childProp::getValueFromConfigObject);
                checkForValueChanges(
                        propertyPath.merge(childPropName),
                        childProp,
                        prop,
                        newChildVal,
                        existingChildVal,
                        logChanges,
                        changeCounter);
            });
        } else {
            if (!Objects.equals(existingValue, newValue)) {
                changeCounter.incrementAndGet();
                if (logChanges) {
                    final String extraText = prop.hasAnnotation(RequiresProxyRestart.class)
                                             || Prop.hasAnnotation(parentProp, RequiresProxyRestart.class)
                            ? ". NOTE: This property requires an application re-start to take effect."
                            : "";

                    LOGGER.info("Config property {} has changed from [{}] to [{}]{}",
                            propertyPath.toString(),
                            existingValue,
                            newValue,
                            extraText);
                }
            }
        }
    }
This is sort of the equivalent to stroom's ConfigMapper class.
It holds the de-serialised config objects in a map that is then used to
provide them for guice injection.
stroom-proxy/stroom-proxy-app/src/main/java/stroom/proxy/app/guice/ProxyConfigProvider.java