Friday, August 17, 2018

OSGi R7 Highlights: CDI Integration

The OSGi Enterprise Release 7 specification targeted for release in the coming months contains a brand new specification: CDI Integration. This specification brings the exciting features and capabilities of the Contexts and Dependency Injection (CDI) specification to OSGi.

CDI itself is a vast specification and with that in mind several goals were established to guide the development of the integration:
  1. Do not reinvent the wheel, follow established approaches such as leveraging the CDI Service Provider Interface model
  2. Make code look and feel as natural to CDI developers as possible using CDI designs and best practices where applicable and generally adopt CDI form and function
  3. Provide uncompromising support for key OSGi features such as services, configuration and the dynamics these entail, while not over-complicating or over-engineering the design
  4. Enable modularity for CDI Portable Extensions such that an ecosystem of portable extensions may emerge

Beans

The most basic interaction a developer has with CDI comes from the "Contexts" portion of the spec and the creation of beans, which stems from defining in which context a bean's instances reside. Generally this is accomplished by applying a scope annotation to a POJO.
@ApplicationScoped
public class ShopImpl implements Shop {
  public List<Product> getProducts() { ... }
}
This POJO is a bean whose scope is defined as @ApplicationScoped whose context defines its instance as visible to the entire application.

Injection

The next interaction with CDI comes from the "Dependency Injection" portion of the spec. This is accomplished by applying the @Inject annotation to a field, method or constructor of a POJO.
@ApplicationScoped
public class ShopImpl implements Shop {
  @Inject
  Logger logger;

  public List<Product> getProducts() { ... }
}
This POJO will now have a Logger instance injected into it when the instance is created.

OSGi Services

With a basic understanding of beans and dependency injection let's move on to the OSGi CDI Integration features. The most important feature provided by the CDI integration is the ability to register services with or obtain services from the OSGi service registry.

Registering a service can be as simple as applying the @Service annotation to a bean.
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  ...
}
This POJO is registered into the service registry with the service type Shop.

Adding service properties is accomplished using annotations that are meta-annotated with the @BeanPropertyType annotation. A couple of examples are the @ServiceDescription and @ServiceRanking annotations defined by this specification.
@ApplicationScoped
@Service
@ServiceDescription("This is the primary implementation of the Shop service.")
@ServiceRanking(1000)
public class ShopImpl implements Shop {
  ...
}

Obtaining services is accomplished by using the @Reference annotation in conjunction with @Inject.
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  ProductStore productStore;

  public List<Product> getProducts() {
    return productStore.all();
  }
}
This POJO is injected with a service of type ProductStore.

Filtering services is accomplished by specifying a target filter from the @Reference annotation and/or by adding one or more annotations meta-annotated with @BeanPropertyType to the injection point. The following example filters services having the service.vendor service property equal to Acme, Inc.
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  @ServiceVendor("Acme, Inc.")
  ProductStore productStore;

  public List<Product> getProducts() {
    return productStore.all();
  }
} 
 

Optionality, Cardinality and Dynamics

In OSGi services may need to be expressed in terms of their optionality (if a service is required at all), cardinality (how many services are required) and their dynamics (if the service(s) may change during the lifetime of the bean's context). These concerns are handled elegantly using the Java type system.

The previous example demonstrated a mandatory, unary cardinality reference.

Optional references are expressed using Java's Optional type.
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  Optional<ProductStore> productStore;

  public List<Product> getProducts() {
    return productStore.map(s -> s.all()).orElse(Collections.emptyList());
  }
}

Multi-cardinality references are expressed using a Collection or List types. The default cardinality in this scenario is 0 (which is to say that the services are optional by default). A minimum cardinality can optionally be expressed in conjunction with multi-cardinality using the @MinimumCardinality annotation.
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  @MinimumCardinality(1)
  List<ProductStore> productStores;

  public List<Product> getProducts() {
    return productStores.stream().flatMap(
      s -> s.all().stream()
    ).collect(Collectors.toList());
  }
}

Dynamic references are expressed using the Provider type.
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  Provider<ProductStore> productStore;

  public List<Product> getProducts() {
    return productStore.get().all();
  }
}

Greediness

One of the key distinctions between Declarative Services and CDI Integration with respect to references to services is greediness. Where the greediness of references in Declarative Services is reluctant by default, the greediness of references in CDI Integration is greedy by default. This means that CDI Integration references will always reflect the best service(s) available. This also means that CDI Integration beans may have a more volatile life cycle depending on their references and how often matching services come and go.

Defining a reference to be reluctant is accomplished using the @Reluctant annotation. (This means that once an adequate service is bound it is unlikely to be replaced with a better service in the future.)
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  @Reluctant
  ProductStore productStore;

  public List<Product> getProducts() {
    return productStore.all();
  }
}

Beans vs. Components

In a traditional CDI application, all beans make up the application and form a single cohesive unit; the CDI container. Modeling external dependencies such as (non-dynamic) references to services and configurations without complexities like proxies or byte code instrumentation means the CDI container has to be treated as a single unit, resulting that whenever any dependency changes in a significant way the entire CDI container must be destroyed and recreated. This is a fundamental difference to the model defined in Declarative Services which permits individual components to exist independently from each other. It is also rather limiting. In order to address this limitation the CDI Integration defines the concept of components. Components are cohesive collections of beans which have a consistent, related life cycle which may operate individually from one another.

The CDI Integration defines 3 component types:
  1. Container Component - All traditional beans (@ApplicationScoped, @RequestScoped, @SessionScoped, @Dependent, custom scopes, etc.) are part of the container component (in fact, any bean which is not @ComponentScoped is part of the container component)
  2. Factory Components - are collections of @ComponentScoped beans rooted by a bean having the stereotype @FactoryComponent (such components are driven by factory configuration)
  3. Single Components - are collections of @ComponentScoped beans rooted by a bean with the stereotype @SingleComponent
These components are arranged in two levels where the container component exists as the first level and any number of factory and/or single component children exist in the second level.

Type (relation to CDI Container)
1 Container Component (1..1)
2 Factory Component (0..n) Single Component (0..n)

Factory and single components exist and react to change independently from each other, just like Declarative Services components, while also depending on the container component. If the container component needs to be recreated then all factory and single components must also be recreated.

With this model, it's possible to replicate the Declarative Services component model, while also supporting the traditional monolithic CDI approach (with the additional capability to share beans between container, factory and single components provided they are @ApplicationScoped or @Dependent and also to use other CDI features like Decorators, Interceptors and CDI Portable Extensions.)

Let's see an example:
@ApplicationScoped
@Service
public class ShopImpl implements Shop {
  @Inject
  @Reference
  Provider<List<ProductStore>> productStores;

  public List<Product> getProducts() {
    return productStores.get().stream().flatMap(
      s -> s.all().stream()
    ).collect(Collectors.toList());
  }
}

@BeanPropertyType
public @interface StoreConfig {
  String vendor_name();
  String data_file();
}

@FactoryComponent("product.store")
@Service
public class ProductStoreImpl implements ProductStore {
  @Inject
  @ComponentProperties
  StoreConfig storeConfig;

  public List<Product> all() {
    return read(storeConfig);
  }
}
This application provides a Shop service that is dynamically tracking a number of ProductStore services. ProductStore instances are created by adding new factory configuration instances using the factory PID product.store. Each ProductStore instance is injected with its component properties which are coerced into a typesafe, user-defined StoreConfig for easy processing.

Conclusion

The CDI Integration specification bridges the powerful features of CDI and OSGi in a clean and concise way which should empower developers. There are many other aspects of the specification that don't fit into a single blog post such as modularity for CDI Portable Extensions, further discussion about BeanPropertyType, configuration dependencies, tracking service events, the relationship with Decorators and Interceptors, etc. So don't forget to read the latest draft of the spec. The CDI Integration specification is an important step forward in OSGi dependency injection story that hopefully opens the OSGi door to a wider audience already familiar with CDI.


Want to find out more about OSGi R7?

No comments:

Post a Comment