Search code examples
javadependency-injectionguice

Is it possible to create a Java Guice dependency injector with an incomplete dependency tree?


Am I able to have an injector to use its providers later and request then after createChildInjector? Here's an example:

class App {
  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new UserProperties());
    User user = new User("Some Name");
    injector.createChildInjector(new MyUserModule(user));
    String myName = injector.getInstance(Key.get(String.class, Names.named("MyName")));
    System.out.println(myName);
  }
}

@Value
class User {
  String name;
}

class UserProperties extends AbstractModule {

  @Provides
  @Singleton
  @Named("MyName")
  public String myName(User user) {
    return user.getName();
  }

  // won't be used because we aren't interested now in getting TargetUser
  // List<User> could also be in a separated module that get all users from database
  @Provides
  @Singleton
  @Named("TargetUser")
  public User targetName(@Named("TargetUserName") String name, List<User> users) {
    return users.stream().filter(user -> user.getName().equals(name)).findAny().get();
  }
}

@AllArgsConstructor
class MyUserModule extends AbstractModule {
  private final User user;

  @Singleton
  @Provides
  public User user() {
    return user;
  }
}

@AllArgsConstructor
class TargetUserModule extends AbstractModule {
  private final String name;

  @Provides
  @Singleton
  @Named("TargetUserName")
  public String targetUserName() {
    return name;
  }
}

I think it is fair for the UserProperties module to know how to provide the target user based on a target user name and a list of users but my application in this example doesn't need a target user and it just want to know "MyName".

So basically, I would like to have a module that would know how to behave if my application asks for this information, but if it does not, it will still be able to compile and run with as much information as I have provided.

Does it make sense?


Solution

  • You can do this with OptionalBinder (javadoc):

    import javax.inject.Qualifier;
    
    /** Binding annotation for the name (a String) of the current User. */
    @Qualifier
    @Target({ FIELD, PARAMETER, METHOD })
    @Retention(RUNTIME)
    @interface UserName {}
    
    class App {
      public static void main(String[] args) {
        Injector injector = Guice.createInjector(new UserPropertyModule());
        User user = new User("Some Name");
        Injector childInjector = injector.createChildInjector(new MyUserModule(user));
        String myName = childInjector.getInstance(Key.get(String.class, UserName.class));
        System.out.println(myName);
      }
    }
    
    @Value
    class User {
      String name;
    }
    
    class UserPropertyModule extends AbstractModule {
    
      @Override
      protected void configure() {
         OptionalBinder.newOptionalBinder(binder(), User.class)
             .setDefault().toProvider(DefaultUserProvider.class);
      }
    
      @Provides @UserName String provideUserName(User user) {
        return user.getName();
      }
    }
    
    class DefaultUserProvider implements Provider<User> {
      @Override
      public User get() {
        // Called to get the default user
      }
    }
    
    @AllArgsConstructor
    class MyUserModule extends AbstractModule {
      private final User user;
    
      @Override
      protected void configure() {
         OptionalBinder.newOptionalBinder(binder(), User.class)
             .setBinding().toInstance(user);
      }
    }
    

    Notes:

    • The above assumes your main module can provide a default value. If it cannot, then you need to inject Optional<User>
    • I modified your main() method to use the child injector to get the user name, but you could get it from either injector
    • I used a binding annotation, since it's more type-safe compared to @Named (plus it allows a place to add Javadoc)