Skip to content

Commit

Permalink
DI to Dagger2; bc if going to utilize, let's utilize that (or replace…
Browse files Browse the repository at this point in the history
… w micronaut)
  • Loading branch information
eschultink committed May 17, 2024
1 parent 67553ee commit f42db6f
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 66 deletions.
12 changes: 7 additions & 5 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,19 @@
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Dagger - Dependency Inject FW - http://square.github.io/dagger/ -->
<!-- Dagger - Dependency Inject FW - https://github.com/google/dagger -->
<!-- TBC: if micronaut, that provides compile-time DI too ... -->
<!-- also, is this very extensible? believe this will in effect require users to provide Dagger 2 module FQN to @Injectable to leverage this -->
<dependency>
<groupId>com.squareup.dagger</groupId>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>1.2.5</version>
<version>2.51.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.dagger</groupId>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>1.2.5</version>
<version>2.51.1</version>
<scope>compile</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.google.appengine.tools.pipeline;

public interface DIContainer<T> {
void inject(T instance);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.google.appengine.tools.pipeline;

import com.google.appengine.tools.pipeline.impl.PipelineManager;
import dagger.Component;

import javax.inject.Singleton;

@Singleton
@Component(
modules = {
DefaultDIModule.class,
}
)
public interface DefaultContainer {

void injects(PipelineManager pipelineManager);

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.tools.pipeline.impl.PipelineManager;
import com.google.appengine.tools.pipeline.impl.backend.*;
import com.google.appengine.tools.pipeline.impl.servlets.PipelineServlet;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.datastore.DatastoreOptions;
import dagger.Module;
Expand All @@ -14,13 +13,7 @@
import javax.inject.Singleton;

//TODO: split internals v stuff that can be re-used by others
@Module(
injects = { //alphabetical order
PipelineServlet.class,
},
library = true,
complete = false
)
@Module
public class DefaultDIModule {

@Provides @Singleton
Expand All @@ -33,6 +26,11 @@ AppEngineBackEnd.Options appEngineBackEndOptions() {
.build();
}

@Provides @Singleton
PipelineBackEnd pipelineBackEnd(AppEngineBackEnd appEngineBackEnd) {
return appEngineBackEnd;
}

@Provides @Singleton
PipelineBackEnd.Options backendOptions(AppEngineBackEnd.Options options) {
return options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* equivalent of Spring component, in a sense
*
* FQN of Dagger 1 module class that should be used when filling this classes dependencies, if any.
* FQN of Dagger 2 module class that should be used when filling this classes dependencies, if any.
*
*
* Usage:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ private void inject(Job<?> job) {
Optional<Injectable> injectable = Optional.ofNullable(job.getClass().getAnnotation(Injectable.class));

if (injectable.isPresent()) {
DIUtil.inject(injectable.get().value().getName(), job);
DIUtil.inject(injectable.get().value(), job);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package com.google.appengine.tools.pipeline.impl.servlets;

import com.google.appengine.tools.pipeline.DaggerDefaultContainer;
import com.google.appengine.tools.pipeline.DefaultDIModule;
import com.google.appengine.tools.pipeline.PipelineOrchestrator;
import com.google.appengine.tools.pipeline.PipelineRunner;
Expand Down Expand Up @@ -112,7 +113,7 @@ public void init() throws ServletException {

// TODO: fix this? may have second copy IF user overrides; OK?
if (pipelineManager == null) {
DIUtil.inject(DefaultDIModule.class.getName(), this);
DIUtil.inject(DaggerDefaultContainer.class, this);
}

//TODO: move these to DI?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
package com.google.appengine.tools.pipeline.impl.util;

/**
* Copyright Worklytics, Co. 2024.
*/
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import dagger.ObjectGraph;

import lombok.NonNull;
import lombok.extern.java.Log;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Helper to get an ObjectGraph out of a Dagger Module.
* Helper to get a component instance out of Dagger, support runtime injection of classes.
* <p>
* This is mostly intended to be used for jobs, thus Default Module is linked somehow to AsyncModule.
*
*/

@Log
public class DIUtil {


private static final Object lock = new Object();

private static final Map<String, ObjectGraph> objectGraphCache = new ConcurrentHashMap<>(1);
private static final Map<Class<?>, Object> componentCache = new ConcurrentHashMap<>(1);

private static final Map<String, ObjectGraph> overriddenObjectGraphCache = new ConcurrentHashMap<>(1);
private static final Map<Class<?>, Object> overriddenComponentCache = new ConcurrentHashMap<>(1);

// just for logging purposes
private static boolean overridden = false;

/**
* Injects and Injectable instance through Reflection
* @param componentClass component class (Dagger-generated)
* @param injectable class to inject
*/
public static void inject(String moduleClassFQN, Object injectable) {
ObjectGraph objectGraph = getFromModuleClass(moduleClassFQN);
objectGraph.inject(injectable);
public static void inject(Class<?> componentClass, Object injectable) {
Object objectGraph = getFromComponentClass(componentClass);

try {
Method injectMethod = objectGraph.getClass().getMethod("inject", injectable.getClass());
injectMethod.setAccessible(true); //avoid 'volatile' thing
injectMethod.invoke(objectGraph, injectable);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException("Couldn't inject object " + e.getMessage(), e);
}
}

/**
Expand All @@ -47,47 +58,41 @@ public static void inject(String moduleClassFQN, Object injectable) {
* - Module has a static instance of the graph it builds and provided by method of signature
* public static ObjectGraph getObjectGraph()
*
* @param moduleFQNClassName module fully qualified name
* @param componentClass module fully qualified name
* @return the object graph for that module
* @throws RuntimeException if the class is not a Dagger Module or doesn't meet the requirements
*/
public static ObjectGraph getFromModuleClass(@NonNull String moduleFQNClassName) {
public static Object getFromComponentClass(@NonNull Class<?> componentClass) {
synchronized (lock) {
if (overridden) {
return overriddenObjectGraphCache.getOrDefault(moduleFQNClassName, objectGraphCache.get(moduleFQNClassName));
return overriddenComponentCache.getOrDefault(componentClass, componentCache.get(componentClass));
}
try {
Class<?> moduleClass = Class.forName(moduleFQNClassName);
Preconditions.checkArgument(moduleClass.isAnnotationPresent(dagger.Module.class), "Class is not a Dagger Module: " + moduleFQNClassName);

ObjectGraph objectGraph = ObjectGraph.create(moduleClass.newInstance());
objectGraphCache.put(moduleFQNClassName, objectGraph);
return objectGraph;
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
Object component = componentClass.getMethod("create").invoke(null);
componentCache.put(componentClass, component);
return component;
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException("Couldn't get an object graph " + e.getMessage(), e);
}
}
}

/**
* A test helper to directly override the default module with the test's graph
* @param graph the object graph to override for default module
*/
@VisibleForTesting
public static void overrideGraphForTests(String key, ObjectGraph graph) {
synchronized (lock) {
overriddenObjectGraphCache.put(key, graph);
overridden = true;
}
}

@VisibleForTesting
public static void overrideComponentInstanceForTests(Class<?> clazz, Object componentInstance) {
synchronized (lock) {
overriddenComponentCache.put(clazz, componentInstance);
overridden = true;
}
}

/**
* Resets the objectGraphCache to its original state (intended to be used on tests' teardown)
* Resets the componentCache to its original state (intended to be used on tests' teardown)
*/
@VisibleForTesting
public static void resetGraph() {
public static void resetComponents() {
synchronized (lock) {
overriddenObjectGraphCache.clear();
overriddenComponentCache.clear();
overridden = false;
log.fine("Reset overridden DI Module");
}
Expand Down
43 changes: 26 additions & 17 deletions java/src/test/java/com/google/appengine/tools/pipeline/DITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import com.google.appengine.tools.pipeline.impl.backend.AppEngineEnvironment;
import com.google.appengine.tools.pipeline.impl.util.DIUtil;
import dagger.Component;
import dagger.Module;
import javax.inject.Inject; // jakarta.inject.Inject not working with dagger yet (maybe in 2.0??)

import dagger.Provides;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;


Expand All @@ -17,18 +19,20 @@

public class DITest extends PipelineTest {


@Module(
injects = {
JobWithDependency.class,
},
includes = {
DefaultDIModule.class,
},
complete = false,
library = true,
overrides = true
@Component(
modules = {
//DefaultDIModule.class,
DIModule.class,
}
)
public interface Container extends DIContainer<DITest> {

//NOTE: have to do this for every class that may need to be injected (eg, Jobs, Tests, etc)
void inject(JobWithDependency job);
}


@Module
public static class DIModule {

@Provides
Expand All @@ -41,17 +45,22 @@ public String getService() {

@Override
public String getVersion() {
return "v0.1";
return "v0.1-test";
}
};
}
}



@BeforeEach
public void setup() {
Container container = DaggerDITest_Container.create();
container.inject(this);
DIUtil.overrideComponentInstanceForTests(DaggerDITest_Container.class, container);
}


@NoArgsConstructor
@Injectable(DIModule.class)
@Injectable(DaggerDITest_Container.class)
public static class JobWithDependency extends Job0<String> {

@Inject
Expand All @@ -67,7 +76,7 @@ public Value<String> run() throws Exception {
public void directInjection() {
JobWithDependency job = new JobWithDependency();

DIUtil.inject(DIModule.class.getName(), job);
DIUtil.inject(DaggerDITest_Container.class, job);
assertNotNull(job.appEngineEnvironment);
}

Expand All @@ -76,6 +85,6 @@ public void directInjection() {
public void testJobWithDependency(PipelineService pipelineService) {
String pipelineId = pipelineService.startNewPipeline(new JobWithDependency());
String value = waitForJobToComplete(pipelineService, pipelineId);
assertEquals("dsaf", value);
assertEquals("v0.1-test", value);
}
}

0 comments on commit f42db6f

Please sign in to comment.