# Maven Based Extensions Lucee 7 introduces a new way to build extensions using Maven-based dependency management and classloading instead of OSGi bundles. This approach is simpler to build, easier to debug, and aligns with standard Java tooling. ## Why Maven? OSGi bundles have their own classloader, which creates isolation but also complexity — particularly around dependency resolution, `Require-Bundle` mismatches, and cold-start failures. The Maven approach uses standard Java classloading and lets Lucee resolve dependencies from Maven coordinates at runtime. Key benefits: - **Standard JARs** — no OSGi manifest headers required - **Maven-style dependency resolution** — familiar tooling, no bundle wiring issues - **Offline installation** — JARs can be embedded directly inside the `.lex` file - **Simpler builds** — standard Maven compilation, no Felix runtime to contend with > **Common OSGi pitfall:** With OSGi bundles, `Require-Bundle` must use the `Bundle-SymbolicName`, NOT the Maven `groupId:artifactId`. These are often completely different strings. Mixing them up causes flaky resolution failures that only surface on cold starts — see [LDEV-6189](https://luceeserver.atlassian.net/browse/LDEV-6189) for a real-world example. The Maven approach eliminates this class of bug entirely. > **Lucee 7.1 OSGi improvement:** If you do still need OSGi bundles, [LDEV-6044](https://luceeserver.atlassian.net/browse/LDEV-6044) (7.1.0.21+) removes the static system packages list and lets Felix 7.x auto-detect packages from the JVM module system. This fixes long-standing issues where OSGi bundles couldn't import modern JDK packages like `java.util.stream` and `java.time`. ## Extension Structure Each Maven-based extension publishes **two artifacts** to Maven Central: | Artifact | Purpose | Example | | --- | --- | --- | | `{name}` | The JAR library | `org.lucee:mail` | | `{name}-extension` | The `.lex` extension package | `org.lucee:mail-extension` | > **Important:** The `artifactId` must follow the `{name}-extension` pattern (e.g., `crypto-extension`, `jsonata-extension`, `redis-extension`). This is how Lucee discovers extensions when scanning a Maven GroupId (see [[extension-provider]]). Note that GitHub repos use the reverse convention (`extension-{name}`), but the Maven `artifactId` must be `{name}-extension` to match the UUID mapping in `ExtensionProvider.java`. ### Directory Layout ``` {name}-extension/ ├── pom.xml # Root POM (packaging=pom) ├── build.xml # Ant build — handles .lex packaging ├── maven-install.sh # Local build script ├── source/ │ ├── java/ │ │ ├── pom.xml # Java POM (packaging=jar) │ │ └── src/ │ │ └── org/lucee/extension/{name}/ │ ├── tld/ # Tag Library Definitions (if needed) │ ├── fld/ # Function Library Definitions (if needed) │ └── images/ │ └── logo.png └── tests/ ``` The root POM orchestrates the build. The `source/java/pom.xml` handles actual Java compilation. The `build.xml` assembles everything into the final `.lex` package. ## Manifest Configuration The extension manifest tells Lucee how to load the extension. Two key settings distinguish the Maven approach from legacy OSGi: ``` Manifest-Version: 1.0 version: "1.0.0.0" id: "E0ACA85A-22DB-48FF-B2D6CD89D5D1709F" name: "My Extension" description: "..." start-bundles: false lucee-core-version: "7.0.0.110" ``` ### `start-bundles: false` This tells Lucee **not** to load the extension's JARs as OSGi bundles. Without this, Lucee would attempt to install JARs into the Felix OSGi framework. ### Class Definitions with Maven Coordinates For extensions that register specific handlers (cache, JDBC, resources, etc.), use `maven:` in the manifest JSON: ``` cache: "[{'class':'org.lucee.extension.aws.dynamodb.DynamoDBCache','maven':'org.lucee:dynamodb:1.0.0.0'}]" jdbc: "[{'class':'org.postgresql.Driver','maven':'org.postgresql:postgresql:42.7.1'}]" ``` The `maven:` attribute tells Lucee to use Maven-style classloading (via `getRPCClassLoader()`) to find the class, rather than looking for it in OSGi bundles. ## Maven Based Cache Providers OSGi-based cache extensions have always registered the cache class alongside the bundle that contains it, so Lucee can resolve the class later when a cache connection is created: ``` cache: "[{'class':'org.lucee.extension.myCache.MyCache','bundle-name':'my.cache.bundle','bundle-version':'1.0.0'}]" ``` Maven-based extensions work the same way — the manifest stores the class name and its Maven coordinates at install time: ``` cache: "[{'class':'org.lucee.extension.aws.dynamodb.DynamoDBCache','maven':'org.lucee:dynamodb:1.0.0.0'}]" ``` When a cache connection is later created using that class name — via the admin UI, application config, or programmatically — Lucee looks up the stored coordinates and loads the class via the Maven classloader. Full support for all cache creation paths (including programmatic) landed in **7.1.0.93+**, **7.0.4.21+**, and **6.2.7.7+** ([LDEV-6270](https://luceeserver.atlassian.net/browse/LDEV-6270)). See **[DynamoDB](https://github.com/lucee/extension-dynamodb)** for a complete reference implementation of this pattern. ## Maven Dependency Format (GAVSO) Maven coordinates in Lucee use **colon-separated** Gradle-style notation: ``` groupId:artifactId:version ``` The full format with optional fields: ``` groupId:artifactId:version:scope:optional:checksum ``` Multiple dependencies are comma-separated: ``` maven: org.postgresql:postgresql:42.7.1,com.google.guava:guava:32.1.3-jre ``` > **Warning:** The checksum field is the 6th colon-separated value. A checksum like `sha256:abc123` would produce 7 parts and fail parsing. Use a plain hex digest without an algorithm prefix. ### How Lucee Processes Maven Dependencies 1. `ExtensionMetadata` reads the raw `maven:` string from the manifest 2. `MavenUtil.toGAVSOs()` parses it into GAVSO objects (GroupId, ArtifactId, Version, Scope, Optional) 3. At install time, Lucee resolves the JARs — first from the embedded `/maven/` folder inside the `.lex`, then from Maven Central (or configured repositories) ## Embedded Maven Repository The recommended pattern bundles JARs **inside the `.lex`** in a Maven repository layout. This allows offline installation without requiring internet access at deploy time. ### `.lex` Structure ``` my-extension.lex ├── META-INF/ │ ├── MANIFEST.MF │ └── logo.png ├── tlds/ │ └── my-tags.tldx └── maven/ └── org/ └── lucee/ └── mylib/ └── 1.0.0/ ├── mylib-1.0.0.jar ├── mylib-1.0.0.pom └── [transitive dependencies in repo layout] ``` When installing an extension, Lucee looks for `/maven/` (or `/mvn/`) folders in the `.lex` and copies them to the Lucee maven directory (`{lucee-server}/../mvn/`), preserving the repository layout. The JARs are then loaded via standard Java classloading, **not** through Felix OSGi. ### Building the Embedded Repo The `build.xml` uses Maven's `dependency:copy-dependencies` goal with repository layout: ```xml ``` The key flag is `-Dmdep.useRepositoryLayout=true`, which outputs dependencies in the `groupId/artifactId/version/` structure that Lucee expects. ### Embedded vs External Dependencies | Approach | Pros | Cons | | --- | --- | --- | | Embedded `/maven/` | Offline install, self-contained | Larger `.lex` file | | External `maven:` only | Smaller `.lex`, deduplication | Requires internet at install time | The recommended pattern uses embedded dependencies — the JAR is bundled with the extension AND loaded via Maven classloading. ## TLD and FLD with Maven For extensions that provide custom tags (TLD) or Built-In Functions (FLD), the `maven=` attribute on class elements tells Lucee how to load the implementing class. When migrating from OSGi, replace `bundle-name`/`bundle-version` with `maven="{maven}"` — `build.xml` substitutes the real coordinates at package time. ### TLD Example (Custom Tags) ```xml mail org.lucee.extension.mail.tag.Mail ... ``` ### FLD Example (Built-In Functions) ```xml myFunction org.lucee.extension.mylib.functions.MyFunction ... ``` > **Note:** `mvn=` is also accepted as an alias for `maven=` in TLD/FLD elements. Add `javax-tag-class` with `maven="{maven}"` if dual javax/jakarta tag support is needed. ### Version Requirements for TLD/FLD Maven Support - **7.1**: TLD `maven=` on custom tags added in [`d5dfad16b`](https://github.com/lucee/lucee/commit/d5dfad16b) — available from **7.1.0.2+** - **7.0**: FLD `maven=` for BIF functions added in [`110d788f9`](https://github.com/lucee/lucee/commit/110d788f9) — available from **7.0.2.86+** - **6.2**: Not available — BIF/tag extensions must use OSGi bundles (see [Lucee 6.2 Compatibility](#lucee-62-compatibility) below) The **only** reason a Maven extension needs Lucee 7.1 is when it defines **custom tags** with `maven=` on ``. That feature was added with Lucee 7.1. Manifest-only handlers and FLD-based BIF functions can target Lucee 7.0. ## Build Flow The typical build process for a Maven-based extension: 1. **Root `pom.xml`** invoked with `mvn clean install` 2. **Maven Antrun plugin** executes `build.xml` 3. **`build.xml`** orchestrates: - Runs Maven in `source/java/` to compile the JAR - Copies dependencies to `target/extension/maven/` in repository layout - Generates the extension `MANIFEST.MF` - Packages everything into a `.lex` file (ZIP format) 4. **Maven deploy** publishes both the JAR and `.lex` to Maven Central For build file snippets, CI configuration, and pitfall details, see [`/docs/technical-specs/maven-extension-migration.yaml`](/docs/technical-specs/maven-extension-migration.yaml). ## Version Compatibility | Feature | Lucee 7.1 | Lucee 7.0 | Lucee 6.2 | | --- | --- | --- | --- | | `/maven/` folder extraction from `.lex` | ✅ | 7.0.0.68+ | 6.2.0.300+ | | Manifest `cache:`/`jdbc:` with `maven:` | ✅ | 7.0.0.68+ | 6.2.0.285+ | | Maven cache provider (all creation paths) | 7.1.0.93+ | 7.0.4.21+ | 6.2.7.7+ | | `start-bundles: false` | ✅ | ✅ | ✅ | | GAVSO coordinate parsing | ✅ | ✅ | ✅ | | TLD `maven=` (custom tags) | 7.1.0.2+ | ❌ | ❌ | | FLD `maven=` (BIF functions) | 7.1.0.2+ | 7.0.2.86+ | ❌ | ### Minimum `lucee-core-version` The `lucee-core-version` in `MANIFEST.MF` must reflect what your extension actually uses — not a blanket 7.1 for every Maven extension. | Extension type | Typical minimum | Example | | --- | --- | --- | | Manifest handlers only (resource, cache, JDBC) | **7.0.0.68+** | [S3](https://github.com/lucee/extension-s3) declares `7.0.0.211-BETA` | | BIF functions (FLD with `maven=`) | **7.0.2.86+** | Most function-only extensions | | **Custom tags (TLD with `maven=`)** | **7.1.0.2+** | Rare — [Image](https://github.com/lucee/extension-image) defines `` | ### What This Means in Practice | Extension type | 7.0.2.86+ / 7.1.0.2+ | 7.0.0.68–7.0.2.85 | 6.2.0.285+ | | --- | --- | --- | --- | | Cache handler (manifest `cache:`) | ✅ Maven | ✅ Maven | ✅ Maven | | JDBC driver (manifest `jdbc:`) | ✅ Maven | ✅ Maven | ✅ Maven | | Resource provider (manifest `resource:`) | ✅ Maven | ✅ Maven | ✅ Maven | | BIF functions (FLD) | ✅ Maven | ❌ OSGi only | ❌ OSGi only | | Custom tags (TLD) | ✅ Maven (7.1 only) | ❌ OSGi only | ❌ OSGi only | ## Lucee 6.2 Compatibility For extensions that provide BIF functions or custom tags on Lucee 6.2, the Maven pattern cannot be used for TLD/FLD class resolution. Instead, you need a **shaded OSGi bundle** with all dependencies included. **Why shading is required:** - OSGi bundles have their own classloader - The OSGi classloader cannot see classes in the `/maven/` folder (different classloader) - Third-party dependencies must be shaded into the OSGi bundle JAR The approach: - Use the Maven shade plugin to include dependencies in your extension JAR - Build as an OSGi bundle with `Bundle-SymbolicName`, `Bundle-Version`, etc. - Place the shaded JAR in the `/jars/` folder - Reference the bundle name/version in FLD/TLD files > **TL;DR:** If you need 6.2 support for BIF/tag extensions, stick with shaded OSGi bundles. For 7.0.2.86+ (FLD) or 7.1.0.2+ (TLD), use the Maven pattern. ## Legacy: Bundled JARs Older extensions use `/jars/`, `/jar/`, `/bundles/`, `/bundle/`, `/lib/`, or `/libs/` folders inside the `.lex` — all six are equivalent. Lucee examines each JAR to determine its type: | JAR Type | Destination | Loaded Via | | --- | --- | --- | | OSGi bundle (has `Bundle-SymbolicName`) | `{lucee-server}/bundles/` | Felix OSGi framework | | Plain JAR | `{lucee-server}/lib/` | Standard classloader | The folder name in your extension is purely organisational — Lucee auto-detects based on the JAR's manifest headers. ## Migrating from OSGi This section guides maintainers through converting a legacy OSGi Lucee extension to the Maven-based model. The worked example is the **[Image extension](https://github.com/lucee/extension-image)** (`3.0.x` OSGi → `3.1.x` Maven). ### When to Migrate **Migrate** when the extension targets Lucee 7+, you want standard Maven dependency management, and you are ready to drop OSGi bundle wiring. **Keep an OSGi line** when you must support Lucee 6.2 for BIF/tag extensions, or need a stable bugfix branch for older installs. A common strategy: bump the major/minor version line for the Maven branch (e.g. Image `3.0.x` for 6.2, `3.1.x` for 7+). ### What Changes 1. **Build** — Maven compiles the JAR; Ant assembles the `.lex` 2. **Manifest** — `start-bundles: false`; keep the extension UUID unchanged 3. **FLD/TLD** — replace `bundle-name`/`bundle-version` with `maven="{maven}"`; `build.xml` substitutes coordinates at package time 4. **Dependencies** — move from `libs/` and manual copies into `source/java/pom.xml` 5. **Artifacts** — publish a **full** `.lex` (with embedded `maven/` repo) and optionally a **lite extension** (`.lite.lex`, Maven classifier `lite`) ### Migration Checklist #### 1. Build system - [ ] Add root `pom.xml` and `source/java/pom.xml` - [ ] Wire `build.xml` via `maven-antrun-plugin` - [ ] Ensure `maven-build` depends on `init` (wipes `target/` each build) - [ ] Copy dependencies with Maven repository layout; run `copy-parent-poms.sh` - [ ] Package full `.lex` and lite extension `.lite.lex` #### 2. Extension metadata - [ ] Set `start-bundles: false` - [ ] Set `lucee-core-version` per table above - [ ] Keep extension `id` (UUID) unchanged #### 3. FLD / TLD (if applicable) - [ ] Switch to `maven="{maven}"` on all `` and `` entries - [ ] Add `javax-tag-class` if dual javax/jakarta tag support is needed #### 4. Dependencies - [ ] Move JARs into `pom.xml`; `provided` scope for `org.lucee:lucee` - [ ] Remove `system` scope, manual `build.xml` JAR copies, and legacy `org.lucee` repackages #### 5. Java source - [ ] Remove OSGi bundle manifest from extension JAR - [ ] Replace OSGi-specific version lookups #### 6. CI and testing - [ ] `mvn clean install -Dgoal=install` - [ ] Test with `lucee/script-runner`, `extensionDir: target/` - [ ] Upload `target/*.lex` as artifacts Details for each step: [`maven-extension-migration.yaml`](/docs/technical-specs/maven-extension-migration.yaml). ### Full vs Lite Extension Most Maven extensions publish two install packages: | Package | File | Contents | | --- | --- | --- | | **Full** | `{name}-extension-{version}.lex` | Metadata + embedded `maven/**` JARs | | **Lite extension** | `{name}-extension-{version}.lite.lex` | Metadata only (no `maven/**`) | The **lite extension** is for Lucee Light/Zero and Docker setups where dependencies resolve from Maven Central at install time. The **full** `.lex` is for offline/self-contained installs and is what CI typically tests against. Maven coordinates for the lite extension use classifier `lite`: ``` org.lucee:image-extension:3.1.0.9-BETA:lex # full org.lucee:image-extension:3.1.0.9-BETA:lite:lex # lite extension ``` See [[extension-installation]] for install methods. ### Testing ```bash mvn -B -e -f pom.xml clean install -Dgoal=install ls -lh target/*.lex ``` Run extension tests via script-runner with `extensionDir` pointing at `target/`. Label test components (e.g. `labels="image"`) and pass `testAdditional` for extension-local tests. If tests use `_internalRequest` for `.cfm` fixtures, paths must use `contractPath()` — not raw filesystem paths. See the tech spec `test_patterns.internal_request` section. ### Common Issues | Problem | Quick fix | | --- | --- | | Old JARs still in `.lex` after dependency removal | `maven-build` must depend on `init`; delete `target/` locally | | Lite extension fails at runtime offline | Use full `.lex`, or pre-populate `{lucee-server}/../mvn/` | | Tags fail on 7.0.x | Extension defines TLD tags — requires 7.1.0.2+ | | `MissingIncludeException` in CI tests | Use `contractPath()` web paths in `_internalRequest` | Full pitfall reference: tech spec `pitfalls` section. ## Reference Extensions These extensions demonstrate the Maven pattern and can be used as templates: | Extension | Use as template for | | --- | --- | | [extension-mail](https://github.com/lucee/extension-mail) | Simple Maven migration with TLD | | [extension-s3](https://github.com/lucee/extension-s3) | Manifest-only handler, lite extension, **7.0** baseline | | [extension-image](https://github.com/lucee/extension-image) | FLD + TLD (custom tags), dependency cleanup, **7.1** minimum | | [extension-dynamodb](https://github.com/lucee/extension-dynamodb) | Cache handler, no FLD/TLD | | [extension-ftp](https://github.com/lucee/extension-ftp) | Resource provider | | [extension-debugger](https://github.com/lucee/extension-debugger) | Debugging extension |