Using Beadledom Client with Guice

The beadledom-client-guice module adds the ability to use Guice for building and customizing clients. JAX-RS features, providers, and filters can all be registered by binding them with Guice. To avoid conflicts and duplicate binding errors client Guice configuration must be namespaced using a binding annotation; both @BindingAnnotation and @Qualifier annotations being supported.

Download

Download using Maven:

<dependency>
  <groupId>com.cerner.beadledom</groupId>
  <artifactId>beadledom-client-guice</artifactId>
  <version>[insert latest version]</version>
</dependency>

Usage

Installing the BeadledomClientModule will make a BeadledomClient object available that can be used for proxying JAX-RS resource interfaces and then making those resources available through Guice.

public class MyClientModule extends AbstractModule {
  @Override
  protected void configure() {
    // Bind the client module with this client's binding annotation
    // so that the client is namespaced for this client only.
    install(BeadledomClientModule.with(MyClientAnnotation.class));

    // Consumers of the client should provide the configuration.
    requireBinding(MyClientConfig.class);
  }

  @Provides
  @Singleton
  MyResource provideMyResource(
      @MyClientAnnotation BeadledomClient client, MyClientConfig config) {
    BeadledomWebTarget target = client.target(config.uri());
    return target.proxy(MyResource.class);
  }
}

Features

JAX-RS features, providers can be bound with the same binding annotation as a particular BeadledomClient and they will automatically be registered with the client. The beadledom-client-guice project includes a Jackson provider configured to match the same default JSON configuration from beadledom based services.

Jackson JSON Provider

The Beadledom JSON Provider client feature includes the ObjectMapperClientFeatureModule for installing the Jackson JSON Provider. The feature can be included by installing the module with the same binding annotation as the BeadledomClientModule.

public class MyClientModule extends AbstractModule {
  @Override
  protected void configure() {
    install(BeadledomClientModule.with(MyClientAnnotation.class));
    install(ObjectMapperClientFeatureModule.with(MyClientAnnotation.class));

    // Additional configuration
  }
}

Follow the below steps to configure the client specific ObjectMapper

  • All the @ProvidesIntoSet methods must also be annotated with the client’s BindingAnnotation i.e., @MyClientAnnotation from the above example.
@ProvidesIntoSet
@MyClientAnnotation
SerializationFeatureFlag getSerializationFeature() {
  return SerializationFeatureFlag.create(SerializationFeature.INDENT_OUTPUT, true);
}
  • When defining the multibinders make sure to pass in the client’s BindingAnnotation as well.
Multibinder.newSetBinder(binder(), Module.class, MyClientAnnotation.class);

For enabling/disabling Jackson’s ObjectMapper features, please refer to the beadledom’s jackson documentation.

Client Configuration

BeadledomClientConfiguration can be used to add custom configurations to the client. It is important that BeadledomClientConfiguration gets bound to a client annotation.

The client options that can be configured are:

connectionPoolSize
Sets the connection pool size.
connectionTimeoutMillis
Sets the connection timeout to be used in milliseconds.
correlationIdName
Sets the Header name for a client.
maxPooledPerRouteSize
Sets the max connection pool size per route.
socketTimeoutMillis
Sets the socket timeout to be used in milliseconds.
sslContext
Sets the SSL Context.
trustStore
Sets the SSL trust store.
ttlMillis
Sets the TTL to be used in milliseconds.
verifier
Sets the hostname verifier.
@Provides
@MyAmazingFeature
BeadledomClientConfiguration provideClientConfig () {
  BeadledomClientConfiguration beadledomClientConfig = BeadledomClientConfiguration.builder()
      .maxPooledPerRouteSize(60)
      .socketTimeoutMillis(60)
      .connectionTimeoutMillis(60)
      .ttlMillis(60)
      .connectionPoolSize(60).build();
  return beadledomClientConfig;
}

Custom Features

Additional JAX-RS features and providers can be installed by following a similar pattern to the Jackson JSON module.

Start by creating a client feature Guice module. It’s usually ideal to extend from the Guice PrivateModule and only expose the bindings that should be available for the client. This is useful when there are additional bindings that are required for the feature/provider, but that shouldn’t be bound/registered with the client or made available to consumers of the client.

The constructor for the client feature module should be private and paired with a static factory method with(Class<? extends Annotation> annotation) since the feature must be namespaced to the same client binding annotation that the feature is being installed for. This is required to prevent duplicate binding issues when multiple clients are in use.

public class MyClientFeatureModule extends PrivateModule {
  private final Class<? extends Annotation> annotation;

  private MyClientFeatureModule(Class<? extends Annotation> annotation) {
    this.annotation = annotation;
  }

  public static MyClientFeatureModule with(Class<? extends Annotation> annotation) {
    // The annotation must be either BindingAnnotation or Qualifier; fail fast otherwise
    BindingAnnotations.checkIsBindingAnnotation(annotation);

    return new MyClientFeature(annotation));
  }

  @Override
  protected void configure() {
    bind(MyClientFeature.class).annotatedWith(annotation).toProvider(MyClientFeatureProvider.class);

    expose(MyClientFeature.class).annotatedWith(annotation);
  }
}

When additional configuration is needed for creating a client feature, then the DynamicBindingProvider class can be used for creating a custom Guice provider.

class MyConfigurableClientFeature {
    public static MyConfigurableClientFeature create(MyConfiguration config) {
      return new MyConfigurableClientFeature(config);
    }

    // ...
}

class MyConfigurableClientFeatureProvider implements Provider<MyConfigurableClientFeature> {
  private final Class<? extends Annotation> annotation;

  private DynamicBindingProvider<MyConfiguration> configurationProvider;

  MyConfigurableClientFeatureProvider(Class<? extends Annotation> annotation) {
    this.annotation = annotation;
  }

  @Inject
  void init(DynamicBindingProvider<MyConfiguration> configurationProvider) {
    this.configurationProvider = configurationProvider;
  }

  @Override
  public OAuth1ClientFilterFeature get() {
    // Get the configuration that is namespaced with the same binding annotation
    MyConfiguration config = configurationProvider.get(annotation);

    return MyConfigurableClientFeature.create(config);
  }
}
public class MyConfigurableClientFeatureModule extends PrivateModule {
  private final Class<? extends Annotation> annotation;

  private MyConfigurableClientFeatureModule(Class<? extends Annotation> annotation) {
    this.annotation = annotation;
  }

  public static MyConfigurableClientFeatureModule with(Class<? extends Annotation> annotation) {
    // The annotation must be either BindingAnnotation or Qualifier; fail fast otherwise
    BindingAnnotations.checkIsBindingAnnotation(annotation);

    return new MyClientFeature(annotation));
  }

  @Override
  protected void configure() {
    // Use the DynamicAnnotations helper to bind a dynamic provider for the configuration class
    // This will make the DynamicProvider<MyConfiguration> available for injection and also
    // require a binding for MyConfiguration that must be provided either by a consumer's
    // module or one of the client modules.
    DynamicAnnotations
        .bindDynamicProvider(binder(), MyConfiguration.class, annotation);
    MyConfigurableClientFeatureProvider provider =
        new MyConfigurableClientFeatureProvider(annotation);
    bind(MyConfigurableClientFeature.class).annotatedWith(annotation)
        .toProvider(provider);

    // By using PrivateModule and only exposing the feature this is meant to install, the
    // dynamic provider from above is hidden from consumers since it is an unnecessary
    // implementation detail.
    expose(MyConfigurableClientFeature.class).annotatedWith(annotation);
  }
}