Java Extensions
Developer guide for writing, packaging, deploying, and debugging Java LIDS extensions — external JARs that plug into a running LAS instance.
Audience: developers extending LIDS / LAS.
What is a LIDS extension?
A LIDS extension is a self-contained Java application (a single JAR) that is dynamically loaded by the LAS at startup or via the management REST API. Extensions reuse the platform via lids-api and samo-server:
- They run inside the LAS JVM as a child Spring
ApplicationContext. - They consume platform services (features, attachments, security, async tasks, scheduling, REST, SOAP, email, templates, …) through
lids-apiinterfaces. - They can publish their own REST/SOAP endpoints, register event listeners, schedule jobs, and persist data.
Typical use cases: customer-specific data checks, import/export formats (VFK, SWDE), domain integrations, custom business actions.
Extension framework architecture
Entry-point class — @Extension + LidsExtension
Each extension JAR contains exactly one main class annotated with @Extension and implementing the marker interface LidsExtension:
package com.berit.lids.extensions.sample.tasks;
import com.asseco.samo.server.spring.extensions.Extension;
import com.berit.lids.api.extensions.LidsExtension;
@Extension(
id = "sample-tasks-extension",
name = "Sample tasks extension",
componentScan = "com.berit.lids.extensions.sample.tasks"
)
public class SampleAsyncTasksExtension implements LidsExtension {
}
| Attribute | Type | Meaning |
|---|---|---|
id | String | Unique identifier. Used by the whitelist (lids.extensions.allowed) and the mandatory list. Must be globally unique across deployed extensions. |
name | String | Human-readable name shown in the LAS console / management API. |
description | String | Optional, free-text. |
componentScan | String[] | Base packages registered into the extensions Spring context for @Component / @Service / @RestController scanning. |
LidsExtension (com.berit.lids.api.extensions.LidsExtension) is an empty marker interface — its only purpose is to let LIDS distinguish LIDS extensions from other @Extension-annotated classes that samo-server may discover.
Loading mechanism
At LAS startup (and on reload):
- The LAS scans
lids.extensions.rootfor*.jarfiles. - A single
URLClassLoaderis created from all JARs, with the WAR class loader as parent (parent-first delegation — extensions cannot override platform classes). - Each JAR is scanned (via the Reflections library) for classes annotated with
@Extension. LIDS keeps only those whose class also implementsLidsExtension. - For each accepted extension, the packages listed in
componentScanare registered into one shared childAnnotationConfigWebApplicationContext. - The child context is refreshed — Spring instantiates
@Components,@Services,@RestControllers, etc., wires constructor dependencies, and publishes them alongside the platform beans.
All extensions share one child class loader and one child Spring context. There is no per-extension isolation; FQCN collisions between extensions are resolved by JAR load order.
Lifecycle hooks
- Use
@PostConstructfor initialization. - Use
@PreDestroyfor cleanup — see Always unregister listeners and callbacks. - The extension context is fully torn down on reload, then re-created — so all stateful resources must be released in
@PreDestroy.
Project setup
Maven layout
Extensions are standard Maven projects (src/main/java):
my-extension/
├── pom.xml
└── src/main/java/com/berit/lids/extensions/myext/
├── MyExtension.java # @Extension main class
├── service/...
└── controller/...
Parent POM dependencies
Minimum required dependencies (versions taken from the sample-extensions reference project — synchronize with the target LAS release):
<properties>
<lids-api.version>4.X.X</lids-api.version>
<samo-server.version>4.X.X</samo-server.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.asseco.samo.server</groupId>
<artifactId>server-spring-base</artifactId>
<version>${samo-server.version}</version>
</dependency>
<dependency>
<groupId>com.berit.lids</groupId>
<artifactId>lids-api</artifactId>
<version>${lids-api.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
Add further samo-server sub-libraries per feature need:
| Feature | Artifact |
|---|---|
| Async tasks | server-utils-async-tasks |
| Scheduled jobs | server-utils-spring-base (SchedulesService) |
server-utils-email | |
| Templates | server-utils-templates |
| Outbound HTTP | server-utils-http-client |
| SOAP services | server-soap-base |
Version compatibility
There is no manifest-level compatibility metadata in the framework. Compatibility is the extension author's responsibility:
- Match
samo-serverandlids-apiversions to the target LAS release. - Do not bundle classes already present in the LAS classpath (Spring, Jackson, JDBC drivers, samo-server, lids-api, …). The parent class loader serves them.
- Bundle only extension-specific third-party libraries.
Writing your first extension
// src/main/java/com/berit/lids/extensions/hello/HelloExtension.java
package com.berit.lids.extensions.hello;
import com.asseco.samo.server.spring.extensions.Extension;
import com.berit.lids.api.extensions.LidsExtension;
@Extension(
id = "hello-extension",
name = "Hello world extension",
componentScan = "com.berit.lids.extensions.hello"
)
public class HelloExtension implements LidsExtension {
}
// src/main/java/com/berit/lids/extensions/hello/controller/HelloController.java
package com.berit.lids.extensions.hello.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/rest/ext/hello")
public class HelloController {
@GetMapping
public String hello() {
return "Hello from LIDS extension!";
}
}
Build (mvn package) → drop the JAR in lids.extensions.root → restart LAS → GET /as/api/rest/ext/hello.
Common patterns and rules
Always unregister listeners and callbacks
When extension code subscribes to platform events (feature listeners, association listeners, graphic-tag listeners, custom callbacks), you must unregister them in @PreDestroy. Extensions can be reloaded — if you leak listener references, the platform keeps invoking stale objects from the previous class loader, causing memory leaks and ClassCastExceptions.
@Service
public class SampleAuditServiceImpl {
private final FeatureService featureService;
private final AuditFeaturesListener listener = new AuditFeaturesListener();
@Autowired
public SampleAuditServiceImpl(FeatureService featureService) {
this.featureService = featureService;
featureService.registerListener(IFeatureEvent.class, listener);
}
@PreDestroy
void preDestroy() {
featureService.unRegisterListener(listener);
}
}
The same rule applies to:
ScheduledExecutorServiceinstances (callshutdown()+awaitTermination)- Async-task type registrations
- Any callback you handed over to a platform service
Constructor injection only
Always use constructor injection with final fields — it makes testing easier and prevents partially-initialized beans.
Package conventions
- Root package:
com.berit.lids.extensions.<extension-name> - Sub-packages:
controller/,service/,repository/,model/entity/,model/dto/,config/,util/ - One
@Extension-annotated class per JAR — placed at the package root.
LAS configuration properties
Set in the LAS *.properties file (e.g. environment.properties):
| Property | Type | Default | Purpose |
|---|---|---|---|
lids.extensions.enabled | boolean | false | Master switch. When false, the loader logs "Extensions not enabled." and skips loading. |
lids.extensions.root | path | — | Directory scanned for *.jar extension files. Required when extensions are enabled. |
lids.extensions.allowed | comma-separated list of id | (empty = all allowed) | Whitelist of extension IDs that may load. IDs come from @Extension(id = …). Filenames are irrelevant. |
lids.extensions.mandatory | comma-separated list of id | — | Extensions that must be present. Missing → startup fails with Missing mandatory extensions: …. |
Example (lids-as.properties):
lids.extensions.enabled=true
lids.extensions.root=D:/lids/extensions
lids.extensions.allowed=import-las-extension,las-extension
lids.extensions.mandatory=las-extension
Deployment
Four equivalent ways to deploy a JAR — pick whichever fits the environment.
Copy to extensions folder
The simplest. Copy my-extension.jar into the directory configured by lids.extensions.root and restart the LAS, or call the reload endpoint (see Management REST API).
LAS Console — Extensions tab
Upload via the web console (/as/console, tab Extensions). The console wraps the management REST API.
IntelliJ IDEA — deploy & debug profile
Set up a Run/Debug Configuration that runs curl (or a Maven plugin) after package. Sample arguments:
-X PUT
-u <username>:<password>
-H "Content-Type: application/java-archive"
--data-binary "@target/my-extension.jar"
"<las_url>:<las_port>/<app_context>/api/rest/management/extensions/files?fileName=my-extension.jar&restartSystem=true"
Then attach a remote JVM debugger to the LAS port.
Install Chocolatey, then choco install curl:
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
choco install curl
REST upload (Postman / curl / CI)
See PUT /files — upload JAR below.
Management REST API
Base path: /<app_context>/api/rest/management/extensions. All endpoints are protected by LAS management security — authenticate as an admin user (<username>:<password> on dev).
GET /info
Returns the configured extensionsRoot and a list of currently loaded extensions (id, name, description).
POST /reload
Re-runs the loader: closes the child class loader and Spring context, then rebuilds both from the current contents of lids.extensions.root.
| Query param | Type | Notes |
|---|---|---|
reloadConfiguration | boolean | If true, re-reads configuration properties before reload. |
GET /files/list
Lists the JAR files currently in lids.extensions.root (name, lastModified, size).
DELETE /files
Removes a JAR file from lids.extensions.root.
| Query param | Type | Notes |
|---|---|---|
fileName | string | required — name of the JAR to delete. |
restartSystem | boolean | Restart the LAS after delete. |
reloadExtensions | boolean | Reload extensions after delete (lighter than restart). |
reloadConfiguration | boolean | Re-read configuration before the reload. |
PUT /files — upload JAR
Uploads (or overwrites) a JAR file in lids.extensions.root.
- Method:
PUT - Consumes:
application/java-archive - Produces:
application/json - Body: raw JAR bytes (binary)
| Query param | Type | Notes |
|---|---|---|
fileName | string | required — name to store the JAR under (e.g. vfk-import-las-extension.jar). |
restartSystem | boolean | Restart the LAS after upload (heaviest, safest). |
reloadExtensions | boolean | Hot-reload extensions after upload (no full restart). |
reloadConfiguration | boolean | Re-read configuration before reload. |
Example:
curl -X PUT -u <username>:<password> \
-H "Content-Type: application/java-archive" \
--data-binary "@target/my-extension.jar" \
"<las_url>:<las_port>/<app_context>/api/rest/management/extensions/files?fileName=my-extension.jar&restartSystem=true"
Hot reload vs. restart
| Action | Class loader | Spring child context | Platform restart |
|---|---|---|---|
reloadExtensions=true | rebuilt | rebuilt | no |
restartSystem=true | rebuilt | rebuilt | full LAS restart |
Reload is faster but requires correct cleanup (see Always unregister listeners and callbacks). When in doubt, restart.
Classpath & isolation — what to watch out for
- All extensions share one child class loader. Two extensions exporting the same FQCN with different bytecode will collide; the first one wins.
- Parent-first delegation. Platform classes always win over identically-named classes bundled inside an extension JAR. Do not bundle Spring, Jackson, JDBC drivers, samo-server, or lids-api in your JAR.
- Third-party / extra samo-server JARs must live on the extensions classpath. If your extension depends on a library that is not already part of the LAS WAR (this sometimes applies even to samo-server sub-libraries — e.g.
server-utils-ws-spring-base,server-utils-ws), drop those JARs intolids.extensions.rootalongside your extension JAR. The loader adds every*.jarin that directory to the shared child class loader. Do not shade them into your extension JAR — shading risks duplicating classes already on the LAS classpath. - No JAR-manifest contract. The loader ignores
MANIFEST.MF; classpath wiring is purely via the@Extensionannotation + Reflections scan. - Mind transient state. On reload, beans are re-created — but objects referenced by platform code (listeners, executors) survive unless you unregistered them.
Reference — sample extensions
Canonical, tested examples live in the sample-extensions repository (https://gitlab/lids/server/extensions/sample-extensions). Each module is a self-contained reference for one extension point:
| Module | Demonstrates |
|---|---|
sample-async-tasks | Async task framework — submit, retry, suspend, request scope |
sample-attachments | CRUD on feature attachments via REST |
sample-business-server | Business-server actions (static and instance) |
sample-data-import-export | XML-based import/export utilities |
sample-db-api | Custom audit events, query building, feature listeners |
sample-email | Plain-text and template-based HTML emails |
sample-feature-history | Querying deleted features and feature history |
sample-http-client | Outbound HTTP client |
sample-rest-api-extension | Adding fields to feature/metadata REST output via JSON output participants |
sample-rest-data-service | Custom REST data service |
sample-scheduled-job | Periodic jobs via SchedulesService |
sample-security | SAMO security integration (user/group CRUD) |
sample-simple-ws | SOAP services — raw XML, WSDL-based, WSDL-less |
sample-templates | Document generation from JSON + templates |
Start from the module closest to your use case, copy its pom.xml and main @Extension class, then strip or extend.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Extension JAR present but loader logs Following extensions are not marked as allowed: my-ext | lids.extensions.allowed whitelist excludes the ID. Add it or empty the list. |
Startup fails with Missing mandatory extensions: my-ext | Whitelisted in mandatory but JAR not present in root, or id mismatch. |
Loader logs Extensions not enabled. | lids.extensions.enabled=false. |
ClassCastException / stale beans after redeploy | Listener / callback not unregistered in @PreDestroy — see Always unregister listeners and callbacks. |
NoSuchMethodError against platform classes | Version skew — extension built against samo-server / lids-api newer than the running LAS, or a shadowed class bundled in the JAR. |
| Two extensions misbehave when both are loaded | FQCN collision between their JARs. Rename / repackage one. |
REST endpoint 404 after upload | Missing componentScan entry for the controller package, or restartSystem / reloadExtensions not set on the upload call. |