rJavaEnv in R PackagesrJavaEnv helps package authors find, install and select Java. Choose your integration according to whether Java runs as an executable, inside the R process through {rJava}, or in a separate R process.
There are two different boundaries: loading the {rJava} namespace can load and lock the JVM library path; .jinit() then starts the JVM. A namespace can be loaded without library(rJava), for example by requireNamespace("rJava"), namespace imports, or IDE completion. Configure Java before any of these.
Upstream rJava issue #334 describes path locking before initialization. Its experimental feature/dynamic branch explores deferred loading; these examples do not assume that branch is installed. Issue #25 explains why restarting a JVM inside the same R process is not a reliable recovery strategy.
These checks do not load rJava or start its JVM in the calling R process:
java_check_version_cmd() checks the executable under JAVA_HOME and returns its major version. It does not use an unrelated java earlier on PATH.java_find_system() discovers system installations.java_resolve(21, install = FALSE) finds a JDK without changing the calling session’s environment. Its default install = TRUE permits downloading.java_check_version_rjava(java_home = path) tests rJava in a fresh R process. It returns the major version, or FALSE if rJava cannot be used there.java_check_compatibility(21, action = "none") reads an already running rJava JVM. It returns FALSE when no JVM is running; that means compatibility is unverified, not that it has checked and rejected a candidate installation. action = "warn", "message" or "stop" reports an unverified or incompatible JVM.java_ensure() and use_java() are setup operations. They can install Java and change the session environment. install = FALSE prevents downloads; it does not make java_ensure() read-only. Avoid setup, downloads and prompts in .onLoad.
A package that calls system2() or processx::run() can scope its environment. Add rJavaEnv to Imports and use local_java_env() inside the wrapper:
run_my_tool <- function(jar_path, input_file) {
rJavaEnv::local_java_env(version = 21, type = "min")
system2("java", c("-jar", shQuote(jar_path), shQuote(input_file)))
}JAVA_HOME and PATH are restored when the function returns, including on error. For a block, use with_java_env():
rJavaEnv::with_java_env(21, {
system2("java", "-version")
}, install = FALSE)These wrappers configure executables. Do not load rJava or an rJava-dependent package inside the scope: its JVM can remain locked after the environment is restored. A command-line wrapper can coexist with an already running rJava JVM; changing its executable does not change that JVM.
Set .use_cache = TRUE for repeated checks when installations are stable. By default suitable system Java is accepted. accept_system_java = FALSE restricts resolution to managed installations matching the selected distribution, backend, platform and architecture, or installs one when install = TRUE.
If Java is an optional feature, list rJava in Suggests and rJavaEnv in Imports. Do not import rJava symbols in NAMESPACE. This lets your package load even before Java is configured, and makes an exported setup helper usable.
setup_java <- function(version = 21, install = TRUE) {
ready <- rJavaEnv::java_ensure(version, type = "min", install = install)
if (!ready) stop("Java setup failed; configure Java before using this feature.")
invisible(TRUE)
}
run_java_feature <- function(input) {
# This must precede requireNamespace(), not just .jinit().
setup_java()
if (!requireNamespace("rJava", quietly = TRUE)) {
stop("Install the rJava package to use this feature.")
}
rJava::.jinit() # Deliberate initialization, after setup.
rJavaEnv::java_check_compatibility(21, type = "min", action = "stop")
# Your rJava operations go here.
}If rJava is already loaded but its JVM is not running, java_ensure() errors without starting it: the library path may already be locked and cannot safely be inferred from JAVA_HOME. If its JVM is already running, a matching version is accepted and an incompatible version causes an error without changing the environment. Restart R before setup in either incompatible or unverified cases.
Experimental rJava builds with deferred JVM loading are an exception: java_ensure() permits setup after namespace loading when their state API explicitly reports that no JVM library has loaded and no JVM is initialized. It also avoids preloading a JVM on Linux in that state. This supports a package that imports rJava and defers Java operations until after setup. The exception does not apply if a JVM library was already loaded, if another package has started Java, or if the state cannot be verified. These experimental builds are not required for the released-rJava patterns shown here.
check_against = "cmd" configures command-line Java even when rJava is loaded. It does not establish compatibility for rJava or preload a JVM library.
If your package has Imports: rJava and imports its symbols, or has Depends: rJava, rJava may load before your own .onLoad. Your package’s setup helper cannot rescue a failed dependency load. Document setup through rJavaEnv directly, independently of your package:
# Start a fresh R session first if rJava was already loaded.
rJavaEnv::java_ensure(21, type = "min")
library(your_package)Do not restart between setup and loading the package: java_ensure() sets only the current session. To configure subsequent project sessions explicitly, use java_env_set(where = "project", java_home = path) or java_quick_install() with the user’s consent. Choose project persistence explicitly; package startup must not edit the user’s profile automatically.
A compatibility guard can stop an operation after your package deliberately initializes rJava. It does not repair a missing JVM, replace a setup step, or change the rJava version:
run_java_feature <- function(input) {
rJava::.jinit()
rJavaEnv::java_check_compatibility(21, type = "min", action = "stop")
# Your rJava operations go here.
}For conflicting Java requirements, run rJava code in a fresh process:
run_heavy_calc <- function(input_data) {
rJavaEnv::with_rjava_env(
version = 21,
func = function(df) {
rJava::.jinit()
version <- rJava::.jcall("java.lang.System", "S", "getProperty", "java.version")
list(version = version, rows = nrow(df))
},
args = list(df = input_data)
)
}Add callr to Imports if this is a required feature, or Suggests with a clear availability check for an optional feature. rJava must be installed in the library paths visible to the child. Return ordinary R data, not Java object references or other pointers into the child process. The function must load its own dependencies and receive its inputs through args.
A package that imports rJava can still use subprocess isolation once it has loaded successfully, but it retains the initial dependency-load requirement. An optional rJava dependency avoids that requirement in the parent.
use_java(21) selects a managed Java 21 installation. java_ensure(21, type = "min") can reuse a suitable system JDK of Java 21 or newer. In java_resolve() and the functions built on it (java_ensure(), local_java_env(), with_java_env() and with_rjava_env()), numeric patch requests such as "21.0.8" compare full runtime versions rather than treating every Java 21 runtime as equivalent. use_java() and java_quick_install() do not check the installed runtime. Exact SDKMAN identifiers select a managed installation by identity; type = "min" is unsupported for identifiers. The identity of an already running rJava JVM cannot be established from its runtime version alone, so identifier requests require a fresh setup.
Prefer setting Java at the workflow level, before installing or loading rJava:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'A standalone Rscript step calling java_ensure() changes only that process; later workflow steps will not inherit its environment. If using rJavaEnv for CI setup, call it inside each R process before loading rJava, or explicitly export the resolved paths through GitHub Actions’ environment and path files.
The repository contains two minimal consumer packages under tools/downstream: a lazy consumer and an eager rJava importer. Run their installed-package tests from the repository root with two real JDKs:
RJE_JAVA_HOME_A=/path/to/jdk17 RJE_JAVA_HOME_B=/path/to/jdk21 \
Rscript --vanilla tools/test-downstream.RThe runner installs this checkout and the consumers into a temporary library. Each scenario uses a fresh R process. It verifies probe-before-switch behavior, scoped cleanup on success and error, eager-import non-initialization, strict handling of a locked JVM, and child-process isolation. It does not download JDKs or modify the user’s package library or cache. The dedicated GitHub Actions workflow supplies Java 17 and 21 on Linux, macOS and Windows.