You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by ti...@apache.org on 2026/06/18 07:10:41 UTC

(opendal) branch main updated: chore(bindings/java): migrate to jni 0.22 (#7777)

This is an automated email from the ASF dual-hosted git repository.

tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 5fe1c549e chore(bindings/java): migrate to jni 0.22 (#7777)
5fe1c549e is described below

commit 5fe1c549e6d58cf7b4197315b28273a80aead1d5
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Thu Jun 18 15:10:36 2026 +0800

    chore(bindings/java): migrate to jni 0.22 (#7777)
    
    Co-authored-by: tison <wa...@gmail.com>
---
 bindings/java/Cargo.toml                           |   2 +-
 bindings/java/src/async_operator.rs                | 524 +++++++++------------
 bindings/java/src/convert.rs                       |  98 ++--
 bindings/java/src/error.rs                         |  72 ++-
 bindings/java/src/executor.rs                      | 128 ++---
 bindings/java/src/layer.rs                         |  34 +-
 bindings/java/src/lib.rs                           | 135 +++---
 .../src/main/java/org/apache/opendal/Metadata.java |   4 +-
 bindings/java/src/operator.rs                      | 264 ++++++-----
 bindings/java/src/operator_input_stream.rs         |  62 +--
 bindings/java/src/operator_output_stream.rs        |  56 ++-
 bindings/java/src/utility.rs                       |  33 +-
 12 files changed, 715 insertions(+), 697 deletions(-)

diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml
index deeda9cb0..e88f5a348 100644
--- a/bindings/java/Cargo.toml
+++ b/bindings/java/Cargo.toml
@@ -31,7 +31,7 @@ crate-type = ["cdylib", "staticlib"]
 doc = false
 
 [dependencies]
-jni = { version = "0.21.1" }
+jni = { version = "0.22.4" }
 # opendal-java won't be published to crates.io, we always use the local version
 opendal = { version = ">=0", path = "../../core", default-features = false, features = [
   "blocking",
diff --git a/bindings/java/src/async_operator.rs b/bindings/java/src/async_operator.rs
index 8e3852439..5d9417277 100644
--- a/bindings/java/src/async_operator.rs
+++ b/bindings/java/src/async_operator.rs
@@ -17,15 +17,17 @@
 
 use std::time::Duration;
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::EnvUnowned;
+use jni::JavaVM;
+use jni::jni_sig;
+use jni::jni_str;
 use jni::objects::JByteArray;
 use jni::objects::JClass;
 use jni::objects::JObject;
 use jni::objects::JString;
 use jni::objects::JValue;
-use jni::objects::JValueOwned;
 use jni::sys::jlong;
-use jni::sys::jobject;
 use jni::sys::jsize;
 use opendal::Entry;
 use opendal::Operator;
@@ -36,28 +38,26 @@ use crate::convert::{
     bytes_to_jbytearray, jmap_to_hashmap, jstring_to_string, offset_length_to_range,
     read_int64_field,
 };
+use crate::error::ThrowException;
 use crate::executor::Executor;
 use crate::executor::executor_or_default;
-use crate::executor::get_current_env;
 use crate::make_metadata;
 use crate::make_operator_info;
 use crate::make_presigned_request;
 use crate::{make_entry, make_list_options, make_stat_options, make_write_options};
 
 #[unsafe(no_mangle)]
-pub extern "system" fn Java_org_apache_opendal_AsyncOperator_constructor(
-    mut env: JNIEnv,
-    _: JClass,
-    scheme: JString,
-    map: JObject,
+pub extern "system" fn Java_org_apache_opendal_AsyncOperator_constructor<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
+    scheme: JString<'local>,
+    map: JObject<'local>,
 ) -> jlong {
-    intern_constructor(&mut env, scheme, map).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_constructor(env, scheme, map))
+        .resolve::<ThrowException>()
 }
 
-fn intern_constructor(env: &mut JNIEnv, scheme: JString, map: JObject) -> Result<jlong> {
+fn intern_constructor(env: &mut Env, scheme: JString, map: JObject) -> Result<jlong> {
     let scheme = jstring_to_string(env, &scheme)?;
     let map = jmap_to_hashmap(env, &map)?;
     let op = Operator::via_iter(scheme, map)?;
@@ -68,9 +68,9 @@ fn intern_constructor(env: &mut JNIEnv, scheme: JString, map: JObject) -> Result
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_duplicate(
-    _: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_duplicate<'local>(
+    _: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
 ) -> jlong {
     let op = unsafe { &mut *op };
@@ -81,9 +81,9 @@ pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_duplicate(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_disposeInternal(
-    _: JNIEnv,
-    _: JObject,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_disposeInternal<'local>(
+    _: EnvUnowned<'local>,
+    _: JObject<'local>,
     op: *mut Operator,
 ) {
     unsafe {
@@ -95,23 +95,21 @@ pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_disposeInter
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_write(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_write<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
-    content: JByteArray,
-    write_options: JObject,
+    path: JString<'local>,
+    content: JByteArray<'local>,
+    write_options: JObject<'local>,
 ) -> jlong {
-    intern_write(&mut env, op, executor, path, content, write_options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_write(env, op, executor, path, content, write_options))
+        .resolve::<ThrowException>()
 }
 
 fn intern_write(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -125,13 +123,12 @@ fn intern_write(
     let path = jstring_to_string(env, &path)?;
     let content = env.convert_byte_array(content)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op
-            .write_options(&path, content, write_opts)
-            .await
-            .map(|_| JValueOwned::Void)
-            .map_err(Into::into);
-        complete_future(id, result)
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.write_options(&path, content, write_opts).await;
+        complete_future(id, move |_env| {
+            result?;
+            Ok(JObject::null())
+        });
     });
 
     Ok(id)
@@ -141,22 +138,20 @@ fn intern_write(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_stat(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_stat<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
-    stat_options: JObject,
+    path: JString<'local>,
+    stat_options: JObject<'local>,
 ) -> jlong {
-    intern_stat(&mut env, op, executor, path, stat_options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_stat(env, op, executor, path, stat_options))
+        .resolve::<ThrowException>()
 }
 
 fn intern_stat(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -168,11 +163,9 @@ fn intern_stat(
     let path = jstring_to_string(env, &path)?;
     let stat_opts = make_stat_options(env, &options)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let metadata = op.stat_options(&path, stat_opts).await.map_err(Into::into);
-        let mut env = unsafe { get_current_env() };
-        let result = metadata.and_then(|metadata| make_metadata(&mut env, metadata));
-        complete_future(id, result.map(JValueOwned::Object))
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.stat_options(&path, stat_opts).await;
+        complete_future(id, move |env| make_metadata(env, result?));
     });
 
     Ok(id)
@@ -182,22 +175,20 @@ fn intern_stat(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_read(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_read<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
-    read_options: JObject,
+    path: JString<'local>,
+    read_options: JObject<'local>,
 ) -> jlong {
-    intern_read(&mut env, op, executor, path, read_options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_read(env, op, executor, path, read_options))
+        .resolve::<ThrowException>()
 }
 
 fn intern_read(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -213,14 +204,14 @@ fn intern_read(
     // Clone operator handle to move into the task
     let op_cloned = unsafe { &*op }.clone();
 
-    executor_or_default(env, executor)?.spawn(async move {
+    executor_or_default(executor)?.spawn(async move {
         let mut read_op = op_cloned.read_with(&path_str);
         read_op = read_op.range(range);
-        let result = read_op.await.map_err(Into::into);
-
-        let mut env = unsafe { get_current_env() };
-        let result = result.and_then(|bs| bytes_to_jbytearray(&mut env, bs.to_bytes()));
-        complete_future(id, result.map(|bs| JValueOwned::Object(bs.into())))
+        let result = read_op.await;
+        complete_future(id, move |env| {
+            let buffer = result?;
+            Ok(bytes_to_jbytearray(env, buffer.to_bytes())?.into())
+        });
     });
 
     Ok(id)
@@ -230,21 +221,19 @@ fn intern_read(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_delete(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_delete<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
+    path: JString<'local>,
 ) -> jlong {
-    intern_delete(&mut env, op, executor, path).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_delete(env, op, executor, path))
+        .resolve::<ThrowException>()
 }
 
 fn intern_delete(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -254,13 +243,12 @@ fn intern_delete(
 
     let path = jstring_to_string(env, &path)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op
-            .delete(&path)
-            .await
-            .map(|_| JValueOwned::Void)
-            .map_err(Into::into);
-        complete_future(id, result)
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.delete(&path).await;
+        complete_future(id, move |_env| {
+            result?;
+            Ok(JObject::null())
+        });
     });
 
     Ok(id)
@@ -270,26 +258,24 @@ fn intern_delete(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_makeBlockingOp(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_makeBlockingOp<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
 ) -> jlong {
-    intern_make_blocking_op(&mut env, op, executor).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_make_blocking_op(env, op, executor))
+        .resolve::<ThrowException>()
 }
 
 fn intern_make_blocking_op(
-    env: &mut JNIEnv,
+    _env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
 ) -> Result<jlong> {
     let op = unsafe { &mut *op };
-    let op = executor_or_default(env, executor)?
-        .enter_with(move || blocking::Operator::new(op.clone()))?;
+    let op =
+        executor_or_default(executor)?.enter_with(move || blocking::Operator::new(op.clone()))?;
     Ok(Box::into_raw(Box::new(op)) as jlong)
 }
 
@@ -297,41 +283,40 @@ fn intern_make_blocking_op(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_makeOperatorInfo(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_makeOperatorInfo<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
-) -> jobject {
-    intern_make_operator_info(&mut env, op).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        JObject::default().into_raw()
-    })
+) -> JObject<'local> {
+    env.with_env(|env| intern_make_operator_info(env, op))
+        .resolve::<ThrowException>()
 }
 
-fn intern_make_operator_info(env: &mut JNIEnv, op: *mut Operator) -> Result<jobject> {
+fn intern_make_operator_info<'local>(
+    env: &mut Env<'local>,
+    op: *mut Operator,
+) -> Result<JObject<'local>> {
     let op = unsafe { &mut *op };
-    Ok(make_operator_info(env, op.info())?.into_raw())
+    make_operator_info(env, op.info())
 }
 
 /// # Safety
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_createDir(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_createDir<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
+    path: JString<'local>,
 ) -> jlong {
-    intern_create_dir(&mut env, op, executor, path).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_create_dir(env, op, executor, path))
+        .resolve::<ThrowException>()
 }
 
 fn intern_create_dir(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -341,13 +326,12 @@ fn intern_create_dir(
 
     let path = jstring_to_string(env, &path)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op
-            .create_dir(&path)
-            .await
-            .map(|_| JValueOwned::Void)
-            .map_err(Into::into);
-        complete_future(id, result)
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.create_dir(&path).await;
+        complete_future(id, move |_env| {
+            result?;
+            Ok(JObject::null())
+        });
     });
 
     Ok(id)
@@ -357,22 +341,20 @@ fn intern_create_dir(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_copy(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_copy<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    source_path: JString,
-    target_path: JString,
+    source_path: JString<'local>,
+    target_path: JString<'local>,
 ) -> jlong {
-    intern_copy(&mut env, op, executor, source_path, target_path).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_copy(env, op, executor, source_path, target_path))
+        .resolve::<ThrowException>()
 }
 
 fn intern_copy(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     source_path: JString,
@@ -384,13 +366,12 @@ fn intern_copy(
     let source_path = jstring_to_string(env, &source_path)?;
     let target_path = jstring_to_string(env, &target_path)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op
-            .copy(&source_path, &target_path)
-            .await
-            .map(|_| JValueOwned::Void)
-            .map_err(Into::into);
-        complete_future(id, result)
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.copy(&source_path, &target_path).await;
+        complete_future(id, move |_env| {
+            result?;
+            Ok(JObject::null())
+        });
     });
 
     Ok(id)
@@ -400,22 +381,20 @@ fn intern_copy(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_rename(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_rename<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    source_path: JString,
-    target_path: JString,
+    source_path: JString<'local>,
+    target_path: JString<'local>,
 ) -> jlong {
-    intern_rename(&mut env, op, executor, source_path, target_path).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_rename(env, op, executor, source_path, target_path))
+        .resolve::<ThrowException>()
 }
 
 fn intern_rename(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     source_path: JString,
@@ -427,13 +406,12 @@ fn intern_rename(
     let source_path = jstring_to_string(env, &source_path)?;
     let target_path = jstring_to_string(env, &target_path)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op
-            .rename(&source_path, &target_path)
-            .await
-            .map(|_| JValueOwned::Void)
-            .map_err(Into::into);
-        complete_future(id, result)
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.rename(&source_path, &target_path).await;
+        complete_future(id, move |_env| {
+            result?;
+            Ok(JObject::null())
+        });
     });
 
     Ok(id)
@@ -443,21 +421,19 @@ fn intern_rename(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_removeAll(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_removeAll<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
+    path: JString<'local>,
 ) -> jlong {
-    intern_remove_all(&mut env, op, executor, path).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_remove_all(env, op, executor, path))
+        .resolve::<ThrowException>()
 }
 
 fn intern_remove_all(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -467,14 +443,12 @@ fn intern_remove_all(
 
     let path = jstring_to_string(env, &path)?;
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op
-            .delete_with(&path)
-            .recursive(true)
-            .await
-            .map(|_| JValueOwned::Void)
-            .map_err(Into::into);
-        complete_future(id, result)
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.delete_with(&path).recursive(true).await;
+        complete_future(id, move |_env| {
+            result?;
+            Ok(JObject::null())
+        });
     });
 
     Ok(id)
@@ -484,22 +458,20 @@ fn intern_remove_all(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_list(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_list<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
-    options: JObject,
+    path: JString<'local>,
+    options: JObject<'local>,
 ) -> jlong {
-    intern_list(&mut env, op, executor, path, options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_list(env, op, executor, path, options))
+        .resolve::<ThrowException>()
 }
 
 fn intern_list(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -510,28 +482,24 @@ fn intern_list(
 
     let path = jstring_to_string(env, &path)?;
     let list_opts = make_list_options(env, &options)?;
-    executor_or_default(env, executor)?.spawn(async move {
-        let entries = op.list_options(&path, list_opts).await.map_err(Into::into);
-        let result = make_entries(entries);
-        complete_future(id, result.map(JValueOwned::Object))
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.list_options(&path, list_opts).await;
+        complete_future(id, move |env| make_entries(env, result?));
     });
 
     Ok(id)
 }
 
-fn make_entries<'local>(entries: Result<Vec<Entry>>) -> Result<JObject<'local>> {
-    let entries = entries?;
-
-    let mut env = unsafe { get_current_env() };
+fn make_entries<'local>(env: &mut Env<'local>, entries: Vec<Entry>) -> Result<JObject<'local>> {
     let jarray = env.new_object_array(
         entries.len() as jsize,
-        "org/apache/opendal/Entry",
+        jni_str!("org/apache/opendal/Entry"),
         JObject::null(),
     )?;
 
     for (idx, entry) in entries.into_iter().enumerate() {
-        let entry = make_entry(&mut env, entry)?;
-        env.set_object_array_element(&jarray, idx as jsize, entry)?;
+        let entry = make_entry(env, entry)?;
+        jarray.set_element(env, idx, &entry)?;
     }
 
     Ok(jarray.into())
@@ -541,22 +509,20 @@ fn make_entries<'local>(entries: Result<Vec<Entry>>) -> Result<JObject<'local>>
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_presignRead(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_presignRead<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
+    path: JString<'local>,
     expire: jlong,
 ) -> jlong {
-    intern_presign_read(&mut env, op, executor, path, expire).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_presign_read(env, op, executor, path, expire))
+        .resolve::<ThrowException>()
 }
 
 fn intern_presign_read(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -568,11 +534,9 @@ fn intern_presign_read(
     let path = jstring_to_string(env, &path)?;
     let expire = Duration::from_nanos(expire as u64);
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op.presign_read(&path, expire).await.map_err(Into::into);
-        let mut env = unsafe { get_current_env() };
-        let result = result.and_then(|req| make_presigned_request(&mut env, req));
-        complete_future(id, result.map(JValueOwned::Object))
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.presign_read(&path, expire).await;
+        complete_future(id, move |env| make_presigned_request(env, result?));
     });
 
     Ok(id)
@@ -581,22 +545,20 @@ fn intern_presign_read(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_presignWrite(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_presignWrite<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
+    path: JString<'local>,
     expire: jlong,
 ) -> jlong {
-    intern_presign_write(&mut env, op, executor, path, expire).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_presign_write(env, op, executor, path, expire))
+        .resolve::<ThrowException>()
 }
 
 fn intern_presign_write(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -608,11 +570,9 @@ fn intern_presign_write(
     let path = jstring_to_string(env, &path)?;
     let expire = Duration::from_nanos(expire as u64);
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op.presign_write(&path, expire).await.map_err(Into::into);
-        let mut env = unsafe { get_current_env() };
-        let result = result.and_then(|req| make_presigned_request(&mut env, req));
-        complete_future(id, result.map(JValueOwned::Object))
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.presign_write(&path, expire).await;
+        complete_future(id, move |env| make_presigned_request(env, result?));
     });
 
     Ok(id)
@@ -622,22 +582,20 @@ fn intern_presign_write(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_presignStat(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_presignStat<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     executor: *const Executor,
-    path: JString,
+    path: JString<'local>,
     expire: jlong,
 ) -> jlong {
-    intern_presign_stat(&mut env, op, executor, path, expire).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_presign_stat(env, op, executor, path, expire))
+        .resolve::<ThrowException>()
 }
 
 fn intern_presign_stat(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     executor: *const Executor,
     path: JString,
@@ -649,82 +607,70 @@ fn intern_presign_stat(
     let path = jstring_to_string(env, &path)?;
     let expire = Duration::from_nanos(expire as u64);
 
-    executor_or_default(env, executor)?.spawn(async move {
-        let result = op.presign_stat(&path, expire).await.map_err(Into::into);
-        let mut env = unsafe { get_current_env() };
-        let result = result.and_then(|req| make_presigned_request(&mut env, req));
-        complete_future(id, result.map(JValueOwned::Object))
+    executor_or_default(executor)?.spawn(async move {
+        let result = op.presign_stat(&path, expire).await;
+        complete_future(id, move |env| make_presigned_request(env, result?));
     });
 
     Ok(id)
 }
 
-fn make_object<'local>(
-    env: &mut JNIEnv<'local>,
-    value: JValueOwned<'local>,
-) -> Result<JObject<'local>> {
-    let o = match value {
-        JValueOwned::Object(o) => o,
-        JValueOwned::Byte(_) => env.new_object("java/lang/Long", "(B)V", &[value.borrow()])?,
-        JValueOwned::Char(_) => env.new_object("java/lang/Char", "(C)V", &[value.borrow()])?,
-        JValueOwned::Short(_) => env.new_object("java/lang/Short", "(S)V", &[value.borrow()])?,
-        JValueOwned::Int(_) => env.new_object("java/lang/Integer", "(I)V", &[value.borrow()])?,
-        JValueOwned::Long(_) => env.new_object("java/lang/Long", "(J)V", &[value.borrow()])?,
-        JValueOwned::Bool(_) => env.new_object("java/lang/Boolean", "(Z)V", &[value.borrow()])?,
-        JValueOwned::Float(_) => env.new_object("java/lang/Float", "(F)V", &[value.borrow()])?,
-        JValueOwned::Double(_) => env.new_object("java/lang/Double", "(D)V", &[value.borrow()])?,
-        JValueOwned::Void => JObject::null(),
-    };
-    Ok(o)
-}
-
-fn complete_future(id: jlong, result: Result<JValueOwned>) {
-    try_complete_future(id, result).expect("complete future must succeed");
-}
-
-fn try_complete_future(id: jlong, result: Result<JValueOwned>) -> Result<()> {
-    let mut env = unsafe { get_current_env() };
-    let future = get_future(&mut env, id)?;
-    match result {
-        Ok(result) => {
-            let result = make_object(&mut env, result)?;
-            env.call_method(
-                future,
-                "complete",
-                "(Ljava/lang/Object;)Z",
-                &[JValue::Object(&result)],
-            )?
-        }
-        Err(err) => {
-            let exception = err.to_exception(&mut env)?;
-            env.call_method(
-                future,
-                "completeExceptionally",
-                "(Ljava/lang/Throwable;)Z",
-                &[JValue::Object(&exception)],
-            )?
+/// Complete the Java `CompletableFuture` identified by `id`.
+///
+/// This runs on a `tokio` worker thread that was permanently attached to the
+/// JVM in `on_thread_start`, so `attach_current_thread` only pushes a scoped
+/// JNI frame here. The `build` closure turns the awaited operation result into a
+/// Java object within that frame; on error the future is completed
+/// exceptionally. All local references created during completion live and die
+/// within the frame.
+fn complete_future<F>(id: jlong, build: F)
+where
+    F: for<'a> FnOnce(&mut Env<'a>) -> Result<JObject<'a>>,
+{
+    let vm = JavaVM::singleton().expect("JavaVM singleton must be initialized");
+    vm.attach_current_thread(|env| -> Result<()> {
+        let future = get_future(env, id)?;
+        match build(env) {
+            Ok(object) => {
+                env.call_method(
+                    &future,
+                    jni_str!("complete"),
+                    jni_sig!("(Ljava/lang/Object;)Z"),
+                    &[JValue::Object(&object)],
+                )?;
+            }
+            Err(err) => {
+                let exception = err.to_exception(env)?;
+                env.call_method(
+                    &future,
+                    jni_str!("completeExceptionally"),
+                    jni_sig!("(Ljava/lang/Throwable;)Z"),
+                    &[JValue::Object(&exception)],
+                )?;
+            }
         }
-    };
-    Ok(())
+        Ok(())
+    })
+    .expect("complete future must succeed");
 }
 
-fn request_id(env: &mut JNIEnv) -> Result<jlong> {
+fn request_id(env: &mut Env) -> Result<jlong> {
     Ok(env
         .call_static_method(
-            "org/apache/opendal/AsyncOperator$AsyncRegistry",
-            "requestId",
-            "()J",
+            jni_str!("org/apache/opendal/AsyncOperator$AsyncRegistry"),
+            jni_str!("requestId"),
+            jni_sig!("()J"),
             &[],
         )?
         .j()?)
 }
 
-fn get_future<'local>(env: &mut JNIEnv<'local>, id: jlong) -> Result<JObject<'local>> {
+fn get_future<'local>(env: &mut Env<'local>, id: jlong) -> Result<JObject<'local>> {
     Ok(env
         .call_static_method(
-            "org/apache/opendal/AsyncOperator$AsyncRegistry",
-            "get",
-            "(J)Ljava/util/concurrent/CompletableFuture;",
+            jni_str!("org/apache/opendal/AsyncOperator$AsyncRegistry"),
+            jni_str!("get"),
+            jni_sig!("(J)Ljava/util/concurrent/CompletableFuture;"),
             &[JValue::Long(id)],
         )?
         .l()?)
diff --git a/bindings/java/src/convert.rs b/bindings/java/src/convert.rs
index fc5bdc73a..f9182eba9 100644
--- a/bindings/java/src/convert.rs
+++ b/bindings/java/src/convert.rs
@@ -16,10 +16,14 @@
 // under the License.
 
 use crate::Result;
-use jni::JNIEnv;
+use jni::Env;
+use jni::jni_sig;
+use jni::jni_str;
+use jni::objects::JByteArray;
+use jni::objects::JMap;
 use jni::objects::JObject;
 use jni::objects::JString;
-use jni::objects::{JByteArray, JMap};
+use jni::strings::JNIString;
 use jni::sys::jlong;
 use opendal::{Error, ErrorKind};
 use std::collections::HashMap;
@@ -30,29 +34,29 @@ pub(crate) fn usize_to_jlong(n: Option<usize>) -> jlong {
     n.map_or(-1, |v| v as jlong)
 }
 
-pub(crate) fn jmap_to_hashmap(
-    env: &mut JNIEnv,
-    params: &JObject,
-) -> Result<HashMap<String, String>> {
-    let map = JMap::from_env(env, params)?;
+pub(crate) fn jmap_to_hashmap(env: &mut Env, params: &JObject) -> Result<HashMap<String, String>> {
+    let map = env.new_cast_local_ref::<JMap>(params)?;
     let mut iter = map.iter(env)?;
 
     let mut result: HashMap<String, String> = HashMap::new();
-    while let Some(e) = iter.next(env)? {
-        let k = JString::from(e.0);
-        let v = JString::from(e.1);
-        result.insert(env.get_string(&k)?.into(), env.get_string(&v)?.into());
+    while let Some(entry) = iter.next(env)? {
+        let k = entry.key(env)?;
+        let v = entry.value(env)?;
+        // SAFETY: a `java.util.Map<String, String>` only yields String keys and values.
+        let k = unsafe { JString::from_raw(env, k.into_raw()) };
+        let v = unsafe { JString::from_raw(env, v.into_raw()) };
+        result.insert(k.mutf8_chars(env)?.into(), v.mutf8_chars(env)?.into());
     }
 
     Ok(result)
 }
 
 pub(crate) fn hashmap_to_jmap<'a>(
-    env: &mut JNIEnv<'a>,
+    env: &mut Env<'a>,
     map: &HashMap<String, String>,
 ) -> Result<JObject<'a>> {
-    let map_object = env.new_object("java/util/HashMap", "()V", &[])?;
-    let jmap = env.get_map(&map_object)?;
+    let map_object = env.new_object(jni_str!("java/util/HashMap"), jni_sig!("()V"), &[])?;
+    let jmap = env.new_cast_local_ref::<JMap>(&map_object)?;
     for (k, v) in map {
         let key = env.new_string(k)?;
         let value = env.new_string(v)?;
@@ -61,44 +65,53 @@ pub(crate) fn hashmap_to_jmap<'a>(
     Ok(map_object)
 }
 
-pub(crate) fn string_to_jstring<'a>(env: &mut JNIEnv<'a>, s: Option<&str>) -> Result<JObject<'a>> {
-    s.map_or_else(
-        || Ok(JObject::null()),
-        |v| Ok(env.new_string(v.to_string())?.into()),
-    )
+pub(crate) fn string_to_jstring<'a>(env: &mut Env<'a>, s: Option<&str>) -> Result<JObject<'a>> {
+    s.map_or_else(|| Ok(JObject::null()), |v| Ok(env.new_string(v)?.into()))
 }
 
-pub(crate) fn read_bool_field(env: &mut JNIEnv<'_>, obj: &JObject, field: &str) -> Result<bool> {
-    Ok(env.get_field(obj, field, "Z")?.z()?)
+pub(crate) fn read_bool_field(env: &mut Env<'_>, obj: &JObject, field: &str) -> Result<bool> {
+    Ok(env
+        .get_field(obj, JNIString::new(field), jni_sig!("Z"))?
+        .z()?)
 }
 
-pub(crate) fn read_int64_field(env: &mut JNIEnv<'_>, obj: &JObject, field: &str) -> Result<i64> {
-    Ok(env.get_field(obj, field, "J")?.j()?)
+pub(crate) fn read_int64_field(env: &mut Env<'_>, obj: &JObject, field: &str) -> Result<i64> {
+    Ok(env
+        .get_field(obj, JNIString::new(field), jni_sig!("J"))?
+        .j()?)
 }
 
-pub(crate) fn read_int_field(env: &mut JNIEnv<'_>, obj: &JObject, field: &str) -> Result<i32> {
-    Ok(env.get_field(obj, field, "I")?.i()?)
+pub(crate) fn read_int_field(env: &mut Env<'_>, obj: &JObject, field: &str) -> Result<i32> {
+    Ok(env
+        .get_field(obj, JNIString::new(field), jni_sig!("I"))?
+        .i()?)
 }
 
 pub(crate) fn read_string_field(
-    env: &mut JNIEnv<'_>,
+    env: &mut Env<'_>,
     obj: &JObject,
     field: &str,
 ) -> Result<Option<String>> {
-    let result = env.get_field(obj, field, "Ljava/lang/String;")?.l()?;
+    let result = env
+        .get_field(obj, JNIString::new(field), jni_sig!("Ljava/lang/String;"))?
+        .l()?;
     if result.is_null() {
         Ok(None)
     } else {
-        Ok(Some(jstring_to_string(env, &JString::from(result))?))
+        // SAFETY: the field is declared as `java.lang.String`.
+        let result = unsafe { JString::from_raw(env, result.into_raw()) };
+        Ok(Some(jstring_to_string(env, &result)?))
     }
 }
 
 pub(crate) fn read_map_field(
-    env: &mut JNIEnv<'_>,
+    env: &mut Env<'_>,
     obj: &JObject,
     field: &str,
 ) -> Result<Option<HashMap<String, String>>> {
-    let result = env.get_field(obj, field, "Ljava/util/Map;")?.l()?;
+    let result = env
+        .get_field(obj, JNIString::new(field), jni_sig!("Ljava/util/Map;"))?
+        .l()?;
     if result.is_null() {
         Ok(None)
     } else {
@@ -107,7 +120,7 @@ pub(crate) fn read_map_field(
 }
 
 pub(crate) fn read_jlong_field_to_usize(
-    env: &mut JNIEnv,
+    env: &mut Env,
     options: &JObject,
     field_name: &str,
 ) -> Result<Option<usize>> {
@@ -123,19 +136,23 @@ pub(crate) fn read_jlong_field_to_usize(
 }
 
 pub(crate) fn read_instant_field_to_timestamp(
-    env: &mut JNIEnv<'_>,
+    env: &mut Env<'_>,
     obj: &JObject,
     field: &str,
 ) -> Result<Option<opendal::raw::Timestamp>> {
-    let result = env.get_field(obj, field, "Ljava/time/Instant;")?.l()?;
+    let result = env
+        .get_field(obj, JNIString::new(field), jni_sig!("Ljava/time/Instant;"))?
+        .l()?;
     if result.is_null() {
         return Ok(None);
     }
 
     let epoch_second = env
-        .call_method(&result, "getEpochSecond", "()J", &[])?
+        .call_method(&result, jni_str!("getEpochSecond"), jni_sig!("()J"), &[])?
         .j()?;
-    let nano = env.call_method(&result, "getNano", "()I", &[])?.i()?;
+    let nano = env
+        .call_method(&result, jni_str!("getNano"), jni_sig!("()I"), &[])?
+        .i()?;
     match opendal::raw::Timestamp::new(epoch_second, nano) {
         Ok(ts) => Ok(Some(ts)),
         Err(err) => Err(Error::new(
@@ -170,7 +187,7 @@ pub(crate) fn offset_length_to_range(offset: i64, length: i64) -> Result<(Bound<
 }
 
 pub(crate) fn bytes_to_jbytearray<'a>(
-    env: &mut JNIEnv<'a>,
+    env: &mut Env<'a>,
     bytes: impl AsRef<[u8]>,
 ) -> Result<JByteArray<'a>> {
     let bytes = bytes.as_ref();
@@ -178,11 +195,6 @@ pub(crate) fn bytes_to_jbytearray<'a>(
     Ok(res)
 }
 
-/// # Safety
-///
-/// The caller must guarantee that the Object passed in is an instance
-/// of `java.lang.String`, passing in anything else will lead to undefined behavior.
-pub(crate) fn jstring_to_string(env: &mut JNIEnv, s: &JString) -> Result<String> {
-    let res = unsafe { env.get_string_unchecked(s)? };
-    Ok(res.into())
+pub(crate) fn jstring_to_string(env: &mut Env, s: &JString) -> Result<String> {
+    Ok(s.mutf8_chars(env)?.into())
 }
diff --git a/bindings/java/src/error.rs b/bindings/java/src/error.rs
index 99f06fbab..a59185c36 100644
--- a/bindings/java/src/error.rs
+++ b/bindings/java/src/error.rs
@@ -19,9 +19,13 @@ use std::fmt::Debug;
 use std::fmt::Display;
 use std::fmt::Formatter;
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::errors::ErrorPolicy;
+use jni::jni_sig;
+use jni::jni_str;
 use jni::objects::JThrowable;
 use jni::objects::JValue;
+use jni::strings::JNIString;
 use opendal::ErrorKind;
 
 pub(crate) struct Error {
@@ -29,22 +33,21 @@ pub(crate) struct Error {
 }
 
 impl Error {
-    pub(crate) fn throw(&self, env: &mut JNIEnv) {
+    pub(crate) fn throw(&self, env: &mut Env) {
         if let Err(err) = self.do_throw(env) {
             match err {
                 jni::errors::Error::JavaException => {
-                    // other calls throws exception; safely ignored
+                    // The exception has been thrown; it will propagate to the JVM.
                 }
-                _ => env.fatal_error(err.to_string()),
+                _ => env.fatal_error(JNIString::new(err.to_string()).as_ref()),
             }
         }
     }
 
     pub(crate) fn to_exception<'local>(
         &self,
-        env: &mut JNIEnv<'local>,
+        env: &mut Env<'local>,
     ) -> jni::errors::Result<JThrowable<'local>> {
-        let class = env.find_class("org/apache/opendal/OpenDALException")?;
         let code = env.new_string(match self.inner.kind() {
             ErrorKind::Unexpected => "Unexpected",
             ErrorKind::Unsupported => "Unsupported",
@@ -62,19 +65,68 @@ impl Error {
         })?;
         let message = env.new_string(format!("{:?}", self.inner))?;
         let exception = env.new_object(
-            class,
-            "(Ljava/lang/String;Ljava/lang/String;)V",
+            jni_str!("org/apache/opendal/OpenDALException"),
+            jni_sig!("(Ljava/lang/String;Ljava/lang/String;)V"),
             &[JValue::Object(&code), JValue::Object(&message)],
         )?;
-        Ok(JThrowable::from(exception))
+        // SAFETY: `exception` was just constructed as an `OpenDALException`, a
+        // subclass of `java.lang.Throwable`.
+        Ok(unsafe { JThrowable::from_raw(env, exception.into_raw()) })
     }
 
-    fn do_throw(&self, env: &mut JNIEnv) -> jni::errors::Result<()> {
+    fn do_throw(&self, env: &mut Env) -> jni::errors::Result<()> {
         let exception = self.to_exception(env)?;
         env.throw(exception)
     }
 }
 
+/// A native-method [`ErrorPolicy`] that converts a Rust [`Error`] into an
+/// `OpenDALException` and a panic into a `RuntimeException`, returning the
+/// default value to the JVM in both cases.
+pub(crate) enum ThrowException {}
+
+impl<T: Default> ErrorPolicy<T, Error> for ThrowException {
+    type Captures<'unowned_env_local: 'native_method, 'native_method> = ();
+
+    fn on_error<'unowned_env_local: 'native_method, 'native_method>(
+        env: &mut Env<'unowned_env_local>,
+        _cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
+        err: Error,
+    ) -> jni::errors::Result<T> {
+        if !env.exception_check() {
+            err.throw(env);
+        }
+        Ok(T::default())
+    }
+
+    fn on_panic<'unowned_env_local: 'native_method, 'native_method>(
+        env: &mut Env<'unowned_env_local>,
+        _cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
+        payload: Box<dyn std::any::Any + Send + 'static>,
+    ) -> jni::errors::Result<T> {
+        if !env.exception_check() {
+            fn downcast_payload(payload: Box<dyn std::any::Any + Send + 'static>) -> String {
+                let payload = match payload.downcast::<&'static str>() {
+                    Ok(payload) => return payload.to_string(),
+                    Err(payload) => payload,
+                };
+
+                let payload = match payload.downcast::<String>() {
+                    Ok(payload) => return *payload,
+                    Err(payload) => payload,
+                };
+
+                let _ = payload;
+                "native method panicked".to_string()
+            }
+
+            let payload = downcast_payload(payload);
+            env.fatal_error(JNIString::new(payload).as_ref());
+        }
+        Ok(T::default())
+    }
+}
+
 impl From<opendal::Error> for Error {
     fn from(err: opendal::Error) -> Self {
         Self { inner: err }
diff --git a/bindings/java/src/executor.rs b/bindings/java/src/executor.rs
index e6e305609..3f15688e7 100644
--- a/bindings/java/src/executor.rs
+++ b/bindings/java/src/executor.rs
@@ -15,16 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use std::cell::RefCell;
 use std::ffi::c_void;
 use std::future::Future;
 use std::num::NonZeroUsize;
+use std::sync::OnceLock;
 use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::{Arc, OnceLock};
 use std::thread::available_parallelism;
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::EnvUnowned;
 use jni::JavaVM;
+use jni::jni_sig;
+use jni::jni_str;
 use jni::objects::JClass;
 use jni::objects::JObject;
 use jni::objects::JValue;
@@ -32,14 +34,15 @@ use jni::sys::{jint, jlong};
 use tokio::task::JoinHandle;
 
 use crate::Result;
+use crate::error::ThrowException;
 
 static mut RUNTIME: OnceLock<Executor> = OnceLock::new();
-thread_local! {
-    static ENV: RefCell<Option<*mut jni::sys::JNIEnv>> = const { RefCell::new(None) };
-}
 
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint {
+pub unsafe extern "system" fn JNI_OnLoad(vm: *mut jni::sys::JavaVM, _: *mut c_void) -> jint {
+    // Register the JavaVM singleton so worker threads can attach to the JVM
+    // later via `JavaVM::singleton()`.
+    let _ = unsafe { JavaVM::from_raw(vm) };
     opendal::init_default_registry();
     jni::sys::JNI_VERSION_1_8
 }
@@ -49,22 +52,12 @@ pub unsafe extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint {
 /// This function could be only called by java vm when unload this lib.
 #[allow(static_mut_refs)]
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn JNI_OnUnload(_: JavaVM, _: *mut c_void) {
+pub unsafe extern "system" fn JNI_OnUnload(_: *mut jni::sys::JavaVM, _: *mut c_void) {
     unsafe {
         RUNTIME.take();
     }
 }
 
-/// # Safety
-///
-/// This function could be only called when the lib is loaded and within an executor thread.
-pub(crate) unsafe fn get_current_env<'local>() -> JNIEnv<'local> {
-    let env = ENV
-        .with(|cell| *cell.borrow_mut())
-        .expect("env must be available");
-    unsafe { JNIEnv::from_raw(env) }.expect("env must be valid")
-}
-
 pub enum Executor {
     Tokio(tokio::runtime::Runtime),
 }
@@ -94,26 +87,25 @@ impl Executor {
 }
 
 #[unsafe(no_mangle)]
-pub extern "system" fn Java_org_apache_opendal_AsyncExecutor_makeTokioExecutor(
-    mut env: JNIEnv,
-    _: JClass,
+pub extern "system" fn Java_org_apache_opendal_AsyncExecutor_makeTokioExecutor<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     cores: usize,
 ) -> jlong {
-    make_tokio_executor(&mut env, cores)
-        .map(|executor| Box::into_raw(Box::new(executor)) as jlong)
-        .unwrap_or_else(|e| {
-            e.throw(&mut env);
-            0
-        })
+    env.with_env(|_env| -> Result<jlong> {
+        let executor = make_tokio_executor(cores)?;
+        Ok(Box::into_raw(Box::new(executor)) as jlong)
+    })
+    .resolve::<ThrowException>()
 }
 
 /// # Safety
 ///
 /// This function should not be called before the AsyncExecutor is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_AsyncExecutor_disposeInternal(
-    _: JNIEnv,
-    _: JObject,
+pub unsafe extern "system" fn Java_org_apache_opendal_AsyncExecutor_disposeInternal<'local>(
+    _: EnvUnowned<'local>,
+    _: JObject<'local>,
     executor: *mut Executor,
 ) {
     unsafe {
@@ -121,8 +113,7 @@ pub unsafe extern "system" fn Java_org_apache_opendal_AsyncExecutor_disposeInter
     }
 }
 
-pub(crate) fn make_tokio_executor(env: &mut JNIEnv, cores: usize) -> Result<Executor> {
-    let vm = Arc::new(env.get_java_vm().expect("JavaVM must be available"));
+pub(crate) fn make_tokio_executor(cores: usize) -> Result<Executor> {
     let counter = AtomicUsize::new(0);
     let executor = tokio::runtime::Builder::new_multi_thread()
         .worker_threads(cores)
@@ -130,37 +121,23 @@ pub(crate) fn make_tokio_executor(env: &mut JNIEnv, cores: usize) -> Result<Exec
             let id = counter.fetch_add(1, Ordering::SeqCst);
             format!("opendal-tokio-worker-{id}")
         })
-        .on_thread_start({
-            let vm = vm.clone();
-            move || {
-                ENV.with(|cell| {
-                    let mut env = vm
-                        .attach_current_thread_as_daemon()
-                        .expect("attach thread must succeed");
-
-                    set_current_thread_name(&mut env)
-                        .expect("current thread name has been set above");
-
-                    *cell.borrow_mut() = Some(env.get_raw());
-                })
-            }
+        .on_thread_start(|| {
+            // `attach_current_thread` creates a permanent attachment; the thread
+            // is detached automatically when it exits.
+            let vm = JavaVM::singleton().expect("JavaVM singleton must be initialized");
+            vm.attach_current_thread(set_current_thread_name)
+                .expect("attach current thread must succeed");
         })
-        .on_thread_stop(move || {
-            // Typically, the thread attached to the JVM will be detached automatically when the thread exits
-            // and the corresponding thread-local AttachGuard is dropped.
-            //
-            // However, there are some edge cases on Windows that may lead to deadlocks. To mitigate this,
-            // we explicitly detach the thread here.
+        .on_thread_stop(|| {
+            // Typically, the thread attached to the JVM will be detached automatically
+            // when the thread exits. However, there are some edge cases on Windows that
+            // may lead to deadlocks. To mitigate this, we explicitly detach the thread here.
             //
-            // See https://github.com/apache/opendal/issues/6869 and https://github.com/jni-rs/jni-rs/issues/701
-            // for more details.
-
-            ENV.with(|cell| {
-                *cell.borrow_mut() = None;
-            });
-
-            // SAFETY: JNIEnv is unset above and we do not use AttachGuard anywhere.
-            unsafe { vm.detach_current_thread() };
+            // See https://github.com/apache/opendal/issues/6869 and
+            // https://github.com/jni-rs/jni-rs/issues/701 for more details.
+            if let Ok(vm) = JavaVM::singleton() {
+                let _ = vm.detach_current_thread();
+            }
         })
         .enable_all()
         .build()
@@ -174,12 +151,12 @@ pub(crate) fn make_tokio_executor(env: &mut JNIEnv, cores: usize) -> Result<Exec
     Ok(Executor::Tokio(executor))
 }
 
-fn set_current_thread_name(env: &mut JNIEnv) -> Result<()> {
+fn set_current_thread_name(env: &mut Env) -> Result<()> {
     let current_thread = env
         .call_static_method(
-            "java/lang/Thread",
-            "currentThread",
-            "()Ljava/lang/Thread;",
+            jni_str!("java/lang/Thread"),
+            jni_str!("currentThread"),
+            jni_sig!("()Ljava/lang/Thread;"),
             &[],
         )?
         .l()?;
@@ -188,9 +165,9 @@ fn set_current_thread_name(env: &mut JNIEnv) -> Result<()> {
         None => unreachable!("thread name must be set"),
     };
     env.call_method(
-        current_thread,
-        "setName",
-        "(Ljava/lang/String;)V",
+        &current_thread,
+        jni_str!("setName"),
+        jni_sig!("(Ljava/lang/String;)V"),
         &[JValue::Object(&thread_name)],
     )?;
     Ok(())
@@ -200,13 +177,10 @@ fn set_current_thread_name(env: &mut JNIEnv) -> Result<()> {
 ///
 /// Crash if the executor is disposed.
 #[inline]
-pub(crate) fn executor_or_default<'a>(
-    env: &mut JNIEnv<'a>,
-    executor: *const Executor,
-) -> Result<&'a Executor> {
+pub(crate) fn executor_or_default(executor: *const Executor) -> Result<&'static Executor> {
     unsafe {
         if executor.is_null() {
-            default_executor(env)
+            default_executor()
         } else {
             // SAFETY: executor must be valid
             Ok(&*executor)
@@ -218,17 +192,15 @@ pub(crate) fn executor_or_default<'a>(
 ///
 /// This function could be only when the lib is loaded.
 #[allow(static_mut_refs)]
-unsafe fn default_executor<'a>(env: &mut JNIEnv<'a>) -> Result<&'a Executor> {
+unsafe fn default_executor() -> Result<&'static Executor> {
     // Return the executor if it's already initialized
     if let Some(runtime) = unsafe { RUNTIME.get() } {
         return Ok(runtime);
     }
 
     // Try to initialize the executor
-    let executor = make_tokio_executor(
-        env,
-        available_parallelism().map(NonZeroUsize::get).unwrap_or(1),
-    )?;
+    let executor =
+        make_tokio_executor(available_parallelism().map(NonZeroUsize::get).unwrap_or(1))?;
 
     Ok(unsafe { RUNTIME.get_or_init(|| executor) })
 }
diff --git a/bindings/java/src/layer.rs b/bindings/java/src/layer.rs
index 5f41567c8..b41a15c18 100644
--- a/bindings/java/src/layer.rs
+++ b/bindings/java/src/layer.rs
@@ -19,7 +19,9 @@ use std::time::Duration;
 
 use crate::Result;
 use crate::convert::jstring_to_string;
-use jni::JNIEnv;
+use crate::error::ThrowException;
+use jni::Env;
+use jni::EnvUnowned;
 use jni::objects::JClass;
 use jni::objects::JString;
 use jni::sys::jboolean;
@@ -31,9 +33,9 @@ use opendal::layers::ConcurrentLimitLayer;
 use opendal::layers::RetryLayer;
 
 #[unsafe(no_mangle)]
-pub extern "system" fn Java_org_apache_opendal_layer_RetryLayer_doLayer(
-    _: JNIEnv,
-    _: JClass,
+pub extern "system" fn Java_org_apache_opendal_layer_RetryLayer_doLayer<'local>(
+    _: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     jitter: jboolean,
     factor: jfloat,
@@ -46,7 +48,7 @@ pub extern "system" fn Java_org_apache_opendal_layer_RetryLayer_doLayer(
     retry = retry.with_factor(factor);
     retry = retry.with_min_delay(Duration::from_nanos(min_delay as u64));
     retry = retry.with_max_delay(Duration::from_nanos(max_delay as u64));
-    if jitter != 0 {
+    if jitter {
         retry = retry.with_jitter()
     }
     if max_times >= 0 {
@@ -56,20 +58,18 @@ pub extern "system" fn Java_org_apache_opendal_layer_RetryLayer_doLayer(
 }
 
 #[unsafe(no_mangle)]
-pub extern "system" fn Java_org_apache_opendal_layer_CapabilityOverrideLayer_doLayer(
-    mut env: JNIEnv,
-    _: JClass,
+pub extern "system" fn Java_org_apache_opendal_layer_CapabilityOverrideLayer_doLayer<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
-    overrides: JString,
+    overrides: JString<'local>,
 ) -> jlong {
-    intern_capability_override_layer(&mut env, op, overrides).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
-    })
+    env.with_env(|env| intern_capability_override_layer(env, op, overrides))
+        .resolve::<ThrowException>()
 }
 
 fn intern_capability_override_layer(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: *mut Operator,
     overrides: JString,
 ) -> Result<jlong> {
@@ -80,9 +80,9 @@ fn intern_capability_override_layer(
 }
 
 #[unsafe(no_mangle)]
-pub extern "system" fn Java_org_apache_opendal_layer_ConcurrentLimitLayer_doLayer(
-    _: JNIEnv,
-    _: JClass,
+pub extern "system" fn Java_org_apache_opendal_layer_ConcurrentLimitLayer_doLayer<'local>(
+    _: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut Operator,
     permits: jlong,
 ) -> jlong {
diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs
index c7e806ae1..7b6952ca9 100644
--- a/bindings/java/src/lib.rs
+++ b/bindings/java/src/lib.rs
@@ -17,10 +17,11 @@
 
 use std::collections::HashMap;
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::jni_sig;
+use jni::jni_str;
 use jni::objects::JObject;
 use jni::objects::JValue;
-use jni::sys::jboolean;
 use jni::sys::jint;
 use jni::sys::jlong;
 use opendal::Entry;
@@ -42,7 +43,7 @@ mod utility;
 
 pub(crate) type Result<T> = std::result::Result<T, error::Error>;
 
-fn make_presigned_request<'a>(env: &mut JNIEnv<'a>, req: PresignedRequest) -> Result<JObject<'a>> {
+fn make_presigned_request<'a>(env: &mut Env<'a>, req: PresignedRequest) -> Result<JObject<'a>> {
     let method = env.new_string(req.method().as_str())?;
     let uri = env.new_string(req.uri().to_string())?;
     let headers = {
@@ -58,8 +59,8 @@ fn make_presigned_request<'a>(env: &mut JNIEnv<'a>, req: PresignedRequest) -> Re
     };
     let headers = convert::hashmap_to_jmap(env, &headers)?;
     let result = env.new_object(
-        "org/apache/opendal/PresignedRequest",
-        "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V",
+        jni_str!("org/apache/opendal/PresignedRequest"),
+        jni_sig!("(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V"),
         &[
             JValue::Object(&method),
             JValue::Object(&uri),
@@ -69,15 +70,17 @@ fn make_presigned_request<'a>(env: &mut JNIEnv<'a>, req: PresignedRequest) -> Re
     Ok(result)
 }
 
-fn make_operator_info<'a>(env: &mut JNIEnv<'a>, info: OperatorInfo) -> Result<JObject<'a>> {
-    let scheme = env.new_string(info.scheme().to_string())?;
-    let root = env.new_string(info.root().to_string())?;
-    let name = env.new_string(info.name().to_string())?;
+fn make_operator_info<'a>(env: &mut Env<'a>, info: OperatorInfo) -> Result<JObject<'a>> {
+    let scheme = env.new_string(info.scheme())?;
+    let root = env.new_string(info.root())?;
+    let name = env.new_string(info.name())?;
     let capability_obj = make_capability(env, info.capability())?;
 
     let result = env.new_object(
-        "org/apache/opendal/OperatorInfo",
-        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/apache/opendal/Capability;)V",
+        jni_str!("org/apache/opendal/OperatorInfo"),
+        jni_sig!(
+            "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/apache/opendal/Capability;)V"
+        ),
         &[
             JValue::Object(&scheme),
             JValue::Object(&root),
@@ -88,56 +91,56 @@ fn make_operator_info<'a>(env: &mut JNIEnv<'a>, info: OperatorInfo) -> Result<JO
     Ok(result)
 }
 
-fn make_capability<'a>(env: &mut JNIEnv<'a>, cap: Capability) -> Result<JObject<'a>> {
+fn make_capability<'a>(env: &mut Env<'a>, cap: Capability) -> Result<JObject<'a>> {
     let capability = env.new_object(
-        "org/apache/opendal/Capability",
-        "(ZZZZZZZZZZZZZZZZZZZZZZJJZZZZZZZZZZZZZZZ)V",
+        jni_str!("org/apache/opendal/Capability"),
+        jni_sig!("(ZZZZZZZZZZZZZZZZZZZZZZJJZZZZZZZZZZZZZZZ)V"),
         &[
-            JValue::Bool(cap.stat as jboolean),
-            JValue::Bool(cap.stat_with_if_match as jboolean),
-            JValue::Bool(cap.stat_with_if_none_match as jboolean),
-            JValue::Bool(cap.stat_with_if_modified_since as jboolean),
-            JValue::Bool(cap.stat_with_if_unmodified_since as jboolean),
-            JValue::Bool(cap.stat_with_version as jboolean),
-            JValue::Bool(cap.read as jboolean),
-            JValue::Bool(cap.read_with_if_match as jboolean),
-            JValue::Bool(cap.read_with_if_none_match as jboolean),
-            JValue::Bool(cap.read_with_override_cache_control as jboolean),
-            JValue::Bool(cap.read_with_override_content_disposition as jboolean),
-            JValue::Bool(cap.read_with_override_content_type as jboolean),
-            JValue::Bool(cap.write as jboolean),
-            JValue::Bool(cap.write_can_multi as jboolean),
-            JValue::Bool(cap.write_can_append as jboolean),
-            JValue::Bool(cap.write_with_content_type as jboolean),
-            JValue::Bool(cap.write_with_content_disposition as jboolean),
-            JValue::Bool(cap.write_with_cache_control as jboolean),
-            JValue::Bool(cap.write_with_if_match as jboolean),
-            JValue::Bool(cap.write_with_if_none_match as jboolean),
-            JValue::Bool(cap.write_with_if_not_exists as jboolean),
-            JValue::Bool(cap.write_with_user_metadata as jboolean),
+            JValue::Bool(cap.stat),
+            JValue::Bool(cap.stat_with_if_match),
+            JValue::Bool(cap.stat_with_if_none_match),
+            JValue::Bool(cap.stat_with_if_modified_since),
+            JValue::Bool(cap.stat_with_if_unmodified_since),
+            JValue::Bool(cap.stat_with_version),
+            JValue::Bool(cap.read),
+            JValue::Bool(cap.read_with_if_match),
+            JValue::Bool(cap.read_with_if_none_match),
+            JValue::Bool(cap.read_with_override_cache_control),
+            JValue::Bool(cap.read_with_override_content_disposition),
+            JValue::Bool(cap.read_with_override_content_type),
+            JValue::Bool(cap.write),
+            JValue::Bool(cap.write_can_multi),
+            JValue::Bool(cap.write_can_append),
+            JValue::Bool(cap.write_with_content_type),
+            JValue::Bool(cap.write_with_content_disposition),
+            JValue::Bool(cap.write_with_cache_control),
+            JValue::Bool(cap.write_with_if_match),
+            JValue::Bool(cap.write_with_if_none_match),
+            JValue::Bool(cap.write_with_if_not_exists),
+            JValue::Bool(cap.write_with_user_metadata),
             JValue::Long(convert::usize_to_jlong(cap.write_multi_max_size)),
             JValue::Long(convert::usize_to_jlong(cap.write_multi_min_size)),
-            JValue::Bool(cap.create_dir as jboolean),
-            JValue::Bool(cap.delete as jboolean),
-            JValue::Bool(cap.copy as jboolean),
-            JValue::Bool(cap.rename as jboolean),
-            JValue::Bool(cap.list as jboolean),
-            JValue::Bool(cap.list_with_limit as jboolean),
-            JValue::Bool(cap.list_with_start_after as jboolean),
-            JValue::Bool(cap.list_with_recursive as jboolean),
-            JValue::Bool(cap.list_with_versions as jboolean),
-            JValue::Bool(cap.list_with_deleted as jboolean),
-            JValue::Bool(cap.presign as jboolean),
-            JValue::Bool(cap.presign_read as jboolean),
-            JValue::Bool(cap.presign_stat as jboolean),
-            JValue::Bool(cap.presign_write as jboolean),
-            JValue::Bool(cap.shared as jboolean),
+            JValue::Bool(cap.create_dir),
+            JValue::Bool(cap.delete),
+            JValue::Bool(cap.copy),
+            JValue::Bool(cap.rename),
+            JValue::Bool(cap.list),
+            JValue::Bool(cap.list_with_limit),
+            JValue::Bool(cap.list_with_start_after),
+            JValue::Bool(cap.list_with_recursive),
+            JValue::Bool(cap.list_with_versions),
+            JValue::Bool(cap.list_with_deleted),
+            JValue::Bool(cap.presign),
+            JValue::Bool(cap.presign_read),
+            JValue::Bool(cap.presign_stat),
+            JValue::Bool(cap.presign_write),
+            JValue::Bool(cap.shared),
         ],
     )?;
     Ok(capability)
 }
 
-fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result<JObject<'a>> {
+fn make_metadata<'a>(env: &mut Env<'a>, metadata: Metadata) -> Result<JObject<'a>> {
     let mode = match metadata.mode() {
         EntryMode::FILE => 0,
         EntryMode::DIR => 1,
@@ -149,9 +152,9 @@ fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result<JObject
         |v| {
             Ok(env
                 .call_static_method(
-                    "java/time/Instant",
-                    "ofEpochSecond",
-                    "(JJ)Ljava/time/Instant;",
+                    jni_str!("java/time/Instant"),
+                    jni_str!("ofEpochSecond"),
+                    jni_sig!("(JJ)Ljava/time/Instant;"),
                     &[
                         JValue::Long(v.into_inner().as_second()),
                         JValue::Long(v.into_inner().subsec_nanosecond() as jlong),
@@ -177,8 +180,10 @@ fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result<JObject
 
     let result = env
         .new_object(
-            "org/apache/opendal/Metadata",
-            "(IJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/time/Instant;Ljava/lang/String;)V",
+            jni_str!("org/apache/opendal/Metadata"),
+            jni_sig!(
+                "(IJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/time/Instant;Ljava/lang/String;)V"
+            ),
             &[
                 JValue::Int(mode as jint),
                 JValue::Long(content_length),
@@ -194,19 +199,19 @@ fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result<JObject
     Ok(result)
 }
 
-fn make_entry<'a>(env: &mut JNIEnv<'a>, entry: Entry) -> Result<JObject<'a>> {
+fn make_entry<'a>(env: &mut Env<'a>, entry: Entry) -> Result<JObject<'a>> {
     let path = env.new_string(entry.path())?;
     let metadata = make_metadata(env, entry.metadata().to_owned())?;
 
     Ok(env.new_object(
-        "org/apache/opendal/Entry",
-        "(Ljava/lang/String;Lorg/apache/opendal/Metadata;)V",
+        jni_str!("org/apache/opendal/Entry"),
+        jni_sig!("(Ljava/lang/String;Lorg/apache/opendal/Metadata;)V"),
         &[JValue::Object(&path), JValue::Object(&metadata)],
     )?)
 }
 
 fn make_write_options<'a>(
-    env: &mut JNIEnv<'a>,
+    env: &mut Env<'a>,
     options: &JObject,
 ) -> Result<opendal::options::WriteOptions> {
     let concurrent = match convert::read_int_field(env, options, "concurrent")? {
@@ -235,7 +240,7 @@ fn make_write_options<'a>(
 }
 
 fn make_list_options<'a>(
-    env: &mut JNIEnv<'a>,
+    env: &mut Env<'a>,
     options: &JObject,
 ) -> Result<opendal::options::ListOptions> {
     Ok(opendal::options::ListOptions {
@@ -247,7 +252,7 @@ fn make_list_options<'a>(
     })
 }
 
-fn make_stat_options(env: &mut JNIEnv, options: &JObject) -> Result<opendal::options::StatOptions> {
+fn make_stat_options(env: &mut Env, options: &JObject) -> Result<opendal::options::StatOptions> {
     Ok(opendal::options::StatOptions {
         if_match: convert::read_string_field(env, options, "ifMatch")?,
         if_none_match: convert::read_string_field(env, options, "ifNoneMatch")?,
@@ -273,7 +278,7 @@ fn make_stat_options(env: &mut JNIEnv, options: &JObject) -> Result<opendal::opt
 }
 
 fn make_read_options<'a>(
-    env: &mut JNIEnv<'a>,
+    env: &mut Env<'a>,
     options: &JObject,
 ) -> Result<opendal::options::ReadOptions> {
     let offset = convert::read_int64_field(env, options, "offset")?;
@@ -286,7 +291,7 @@ fn make_read_options<'a>(
 }
 
 fn make_reader_options<'a>(
-    _: &mut JNIEnv<'a>,
+    _: &mut Env<'a>,
     _: &JObject,
 ) -> Result<opendal::options::ReaderOptions> {
     Ok(opendal::options::ReaderOptions::default())
diff --git a/bindings/java/src/main/java/org/apache/opendal/Metadata.java b/bindings/java/src/main/java/org/apache/opendal/Metadata.java
index 19f2f91be..47052979d 100644
--- a/bindings/java/src/main/java/org/apache/opendal/Metadata.java
+++ b/bindings/java/src/main/java/org/apache/opendal/Metadata.java
@@ -140,8 +140,10 @@ public class Metadata {
                     return EntryMode.FILE;
                 case 1:
                     return EntryMode.DIR;
-                default:
+                case 2:
                     return EntryMode.UNKNOWN;
+                default:
+                    throw new IllegalArgumentException("invalid EntryMode: " + mode);
             }
         }
     }
diff --git a/bindings/java/src/operator.rs b/bindings/java/src/operator.rs
index c91824d43..8cc8aac66 100644
--- a/bindings/java/src/operator.rs
+++ b/bindings/java/src/operator.rs
@@ -15,20 +15,21 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::EnvUnowned;
+use jni::jni_str;
 use jni::objects::JByteArray;
 use jni::objects::JClass;
 use jni::objects::JObject;
+use jni::objects::JObjectArray;
 use jni::objects::JString;
-use jni::sys::jbyteArray;
 use jni::sys::jlong;
-use jni::sys::jobject;
-use jni::sys::jobjectArray;
 use jni::sys::jsize;
 use opendal::blocking;
 
 use crate::Result;
 use crate::convert::{bytes_to_jbytearray, jstring_to_string};
+use crate::error::ThrowException;
 use crate::make_metadata;
 use crate::{make_entry, make_list_options, make_stat_options, make_write_options};
 
@@ -36,9 +37,9 @@ use crate::{make_entry, make_list_options, make_stat_options, make_write_options
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_disposeInternal(
-    _: JNIEnv,
-    _: JObject,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_disposeInternal<'local>(
+    _: EnvUnowned<'local>,
+    _: JObject<'local>,
     op: *mut blocking::Operator,
 ) {
     unsafe {
@@ -50,9 +51,9 @@ pub unsafe extern "system" fn Java_org_apache_opendal_Operator_disposeInternal(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_duplicate(
-    _: JNIEnv,
-    _: JObject,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_duplicate<'local>(
+    _: EnvUnowned<'local>,
+    _: JObject<'local>,
     op: *mut blocking::Operator,
 ) -> jlong {
     let op = unsafe { &mut *op };
@@ -63,60 +64,60 @@ pub unsafe extern "system" fn Java_org_apache_opendal_Operator_duplicate(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_read(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_read<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
-    read_options: JObject,
-) -> jbyteArray {
-    let op_ref = unsafe { &mut *op };
-    intern_read(&mut env, op_ref, path, read_options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        JByteArray::default().into_raw()
+    path: JString<'local>,
+    read_options: JObject<'local>,
+) -> JByteArray<'local> {
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_read(env, op_ref, path, read_options)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_read(
-    env: &mut JNIEnv,
+fn intern_read<'local>(
+    env: &mut Env<'local>,
     op: &mut blocking::Operator,
-    path: JString,
-    options: JObject,
-) -> Result<jbyteArray> {
+    path: JString<'local>,
+    options: JObject<'local>,
+) -> Result<JByteArray<'local>> {
     use crate::make_read_options;
 
     let path = jstring_to_string(env, &path)?;
     let options = make_read_options(env, &options)?;
     let content = op.read_options(&path, options)?;
 
-    let result = bytes_to_jbytearray(env, content.to_bytes())?;
-    Ok(result.into_raw())
+    bytes_to_jbytearray(env, content.to_bytes())
 }
 
 /// # Safety
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_write(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_write<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
-    content: JByteArray,
-    write_options: JObject,
+    path: JString<'local>,
+    content: JByteArray<'local>,
+    write_options: JObject<'local>,
 ) {
-    let op_ref = unsafe { &mut *op };
-    intern_write(&mut env, op_ref, path, content, write_options).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_write(env, op_ref, path, content, write_options)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_write(
-    env: &mut JNIEnv,
+fn intern_write<'local>(
+    env: &mut Env<'local>,
     op: &mut blocking::Operator,
-    path: JString,
-    content: JByteArray,
-    options: JObject,
+    path: JString<'local>,
+    content: JByteArray<'local>,
+    options: JObject<'local>,
 ) -> Result<()> {
     let path = jstring_to_string(env, &path)?;
     let content = env.convert_byte_array(content)?;
@@ -130,49 +131,54 @@ fn intern_write(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_stat(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_stat<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
-    stat_options: JObject,
-) -> jobject {
-    let op_ref = unsafe { &mut *op };
-    intern_stat(&mut env, op_ref, path, stat_options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        JObject::default().into_raw()
+    path: JString<'local>,
+    stat_options: JObject<'local>,
+) -> JObject<'local> {
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_stat(env, op_ref, path, stat_options)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_stat(
-    env: &mut JNIEnv,
+fn intern_stat<'local>(
+    env: &mut Env<'local>,
     op: &mut blocking::Operator,
-    path: JString,
-    options: JObject,
-) -> Result<jobject> {
+    path: JString<'local>,
+    options: JObject<'local>,
+) -> Result<JObject<'local>> {
     let path = jstring_to_string(env, &path)?;
     let stat_opts = make_stat_options(env, &options)?;
     let metadata = op.stat_options(&path, stat_opts)?;
-    Ok(make_metadata(env, metadata)?.into_raw())
+    make_metadata(env, metadata)
 }
 
 /// # Safety
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_delete(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_delete<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
+    path: JString<'local>,
 ) {
-    let op_ref = unsafe { &mut *op };
-    intern_delete(&mut env, op_ref, path).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_delete(env, op_ref, path)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_delete(env: &mut JNIEnv, op: &mut blocking::Operator, path: JString) -> Result<()> {
+fn intern_delete<'local>(
+    env: &mut Env<'local>,
+    op: &mut blocking::Operator,
+    path: JString<'local>,
+) -> Result<()> {
     let path = jstring_to_string(env, &path)?;
     Ok(op.delete(&path)?)
 }
@@ -181,19 +187,24 @@ fn intern_delete(env: &mut JNIEnv, op: &mut blocking::Operator, path: JString) -
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_createDir(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_createDir<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
+    path: JString<'local>,
 ) {
-    let op_ref = unsafe { &mut *op };
-    intern_create_dir(&mut env, op_ref, path).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_create_dir(env, op_ref, path)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_create_dir(env: &mut JNIEnv, op: &mut blocking::Operator, path: JString) -> Result<()> {
+fn intern_create_dir<'local>(
+    env: &mut Env<'local>,
+    op: &mut blocking::Operator,
+    path: JString<'local>,
+) -> Result<()> {
     let path = jstring_to_string(env, &path)?;
     Ok(op.create_dir(&path)?)
 }
@@ -202,24 +213,25 @@ fn intern_create_dir(env: &mut JNIEnv, op: &mut blocking::Operator, path: JStrin
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_copy(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_copy<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    source_path: JString,
-    target_path: JString,
+    source_path: JString<'local>,
+    target_path: JString<'local>,
 ) {
-    let op_ref = unsafe { &mut *op };
-    intern_copy(&mut env, op_ref, source_path, target_path).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_copy(env, op_ref, source_path, target_path)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_copy(
-    env: &mut JNIEnv,
+fn intern_copy<'local>(
+    env: &mut Env<'local>,
     op: &mut blocking::Operator,
-    source_path: JString,
-    target_path: JString,
+    source_path: JString<'local>,
+    target_path: JString<'local>,
 ) -> Result<()> {
     let source_path = jstring_to_string(env, &source_path)?;
     let target_path = jstring_to_string(env, &target_path)?;
@@ -232,24 +244,25 @@ fn intern_copy(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_rename(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_rename<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    source_path: JString,
-    target_path: JString,
+    source_path: JString<'local>,
+    target_path: JString<'local>,
 ) {
-    let op_ref = unsafe { &mut *op };
-    intern_rename(&mut env, op_ref, source_path, target_path).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_rename(env, op_ref, source_path, target_path)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_rename(
-    env: &mut JNIEnv,
+fn intern_rename<'local>(
+    env: &mut Env<'local>,
     op: &mut blocking::Operator,
-    source_path: JString,
-    target_path: JString,
+    source_path: JString<'local>,
+    target_path: JString<'local>,
 ) -> Result<()> {
     let source_path = jstring_to_string(env, &source_path)?;
     let target_path = jstring_to_string(env, &target_path)?;
@@ -261,19 +274,24 @@ fn intern_rename(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_removeAll(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_removeAll<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
+    path: JString<'local>,
 ) {
-    let op_ref = unsafe { &mut *op };
-    intern_remove_all(&mut env, op_ref, path).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_remove_all(env, op_ref, path)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_remove_all(env: &mut JNIEnv, op: &mut blocking::Operator, path: JString) -> Result<()> {
+fn intern_remove_all<'local>(
+    env: &mut Env<'local>,
+    op: &mut blocking::Operator,
+    path: JString<'local>,
+) -> Result<()> {
     use opendal::options::DeleteOptions;
     let path = jstring_to_string(env, &path)?;
 
@@ -290,40 +308,40 @@ fn intern_remove_all(env: &mut JNIEnv, op: &mut blocking::Operator, path: JStrin
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_Operator_list(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_Operator_list<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
-    options: JObject,
-) -> jobjectArray {
-    let op_ref = unsafe { &mut *op };
-    intern_list(&mut env, op_ref, path, options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        JObject::default().into_raw()
+    path: JString<'local>,
+    options: JObject<'local>,
+) -> JObjectArray<'local> {
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_list(env, op_ref, path, options)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_list(
-    env: &mut JNIEnv,
+fn intern_list<'local>(
+    env: &mut Env<'local>,
     op: &mut blocking::Operator,
-    path: JString,
-    options: JObject,
-) -> Result<jobjectArray> {
+    path: JString<'local>,
+    options: JObject<'local>,
+) -> Result<JObjectArray<'local>> {
     let path = jstring_to_string(env, &path)?;
     let list_opts = make_list_options(env, &options)?;
     let entries = op.list_options(&path, list_opts)?;
 
     let jarray = env.new_object_array(
         entries.len() as jsize,
-        "org/apache/opendal/Entry",
+        jni_str!("org/apache/opendal/Entry"),
         JObject::null(),
     )?;
 
     for (idx, entry) in entries.into_iter().enumerate() {
         let entry = make_entry(env, entry)?;
-        env.set_object_array_element(&jarray, idx as jsize, entry)?;
+        jarray.set_element(env, idx, &entry)?;
     }
 
-    Ok(jarray.into_raw())
+    Ok(jarray)
 }
diff --git a/bindings/java/src/operator_input_stream.rs b/bindings/java/src/operator_input_stream.rs
index f79abdb77..57a729979 100644
--- a/bindings/java/src/operator_input_stream.rs
+++ b/bindings/java/src/operator_input_stream.rs
@@ -15,38 +15,41 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::EnvUnowned;
 use jni::objects::JByteArray;
 use jni::objects::JClass;
 use jni::objects::JObject;
 use jni::objects::JString;
-use jni::sys::jbyteArray;
 use jni::sys::jlong;
 use opendal::blocking;
 use opendal::blocking::StdBytesIterator;
 
 use crate::convert::jstring_to_string;
+use crate::error::ThrowException;
 
 /// # Safety
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_constructReader(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_constructReader<
+    'local,
+>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
-    options: JObject,
+    path: JString<'local>,
+    options: JObject<'local>,
 ) -> jlong {
-    let op_ref = unsafe { &mut *op };
-    intern_construct_reader(&mut env, op_ref, path, options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_construct_reader(env, op_ref, path, options)
     })
+    .resolve::<ThrowException>()
 }
 
 fn intern_construct_reader(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: &mut blocking::Operator,
     path: JString,
     options: JObject,
@@ -71,9 +74,9 @@ fn intern_construct_reader(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_disposeReader(
-    _: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_disposeReader<'local>(
+    _: EnvUnowned<'local>,
+    _: JClass<'local>,
     reader: *mut StdBytesIterator,
 ) {
     unsafe {
@@ -85,31 +88,28 @@ pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_dispos
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_readNextBytes(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_OperatorInputStream_readNextBytes<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     reader: *mut StdBytesIterator,
-) -> jbyteArray {
-    let reader_ref = unsafe { &mut *reader };
-    intern_read_next_bytes(&mut env, reader_ref).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        JByteArray::default().into_raw()
+) -> JByteArray<'local> {
+    env.with_env(|env| {
+        let reader_ref = unsafe { &mut *reader };
+        intern_read_next_bytes(env, reader_ref)
     })
+    .resolve::<ThrowException>()
 }
 
-fn intern_read_next_bytes(
-    env: &mut JNIEnv,
+fn intern_read_next_bytes<'local>(
+    env: &mut Env<'local>,
     reader: &mut StdBytesIterator,
-) -> crate::Result<jbyteArray> {
+) -> crate::Result<JByteArray<'local>> {
     match reader
         .next()
         .transpose()
         .map_err(|err| opendal::Error::new(opendal::ErrorKind::Unexpected, err.to_string()))?
     {
-        None => Ok(JObject::null().into_raw()),
-        Some(content) => {
-            let result = env.byte_array_from_slice(&content)?;
-            Ok(result.into_raw())
-        }
+        None => Ok(JByteArray::default()),
+        Some(content) => Ok(env.byte_array_from_slice(&content)?),
     }
 }
diff --git a/bindings/java/src/operator_output_stream.rs b/bindings/java/src/operator_output_stream.rs
index 4c3c9e3bb..c7ae2fc1e 100644
--- a/bindings/java/src/operator_output_stream.rs
+++ b/bindings/java/src/operator_output_stream.rs
@@ -15,7 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::EnvUnowned;
 use jni::objects::JByteArray;
 use jni::objects::JClass;
 use jni::objects::JObject;
@@ -24,27 +25,30 @@ use jni::sys::jlong;
 use opendal::blocking;
 
 use crate::convert::jstring_to_string;
+use crate::error::ThrowException;
 
 /// # Safety
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OperatorOutputStream_constructWriter(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_OperatorOutputStream_constructWriter<
+    'local,
+>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     op: *mut blocking::Operator,
-    path: JString,
-    options: JObject,
+    path: JString<'local>,
+    options: JObject<'local>,
 ) -> jlong {
-    let op_ref = unsafe { &mut *op };
-    intern_construct_write(&mut env, op_ref, path, options).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        0
+    env.with_env(|env| {
+        let op_ref = unsafe { &mut *op };
+        intern_construct_write(env, op_ref, path, options)
     })
+    .resolve::<ThrowException>()
 }
 
 fn intern_construct_write(
-    env: &mut JNIEnv,
+    env: &mut Env,
     op: &mut blocking::Operator,
     path: JString,
     options: JObject,
@@ -61,15 +65,16 @@ fn intern_construct_write(
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OperatorOutputStream_disposeWriter(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_OperatorOutputStream_disposeWriter<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     writer: *mut blocking::Writer,
 ) {
-    let mut writer = unsafe { Box::from_raw(writer) };
-    intern_dispose_write(&mut writer).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|_env| {
+        let mut writer = unsafe { Box::from_raw(writer) };
+        intern_dispose_write(&mut writer)
     })
+    .resolve::<ThrowException>()
 }
 
 fn intern_dispose_write(writer: &mut blocking::Writer) -> crate::Result<()> {
@@ -81,20 +86,21 @@ fn intern_dispose_write(writer: &mut blocking::Writer) -> crate::Result<()> {
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OperatorOutputStream_writeBytes(
-    mut env: JNIEnv,
-    _: JClass,
+pub unsafe extern "system" fn Java_org_apache_opendal_OperatorOutputStream_writeBytes<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
     writer: *mut blocking::Writer,
-    content: JByteArray,
+    content: JByteArray<'local>,
 ) {
-    let writer_ref = unsafe { &mut *writer };
-    intern_write_bytes(&mut env, writer_ref, content).unwrap_or_else(|e| {
-        e.throw(&mut env);
+    env.with_env(|env| {
+        let writer_ref = unsafe { &mut *writer };
+        intern_write_bytes(env, writer_ref, content)
     })
+    .resolve::<ThrowException>()
 }
 
 fn intern_write_bytes(
-    env: &mut JNIEnv,
+    env: &mut Env,
     writer: &mut blocking::Writer,
     content: JByteArray,
 ) -> crate::Result<()> {
diff --git a/bindings/java/src/utility.rs b/bindings/java/src/utility.rs
index 5524c328c..3a8cbc764 100644
--- a/bindings/java/src/utility.rs
+++ b/bindings/java/src/utility.rs
@@ -17,30 +17,31 @@
 
 use std::collections::HashSet;
 
-use jni::JNIEnv;
+use jni::Env;
+use jni::EnvUnowned;
+use jni::jni_str;
 use jni::objects::JClass;
 use jni::objects::JObject;
-use jni::sys::jobjectArray;
+use jni::objects::JObjectArray;
 use jni::sys::jsize;
 
 use crate::Result;
 use crate::convert::string_to_jstring;
+use crate::error::ThrowException;
 
 /// # Safety
 ///
 /// This function should not be called before the Operator is ready.
 #[unsafe(no_mangle)]
-pub unsafe extern "system" fn Java_org_apache_opendal_OpenDAL_loadEnabledServices(
-    mut env: JNIEnv,
-    _: JClass,
-) -> jobjectArray {
-    intern_load_enabled_services(&mut env).unwrap_or_else(|e| {
-        e.throw(&mut env);
-        JObject::default().into_raw()
-    })
+pub unsafe extern "system" fn Java_org_apache_opendal_OpenDAL_loadEnabledServices<'local>(
+    mut env: EnvUnowned<'local>,
+    _: JClass<'local>,
+) -> JObjectArray<'local> {
+    env.with_env(|env| intern_load_enabled_services(env))
+        .resolve::<ThrowException>()
 }
 
-fn intern_load_enabled_services(env: &mut JNIEnv) -> Result<jobjectArray> {
+fn intern_load_enabled_services<'local>(env: &mut Env<'local>) -> Result<JObjectArray<'local>> {
     let services = HashSet::from([
         opendal::services::ALIYUN_DRIVE_SCHEME,
         opendal::services::ALLUXIO_SCHEME,
@@ -92,12 +93,16 @@ fn intern_load_enabled_services(env: &mut JNIEnv) -> Result<jobjectArray> {
         opendal::services::YANDEX_DISK_SCHEME,
     ]);
 
-    let res = env.new_object_array(services.len() as jsize, "java/lang/String", JObject::null())?;
+    let res = env.new_object_array(
+        services.len() as jsize,
+        jni_str!("java/lang/String"),
+        JObject::null(),
+    )?;
 
     for (idx, service) in services.into_iter().enumerate() {
         let srv = string_to_jstring(env, Some(service))?;
-        env.set_object_array_element(&res, idx as jsize, srv)?;
+        res.set_element(env, idx, &srv)?;
     }
 
-    Ok(res.into_raw())
+    Ok(res)
 }