Skip to main content

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-api interfaces.
  • 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 {
}
AttributeTypeMeaning
idStringUnique identifier. Used by the whitelist (lids.extensions.allowed) and the mandatory list. Must be globally unique across deployed extensions.
nameStringHuman-readable name shown in the LAS console / management API.
descriptionStringOptional, free-text.
componentScanString[]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):

  1. The LAS scans lids.extensions.root for *.jar files.
  2. A single URLClassLoader is created from all JARs, with the WAR class loader as parent (parent-first delegation — extensions cannot override platform classes).
  3. Each JAR is scanned (via the Reflections library) for classes annotated with @Extension. LIDS keeps only those whose class also implements LidsExtension.
  4. For each accepted extension, the packages listed in componentScan are registered into one shared child AnnotationConfigWebApplicationContext.
  5. The child context is refreshed — Spring instantiates @Components, @Services, @RestControllers, etc., wires constructor dependencies, and publishes them alongside the platform beans.
One shared context

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 @PostConstruct for initialization.
  • Use @PreDestroy for 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:

FeatureArtifact
Async tasksserver-utils-async-tasks
Scheduled jobsserver-utils-spring-base (SchedulesService)
Emailserver-utils-email
Templatesserver-utils-templates
Outbound HTTPserver-utils-http-client
SOAP servicesserver-soap-base

Version compatibility

There is no manifest-level compatibility metadata in the framework. Compatibility is the extension author's responsibility:

  • Match samo-server and lids-api versions 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:

  • ScheduledExecutorService instances (call shutdown() + 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):

PropertyTypeDefaultPurpose
lids.extensions.enabledbooleanfalseMaster switch. When false, the loader logs "Extensions not enabled." and skips loading.
lids.extensions.rootpathDirectory scanned for *.jar extension files. Required when extensions are enabled.
lids.extensions.allowedcomma-separated list of id(empty = all allowed)Whitelist of extension IDs that may load. IDs come from @Extension(id = …). Filenames are irrelevant.
lids.extensions.mandatorycomma-separated list of idExtensions 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.

Curl on Windows

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 paramTypeNotes
reloadConfigurationbooleanIf 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 paramTypeNotes
fileNamestringrequired — name of the JAR to delete.
restartSystembooleanRestart the LAS after delete.
reloadExtensionsbooleanReload extensions after delete (lighter than restart).
reloadConfigurationbooleanRe-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 paramTypeNotes
fileNamestringrequired — name to store the JAR under (e.g. vfk-import-las-extension.jar).
restartSystembooleanRestart the LAS after upload (heaviest, safest).
reloadExtensionsbooleanHot-reload extensions after upload (no full restart).
reloadConfigurationbooleanRe-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

ActionClass loaderSpring child contextPlatform restart
reloadExtensions=truerebuiltrebuiltno
restartSystem=truerebuiltrebuiltfull 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

Pitfalls
  • 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 into lids.extensions.root alongside your extension JAR. The loader adds every *.jar in 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 @Extension annotation + 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:

ModuleDemonstrates
sample-async-tasksAsync task framework — submit, retry, suspend, request scope
sample-attachmentsCRUD on feature attachments via REST
sample-business-serverBusiness-server actions (static and instance)
sample-data-import-exportXML-based import/export utilities
sample-db-apiCustom audit events, query building, feature listeners
sample-emailPlain-text and template-based HTML emails
sample-feature-historyQuerying deleted features and feature history
sample-http-clientOutbound HTTP client
sample-rest-api-extensionAdding fields to feature/metadata REST output via JSON output participants
sample-rest-data-serviceCustom REST data service
sample-scheduled-jobPeriodic jobs via SchedulesService
sample-securitySAMO security integration (user/group CRUD)
sample-simple-wsSOAP services — raw XML, WSDL-based, WSDL-less
sample-templatesDocument 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

SymptomLikely cause
Extension JAR present but loader logs Following extensions are not marked as allowed: my-extlids.extensions.allowed whitelist excludes the ID. Add it or empty the list.
Startup fails with Missing mandatory extensions: my-extWhitelisted in mandatory but JAR not present in root, or id mismatch.
Loader logs Extensions not enabled.lids.extensions.enabled=false.
ClassCastException / stale beans after redeployListener / callback not unregistered in @PreDestroy — see Always unregister listeners and callbacks.
NoSuchMethodError against platform classesVersion 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 loadedFQCN collision between their JARs. Rename / repackage one.
REST endpoint 404 after uploadMissing componentScan entry for the controller package, or restartSystem / reloadExtensions not set on the upload call.