An Inversion of Control design pattern that is:
- Easy to use (no magic!)
- No config files
- No reflection
- No bytecode generation
- No proxies
- No annotations
- Easy to debug (dependency graph is verified at compile-time)
- Dependency-free
To get started, add this Maven dependency:
<dependency>
<groupId>com.github.cowwoc.pouch</groupId>
<artifactId>core</artifactId>
<version>5.2</version>
</dependency>
Service Locators are registries that contain one or more values. Values can be bound to one or more scopes. A scope is the context within which a value is defined. Scopes guarantee that values will remain unchanged during their lifetime.
Pouch doesn't advocate specific designs. You're expected to use whatever design best suits your use-case. Here is one design that has worked for me:
public enum RunMode
{
DEBUG,
RELEASE
}
public interface JvmScope extends AutoCloseable
{
RunMode getRunMode();
}
import javax.sql.DataSource;
public interface DatabaseScope extends JvmScope
{
DataSource getDataSource();
}
import javax.sql.DataSource;
public interface TransactionScope extends DatabaseScope
{
Connection getConnection();
}
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public interface ServerScope extends DatabaseScope
{
HttpScope createRequest(HttpServletRequest request, HttpServletResponse response);
}
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public interface RequestScope extends ServerScope
{
HttpServletRequest getRequest();
HttpServletResponse getResponse();
}
import org.eclipse.jetty.client.HttpClient;
import java.net.URI;
public interface ClientScope
{
URI getServer();
HttpClient getHttpClient();
}
Imagine we have:
JvmScope
: values specific to the lifetime of the current JVMDatabaseScope
: values specific to the lifetime of the current database connectionRequestScope
: values specific to the lifetime of the current HTTP request
Notice that a JvmScope
contains values whose lifetime span multiple database connections.
Further, DatabaseScope
contains values whose lifetime span multiple HTTP requests.
In light of this, we define RequestScope
as extending DatabaseScope
and DatabaseScope
as extending
JvmScope
.
When a new RequestScope
is constructed, we pass in the parent scope.
The lifetime of child scopes must be equal to or less than the lifetime of the parent scope.
This means that child scopes can't exist without the parent.
For example, the above conceptual model assumes that
an RequestScope
cannot exist without a DatabaseScope
but a DatabaseScope
may exist without an RequestScope
.
This enables a worker thread to interact with the database outside an HTTP request.
But an HTTP request can't exist without a database connection.
When a child scope is asked for a value that is bound to its parent, it delegates to the parent.
For example, notice how AbstractDatabaseScope.getRunMode()
delegates to JvmScope.getRunMode()
below.
When running in a multi-threaded environment, such as a web server, you might want to wait for ongoing HTTP requests to complete before shutting down the server. You can use the ConcurrentChildScopes class to implement this as follows:
import com.github.cowwoc.pouch.core.ConcurrentChildScopes;
import com.github.cowwoc.pouch.core.Scopes;
import com.github.cowwoc.pouch.core.ConcurrentChildScopes;
public abstract class AbstractJvmScope implements JvmScope
{
private final RunMode runMode;
private final Duration closeTimeout;
private final ConcurrentChildScopes children = new ConcurrentChildScopes();
protected AbstractJvmScope(RunMode runMode)
{
this.runMode = runMode;
this.closeTimeout = switch (runMode)
{
case DEBUG -> Duration.ofMinutes(10);
case RELEASE -> Duration.ofSeconds(30);
};
}
public void addChild(AutoCloseable child)
{
children.add(child);
}
public void removeChild(AutoCloseable child)
{
children.remove(child);
}
@Override
public void close()
{
Scopes.runAll(() -> children.shutdown(closeTimeout));
}
}
import javax.sql.DataSource;
public abstract class AbstractDatabaseScope extends AbstractJvmScope
implements DatabaseScope
{
private final JvmScope parent;
protected AbstractDatabaseScope(JvmScope parent)
{
this.parent = parent;
}
public RunMode getRunMode()
{
return parent.getRunMode();
}
@Override
public void close()
{
}
}
import javax.sql.DataSource;
public final class DefaultTransactionScope implements TransactionScope
{
private final DatabaseScope parent;
public DefaultTransactionScope(DatabaseScope parent)
{
this.parent = parent;
}
public RunMode getRunMode()
{
return parent.getRunMode();
}
public DataSource getDataSource()
{
return parent.getDataSource();
}
@Override
public void close()
{
}
}
public final class MainDatabaseScope extends AbstractDatabaseScope
{
private final Factory<? extends DataSource> dataSource;
public MainDatabaseScope(JvmScope parent)
{
super(parent);
this.dataSource = new MainDataSourceFactory(this, configuration);
parent.addChild(this)
}
@Override
public DataSource getDataSource()
{
return dataSource.getValue();
}
@Override
public void close()
{
dataSource.close();
super.close();
parent.removeChild(this);
}
}
What's interesting about this design is that you can easily isolate unit tests from each other, as if they were running in separate JVMs.
public class UnitTest
{
@Test
public first()
{
try (ServerScope scope = new TestServerScope())
{
Datasource ds = scope.getDataSource();
// ...
}
}
@Test
public second()
{
try (ServerScope scope = new TestServerScope())
{
Datasource ds = scope.getDataSource();
// ...
}
}
}
You can even configure each test to run against a separate database:
public final class TestDatabaseScope extends AbstractDatabaseScope
{
private final Factory<? extends DataSource> dataSource;
public TestDatabaseScope(JvmScope parent, Configuration configuration, int id)
{
super(parent, configuration);
dataSource = new TestDataSourceFactory(this, configuration, id);
}
@Override
public DataSource getDataSource()
{
return dataSource.getValue();
}
@Override
public void close()
{
dataSource.close();
super.close();
}
}
import javax.sql.DataSource;
public final class MainDataSourceFactory extends ConcurrentLazyFactory<DataSource>
{
private final DatabaseScope scope;
public MainDataSourceFactory(DatabaseScope scope)
{
this.scope = scope;
}
@Override
protected DataSource createValue()
{
// Return a DataSource pointing to the main database
}
@Override
protected void disposeValue(DataSource dataSource)
{
dataSource.close();
}
}
import com.github.cowwoc.pouch.core.ConcurrentLazyFactory;
import java.net.URI;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public final class TestDataSourceFactory extends ConcurrentLazyFactory<DataSource>
{
private final DatabaseScope scope;
private final String databaseName;
public TestDataSourceFactory(DatabaseScope scope, int id)
{
this.scope = scope;
this.databaseName = "test-" + id;
}
private Connection getConnection()
{
// Return connection to the database
}
@Override
protected DataSource createValue()
{
// Create a test-specific database
try (Connection connection = getConnection())
{
connection.setAutoCommit(true);
try (Statement statement = connection.createStatement())
{
statement.executeUpdate("DROP DATABASE IF EXISTS " + databaseName);
statement.executeUpdate("CREATE DATABASE " + databaseName);
}
}
catch (SQLException e)
{
throw new RuntimeException("", e);
}
// Return a DataSource pointing to the test-specific database
}
@Override
protected void disposeValue(DataSource dataSource)
{
try (Connection connection = dataSource.getConnection())
{
try (Statement statement = connection.createStatement())
{
statement.executeUpdate("DROP DATABASE IF EXISTS " + configuration.databaseName());
}
}
catch (SQLException e)
{
throw new DataAccessException("", e);
}
dataSource.close();
}
}
Scopes that need to return different values over their lifetime can return a Builder or Supplier that will, in turn, return different values on every invocation.
Here is a contrived example:
public interface ClientScope
{
Supplier<LocalDateTime> getServerTime();
}
public Main
{
public static void main (String[]args)
{
try (ClientScope scope = new MainClientScope())
{
Supplier<LocalTime> serverTime = scope.getServerTime();
for (int i = 0; i < 10; ++i)
System.out.println("Server time: " + serverTime.get());
}
}
}
The easiest way to break circular dependencies is using lazy initialization.
Dependency injection frameworks recommend injecting Provider<Foo>
in place of Foo
,
where the former constructs Foo
lazily.
class DogTrainer
{
private final Cat cat;
private final Dog dog;
public DogTrainer(Cat cat, Dog dog)
{
this.cat = cat;
this.dog = dog;
}
void meow()
{
cat.meow();
}
void bark()
{
dog.bark();
}
}
becomes:
class DogTrainer
{
private final Provider<Cat> catProvider;
private final Provider<Dog> dogProvider;
public DogTrainer(Provider<Cat> catProvider, Provider<Dog> dogProvider)
{
this.catProvider = catProvider;
this.dogProvider = dogProvider;
}
void meow()
{
Cat cat = catProvider.get();
cat.meow();
}
void bark()
{
Dog dog = dogProvider.get();
dog.bark();
}
}
Pouch recommends using Reference<Foo>
in place of Foo
, where the former constructs Foo
lazily.
Alternatively, you could replace Foo
by a scope that constructs it lazily.
class DogTrainer
{
private final Cat cat;
private final Dog dog;
public DogTrainer(Cat cat, Dog dog)
{
this.cat = cat;
this.dog = dog;
}
void meow()
{
cat.meow();
}
void bark()
{
dog.bark();
}
}
becomes:
class AnimalPen
{
private final Reference<Cat> catReference;
private final Reference<Dog> dogReference;
public AnimalPen(Reference<Cat> catReference, Reference<Dog> dogReference)
{
this.catReference = catReference;
this.dogReference = dogReference;
}
void meow()
{
Cat cat = catReference.get();
cat.meow();
}
void bark()
{
Dog dog = dogReference.get();
dog.bark();
}
}
or
class DogTrainer
{
private final JvmScope scope;
public DogTrainer(JvmScope scope)
{
this.scope = scope;
}
void meow()
{
Cat cat = scope.getCat();
cat.meow();
}
void bark()
{
Dog dog = scope.getDog();
dog.bark();
}
}
The scope approach makes it easier to look up multiple values, or pass the scope to another object.
The library contains two types of classes: ones that are thread-safe and ones that are not.
For example,
ConcurrentLazyFactory
is the thread-safe equivalent
of LazyFactory.
LazyFactory
is faster than ConcurrentLazyFactory
, but doesn't support access from multiple threads.
Classes that are required to support multithreaded access
(such as the application scope) must use the thread-safe classes.
The jersey plugin contains a working example. Download a copy and try it for yourself.
The following sample code demonstrates integration with various third-party libraries:
- Jersey: Integrates pouch with Jersey.
- Dropwizard: Integrates pouch with Dropwizard.
- Requirements: Fluent Design by Contract for Java APIs.
- JayWire: the power of dependency injection without the "magic".
Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
- Icons made by Flat Icons from www.flaticon.com is licensed by CC 3.0 BY