CUBRID loadjava — JAR · .class Installer for the JavaSP Classloader Tree
Theoretical Background
Section titled “Theoretical Background”A stored-procedure code installer is the utility that gets
user code from where the developer wrote it (build artefact on
the local filesystem) to where the engine can find it (a
canonical install path the runtime watches). Engines that ship
their own runtimes for stored procedures need one — Postgres has
CREATE FUNCTION ... LANGUAGE plpython3u AS $$ ... $$ (no
external installer; code lives in the catalog), Oracle has
loadjava (binary copy + catalog registration), DB2 has
db2 INSTALL JAR, MongoDB has db.system.js.save().
CUBRID ships a JVM sidecar (cub_pl — see cubrid-pl-javasp.md)
that hosts JavaSP user code. The sidecar’s classloader hierarchy
roots user JARs at a per-database directory; loadjava is the
filesystem-side tool that drops a .class or .jar into that
directory at the right path under the right package.
The deliberate design choice is filesystem-side install + JVM
discovery via mtime. The installer never connects to the
database server; it just copies files. The JVM’s classloader
manager (in pl_engine/pl_server/) checks the directory’s
modification time periodically and reloads when it changes, so a
fresh loadjava is picked up by the next SP invocation without
a JVM restart. The unusual fs::remove immediately before
copying (rather than relying on copy_options::overwrite_existing
alone) is what guarantees the parent directory’s mtime advances
even when the new file’s metadata happens to match the old.
CUBRID’s Approach
Section titled “CUBRID’s Approach”Two install trees per database
Section titled “Two install trees per database”For each registered database, the JVM watches two directory trees:
| Tree | Path | Loader | Purpose |
|---|---|---|---|
| Dynamic | $CUBRID_DATABASES/<db>/java/<package>/<file> | ContextClassLoader (per-context, picks up changes via mtime polling) | Default install location; JARs here can be added/updated without restarting cub_pl |
| Static | $CUBRID_DATABASES/<db>/java_static/<package>/<file> | ServerClassLoader (loaded once at cub_pl start) | Install location for code that should be in the privileged trusted classpath; updates require cub_pl restart |
loadjava defaults to the dynamic tree; --jni (-j) selects
the static tree. The static tree is named java_static and
intended specifically for classes that need to call native
libraries via JNI — those need the security-manager allowance
that comes with the higher classloader.
Install flow
Section titled “Install flow”loadjava [--overwrite] [--package <pkg>] [--jni] <db_name> <src.class|src.jar> │ ├─ parse_argument: │ -y / --overwrite → Force_overwrite = true │ -p <pkg> / --package → validate against /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/ │ then dot → SEPARATOR (e.g. "org.example.foo" → "org/example/foo") │ -j / --jni → Path = "java_static" (else "java") │ ├─ check_arguments: │ cfg_find_db (Dbname) → resolves $CUBRID_DATABASES/<db>; sets Root │ fs::exists (Src_class) → source file must exist │ extension check → must be .class or .jar │ └─ do_load_java: class_file_name = src.filename () check_overwrite (package_path, class_file_name) ├─ if dest exists in either java/ or java_static/: │ if !Force_overwrite: prompt "(y/n)"; bail if not 'y' │ fs::remove (existing) — updates parent mtime └─ create_package_directories (Root / Path / package_path) fs::create_directories with 0744 perms copy_class_file (Src_class, dest_path) fs::copy with overwrite_existingPackage-name regex
Section titled “Package-name regex”// loadjava.cpp:53static const std::string JAVA_PACKAGE_PATTERN = "^([a-z_]{1}[a-z0-9_]*(\\.[a-z_]{1}[a-z0-9_]*)*)$";The regex enforces the conventional Java package shape:
- Each segment starts with a lowercase letter or
_. - Subsequent characters can be lowercase, digits, or
_. - Segments separated by
.. - Match is case-insensitive (
icase) soFoo.Baris rejected as containing uppercase butfoo.baris accepted; the regex doesn’t change behaviour by case but the validation is deliberately strict on the lowercase convention.
A package-name violation prints "invalid java package name"
and bails. Empty package (no --package) is valid — installs
go to the unpackaged root (<dbpath>/java/<file>).
The mtime-bumping fs::remove
Section titled “The mtime-bumping fs::remove”The conventional fs::copy with overwrite_existing would be
sufficient if the JVM watched file mtimes. But the
classloader-manager (in pl_engine/pl_server/) polls the
directory mtime, not per-file mtimes — directory mtime is
what reliably advances when files are added or removed. So
loadjava explicitly deletes the destination first:
// check_overwrite — loadjava.cppif (exists_static && fs::is_directory (static_path) == false) { fs::remove (static_path);}if (exists_dynamic && fs::is_directory (dynamic_path) == false) { fs::remove (dynamic_path);}Then the subsequent fs::copy lands a fresh file in the
now-empty slot. Two file-system operations (delete + create)
guarantee the directory mtime advances even when the bytes are
unchanged.
The CBRD-24695 comment beside the deletes pins this to a specific bug fix — before it, a JAR replaced with an identical-size, identical-mtime file wouldn’t trigger classloader reload.
Cross-tree overwrite check
Section titled “Cross-tree overwrite check”check_overwrite checks both the static and dynamic trees,
not just the install target. This means installing to the
dynamic tree will detect (and prompt about, or remove) a
duplicate in the static tree:
fs::path static_path = Root / STATIC_PATH / package_path / class_file_name;fs::path dynamic_path = Root / DYNAMIC_PATH / package_path / class_file_name;bool exists_static = fs::exists (static_path);bool exists_dynamic = fs::exists (dynamic_path);
if (exists_static || exists_dynamic) { if (!Force_overwrite) prompt; bail-on-no; if (exists_static) fs::remove (static_path); if (exists_dynamic) fs::remove (dynamic_path);}The cross-tree removal prevents a stale classloader from
shadowing the new install — if the same Foo.class exists in
both, the static loader (loaded first at JVM start) wins, so
installing a fresh dynamic copy without removing the static one
would have no effect.
No database connection
Section titled “No database connection”loadjava is filesystem-side only. It does not
db_restart, doesn’t speak to cub_master or cub_pl, and
needs no auth credentials. The install is just a file copy
under the database’s directory.
This is by design:
- The install needs to work even when the database is offline.
- The classloader-manager in the JVM picks up changes via mtime polling; no notification protocol needed.
- The JavaSP catalog rows that reference the installed JAR
(
_db_stored_procedure_code— seecubrid-pl-javasp.md§“Catalog rows”) are written byCREATE PROCEDURE/CREATE FUNCTIONstatements through csql, not byloadjava.
The split (filesystem install via loadjava, catalog
registration via DDL) is what allows operators to stage a JAR
on disk before the corresponding CREATE PROCEDURE is issued
(or to update a JAR for an already-registered procedure
without re-issuing the DDL).
Operator example
Section titled “Operator example”# Install foo.jar into the demodb dynamic tree under no packageloadjava demodb /tmp/foo.jar
# Install Bar.class into demodb dynamic tree under com.exampleloadjava --package com.example demodb /tmp/Bar.class
# Force-install Baz.jar without prompt; static (JNI-allowed) treeloadjava -y --jni demodb /tmp/Baz.jarFiles land at:
$CUBRID_DATABASES/demodb/java/foo.jar$CUBRID_DATABASES/demodb/java/com/example/Bar.class$CUBRID_DATABASES/demodb/java_static/Baz.jar
The next time a stored procedure references a class in any of these JARs, the classloader-manager notices the directory’s mtime change and reloads.
Source Walkthrough
Section titled “Source Walkthrough”| Symbol | Role |
|---|---|
main | Entry; orchestrates the four phases (utility init, parse, check, do_load_java) |
parse_argument | getopt_long on -y -p <pkg> -j -h; populates Force_overwrite, package_path, Path, Dbname, Src_class |
check_arguments | Resolves db path via cfg_find_db; checks source file exists and has .class or .jar extension |
do_load_java | Calls check_overwrite → create_package_directories → copy_class_file |
check_overwrite | Cross-tree existence check; prompts for non--y; explicit fs::remove to bump directory mtime |
create_package_directories | fs::create_directories with 0744 perms |
copy_class_file | fs::copy with overwrite_existing |
usage | Prints message-catalog usage text |
JAVA_PACKAGE_PATTERN (regex) | Package-name validation pattern |
JAVA_DIR / JAVA_STATIC_DIR (defines) | Tree names |
Position hints (as of 2026-05-05)
Section titled “Position hints (as of 2026-05-05)”| Symbol | Path |
|---|---|
main | src/executables/loadjava.cpp:325 |
parse_argument | src/executables/loadjava.cpp:76 |
check_arguments | src/executables/loadjava.cpp:166 |
do_load_java | src/executables/loadjava.cpp:295 |
check_overwrite | src/executables/loadjava.cpp:229 |
JAVA_PACKAGE_PATTERN | src/executables/loadjava.cpp:53 |
Symbol names are the canonical anchor; line numbers are hints
scoped to the updated: date.
Cross-check Notes
Section titled “Cross-check Notes”- Standalone binary, not a
cubridverb.loadjavais inutil_front.c’s legacy-shim table only by name pattern (it isn’t), so operators invoke it directly. It’s not inua_Utility_Mapeither. The build target is its own binary. -j/--jniis misleadingly named. It selects thejava_statictree, which is also called the “JNI tree” because that’s where classes allowed to load native libraries live. The flag doesn’t do anything JNI-specific itself; it just changes the install path.- No catalog write. Registration of the SP in
_db_stored_procedure*rows isCREATE PROCEDURE’s job, notloadjava’s. A common operator mistake is runningloadjavaand expecting the SP to be callable — theCREATE PROCEDURE Foo (...) AS LANGUAGE JAVA NAME 'Foo.bar'is the second step. fs::removebeforefs::copyis intentional. Removing the existing file before the copy is what makes the parent directory’s mtime advance. Operators who shortcut withcp foo.jar $CUBRID_DATABASES/demodb/java/instead ofloadjavawill install the file but won’t trigger classloader reload until something else touches the directory.- C++17
<filesystem>.loadjava.cppis one of the few files insrc/executables/that uses C++17 stdlib. Build systems that target older toolchains may need feature flags; the rest of the executables family is largely C99 / C++11. - No multi-file install.
loadjavainstalls exactly one file per invocation. To install N files, run N times. There’s no--directoryor wildcard mode.
Open Questions
Section titled “Open Questions”- Removal counterpart. No
unloadjavaexists. Removing an installed JAR requiresrmfrom the install tree (which the classloader will pick up via mtime). A first-classunloadjavathat cross-checks against catalog references would be safer. - Package-name case sensitivity. The regex is
case-insensitive, but Java itself is case-sensitive about
package names. A user who accidentally names a directory
Fooand a classfoo.Barwould have a mismatch the installer doesn’t catch. - Permissions. The install is
0744, group/other readable. In environments wherecub_plruns as a different user from the operator runningloadjava, group readability is what makes the install accessible. Tighter setups (different group, not group-readable) would need a post-installchgrp.
Sources
Section titled “Sources”src/executables/loadjava.cpp— the entire utility (single file, 356 lines)src/executables/AGENTS.md— agent guide- Adjacent docs:
cubrid-pl-javasp.md(JavaSP runtime; the classloader-manager that watches the directoriesloadjavawrites into),cubrid-pl-plcsql.md(the other PL family member; doesn’t useloadjavabecause PL/CSQL stores its compiled bytecode in the catalog rather than on disk),cubrid-pl-server-bridge.md(the callback channel both runtimes use),cubrid-cub-admin.md(the unified admin CLI; loadjava is not in it)