В последнее время чаще стал встречать команды, которые используют Guice в качестве DI фреймворка. Стал его бояться (слезать с любимого Spring!?), и, как это обычно в жизни и бывает, страхи мои материализовались — я попал на проект, на котором активно используется Guice…
В интернете уже накопилось изрядное количество публикаций (в том числе и русскоязычных) по данному фреймворку, что не может не радовать. Однако, на проекте я столкнулся с ситуацией, готовое решение для которой мне не удалось найти.
В статье я в очередной раз покажу практическое использование Guice и некоторых его extensions: assistedinject, mutibindings , а также работу с generics. Сначала я опишу суть задачи, а затем итеративно приду к ее решению. Подразумеваю у читателя наличие базовых представлений о фреймворке и о DI в целом, поэтому опущу основы. Тем более, есть отличная документация.
Исходный код проекта и историю его итераций можно найти на гитхабе.
public class Request {
public String parameter;
public int argument;
}
public abstract class Worker{
protected final int argument;
public Worker(int argument) {
this.argument = argument;
}
public abstract void doWork();
}
public class Worker1 extends Worker {
private ServiceA serviceA;
private ServiceB serviceB;
public Worker1(ServiceA serviceA, ServiceB serviceB, int argument) {
super(argument);
this.serviceA = serviceA;
this.serviceB = serviceB;
}
@Override
public void doWork() {
System.out.println(String.format("Worker1 starts work with argument %d services %s and %s", argument, serviceA, serviceB));
}
}
public class Worker2 extends Worker {
private ServiceB serviceB;
private ServiceC serviceC;
public Worker2(ServiceB serviceB, ServiceC serviceC, int argument) {
super(argument);
this.serviceB = serviceB;
this.serviceC = serviceC;
}
@Override
public void doWork() {
System.out.println(String.format("Worker2 starts work with argument %d services %s and %s", argument, serviceB, serviceC));
}
}
public class RequestHandler {
private final ServiceA serviceA;
private final ServiceB serviceB;
private final ServiceC serviceC;
public RequestHandler(ServiceA serviceA,
ServiceB serviceB,
ServiceC serviceC) {
this.serviceA = serviceA;
this.serviceB = serviceB;
this.serviceC = serviceC;
}
public void handleRequest(Request request) {
Worker worker = null;
if (request.parameter.equals("case1")) {
worker = new Worker1(serviceA, serviceB, request.argument);
} else if (request.parameter.equals("case2")) {
worker = new Worker2(serviceB, serviceC, request.argument);
}
//Здесь и далее не буду добавлять проверки на корректность входного
//параметра, чтобы не загромождать код
worker.setServiceA(serviceA);
worker.setServiceB(serviceB);
worker.setServiceC(serviceC);
worker.doWork();
}
}
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>${guice.version}</version>
</dependency>
public abstract class Worker {
public abstract void doWork();
}
public class Worker1 extends Worker{
private ServiceA serviceA;
private ServiceB serviceB;
@Inject
public Worker1(ServiceA serviceA, ServiceB serviceB) {
this.serviceA = serviceA;
this.serviceB = serviceB;
}
@Override
public void doWork() {
System.out.println(String.format("Worker1 starts work with %s and %s", serviceA, serviceB));
}
}
@Singleton
public class RequestHandler {
private Provider<Worker1> worker1Provider;
private Provider<Worker2> worker2Provider;
@Inject
public RequestHandler(Provider<Worker1> worker1Provider,
Provider<Worker2> worker2Provider) {
this.worker1Provider = worker1Provider;
this.worker2Provider = worker2Provider;
}
public void handleRequest(Request request) {
Worker worker = null;
if (request.parameter.equals("case1")) {
worker = worker1Provider.get();
} else if (request.parameter.equals("case2")) {
worker = worker2Provider.get();
}
worker.doWork();
}
}
Provider<T> — an object capable of providing instances of type TВ данном случае Provider — это фабрика, предоставляемая фреймворком Guice. После того, как получена зависимость на провайдер, типизированный классом Worker, при каждом вызове метода
.get()
получаем новый экземпляр класса Worker (если, конечно, Worker не объявлен, как Singleton). public static void main( String[] args ) {
Request request = new Request();
request.parameter = "case1";
request.argument = 5;
Injector injector = Guice.createInjector();
RequestHandler requestHandler = injector.getInstance(RequestHandler.class);
requestHandler.handleRequest(request);
request.parameter = "case2";
requestHandler.handleRequest(request);
}
@Inject
public Worker1(ServiceA serviceA, ServiceB serviceB, int argument) {
@Singleton
public class WorkerFactory {
private ServiceA serviceA;
private ServiceB serviceB;
private ServiceC serviceC;
@Inject
public WorkerFactory(ServiceA serviceA,
ServiceB serviceB,
ServiceC serviceC) {
this.serviceA = serviceA;
this.serviceB = serviceB;
this.serviceC = serviceC;
}
public Worker1 createWorker1 (int argument) {
return new Worker1(serviceA, serviceB, argument);
}
public Worker2 createWorker2 (int argument) {
return new Worker2(serviceB, serviceC, argument);
}
}
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-assistedinject</artifactId>
<version>${guice.version}</version>
</dependency>
public interface WorkerFactory {
Worker1 createWorker1 (int argument);
Worker2 createWorker2 (int argument);
}
public class Module extends AbstractModule {
@Override
protected void configure() {
install(new FactoryModuleBuilder().implement(Worker1.class, Worker1.class)
.implement(Worker2.class, Worker2.class)
.build(WorkerFactory.class));
}
}
FactoryModuleBuilder — provides a factory that combines the caller's arguments with injector-supplied values to construct objects.У нас появляется возможность комбинировать пользовательские параметры с объектами, предоставляемые Guice.
build(WorkerFactory.class)
сообщает Guice, что нужно предоставить реализацию фабрики WorkerFactory;implement (Class<T> source, Class<? extends T> target)
source — интерфейс возвращаемого значения, target — его реализация, которую наша фабрика будет предоставлять;install(new FactoryModuleBuilder())
— завершаем конфигурацию, добавляя новый модуль к нашему. @AssistedInject
public Worker1(ServiceA serviceA, ServiceB serviceB, @Assisted int argument)
@Singleton
public class RequestHandler {
private WorkerFactory workerFactory;
@Inject
public RequestHandler(WorkerFactory workerFactory) {
this.workerFactory = workerFactory;
}
public void handleRequest(Request request) {
Worker worker = null;
if (request.parameter.equals("case1")) {
worker = workerFactory.createWorker1(request.argument);
} else if (request.parameter.equals("case2")) {
worker = workerFactory.createWorker2(request.argument);
}
worker.doWork();
}
}
Injector injector = Guice.createInjector(new Module());
public interface WorkerFactory<T extends Worker> {
T createWorker (int argument);
}
public class Module extends AbstractModule{
@Override
protected void configure() {
install(new FactoryModuleBuilder().implement(Worker.class, Worker1.class)
.build(new TypeLiteral<WorkerFactory<Worker1>>() {}));
install(new FactoryModuleBuilder().implement(Worker.class, Worker2.class)
.build(new TypeLiteral<WorkerFactory<Worker2>>() {}));
}
}
TypeLiteral<T> — represents a generic type T. Java doesn't yet provide a way to represent generic types, so this class doesТаким образом, раз Java не имеет представления о параметризованном классе, Guice создал свое.
@Singleton
public class RequestHandler {
private WorkerFactory<Worker1> worker1Factory;
private WorkerFactory<Worker2> worker2Factory;
@Inject
public RequestHandler(WorkerFactory<Worker1> worker1Factory,
WorkerFactory<Worker2> worker2Factory) {
this.worker1Factory = worker1Factory;
this.worker2Factory = worker2Factory;
}
public void handleRequest(Request request) {
Worker worker = null;
if (request.parameter.equals("case1")) {
worker = worker1Factory.createWorker(request.argument);
} else if (request.parameter.equals("case2")) {
worker = worker2Factory.createWorker(request.argument);
}
worker.doWork();
}
}
WorkerFactory<WorkerN> workerNFactory
. Теперь поправим и это, используя расширение multibindings. В частности, будем использовать MapBinder:MapBinder — an API to bind multiple map entries separately, only to later inject them as a complete map.MapBinder позволяет собрать все зависимости вместе в одну мапу, а потом ее разом заинжектить.
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-multibindings</artifactId>
<version>4.2.0</version>
</dependency>
MapBinder<String, WorkerFactory> binder = MapBinder.newMapBinder(binder(), String.class, WorkerFactory.class);
new TypeLiteral<WorkerFactory<Worker1>>(){}
addBinding()
и to()
. Обратите внимание на наличие перегруженной версии метода, принимающей TypeLiteral. Так будет выглядеть модуль полностью:public class Module extends AbstractModule{
@Override
protected void configure() {
install(new FactoryModuleBuilder().implement(Worker.class, Worker1.class)
.build(new TypeLiteral<WorkerFactory<Worker1>>() {}));
install(new FactoryModuleBuilder().implement(Worker.class, Worker2.class)
.build(new TypeLiteral<WorkerFactory<Worker2>>() {}));
MapBinder<String, WorkerFactory> binder = MapBinder.newMapBinder(binder(), String.class, WorkerFactory.class);
binder.addBinding("case1").to(new TypeLiteral<WorkerFactory<Worker1>>(){});
binder.addBinding("case2").to(new TypeLiteral<WorkerFactory<Worker2>>(){});
}
}
@Singleton
public class RequestHandler {
private Map<String, WorkerFactory> workerFactoryMap;
@Inject
public RequestHandler(Map<String, WorkerFactory> workerFactoryMap) {
this.workerFactoryMap = workerFactoryMap;
}
public void handleRequest(Request request) {
Worker worker = workerFactoryMap.get(request.parameter)
.createWorker(request.argument);
worker.doWork();
}
}
get()
. К сожалению, не доступен сервер mySQL