Skip to content

CUBRID loadjava — JAR · .class Installer for the JavaSP Classloader Tree

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.

For each registered database, the JVM watches two directory trees:

TreePathLoaderPurpose
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.

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_existing
// loadjava.cpp:53
static 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) so Foo.Bar is rejected as containing uppercase but foo.bar is 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 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.cpp
if (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.

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.

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 — see cubrid-pl-javasp.md §“Catalog rows”) are written by CREATE PROCEDURE / CREATE FUNCTION statements through csql, not by loadjava.

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).

Terminal window
# Install foo.jar into the demodb dynamic tree under no package
loadjava demodb /tmp/foo.jar
# Install Bar.class into demodb dynamic tree under com.example
loadjava --package com.example demodb /tmp/Bar.class
# Force-install Baz.jar without prompt; static (JNI-allowed) tree
loadjava -y --jni demodb /tmp/Baz.jar

Files 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.

SymbolRole
mainEntry; orchestrates the four phases (utility init, parse, check, do_load_java)
parse_argumentgetopt_long on -y -p <pkg> -j -h; populates Force_overwrite, package_path, Path, Dbname, Src_class
check_argumentsResolves db path via cfg_find_db; checks source file exists and has .class or .jar extension
do_load_javaCalls check_overwritecreate_package_directoriescopy_class_file
check_overwriteCross-tree existence check; prompts for non--y; explicit fs::remove to bump directory mtime
create_package_directoriesfs::create_directories with 0744 perms
copy_class_filefs::copy with overwrite_existing
usagePrints message-catalog usage text
JAVA_PACKAGE_PATTERN (regex)Package-name validation pattern
JAVA_DIR / JAVA_STATIC_DIR (defines)Tree names
SymbolPath
mainsrc/executables/loadjava.cpp:325
parse_argumentsrc/executables/loadjava.cpp:76
check_argumentssrc/executables/loadjava.cpp:166
do_load_javasrc/executables/loadjava.cpp:295
check_overwritesrc/executables/loadjava.cpp:229
JAVA_PACKAGE_PATTERNsrc/executables/loadjava.cpp:53

Symbol names are the canonical anchor; line numbers are hints scoped to the updated: date.

  • Standalone binary, not a cubrid verb. loadjava is in util_front.c’s legacy-shim table only by name pattern (it isn’t), so operators invoke it directly. It’s not in ua_Utility_Map either. The build target is its own binary.
  • -j / --jni is misleadingly named. It selects the java_static tree, 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 is CREATE PROCEDURE’s job, not loadjava’s. A common operator mistake is running loadjava and expecting the SP to be callable — the CREATE PROCEDURE Foo (...) AS LANGUAGE JAVA NAME 'Foo.bar' is the second step.
  • fs::remove before fs::copy is intentional. Removing the existing file before the copy is what makes the parent directory’s mtime advance. Operators who shortcut with cp foo.jar $CUBRID_DATABASES/demodb/java/ instead of loadjava will install the file but won’t trigger classloader reload until something else touches the directory.
  • C++17 <filesystem>. loadjava.cpp is one of the few files in src/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. loadjava installs exactly one file per invocation. To install N files, run N times. There’s no --directory or wildcard mode.
  • Removal counterpart. No unloadjava exists. Removing an installed JAR requires rm from the install tree (which the classloader will pick up via mtime). A first-class unloadjava that 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 Foo and a class foo.Bar would have a mismatch the installer doesn’t catch.
  • Permissions. The install is 0744, group/other readable. In environments where cub_pl runs as a different user from the operator running loadjava, group readability is what makes the install accessible. Tighter setups (different group, not group-readable) would need a post-install chgrp.
  • 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 directories loadjava writes into), cubrid-pl-plcsql.md (the other PL family member; doesn’t use loadjava because 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)