-
Notifications
You must be signed in to change notification settings - Fork 18
Runtime Extensions
Since 0.10.4
phoss-ap can be extended at runtime with your own code — custom SPI implementations (notification handlers, verifiers, document forwarders, ...) and additional JDBC drivers — without rebuilding the application jar. You compile a small, thin jar of your own classes and drop it into an extension directory that the AP adds to its classpath on startup.
Requires phoss-ap 0.10.4 or later. The mechanism relies on the Spring Boot
PropertiesLauncherlayout and theLOADER_PATHenvironment variable, both of which were introduced in 0.10.4. On earlier versions the application jar uses the plainJarLauncherand ignoresLOADER_PATH; extensions must instead be compiled into a custom build.
- How it works
- Available extension points
- Writing an extension
- Registering the SPI implementation
- Loading it — plain JVM / shell script
- Loading it — Docker bind mount
- Loading it — Docker baked into an image
- Confirming it is working
- Custom JDBC drivers
- Reference module: phoss-ap-extension-demo
The phoss-ap-webapp Spring Boot jar is repackaged with the ZIP layout, which selects Spring
Boot's PropertiesLauncher
instead of the default JarLauncher. PropertiesLauncher appends every location listed in the
LOADER_PATH environment variable (or the -Dloader.path system property) to the runtime
classpath. Directories are scanned recursively for *.jar files.
The official Docker image sets:
ENV LOADER_PATH=/extand creates /ext as a plain directory. Any jar placed in /ext therefore joins the same
classpath as the application. Because all phoss-ap extension points are plain
java.util.ServiceLoader
lookups, an extension jar that carries the right META-INF/services entries is discovered
automatically — there is no further wiring, configuration flag, or restart hook.
+------------------------------+
| phoss-ap-webapp.jar | BOOT-INF/lib (application + shipped deps)
| (PropertiesLauncher) |
+--------------+---------------+
| LOADER_PATH=/ext
v
+------------------------------+
| /ext/my-extension.jar | your SPI implementations + META-INF/services
| /ext/some-jdbc-driver.jar | optional extra JDBC driver
+------------------------------+
All of the following are java.util.ServiceLoader SPIs defined in the phoss-ap-api module
(package com.helger.phoss.ap.api.spi). Implement one or more of them:
| SPI interface | Purpose | Cardinality |
|---|---|---|
IAPLifecycleEventSPI |
Receive success/lifecycle callbacks — document received / forwarded / sent, reporting success, MLS correlation, scheduler cycle completions. | Many |
IAPNotificationHandlerSPI |
Receive failure callbacks — verification rejections, duplicate rejection, permanent send/forward failures, unexpected exceptions. | Many |
IInboundDocumentVerifierSPI |
Optional verification of an inbound document. All registered verifiers must pass for the document to be accepted. | Many (all must pass) |
IOutboundDocumentVerifierSPI |
Optional verification of an outbound document before sending. All registered verifiers must pass. | Many (all must pass) |
IPeppolReceiverCheckSPI |
Decide whether this AP services a given receiver participant. If not, the inbound AS4 message is rejected before database storage. | Many |
IDocumentForwarderProviderSPI |
Provide a deployment-specific document forwarder (e.g. a message queue). The AP selects one provider by matching its getID() against the configured forwarding.spi.id value. |
One selected by ID |
The built-in modules phoss-ap-otel (IAPLifecycleEventSPI + IAPNotificationHandlerSPI),
phoss-ap-sentry (IAPNotificationHandlerSPI) and phoss-ap-validation
(I*DocumentVerifierSPI) are themselves implementations of these SPIs and can be used as
references.
Create a normal Maven jar module. The single rule that matters is dependency scope: everything
the running AP already ships must be provided, so it is not bundled into your jar. The result is
a thin jar containing only your own classes — exactly what belongs in /ext.
<dependencies>
<!-- Compile against the SPI interfaces, but do NOT bundle them: the AP provides them. -->
<dependency>
<groupId>com.helger.phoss.ap</groupId>
<artifactId>phoss-ap-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Bundle ONLY dependencies that the AP does not already ship. -->
</dependencies>Apply provided to every shared dependency (phoss-ap-*, phase4, ph-commons, slf4j, the AWS
SDK, ...) and bundle only what is genuinely unique to your extension. This is what keeps the flat
classpath free of version conflicts. Because the phoss-ap parent POM is a Maven BOM, you can import
it to inherit aligned versions for the provided dependencies.
A minimal logging-only implementation of IAPLifecycleEventSPI looks like this:
package com.example.myext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.annotation.style.IsSPIImplementation;
import com.helger.phoss.ap.api.spi.IAPLifecycleEventSPI;
// ... interface method parameter imports omitted for brevity
@IsSPIImplementation
public final class MyLifecycleEventSPI implements IAPLifecycleEventSPI
{
private static final Logger LOGGER = LoggerFactory.getLogger (MyLifecycleEventSPI.class);
public void onOutboundDocumentSent (final String sTransactionID,
final String sSbdhInstanceID,
final java.time.Duration aSendingDuration,
final int nAttempts)
{
LOGGER.info ("Document " + sSbdhInstanceID + " sent after " + nAttempts + " attempt(s)");
// ... your real integration goes here (message queue, webhook, custom store, ...)
}
// ... implement the remaining interface methods
}Register each implementation the same way phoss-ap's own modules do — a plain text file under
src/main/resources, named after the fully-qualified interface name, listing your implementation
class(es), one per line:
src/main/resources/
META-INF/services/
com.helger.phoss.ap.api.spi.IAPLifecycleEventSPI -> com.example.myext.MyLifecycleEventSPI
com.helger.phoss.ap.api.spi.IAPNotificationHandlerSPI -> com.example.myext.MyNotificationHandlerSPI
The @IsSPIImplementation annotation is a documentation/validation marker; the META-INF/services
file is what ServiceLoader actually reads.
Point LOADER_PATH at a directory containing your extension jar(s) and launch the application jar
normally. This works because the 0.10.4+ jar uses PropertiesLauncher.
#!/bin/bash
set -euo pipefail
# 1. Build the thin extension jar
mvn -pl my-extension -am package
# 2. Stage it into a local extension directory
mkdir -p ./ext
cp my-extension/target/my-extension-*.jar ./ext/
# 3. Run the AP with the extension directory on the classpath
LOADER_PATH=./ext java -jar phoss-ap-webapp-0.10.4.jarLOADER_PATH accepts a comma-separated list, so multiple directories/jars are possible, e.g.
LOADER_PATH=./ext,./drivers.
The official image already sets LOADER_PATH=/ext, so you only need to bind-mount a host directory
that contains your jar(s) at /ext:
mkdir -p ./ext
cp my-extension/target/my-extension-*.jar ./ext/
docker run --rm -p 8080:8080 \
-v "$(pwd)/ext:/ext" \
-e PHOSSAP_JDBC_URL=jdbc:postgresql://host.docker.internal:5432/phoss-ap \
-e PHOSSAP_JDBC_USER=peppol \
-e PHOSSAP_JDBC_PASSWORD=peppol \
phelger/phoss-apOn Apple Silicon use the phelger/phoss-ap-arm64 image instead.
To ship a self-contained image, layer your extension on top of a phoss-ap image and copy the jar
into /ext:
ARG PHOSS_AP_IMAGE=phelger/phoss-ap:latest
FROM ${PHOSS_AP_IMAGE}
# Keep the image self-contained even on older bases
ENV LOADER_PATH=/ext
COPY target/my-extension-*.jar /ext/my-extension.jarmvn -pl my-extension -am package
docker build -t phoss-ap-with-my-ext .
docker run --rm -p 8080:8080 \
-e PHOSSAP_JDBC_URL=jdbc:postgresql://host.docker.internal:5432/phoss-ap \
-e PHOSSAP_JDBC_USER=peppol \
-e PHOSSAP_JDBC_PASSWORD=peppol \
phoss-ap-with-my-ext/ext is a plain directory (not a Docker VOLUME), so the COPY persists into the image and is
overridden cleanly if you later bind-mount a different /ext.
An SPI implementation is instantiated the first time the AP dispatches to it (and, if you log from the constructor, at load time). Add a log line to prove the wiring, e.g.:
[my-ext] MyLifecycleEventSPI was loaded via SPI
[my-ext] Document 1234-... sent after 1 attempt(s)
If you see your log lines as documents flow through the AP, the extension is being loaded and invoked. If you do not, check that:
- You are running 0.10.4 or later (earlier jars ignore
LOADER_PATH). -
LOADER_PATHpoints at the directory that actually contains the jar. - The
META-INF/services/<interface>file exists in the jar and names the correct class. - The jar is thin — it must not re-bundle
phoss-ap-apior other shipped classes.
For deeper diagnosis set -Dloader.debug=true (or _LOADER_DEBUG=true as an environment variable)
to have PropertiesLauncher print the classpath entries it adds.
The same directory is the supported way to add a JDBC driver that is not shipped by default (the
official image bundles only the PostgreSQL driver). Drop the driver jar into /ext (or your
LOADER_PATH directory) alongside any driver-specific Flyway module and configure the database via
the standard properties. This keeps proprietary or heavyweight drivers out of the base image while
still making them available at runtime. See Configuration Properties for the database settings.
The repository ships a complete, runnable example in the phoss-ap-extension-demo module. It
contains two logging-only SPI implementations (DemoLifecycleEventSPI and
DemoNotificationHandlerSPI), their META-INF/services registrations, an example Dockerfile, and
two helper scripts:
-
assemble.sh— builds the thin jar and, optionally, a Docker image with the extension baked in (it automatically selects the-arm64image name and base on macOS). -
run-docker-ph.sh— runs that image locally (also selecting the right image on macOS).
Copy that module as the starting point for your own extension. See also Maven Module Structure for how it fits into the build, and OpenTelemetry Integration for a production example of the lifecycle/notification SPIs in action.
It is appreciated if you star the GitHub project if you like it.
Donation link: https://paypal.me/PhilipHelger
- Home
- News and noteworthy
- Running phoss AP
- Architecture Overview
- API Specification
- Configuration Properties
- Code Lists
- Database Design Notes
- Maven Module Structure
- Runtime Extensions
- OpenTelemetry Integration
- Security Considerations
- Peppol Specifics
- Testing Without Peppol Network
- Known Users
- Migrating from phase4-peppol-standalone
- Contributing