Skip to content

Discussion: How to Structure Integrations

Tim Quinn edited this page Feb 8, 2021 · 11 revisions

Overview

We have several integrations of Helidon with other technologies, for both Helidon SE and Helidon MP. I have built another one—​for Micrometer support—​and before I recreate the PR for it Tomas suggested in a Slack DM that we have a broader discussion for how to handle these integrations.

So far, we have used various approaches for how to structure the Helidon SE and Helidon MP modules for these integrations, as Tomas summarized:

Approach Location for SE module Location for MP module

First-class citizen

helidon/xxx

helidon/microprofile/xxx

Integration-level citizen

Two modules

Repeat xxx for SE suffix

helidon/integrations/xxx/xxx

helidon/integrations/xxx/cdi

Use base for SE suffix

helidon/integrations/xxx/base

helidon/integrations/xxx/cdi

One module

helidon/integrations/xxx

Example pom.xml contents for user projects

  • In any of the two-module approaches

    As before…​

    • An SE app depends on the Helidon SE integration artifact.

    • An MP app depends on the Helidon MP integration artifact.

  • In the one-module approach

    Both an SE app and an MP app would declare a dependency on the single Helidon integration artifact.

    The MP app also declares a dependency on some Helidon MP artifact (the MP server, e.g.) that brings in CDI. This could be via a dependency on one of the MP bundles. This dependency is one that the app would need anyway; just mentioning it for completeness.

As Tomas explained in his comments below, the single Helidon integration artifact contains a CDI extension which is activated only when CDI is actually present at runtime (and which loads portable extensions using the Java service loader mechanism).

How does it work?

To make the "Integration-level citizen/one module" approach (e.g, Neo4J) work, the Helidon integrations/xxx pom.xml declares a dependency on CDI using

<scope>provided</scope>
<optional>true</optional>

and the accompanying

requires static jakarta.activation;
requires static jakarta.enterprise.cdi.api;
requires static jakarta.inject.api;
requires static jakarta.interceptor.api;

(emphasis on static) in module-info.java.

This technique allows the code in integrations/xxx which refers to CDI (e.g., the Extension interface, interceptors, injection) to compile but does not itself cause CDI to be part of the packaging of any app that depends on integrations/xxx.

This is much like compile, but indicates you expect the JDK or a container to provide the dependency at runtime. For example, when building a web application for the Java Enterprise Edition, you would set the dependency on the Servlet API and related Java EE APIs to scope provided because the web container provides those classes. A dependency with this scope is added to the classpath used for compilation and test, but not the runtime classpath. It is not transitive."

To align with this explanation, the Helidon MP application is the container; the app—​by virtue of its dependency on a Helidon artifact that brings in CDI—​guarantees that CDI will be present at runtime to satisfy the provided dependency.

Notes

  • First-class citizen

    • Adv: Follows our familiar approach of putting SE modules as children of the top-level and modules for Helidon MP under microprofile.

    • Dis: Not appropriate if xxx is not truly a MicroProfile technology. e.g., Micrometer

  • Integration, two modules

    • Adv: Follows our familiar approach which separates the Helidon SE and Helidon MP code.

    • Dis:

      • Seems slightly odd to repeat the technology name in the SE module location: integrations/xxx/xxx.

      • And yet, using base for the lowest-level name does not add much information.

  • Integration, one module

    • Adv: Reduces the number of modules in the system.

    • Dis:

      • Might look slightly odd to have annotations in a module that is usable from a Helidon SE app.

      • CDI behavior of xxx is triggered somewhat indirectly by the app developer declaring a dependency on some other module that has a harder dependency on CDI.

Tomas

  • We also have the integrations/cdi which are not aligned with any of the above

  • First-class citizen

    • For modules that are not really integrations. For example "scheduling" falls into this category.

    • I am still not certain we should use microprofile/xxx for the CDI integration, unless it is an actual MP specification

  • Otherwise I am for using the "One module" approach unless

    • There is a lot of code needed to integrate CDI (e.g. more code for CDI then for SE) - in such a case I am for integrations/xxx/xxx + integrations/xxx/cdi

    • You would end up with a dependency on the classpath that is only needed for CDI in SE

The CDI integration is triggered by the existence of CDI on the classpath (no need for any special module). If there is no CDI, it is ignored. When you build an application, you would not expect an integration module to bring in the whole CDI ecosystem anyway. The existence of CDI annotations is slightly odd, but mostly to us, as end users do not use these classes - there should be a clear entry point for SE application developer. We probably should enforce this by adding @Deprecated to public constructors of any CDI extension, and by not exposing constructors of other CDI classes (ideally making them package local, so users do not see them at all).