跳转到内容

此页面尚未翻译成中文,以下为英文原文。

Singleton

此内容尚不支持你的语言。

It makes sense for the configuration for an application to be a sort of Singleton.

In the most common case, to instantiate a Config object, you would do:

MyConfig cfg = ConfigFactory.create(MyConfig.class);

People tends to use this as:

class MyApp {
private static MyConfig cfg =
ConfigFactory.create(MyConfig.class);
public void doSomething() {
UserInterface ui = new UserInterface(cfg);
Model model = new Model(cfg);
ui.setModel(model);
// do something more with cfg...
}
}

The problem is that, it may be not very practical to pass the cfg object inside complex applications, and if you use the ConfigFactory.create() in multiple places you’ll end up in having multiple instances of the cfg objects. And this may not be what you need.

For instance, if you have a J2EE Web application, to have a config object inside your servlets, you should configure a ServletContextListener in your web.xml and bind the configuration object to the ServletContext.

Then retrieve the cfg object in your servlets in the init() method.

Example:

public class MyServletContextListener
implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
MyConfig cfg = ConfigFactory.create(MyConfig.class);
sce.getServletContext()
.setAttribute("com.acme.foo.bar.MyConfig", cfg);
}
public void contextDestroyed(ServletContextEvent sce) {
sce.getServletContext()
.removeAttribute("com.acme.foo.bar.MyConfig");
}
}
// then in your servlets
public class MyServlet extends HttpServlet {
private MyConfig cfg = null;
public void init(ServletConfig config)
throws ServletException {
cfg = (MyConfig)config.getServletContext()
.getAttribute("com.acme.foo.bar.MyConfig");
}
public void destroy() {
cfg = null;
}
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
// do something with cfg;
}
}

I don’t dislike the above example, since it’s a kind of dependency injection mechanism (you don’t necessarily need a framework to do IoC) and I usually prefer this over Singletons (See SingletonsAreEvil), but… this is still pretty much of code. Boilerplate code. The kind of code OWNER is supposed to remove.

So, this is why a sort of Singleton has been provided: I called it ConfigCache, since it’s a little bit more than a mere (evil) Singleton. And this makes me feel a bit better about it :-)

So, as we’ve seen many times before, you should be familiar with the ConfigFactory:

MyConfig instance = ConfigFactory.create(MyConfig.class);

The same way you can use the new ConfigCache:

MyConfig instance = ConfigCache.getOrCreate(MyConfig.class);

The difference is that, when using ConfigFactory a new instance of the MyConfig object is created every time, instead when using the ConfigCache, instances are returned from an internal cache.

MyConfig firstFromFactory = ConfigFactory.create(MyConfig.class);
MyConfig secondFromFactory = ConfigFactory.create(MyConfig.class);
// firstFromFactory not same as secondFromFactory
MyConfig firstFromCache = ConfigCache.getOrCreate(MyConfig.class);
MyConfig secondFromCache = ConfigCache.getOrCreate(MyConfig.class);
// firstFromCache same as secondFromCache

You can assign an id to an instance:

MyConfig firstFromCache = ConfigCache.getOrCreate("foo", MyConfig.class);
MyConfig secondFromCache = ConfigCache.getOrCreate("foo", MyConfig.class);
MyConfig thirdFromCache = ConfigCache.getOrCreate("bar", MyConfig.class);
// firstFromCache same as secondFromCache
// thirdFromCache not same as secodFromCache or firstFromCache

The id is defined as java.lang.Object, but you can use a String such as a name, as in the above example.

A String constant declared on the mapping interface itself makes a good id, and it is what #148 was asking for — fetching a configuration by something it declares, rather than by its class:

public interface SampleConfiguration extends Config {
String PORT = "abc.port";
@Key(PORT)
int port();
}
ConfigCache.getOrCreate(SampleConfiguration.PORT, SampleConfiguration.class); // once, at startup
SampleConfiguration config = ConfigCache.get(SampleConfiguration.PORT); // anywhere else
Why the id is chosen and not deduced.

It would be tempting to skip the first line and let the cache find "the configuration that declares this property". It cannot: a property name is not an identity. Two mapping interfaces reading one file may perfectly well declare the same key — that is the ordinary way of splitting a large configuration into the parts each module cares about — so the question would have two answers and the library would be picking one of them for you.

In some cases, it may be useful list all configuration objects in an application, for instance for debugging. This can be accomplished using the ConfigCache.list() method, which returns a set of the id objects in the cache. This set can be used to iterate over all configuration objects in the cache, for instance as follows.

for (Object id : ConfigCache.list()) {
Config cfg = ConfigCache.get(id);
// do something
}

As for the ConfigFactory you can pass a list of imports to ConfigCache. In fact the ConfigCache interface is pretty similar to ConfigFactory:

public final class org.aeonbits.owner.ConfigCache {
public static <T extends Config> T getOrCreate(Class<? extends T> clazz, Map<?, ?>... imports);
public static <T extends Config> T getOrCreate(Factory factory, Class<? extends T> clazz, Map<?, ?>... imports);
public static <T extends Config> T getOrCreate(Object id, Class<? extends T> clazz, Map<?, ?>... imports);
public static <T extends Config> T getOrCreate(Factory factory, Object id, Class<? extends T> clazz, Map<?, ?>... imports);
public static <T extends Config> T get(Object id);
public static <T extends Config> T add(Object id, T config);
public static void clear();
public static <T extends Config> T remove(Object id);
}

The ConfigCache is designed to be thread safe, so you don’t have to worry about concurrent access.

Thread safe is not the same as thread isolated, and the difference is the whole of #283: several threads may ask the cache for a configuration at the same time without corrupting it, but getOrCreate(MyConfig.class) uses the class as the id, so what they all get is one object. If that object is Mutable, what one thread writes the next one reads.

Wrapping it in a ThreadLocal does not change that: the supplier returns the shared instance, so every thread caches the same object.

// still one single instance for the whole JVM
ThreadLocal<MyConfig> config =
ThreadLocal.withInitial(() -> ConfigCache.getOrCreate(MyConfig.class));

An instance per thread needs no new API — the id is any object, so make the thread the id:

MyConfig config = ConfigCache.getOrCreate(Thread.currentThread().getName(), MyConfig.class);
The cache does not forget.

An id per thread leaves an instance per thread in the cache, and it stays there after the thread is gone: nothing evicts it but ConfigCache.remove(id). That is fine for a fixed set of long-lived workers, and it is a leak for threads that come and go — a request per thread, a pool that renames its threads. For those, put the ThreadLocal over the factory instead of over the cache: same isolation, and the instance is collected with the thread that owned it.

private static final ThreadLocal<MyConfig> CONFIG =
ThreadLocal.withInitial(() -> ConfigFactory.create(MyConfig.class));

Note the static final: a ThreadLocal built inside the method that uses it is a new ThreadLocal on every call, which caches nothing at all.

Since a cached instance is the one created the first time, it also keeps the settings of the factory that created it. That matters for the prefix configured on a factory, which is read when the Config object is created: asking ConfigCache for the same id again returns the existing object, prefix included, whatever the factory handed over the second time says.

If your application already has a container — Guice, Dagger, Spring, CDI — then it, and not ConfigCache, is where a configuration should be kept. There is nothing to integrate: a configuration is an object that a factory produces, so it is bound like any other object your code did not new itself. In Guice:

public class ConfigModule extends AbstractModule {
@Provides @Singleton
Factory ownerFactory() {
Factory factory = ConfigFactory.newInstance();
factory.registerLoader(new MyLoader()); // whatever this application needs
return factory;
}
@Provides @Singleton
MyConfig myConfig(Factory factory) {
return factory.create(MyConfig.class);
}
}
@Inject MyConfig config;

@Provides rather than toInstance so that the configuration is built when it is first needed rather than at startup, and @Singleton so that it is built once. Dagger is the same with @Module, Spring the same with a @Bean method. No implementation class of your own, and no delegation: the interface is what the factory returns, so the container has an instance of it to bind.

Bind the factory too, not only the configuration.

A factory of your own is what makes the loaders, the value handlers, the converters and the properties of this application separate from anybody else's — including those of a library that also uses OWNER. It also means the container can build the things you register on it: a Loader that reads from your database, a ValueHandler that decrypts with your key management, a converter that needs your ObjectMapper. All three are registered as objects, so whatever built them is fine.

What cannot come from the container is a class named in an annotation — @ConverterClass, @TokenizerClass, @PreprocessorClasses, @DecryptorClass — because an annotation can only carry a class literal, and this library builds it. When one of those needs a collaborator, register it instead: that is what the object-shaped registrations above are for, and for decryption it is the value handler rather than @DecryptorClass.

Project maintained by Matteo Baccan, and the awesome contributors.

Developed with IntelliJ IDEA

Hosted on GitHub