[jnigen] Some summarizer improvements (https://github.com/dart-lang/jnigen/issues/114)

diff --git a/pkgs/jnigen/example/in_app_java/jnigen.yaml b/pkgs/jnigen/example/in_app_java/jnigen.yaml
index 58bd7d9..e344270 100644
--- a/pkgs/jnigen/example/in_app_java/jnigen.yaml
+++ b/pkgs/jnigen/example/in_app_java/jnigen.yaml
@@ -12,4 +12,5 @@
 source_path:
   - 'android/app/src/main/java'
 classes:
-  - 'com.example.in_app_java.AndroidUtils'
+  - 'com.example.in_app_java.AndroidUtils' # from source_path
+  - 'android.os.Build' # from gradle's compile classpath
diff --git a/pkgs/jnigen/example/in_app_java/lib/android_utils.dart b/pkgs/jnigen/example/in_app_java/lib/android_utils.dart
index 9dc7ffb..e83f716 100644
--- a/pkgs/jnigen/example/in_app_java/lib/android_utils.dart
+++ b/pkgs/jnigen/example/in_app_java/lib/android_utils.dart
@@ -42,3 +42,299 @@
           jni.JniObject mainActivity, jni.JniObject text, int duration) =>
       _showToast(mainActivity.reference, text.reference, duration).check();
 }
+
+/// from: android.os.Build
+class Build extends jni.JniObject {
+  Build.fromRef(ffi.Pointer<ffi.Void> ref) : super.fromRef(ref);
+
+  static final _get_BOARD =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__BOARD")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String BOARD
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get BOARD => jni.JniString.fromRef(_get_BOARD().object);
+
+  static final _get_BOOTLOADER =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__BOOTLOADER")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String BOOTLOADER
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get BOOTLOADER =>
+      jni.JniString.fromRef(_get_BOOTLOADER().object);
+
+  static final _get_BRAND =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__BRAND")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String BRAND
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get BRAND => jni.JniString.fromRef(_get_BRAND().object);
+
+  static final _get_CPU_ABI =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__CPU_ABI")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String CPU_ABI
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get CPU_ABI =>
+      jni.JniString.fromRef(_get_CPU_ABI().object);
+
+  static final _get_CPU_ABI2 =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__CPU_ABI2")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String CPU_ABI2
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get CPU_ABI2 =>
+      jni.JniString.fromRef(_get_CPU_ABI2().object);
+
+  static final _get_DEVICE =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__DEVICE")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String DEVICE
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get DEVICE =>
+      jni.JniString.fromRef(_get_DEVICE().object);
+
+  static final _get_DISPLAY =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__DISPLAY")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String DISPLAY
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get DISPLAY =>
+      jni.JniString.fromRef(_get_DISPLAY().object);
+
+  static final _get_FINGERPRINT =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__FINGERPRINT")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String FINGERPRINT
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get FINGERPRINT =>
+      jni.JniString.fromRef(_get_FINGERPRINT().object);
+
+  static final _get_HARDWARE =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__HARDWARE")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String HARDWARE
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get HARDWARE =>
+      jni.JniString.fromRef(_get_HARDWARE().object);
+
+  static final _get_HOST =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__HOST")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String HOST
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get HOST => jni.JniString.fromRef(_get_HOST().object);
+
+  static final _get_ID =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__ID")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String ID
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get ID => jni.JniString.fromRef(_get_ID().object);
+
+  static final _get_MANUFACTURER =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__MANUFACTURER")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String MANUFACTURER
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get MANUFACTURER =>
+      jni.JniString.fromRef(_get_MANUFACTURER().object);
+
+  static final _get_MODEL =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__MODEL")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String MODEL
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get MODEL => jni.JniString.fromRef(_get_MODEL().object);
+
+  static final _get_ODM_SKU =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__ODM_SKU")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String ODM_SKU
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get ODM_SKU =>
+      jni.JniString.fromRef(_get_ODM_SKU().object);
+
+  static final _get_PRODUCT =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__PRODUCT")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String PRODUCT
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get PRODUCT =>
+      jni.JniString.fromRef(_get_PRODUCT().object);
+
+  static final _get_RADIO =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__RADIO")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String RADIO
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get RADIO => jni.JniString.fromRef(_get_RADIO().object);
+
+  static final _get_SERIAL =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__SERIAL")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String SERIAL
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get SERIAL =>
+      jni.JniString.fromRef(_get_SERIAL().object);
+
+  static final _get_SKU =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__SKU")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String SKU
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get SKU => jni.JniString.fromRef(_get_SKU().object);
+
+  static final _get_SOC_MANUFACTURER =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__SOC_MANUFACTURER")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String SOC_MANUFACTURER
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get SOC_MANUFACTURER =>
+      jni.JniString.fromRef(_get_SOC_MANUFACTURER().object);
+
+  static final _get_SOC_MODEL =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__SOC_MODEL")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String SOC_MODEL
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get SOC_MODEL =>
+      jni.JniString.fromRef(_get_SOC_MODEL().object);
+
+  static final _get_SUPPORTED_32_BIT_ABIS =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__SUPPORTED_32_BIT_ABIS")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String[] SUPPORTED_32_BIT_ABIS
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniObject get SUPPORTED_32_BIT_ABIS =>
+      jni.JniObject.fromRef(_get_SUPPORTED_32_BIT_ABIS().object);
+
+  static final _get_SUPPORTED_64_BIT_ABIS =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__SUPPORTED_64_BIT_ABIS")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String[] SUPPORTED_64_BIT_ABIS
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniObject get SUPPORTED_64_BIT_ABIS =>
+      jni.JniObject.fromRef(_get_SUPPORTED_64_BIT_ABIS().object);
+
+  static final _get_SUPPORTED_ABIS =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "get_Build__SUPPORTED_ABIS")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String[] SUPPORTED_ABIS
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniObject get SUPPORTED_ABIS =>
+      jni.JniObject.fromRef(_get_SUPPORTED_ABIS().object);
+
+  static final _get_TAGS =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__TAGS")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String TAGS
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get TAGS => jni.JniString.fromRef(_get_TAGS().object);
+
+  static final _get_TIME =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__TIME")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final long TIME
+  static int get TIME => _get_TIME().long;
+
+  static final _get_TYPE =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__TYPE")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String TYPE
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get TYPE => jni.JniString.fromRef(_get_TYPE().object);
+
+  /// from: static public final java.lang.String UNKNOWN
+  static const UNKNOWN = "unknown";
+
+  static final _get_USER =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("get_Build__USER")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public final java.lang.String USER
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString get USER => jni.JniString.fromRef(_get_USER().object);
+
+  static final _ctor =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>("Build__ctor")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: public void <init>()
+  Build() : super.fromRef(_ctor().object);
+
+  static final _getSerial =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "Build__getSerial")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public java.lang.String getSerial()
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString getSerial() =>
+      jni.JniString.fromRef(_getSerial().object);
+
+  static final _getFingerprintedPartitions =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "Build__getFingerprintedPartitions")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public java.util.List getFingerprintedPartitions()
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniObject getFingerprintedPartitions() =>
+      jni.JniObject.fromRef(_getFingerprintedPartitions().object);
+
+  static final _getRadioVersion =
+      jniLookup<ffi.NativeFunction<jni.JniResult Function()>>(
+              "Build__getRadioVersion")
+          .asFunction<jni.JniResult Function()>();
+
+  /// from: static public java.lang.String getRadioVersion()
+  /// The returned object must be deleted after use, by calling the `delete` method.
+  static jni.JniString getRadioVersion() =>
+      jni.JniString.fromRef(_getRadioVersion().object);
+}
diff --git a/pkgs/jnigen/example/in_app_java/lib/main.dart b/pkgs/jnigen/example/in_app_java/lib/main.dart
index 37e8676..dbb34f9 100644
--- a/pkgs/jnigen/example/in_app_java/lib/main.dart
+++ b/pkgs/jnigen/example/in_app_java/lib/main.dart
@@ -11,8 +11,9 @@
 
 JniObject activity = JniObject.fromRef(Jni.getCurrentActivity());
 
-void showToast(String text) {
-  AndroidUtils.showToast(activity, text.jniString(), 0);
+/// Display device model number as Toast
+void showToast() {
+  AndroidUtils.showToast(activity, Build.MODEL, 0);
 }
 
 void main() {
@@ -50,8 +51,8 @@
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             ElevatedButton(
-              child: const Text('Show toast'),
-              onPressed: () => showToast('😀'),
+              child: const Text('Show Device Model'),
+              onPressed: () => showToast(),
             ),
           ],
         ),
diff --git a/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c b/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c
index 71e05c8..c2e9ced 100644
--- a/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c
+++ b/pkgs/jnigen/example/in_app_java/src/android_utils/android_utils.c
@@ -52,3 +52,439 @@
                                   text, duration);
   return (JniResult){.result = {.j = 0}, .exception = check_exception()};
 }
+
+// android.os.Build
+jclass _c_Build = NULL;
+
+jmethodID _m_Build__ctor = NULL;
+FFI_PLUGIN_EXPORT
+JniResult Build__ctor() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_method(_c_Build, &_m_Build__ctor, "<init>", "()V");
+  if (_m_Build__ctor == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  jobject _result = (*jniEnv)->NewObject(jniEnv, _c_Build, _m_Build__ctor);
+  return (JniResult){.result = {.l = to_global_ref(_result)},
+                     .exception = check_exception()};
+}
+
+jmethodID _m_Build__getSerial = NULL;
+FFI_PLUGIN_EXPORT
+JniResult Build__getSerial() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_method(_c_Build, &_m_Build__getSerial, "getSerial",
+                     "()Ljava/lang/String;");
+  if (_m_Build__getSerial == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  jobject _result =
+      (*jniEnv)->CallStaticObjectMethod(jniEnv, _c_Build, _m_Build__getSerial);
+  return (JniResult){.result = {.l = to_global_ref(_result)},
+                     .exception = check_exception()};
+}
+
+jmethodID _m_Build__getFingerprintedPartitions = NULL;
+FFI_PLUGIN_EXPORT
+JniResult Build__getFingerprintedPartitions() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_method(_c_Build, &_m_Build__getFingerprintedPartitions,
+                     "getFingerprintedPartitions", "()Ljava/util/List;");
+  if (_m_Build__getFingerprintedPartitions == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  jobject _result = (*jniEnv)->CallStaticObjectMethod(
+      jniEnv, _c_Build, _m_Build__getFingerprintedPartitions);
+  return (JniResult){.result = {.l = to_global_ref(_result)},
+                     .exception = check_exception()};
+}
+
+jmethodID _m_Build__getRadioVersion = NULL;
+FFI_PLUGIN_EXPORT
+JniResult Build__getRadioVersion() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_method(_c_Build, &_m_Build__getRadioVersion, "getRadioVersion",
+                     "()Ljava/lang/String;");
+  if (_m_Build__getRadioVersion == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  jobject _result = (*jniEnv)->CallStaticObjectMethod(
+      jniEnv, _c_Build, _m_Build__getRadioVersion);
+  return (JniResult){.result = {.l = to_global_ref(_result)},
+                     .exception = check_exception()};
+}
+
+jfieldID _f_Build__BOARD = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__BOARD() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__BOARD, "BOARD", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__BOARD));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__BOOTLOADER = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__BOOTLOADER() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__BOOTLOADER, "BOOTLOADER",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__BOOTLOADER));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__BRAND = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__BRAND() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__BRAND, "BRAND", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__BRAND));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__CPU_ABI = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__CPU_ABI() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__CPU_ABI, "CPU_ABI",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__CPU_ABI));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__CPU_ABI2 = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__CPU_ABI2() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__CPU_ABI2, "CPU_ABI2",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__CPU_ABI2));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__DEVICE = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__DEVICE() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__DEVICE, "DEVICE",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__DEVICE));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__DISPLAY = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__DISPLAY() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__DISPLAY, "DISPLAY",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__DISPLAY));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__FINGERPRINT = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__FINGERPRINT() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__FINGERPRINT, "FINGERPRINT",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__FINGERPRINT));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__HARDWARE = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__HARDWARE() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__HARDWARE, "HARDWARE",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__HARDWARE));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__HOST = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__HOST() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__HOST, "HOST", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__HOST));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__ID = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__ID() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__ID, "ID", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__ID));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__MANUFACTURER = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__MANUFACTURER() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__MANUFACTURER, "MANUFACTURER",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref((*jniEnv)->GetStaticObjectField(
+      jniEnv, _c_Build, _f_Build__MANUFACTURER));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__MODEL = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__MODEL() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__MODEL, "MODEL", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__MODEL));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__ODM_SKU = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__ODM_SKU() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__ODM_SKU, "ODM_SKU",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__ODM_SKU));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__PRODUCT = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__PRODUCT() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__PRODUCT, "PRODUCT",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__PRODUCT));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__RADIO = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__RADIO() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__RADIO, "RADIO", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__RADIO));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SERIAL = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SERIAL() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SERIAL, "SERIAL",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__SERIAL));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SKU = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SKU() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SKU, "SKU", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__SKU));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SOC_MANUFACTURER = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SOC_MANUFACTURER() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SOC_MANUFACTURER, "SOC_MANUFACTURER",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref((*jniEnv)->GetStaticObjectField(
+      jniEnv, _c_Build, _f_Build__SOC_MANUFACTURER));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SOC_MODEL = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SOC_MODEL() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SOC_MODEL, "SOC_MODEL",
+                    "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__SOC_MODEL));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SUPPORTED_32_BIT_ABIS = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SUPPORTED_32_BIT_ABIS() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SUPPORTED_32_BIT_ABIS,
+                    "SUPPORTED_32_BIT_ABIS", "[Ljava/lang/String;");
+  jobject _result = to_global_ref((*jniEnv)->GetStaticObjectField(
+      jniEnv, _c_Build, _f_Build__SUPPORTED_32_BIT_ABIS));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SUPPORTED_64_BIT_ABIS = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SUPPORTED_64_BIT_ABIS() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SUPPORTED_64_BIT_ABIS,
+                    "SUPPORTED_64_BIT_ABIS", "[Ljava/lang/String;");
+  jobject _result = to_global_ref((*jniEnv)->GetStaticObjectField(
+      jniEnv, _c_Build, _f_Build__SUPPORTED_64_BIT_ABIS));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__SUPPORTED_ABIS = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__SUPPORTED_ABIS() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__SUPPORTED_ABIS, "SUPPORTED_ABIS",
+                    "[Ljava/lang/String;");
+  jobject _result = to_global_ref((*jniEnv)->GetStaticObjectField(
+      jniEnv, _c_Build, _f_Build__SUPPORTED_ABIS));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__TAGS = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__TAGS() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__TAGS, "TAGS", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__TAGS));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__TIME = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__TIME() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__TIME, "TIME", "J");
+  int64_t _result =
+      (*jniEnv)->GetStaticLongField(jniEnv, _c_Build, _f_Build__TIME);
+  return (JniResult){.result = {.j = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__TYPE = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__TYPE() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__TYPE, "TYPE", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__TYPE));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
+
+jfieldID _f_Build__USER = NULL;
+FFI_PLUGIN_EXPORT
+JniResult get_Build__USER() {
+  load_env();
+  load_class_gr(&_c_Build, "android/os/Build");
+  if (_c_Build == NULL)
+    return (JniResult){.result = {.j = 0}, .exception = check_exception()};
+  load_static_field(_c_Build, &_f_Build__USER, "USER", "Ljava/lang/String;");
+  jobject _result = to_global_ref(
+      (*jniEnv)->GetStaticObjectField(jniEnv, _c_Build, _f_Build__USER));
+  return (JniResult){.result = {.l = _result}, .exception = check_exception()};
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java
index bd54b3f..d636f65 100644
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java
@@ -4,142 +4,78 @@
 
 package com.github.dart_lang.jnigen.apisummarizer;
 
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
+import static com.github.dart_lang.jnigen.apisummarizer.util.ExceptionUtil.wrapCheckedException;
+
 import com.github.dart_lang.jnigen.apisummarizer.disasm.AsmSummarizer;
 import com.github.dart_lang.jnigen.apisummarizer.doclet.SummarizerDoclet;
 import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
+import com.github.dart_lang.jnigen.apisummarizer.util.JsonUtil;
 import com.github.dart_lang.jnigen.apisummarizer.util.Log;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
+import com.github.dart_lang.jnigen.apisummarizer.util.StreamUtil;
+import java.io.*;
 import java.util.*;
+import java.util.jar.JarFile;
 import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
 import javax.tools.DocumentationTool;
 import javax.tools.ToolProvider;
 import jdk.javadoc.doclet.Doclet;
 import org.apache.commons.cli.*;
 
 public class Main {
+  public enum Backend {
+    // Produce API descriptions from source files using Doclet API.
+    DOCLET,
+    // Produce API descriptions from class files under classpath.
+    ASM,
+    // Prefer source but fall back to JARs in classpath if sources not found.
+    AUTO,
+  }
+
+  public static class SummarizerOptions {
+    String sourcePath;
+    String classPath;
+    boolean useModules;
+    Backend backend;
+    String modulesList;
+    boolean addDependencies;
+    String toolArgs;
+    boolean verbose;
+    String outputFile;
+    String[] args;
+
+    SummarizerOptions() {}
+
+    public static SummarizerOptions fromCommandLine(CommandLine cmd) {
+      var opts = new SummarizerOptions();
+      opts.sourcePath = cmd.getOptionValue("sources", ".");
+      var backendString = cmd.getOptionValue("backend", "auto");
+      opts.backend = Backend.valueOf(backendString.toUpperCase());
+      opts.classPath = cmd.getOptionValue("classes", null);
+      opts.useModules = cmd.hasOption("use-modules");
+      opts.modulesList = cmd.getOptionValue("module-names", null);
+      opts.addDependencies = cmd.hasOption("recursive");
+      opts.toolArgs = cmd.getOptionValue("doctool-args", null);
+      opts.verbose = cmd.hasOption("verbose");
+      opts.outputFile = cmd.getOptionValue("output-file", null);
+      opts.args = cmd.getArgs();
+      return opts;
+    }
+  }
+
   private static final CommandLineParser parser = new DefaultParser();
-  static SummarizerOptions config;
+  static SummarizerOptions options;
 
-  public static void writeAll(List<ClassDecl> decls) {
-    var mapper = new ObjectMapper();
-    Log.timed("Writing JSON");
-    mapper.enable(SerializationFeature.INDENT_OUTPUT);
-    mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
-    try {
-      mapper.writeValue(System.out, decls);
-    } catch (IOException e) {
-      e.printStackTrace();
-    }
-    Log.timed("Finished");
-  }
-
-  public static void runDoclet(List<String> qualifiedNames, SummarizerOptions options) {
-    runDocletWithClass(SummarizerDoclet.class, qualifiedNames, options);
-  }
-
-  public static void runDocletWithClass(
-      Class<? extends Doclet> docletClass, List<String> qualifiedNames, SummarizerOptions options) {
-    List<File> javaFilePaths =
-        qualifiedNames.stream()
-            .map(s -> findSourceLocation(s, options.sourcePaths.split(File.pathSeparator)))
-            .collect(Collectors.toList());
-    Log.setVerbose(options.verbose);
-
-    var files =
-        javaFilePaths.stream()
-            .flatMap(
-                path -> recursiveListFiles(path, file -> file.getName().endsWith(".java")).stream())
-            .map(File::getPath)
-            .toArray(String[]::new);
-
-    DocumentationTool javadoc = ToolProvider.getSystemDocumentationTool();
-    var fileManager = javadoc.getStandardFileManager(null, null, null);
-    var fileObjects = fileManager.getJavaFileObjects(files);
-
-    var cli = new ArrayList<String>();
-    cli.add((options.useModules ? "--module-" : "--") + "source-path=" + options.sourcePaths);
-    if (options.classPaths != null) {
-      cli.add("--class-path=" + options.classPaths);
-    }
-    if (options.addDependencies) {
-      cli.add("--expand-requires=all");
-    }
-    cli.addAll(List.of("-encoding", "utf8"));
-
-    if (options.toolOptions != null) {
-      cli.addAll(List.of(options.toolOptions.split(" ")));
-    }
-
-    javadoc.getTask(null, fileManager, System.err::println, docletClass, cli, fileObjects).call();
-  }
-
-  public static void main(String[] args) {
-    CommandLine cl = parseArgs(args);
-    config = SummarizerOptions.fromCommandLine(cl);
-    if (config.useAsm) {
-      System.err.println("use-asm: ignoring all flags other than --classes (-c)");
-      try {
-        writeAll(AsmSummarizer.run(config.classPaths.split(File.pathSeparator), cl.getArgs()));
-      } catch (IOException e) {
-        e.printStackTrace();
-      }
-      return;
-    }
-    runDoclet(List.of(cl.getArgs()), config);
-  }
-
-  public static File findSourceLocation(String qualifiedName, String[] paths) {
-    var s = qualifiedName.replace(".", "/");
-    for (var folder : paths) {
-      var f = new File(folder, s + ".java");
-      if (f.exists() && f.isFile()) {
-        return f;
-      }
-      var d = new File(folder, s);
-      if (d.exists() && d.isDirectory()) {
-        return d;
-      }
-    }
-    throw new RuntimeException("cannot find class: " + s);
-  }
-
-  public static List<File> recursiveListFiles(File file, FileFilter filter) {
-    if (!file.isDirectory()) {
-      return List.of(file);
-    }
-    var files = new ArrayList<File>();
-    var queue = new ArrayDeque<File>();
-    queue.add(file);
-    while (!queue.isEmpty()) {
-      var dir = queue.poll();
-      var list = dir.listFiles(entry -> entry.isDirectory() || filter.accept(entry));
-      if (list == null) {
-        throw new IllegalArgumentException();
-      }
-      Arrays.sort(list);
-      for (var path : list) {
-        if (path.isDirectory()) {
-          queue.add(path);
-        } else {
-          files.add(path);
-        }
-      }
-    }
-    return files;
-  }
-
-  public static CommandLine parseArgs(String[] args) {
+  public static SummarizerOptions parseArgs(String[] args) {
     var options = new Options();
     Option sources = new Option("s", "sources", true, "paths to search for source files");
     Option classes = new Option("c", "classes", true, "paths to search for compiled classes");
     Option backend =
         new Option(
-            "b", "backend", true, "backend to use for summary generation ('doclet' or 'asm').");
+            "b",
+            "backend",
+            true,
+            "backend to use for summary generation ('doclet', 'asm' or 'auto' (default)).");
     Option useModules = new Option("M", "use-modules", false, "use Java modules");
     Option recursive = new Option("r", "recursive", false, "Include dependencies of classes");
     Option moduleNames =
@@ -147,9 +83,19 @@
     Option doctoolArgs =
         new Option("D", "doctool-args", true, "Arguments to pass to the documentation tool");
     Option verbose = new Option("v", "verbose", false, "Enable verbose output");
+    Option outputFile =
+        new Option("o", "output-file", true, "Write JSON to file instead of stdout");
     for (Option opt :
         new Option[] {
-          sources, classes, backend, useModules, recursive, moduleNames, doctoolArgs, verbose
+          sources,
+          classes,
+          backend,
+          useModules,
+          recursive,
+          moduleNames,
+          doctoolArgs,
+          verbose,
+          outputFile,
         }) {
       options.addOption(opt);
     }
@@ -171,36 +117,215 @@
               + "Class or package names should be fully qualified.\n\n",
           options);
       System.exit(1);
-      return null;
+      throw new RuntimeException("Unreachable code");
     }
-    return cmd;
+    return SummarizerOptions.fromCommandLine(cmd);
   }
 
-  public static class SummarizerOptions {
-    String sourcePaths, classPaths;
-    boolean useModules, useAsm;
-    String modulesList;
-    boolean addDependencies;
-    String toolOptions;
-    boolean verbose;
+  public static List<ClassDecl> runDocletWithClass(
+      Class<? extends Doclet> docletClass, List<File> javaFilePaths, SummarizerOptions options) {
+    Log.setVerbose(options.verbose);
 
-    public static SummarizerOptions fromCommandLine(CommandLine cmd) {
-      var opts = new SummarizerOptions();
-      opts.sourcePaths = cmd.getOptionValue("sources", ".");
-      var backend = cmd.getOptionValue("backend", "doclet");
-      if (backend.equalsIgnoreCase("asm")) {
-        opts.useAsm = true;
-      } else if (!backend.equalsIgnoreCase("doclet")) {
-        System.err.println("supported backends: asm, doclet");
-        System.exit(1);
+    var files = javaFilePaths.stream().map(File::getPath).toArray(String[]::new);
+
+    DocumentationTool javadoc = ToolProvider.getSystemDocumentationTool();
+    var fileManager = javadoc.getStandardFileManager(null, null, null);
+    var fileObjects = fileManager.getJavaFileObjects(files);
+
+    var cli = new ArrayList<String>();
+    cli.add((options.useModules ? "--module-" : "--") + "source-path=" + options.sourcePath);
+    if (options.classPath != null) {
+      cli.add("--class-path=" + options.classPath);
+    }
+    if (options.addDependencies) {
+      cli.add("--expand-requires=all");
+    }
+    cli.addAll(List.of("-encoding", "utf8"));
+
+    if (options.toolArgs != null) {
+      cli.addAll(List.of(options.toolArgs.split(" ")));
+    }
+
+    javadoc.getTask(null, fileManager, System.err::println, docletClass, cli, fileObjects).call();
+
+    return SummarizerDoclet.getClasses();
+  }
+
+  public static List<ClassDecl> runDoclet(List<File> javaFilePaths, SummarizerOptions options) {
+    return runDocletWithClass(SummarizerDoclet.class, javaFilePaths, options);
+  }
+
+  /**
+   * Lists all files under given directory, which satisfy the condition of filter. The order of
+   * listing shall be deterministic.
+   */
+  public static List<File> recursiveListFiles(File file, FileFilter filter) {
+    if (!file.exists()) {
+      throw new RuntimeException("File not found: " + file.getPath());
+    }
+
+    if (!file.isDirectory()) {
+      return List.of(file);
+    }
+
+    // List files using a breadth-first traversal.
+    var files = new ArrayList<File>();
+    var queue = new ArrayDeque<File>();
+    queue.add(file);
+    while (!queue.isEmpty()) {
+      var dir = queue.poll();
+      var list = dir.listFiles(entry -> entry.isDirectory() || filter.accept(entry));
+      if (list == null) throw new IllegalArgumentException("File.listFiles returned null!");
+      Arrays.sort(list);
+      for (var path : list) {
+        if (path.isDirectory()) {
+          queue.add(path);
+        } else {
+          files.add(path);
+        }
       }
-      opts.classPaths = cmd.getOptionValue("classes", null);
-      opts.useModules = cmd.hasOption("use-modules");
-      opts.modulesList = cmd.getOptionValue("module-names", null);
-      opts.addDependencies = cmd.hasOption("recursive");
-      opts.toolOptions = cmd.getOptionValue("doctool-args", null);
-      opts.verbose = cmd.hasOption("verbose");
-      return opts;
+    }
+    return files;
+  }
+
+  /**
+   * Finds and returns source file (s) corresponding to qualified name. It's assumed that package
+   * hierarchy in Java is same as the filesystem hierarchy, i.e. each package corresponds to a
+   * directory and each class corresponds to a file in its respective package. If the respective
+   * file or directory does not exist, the returned optional is empty.
+   */
+  public static Optional<List<File>> findSourceFiles(String qualifiedName, String[] sourcePaths) {
+    return findFiles(qualifiedName, sourcePaths, ".java");
+  }
+
+  public static Optional<List<File>> findFiles(
+      String qualifiedName, String[] searchPaths, String suffix) {
+    var s = qualifiedName.replace(".", "/");
+    for (var folder : searchPaths) {
+      var f = new File(folder, s + suffix);
+      if (f.exists() && f.isFile()) {
+        return Optional.of(List.of(f));
+      }
+      var d = new File(folder, s);
+      if (d.exists() && d.isDirectory()) {
+        return Optional.of(recursiveListFiles(d, file -> file.getName().endsWith(".java")));
+      }
+    }
+    return Optional.empty();
+  }
+
+  public static Optional<List<InputStream>> findClassInputStreamsInJar(
+      JarFile jar, String relativePath) {
+    var suffix = ".class";
+    var classEntry = jar.getEntry(relativePath + suffix);
+    if (classEntry != null) {
+      return Optional.of(List.of(wrapCheckedException(jar::getInputStream, classEntry)));
+    }
+    var dirPath = relativePath.endsWith("/") ? relativePath : relativePath + "/";
+    var dirEntry = jar.getEntry(dirPath);
+    if (dirEntry != null && dirEntry.isDirectory()) {
+      var result =
+          jar.stream()
+              .map(je -> (ZipEntry) je)
+              .filter(
+                  entry -> {
+                    var name = entry.getName();
+                    return name.endsWith(suffix) && name.startsWith(dirPath);
+                  })
+              .map(entry -> wrapCheckedException(jar::getInputStream, entry))
+              .collect(Collectors.toList());
+      return Optional.of(result);
+    }
+    return Optional.empty();
+  }
+
+  /**
+   * Finds and returns class file(s) corresponding to qualified name from JAR files in classpath.
+   */
+  public static Optional<List<InputStream>> findClassInputStreams(
+      String binaryName, String[] classPaths) {
+    String relativePath = binaryName.replace(".", "/");
+    for (var path : classPaths) {
+      var file = new File(path);
+      // A path in classpath can be a directory or JAR. These cases require different logic.
+      if (file.isDirectory()) {
+        var directorySearchResult = findFiles(binaryName, classPaths, ".class");
+        if (directorySearchResult.isPresent()) {
+          var list = directorySearchResult.get();
+          return Optional.of(
+              StreamUtil.map(
+                  list, fileElement -> wrapCheckedException(FileInputStream::new, fileElement)));
+        }
+        continue;
+      }
+      try {
+        JarFile jar = new JarFile(file);
+        var inJar = findClassInputStreamsInJar(jar, relativePath);
+        if (inJar.isPresent()) {
+          return inJar;
+        } else {
+          jar.close();
+        }
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    return Optional.empty();
+  }
+
+  public static void main(String[] args) throws FileNotFoundException {
+    options = parseArgs(args);
+    OutputStream output;
+    if (options.outputFile == null || options.outputFile.equals("-")) {
+      output = System.out;
+    } else {
+      output = new FileOutputStream(options.outputFile);
+    }
+    var sourcePaths =
+        options.sourcePath != null ? options.sourcePath.split(File.pathSeparator) : new String[] {};
+    var classPaths =
+        options.classPath != null ? options.classPath.split(File.pathSeparator) : new String[] {};
+    var classStreams = new ArrayList<InputStream>();
+    var sourceFiles = new ArrayList<File>();
+    var notFound = new ArrayList<String>();
+    for (var qualifiedName : options.args) {
+      var found = false;
+      if (options.backend != Backend.ASM) {
+        var sources = findSourceFiles(qualifiedName, sourcePaths);
+        if (sources.isPresent()) {
+          sourceFiles.addAll(sources.get());
+          found = true;
+        }
+      }
+      if (options.backend != Backend.DOCLET && !found) {
+        var classes = findClassInputStreams(qualifiedName, classPaths);
+        if (classes.isPresent()) {
+          classStreams.addAll(classes.get());
+          found = true;
+        }
+      }
+      if (!found) {
+        notFound.add(qualifiedName);
+      }
+    }
+
+    if (!notFound.isEmpty()) {
+      System.err.println("Not found: " + notFound);
+      System.exit(1);
+    }
+
+    switch (options.backend) {
+      case DOCLET:
+        JsonUtil.writeJSON(runDoclet(sourceFiles, options), output);
+        break;
+      case ASM:
+        JsonUtil.writeJSON(AsmSummarizer.run(classStreams), output);
+        break;
+      case AUTO:
+        var decls = runDoclet(sourceFiles, options);
+        decls.addAll(AsmSummarizer.run(classStreams));
+        JsonUtil.writeJSON(decls, output);
+        break;
     }
   }
 }
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java
index feee922..9cf03d9 100644
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmSummarizer.java
@@ -4,75 +4,20 @@
 
 package com.github.dart_lang.jnigen.apisummarizer.disasm;
 
+import static com.github.dart_lang.jnigen.apisummarizer.util.ExceptionUtil.wrapCheckedException;
+
 import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
-import java.io.IOException;
-import java.util.Arrays;
+import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.jar.JarFile;
 import java.util.stream.Collectors;
-import java.util.zip.ZipEntry;
 import org.objectweb.asm.ClassReader;
 
-/** Class that summarizes Java APIs in compiled JARs using ASM not working yet. */
 public class AsmSummarizer {
 
-  private static class JarClass {
-    JarFile jar;
-    ZipEntry entry;
-
-    public JarClass(JarFile jar, ZipEntry entry) {
-      this.jar = jar;
-      this.entry = entry;
-    }
-  }
-
-  public static List<JarClass> findJarLocation(
-      String binaryName, List<JarFile> jars, String suffix) {
-    String path = binaryName.replace(".", "/");
-    for (var jar : jars) {
-      var classEntry = jar.getEntry(path + suffix);
-      if (classEntry != null) {
-        return List.of(new JarClass(jar, classEntry));
-      }
-      var dirPath = path.endsWith("/") ? path : path + "/";
-      var dirEntry = jar.getEntry(dirPath);
-      if (dirEntry != null && dirEntry.isDirectory()) {
-        return jar.stream()
-            .map(je -> (ZipEntry) je)
-            .filter(
-                entry -> {
-                  var name = entry.getName();
-                  return name.endsWith(suffix) && name.startsWith(dirPath);
-                })
-            .map(entry -> new JarClass(jar, entry))
-            .collect(Collectors.toList());
-      }
-    }
-    throw new RuntimeException("Cannot find class");
-  }
-
-  public static List<ClassDecl> run(String[] jarPaths, String[] classes) throws IOException {
-    var jars =
-        Arrays.stream(jarPaths)
-            .map(
-                filename -> {
-                  try {
-                    return new JarFile(filename);
-                  } catch (IOException e) {
-                    throw new RuntimeException(e);
-                  }
-                })
-            .collect(Collectors.toList());
-    return Arrays.stream(classes)
-        .flatMap(c -> findJarLocation(c, jars, ".class").stream())
-        .map(
-            classFile -> {
-              try {
-                return new ClassReader(classFile.jar.getInputStream(classFile.entry));
-              } catch (IOException e) {
-                throw new RuntimeException(e);
-              }
-            })
+  public static List<ClassDecl> run(ArrayList<InputStream> inputs) {
+    return inputs.stream()
+        .map(input -> wrapCheckedException(ClassReader::new, input))
         .flatMap(
             reader -> {
               var visitor = new AsmClassVisitor();
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/ElementBuilders.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/ElementBuilders.java
index b1af92d..bb52e46 100644
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/ElementBuilders.java
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/ElementBuilders.java
@@ -109,8 +109,6 @@
     if (tree == null) {
       return null;
     }
-    // Leave it as is, for now
-    // tree.accept(new TreeScanner(), j);
     return new JavaDocComment(tree.toString());
   }
 
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDoclet.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDoclet.java
index 0b6d98b..b1664e5 100644
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDoclet.java
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDoclet.java
@@ -4,14 +4,162 @@
 
 package com.github.dart_lang.jnigen.apisummarizer.doclet;
 
-import com.github.dart_lang.jnigen.apisummarizer.Main;
+import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
+import com.github.dart_lang.jnigen.apisummarizer.elements.Method;
+import com.github.dart_lang.jnigen.apisummarizer.elements.Package;
+import com.github.dart_lang.jnigen.apisummarizer.util.Log;
+import com.github.dart_lang.jnigen.apisummarizer.util.SkipException;
+import java.util.*;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.*;
+import javax.lang.model.util.ElementScanner9;
+import jdk.javadoc.doclet.Doclet;
 import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
 
-public class SummarizerDoclet extends SummarizerDocletBase {
+public class SummarizerDoclet implements Doclet {
+  private AstEnv utils;
+
+  @Override
+  public void init(Locale locale, Reporter reporter) {}
+
+  @Override
+  public String getName() {
+    return "ApiSummarizer";
+  }
+
+  @Override
+  public Set<? extends Option> getSupportedOptions() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public SourceVersion getSupportedSourceVersion() {
+    return SourceVersion.RELEASE_11;
+  }
+
+  public static List<ClassDecl> getClasses() {
+    return classes;
+  }
+
+  public static List<ClassDecl> classes;
+
   @Override
   public boolean run(DocletEnvironment docletEnvironment) {
-    var result = super.run(docletEnvironment);
-    Main.writeAll(types);
-    return result;
+    Log.timed("Initializing doclet");
+    utils = AstEnv.fromEnvironment(docletEnvironment);
+    SummarizingScanner scanner = new SummarizingScanner();
+    docletEnvironment.getSpecifiedElements().forEach(e -> scanner.scan(e, new SummaryCollector()));
+    classes = scanner.types;
+    return true;
+  }
+
+  public static class SummaryCollector {
+    Stack<Package> packages = new Stack<>();
+    Stack<ClassDecl> types = new Stack<>();
+    Method method;
+  }
+
+  public class SummarizingScanner extends ElementScanner9<Void, SummaryCollector> {
+    List<Package> packages = new ArrayList<>();
+    List<ClassDecl> types = new ArrayList<>();
+    ElementBuilders builders = new ElementBuilders(utils);
+
+    // Each element in collector is a stack
+    // which is used to get topmost element
+    // and append the child to it.
+    // Eg: A variable element is always appended to topmost
+    // class
+    @Override
+    public Void scan(Element e, SummaryCollector collector) {
+      return super.scan(e, collector);
+    }
+
+    @Override
+    public Void visitPackage(PackageElement e, SummaryCollector collector) {
+      Log.verbose("Visiting package: %s", e.getQualifiedName());
+      collector.packages.push(new Package());
+      System.out.println("package: " + e.getQualifiedName());
+      var result = super.visitPackage(e, collector);
+      var collectedPackage = collector.packages.pop();
+      packages.add(collectedPackage);
+      return result;
+    }
+
+    @Override
+    public Void visitType(TypeElement e, SummaryCollector collector) {
+      if (!collector.types.isEmpty()) {
+        return null;
+      }
+      Log.verbose("Visiting class: %s, %s", e.getQualifiedName(), collector.types);
+      switch (e.getKind()) {
+        case CLASS:
+        case INTERFACE:
+        case ENUM:
+          try {
+            var cls = builders.classDecl(e);
+            collector.types.push(cls);
+            super.visitType(e, collector);
+            types.add(collector.types.pop());
+          } catch (SkipException skip) {
+            Log.always("Skip type: %s", e.getQualifiedName());
+          }
+          break;
+        case ANNOTATION_TYPE:
+          Log.always("Skip annotation type: %s", e.getQualifiedName());
+          break;
+      }
+      return null;
+    }
+
+    @Override
+    public Void visitVariable(VariableElement e, SummaryCollector collector) {
+      var vk = e.getKind();
+      var cls = collector.types.peek();
+      switch (vk) {
+        case ENUM_CONSTANT:
+          cls.values.add(e.getSimpleName().toString());
+          break;
+        case FIELD:
+          cls.fields.add(builders.field(e));
+          break;
+        case PARAMETER:
+          if (collector.method == null) {
+            throw new RuntimeException("Parameter encountered outside executable element");
+          }
+          var method = collector.method;
+          method.params.add(builders.param(e));
+          break;
+        default:
+          System.out.println("Unknown type of variable element: " + vk);
+      }
+      return null;
+    }
+
+    @Override
+    public Void visitExecutable(ExecutableElement element, SummaryCollector collector) {
+      var cls = collector.types.peek();
+      switch (element.getKind()) {
+        case METHOD:
+        case CONSTRUCTOR:
+          try {
+            var method = builders.method(element);
+            collector.method = method;
+            super.visitExecutable(element, collector);
+            collector.method = null;
+            cls.methods.add(method);
+          } catch (SkipException skip) {
+            Log.always("Skip method: %s", element.getSimpleName());
+          }
+          break;
+        case STATIC_INIT:
+          cls.hasStaticInit = true;
+          break;
+        case INSTANCE_INIT:
+          cls.hasInstanceInit = true;
+          break;
+      }
+      return null;
+    }
   }
 }
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDocletBase.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDocletBase.java
deleted file mode 100644
index 7d9bade..0000000
--- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/doclet/SummarizerDocletBase.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.github.dart_lang.jnigen.apisummarizer.doclet;
-
-import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
-import com.github.dart_lang.jnigen.apisummarizer.elements.Method;
-import com.github.dart_lang.jnigen.apisummarizer.elements.Package;
-import com.github.dart_lang.jnigen.apisummarizer.util.Log;
-import com.github.dart_lang.jnigen.apisummarizer.util.SkipException;
-import java.util.*;
-import javax.lang.model.SourceVersion;
-import javax.lang.model.element.*;
-import javax.lang.model.util.ElementScanner9;
-import jdk.javadoc.doclet.Doclet;
-import jdk.javadoc.doclet.DocletEnvironment;
-import jdk.javadoc.doclet.Reporter;
-
-public class SummarizerDocletBase implements Doclet {
-  private AstEnv utils;
-
-  @Override
-  public void init(Locale locale, Reporter reporter) {}
-
-  @Override
-  public String getName() {
-    return "ApiSummarizer";
-  }
-
-  @Override
-  public Set<? extends Option> getSupportedOptions() {
-    return Collections.emptySet();
-  }
-
-  @Override
-  public SourceVersion getSupportedSourceVersion() {
-    return SourceVersion.RELEASE_11;
-  }
-
-  public static List<ClassDecl> types;
-
-  @Override
-  public boolean run(DocletEnvironment docletEnvironment) {
-    Log.timed("Initializing doclet");
-    utils = AstEnv.fromEnvironment(docletEnvironment);
-    SummarizingScanner p = new SummarizingScanner();
-    docletEnvironment.getSpecifiedElements().forEach(e -> p.scan(e, new SummaryCollector()));
-    types = p.types;
-    return true;
-  }
-
-  public static class SummaryCollector {
-    Stack<Package> packages = new Stack<>();
-    Stack<ClassDecl> types = new Stack<>();
-    Method method;
-  }
-
-  public class SummarizingScanner extends ElementScanner9<Void, SummaryCollector> {
-    List<Package> packages = new ArrayList<>();
-    List<ClassDecl> types = new ArrayList<>();
-    ElementBuilders builders = new ElementBuilders(utils);
-
-    // Each element in collector is a stack
-    // which is used to get topmost element
-    // and append the child to it.
-    // Eg: A variable element is always appended to topmost
-    // class
-    @Override
-    public Void scan(Element e, SummaryCollector collector) {
-      return super.scan(e, collector);
-    }
-
-    @Override
-    public Void visitPackage(PackageElement e, SummaryCollector collector) {
-      Log.verbose("Visiting package: %s", e.getQualifiedName());
-      collector.packages.push(new Package());
-      System.out.println("package: " + e.getQualifiedName());
-      var result = super.visitPackage(e, collector);
-      var collectedPackage = collector.packages.pop();
-      packages.add(collectedPackage);
-      return result;
-    }
-
-    @Override
-    public Void visitType(TypeElement e, SummaryCollector collector) {
-      if (!collector.types.isEmpty()) {
-        return null;
-      }
-      Log.verbose("Visiting class: %s, %s", e.getQualifiedName(), collector.types);
-      switch (e.getKind()) {
-        case CLASS:
-        case INTERFACE:
-        case ENUM:
-          try {
-            var cls = builders.classDecl(e);
-            collector.types.push(cls);
-            super.visitType(e, collector);
-            types.add(collector.types.pop());
-          } catch (SkipException skip) {
-            Log.always("Skip type: %s", e.getQualifiedName());
-          }
-          break;
-        case ANNOTATION_TYPE:
-          Log.always("Skip annotation type: %s", e.getQualifiedName());
-          break;
-      }
-      return null;
-    }
-
-    @Override
-    public Void visitVariable(VariableElement e, SummaryCollector collector) {
-      var vk = e.getKind();
-      var cls = collector.types.peek();
-      switch (vk) {
-        case ENUM_CONSTANT:
-          cls.values.add(e.getSimpleName().toString());
-          break;
-        case FIELD:
-          cls.fields.add(builders.field(e));
-          break;
-        case PARAMETER:
-          if (collector.method == null) {
-            throw new RuntimeException("Parameter encountered outside executable element");
-          }
-          var method = collector.method;
-          method.params.add(builders.param(e));
-          break;
-        default:
-          System.out.println("Unknown type of variable element: " + vk);
-      }
-      return null;
-    }
-
-    @Override
-    public Void visitExecutable(ExecutableElement element, SummaryCollector collector) {
-      var cls = collector.types.peek();
-      switch (element.getKind()) {
-        case METHOD:
-        case CONSTRUCTOR:
-          try {
-            var method = builders.method(element);
-            collector.method = method;
-            super.visitExecutable(element, collector);
-            collector.method = null;
-            cls.methods.add(method);
-          } catch (SkipException skip) {
-            Log.always("Skip method: %s", element.getSimpleName());
-          }
-          break;
-        case STATIC_INIT:
-          cls.hasStaticInit = true;
-          break;
-        case INSTANCE_INIT:
-          cls.hasInstanceInit = true;
-          break;
-      }
-      return null;
-    }
-  }
-}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/ExceptionUtil.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/ExceptionUtil.java
new file mode 100644
index 0000000..3ed92a5
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/ExceptionUtil.java
@@ -0,0 +1,20 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+public class ExceptionUtil {
+  @FunctionalInterface
+  public interface FunctionThrowingException<T, R> {
+    R function(T value) throws Exception;
+  }
+
+  /**
+   * Wraps a function throwing a checked exception and throws all checked exceptions as runtime
+   * exceptions.
+   */
+  public static <T, R> R wrapCheckedException(FunctionThrowingException<T, R> function, T value) {
+    try {
+      return function.function(value);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JsonUtil.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JsonUtil.java
new file mode 100644
index 0000000..cf7e8a4
--- /dev/null
+++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JsonUtil.java
@@ -0,0 +1,24 @@
+package com.github.dart_lang.jnigen.apisummarizer.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+public class JsonUtil {
+  public static void writeJSON(List<ClassDecl> classes, OutputStream output) {
+    var mapper = new ObjectMapper();
+    Log.timed("Writing JSON");
+    mapper.enable(SerializationFeature.INDENT_OUTPUT);
+    mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+    try {
+      mapper.writeValue(output, classes);
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+    Log.timed("Finished");
+  }
+}
diff --git a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/DocletSummarizerTests.java b/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/DocletSummarizerTests.java
deleted file mode 100644
index ff130b4..0000000
--- a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/DocletSummarizerTests.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.github.dart_lang.jnigen.apisummarizer;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class DocletSummarizerTests {
-  private List<ClassDecl> parsedDecls;
-  private final Map<String, ClassDecl> classesByName = new HashMap<>();
-
-  @Before
-  public void setUp() {
-    var opts = new Main.SummarizerOptions();
-    opts.sourcePaths = "src/test/resources/";
-    // javadoc tool API is quite inflexible, in that we cannot pass an doclet object, but a class
-    // So any state we want to access from it has to be either serialized or saved in static fields.
-    // This means we lose lot of control over loading of files etc..
-    // Here, TestDoclet simply stores the result in a static variable which we can get and check
-    // later.
-    Main.runDocletWithClass(TestDoclet.class, List.of("com.example.Example"), opts);
-    parsedDecls = TestDoclet.getClassDecls();
-    for (var decl : parsedDecls) {
-      classesByName.put(decl.binaryName, decl);
-    }
-  }
-
-  @Test
-  public void checkNumberOfClasses() {
-    Assert.assertEquals(2, parsedDecls.size());
-  }
-
-  @Test
-  public void checkNamesOfClasses() {
-    var names = parsedDecls.stream().map(decl -> decl.binaryName).collect(Collectors.toSet());
-    assertTrue(names.contains("com.example.Example"));
-    assertTrue(names.contains("com.example.Example$Aux"));
-  }
-
-  @Test
-  public void checkNumberOfFieldsAndMethods() {
-    var example = classesByName.get("com.example.Example");
-    assertEquals("Example", example.simpleName);
-    assertEquals(3, example.fields.size());
-    assertEquals(3, example.methods.size());
-  }
-}
diff --git a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/JSONComparisonTest.java b/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/JSONComparisonTest.java
new file mode 100644
index 0000000..4d15ad2
--- /dev/null
+++ b/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/JSONComparisonTest.java
@@ -0,0 +1,41 @@
+package com.github.dart_lang.jnigen.apisummarizer;
+
+import java.io.File;
+import java.io.IOException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JSONComparisonTest {
+  static final File exampleClassJsonOutput =
+      new File("src/test/resources/exampleClassSummary.json");
+
+  @SuppressWarnings("SameParameterValue")
+  private int gitDiff(File a, File b) throws IOException, InterruptedException {
+    return gitDiff(a.getPath(), b.getPath());
+  }
+
+  private int gitDiff(String a, String b) throws IOException, InterruptedException {
+    String colorSupport = "--color=never";
+    if (System.console() != null && System.getenv().get("TERM") != null) {
+      colorSupport = "--color=always";
+    }
+    String[] compareCommand = {
+      "git", "diff", colorSupport, "--no-index", a, b,
+    };
+    var compare = Runtime.getRuntime().exec(compareCommand);
+    compare.getErrorStream().transferTo(System.err);
+    compare.getInputStream().transferTo(System.out);
+    return compare.waitFor();
+  }
+
+  @Test
+  public void testExampleSummary() throws IOException, InterruptedException {
+    var tempFile = File.createTempFile("summarizer_test", ".json");
+    Main.main(
+        new String[] {
+          "-s", "src/test/resources", "com.example.Example", "-o", tempFile.getPath(),
+        });
+    int comparison = gitDiff(exampleClassJsonOutput, tempFile);
+    Assert.assertEquals(0, comparison);
+  }
+}
diff --git a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/TestDoclet.java b/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/TestDoclet.java
deleted file mode 100644
index 5a3949b..0000000
--- a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/TestDoclet.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.github.dart_lang.jnigen.apisummarizer;
-
-import com.github.dart_lang.jnigen.apisummarizer.doclet.SummarizerDocletBase;
-import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl;
-import java.util.List;
-import jdk.javadoc.doclet.DocletEnvironment;
-
-public class TestDoclet extends SummarizerDocletBase {
-  @Override
-  public boolean run(DocletEnvironment docletEnvironment) {
-    return super.run(docletEnvironment);
-  }
-
-  public static List<ClassDecl> getClassDecls() {
-    return types;
-  }
-}
diff --git a/pkgs/jnigen/java/src/test/resources/exampleClassSummary.json b/pkgs/jnigen/java/src/test/resources/exampleClassSummary.json
new file mode 100644
index 0000000..81fabd7
--- /dev/null
+++ b/pkgs/jnigen/java/src/test/resources/exampleClassSummary.json
@@ -0,0 +1,139 @@
+[ {
+  "declKind" : "CLASS",
+  "modifiers" : [ "public" ],
+  "simpleName" : "Example",
+  "binaryName" : "com.example.Example",
+  "packageName" : "com.example",
+  "methods" : [ {
+    "name" : "<init>",
+    "params" : [ {
+      "name" : "instanceField",
+      "type" : {
+        "shorthand" : "int",
+        "kind" : "PRIMITIVE",
+        "type" : {
+          "name" : "int"
+        }
+      }
+    } ],
+    "returnType" : {
+      "shorthand" : "void",
+      "kind" : "PRIMITIVE",
+      "type" : {
+        "name" : "void"
+      }
+    }
+  }, {
+    "modifiers" : [ "static" ],
+    "name" : "getStaticField",
+    "returnType" : {
+      "shorthand" : "java.lang.String",
+      "kind" : "DECLARED",
+      "type" : {
+        "binaryName" : "java.lang.String",
+        "simpleName" : "String"
+      }
+    }
+  }, {
+    "name" : "getInstanceField",
+    "returnType" : {
+      "shorthand" : "int",
+      "kind" : "PRIMITIVE",
+      "type" : {
+        "name" : "int"
+      }
+    }
+  } ],
+  "fields" : [ {
+    "modifiers" : [ "static", "final" ],
+    "name" : "staticFinalField",
+    "type" : {
+      "shorthand" : "boolean",
+      "kind" : "PRIMITIVE",
+      "type" : {
+        "name" : "boolean"
+      }
+    },
+    "defaultValue" : true
+  }, {
+    "modifiers" : [ "static" ],
+    "name" : "staticField",
+    "type" : {
+      "shorthand" : "java.lang.String",
+      "kind" : "DECLARED",
+      "type" : {
+        "binaryName" : "java.lang.String",
+        "simpleName" : "String"
+      }
+    }
+  }, {
+    "name" : "instanceField",
+    "type" : {
+      "shorthand" : "int",
+      "kind" : "PRIMITIVE",
+      "type" : {
+        "name" : "int"
+      }
+    }
+  } ],
+  "superclass" : {
+    "shorthand" : "java.lang.Object",
+    "kind" : "DECLARED",
+    "type" : {
+      "binaryName" : "java.lang.Object",
+      "simpleName" : "Object"
+    }
+  },
+  "hasStaticInit" : false,
+  "hasInstanceInit" : false
+}, {
+  "declKind" : "CLASS",
+  "modifiers" : [ "static", "public" ],
+  "simpleName" : "Aux",
+  "binaryName" : "com.example.Example$Aux",
+  "parentName" : "com.example.Example",
+  "packageName" : "com.example",
+  "methods" : [ {
+    "modifiers" : [ "public" ],
+    "name" : "<init>",
+    "returnType" : {
+      "shorthand" : "void",
+      "kind" : "PRIMITIVE",
+      "type" : {
+        "name" : "void"
+      }
+    }
+  }, {
+    "modifiers" : [ "static" ],
+    "name" : "getAnExample",
+    "returnType" : {
+      "shorthand" : "com.example.Example",
+      "kind" : "DECLARED",
+      "type" : {
+        "binaryName" : "com.example.Example",
+        "simpleName" : "Example"
+      }
+    }
+  } ],
+  "fields" : [ {
+    "modifiers" : [ "static" ],
+    "name" : "nothing",
+    "type" : {
+      "shorthand" : "int",
+      "kind" : "PRIMITIVE",
+      "type" : {
+        "name" : "int"
+      }
+    }
+  } ],
+  "superclass" : {
+    "shorthand" : "com.example.Example",
+    "kind" : "DECLARED",
+    "type" : {
+      "binaryName" : "com.example.Example",
+      "simpleName" : "Example"
+    }
+  },
+  "hasStaticInit" : false,
+  "hasInstanceInit" : false
+} ]
\ No newline at end of file
diff --git a/pkgs/jnigen/tool/pr_checks.dart b/pkgs/jnigen/tool/pr_checks.dart
index 0b8ad52..f03f318 100644
--- a/pkgs/jnigen/tool/pr_checks.dart
+++ b/pkgs/jnigen/tool/pr_checks.dart
@@ -76,6 +76,18 @@
     ..chainCommand("dart", ["test", "-j", "1"]);
   unawaited(jniAnalyze.run().then((f) => jniTest.run()));
 
+  final javaFiles = Directory.fromUri(jnigenPath.resolve("java/"))
+      .listSync(recursive: true)
+      .where((file) => file.path.endsWith(".java"))
+      .map((file) => file.path)
+      .toList();
+
+  final checkJavaFormat = Runner("Check java format", jnigenPath)
+    ..chainCommand(
+        "google-java-format", ["-n", "--set-exit-if-changed", ...javaFiles]);
+
+  unawaited(checkJavaFormat.run());
+
   final ffigenBindingsPath = getRepositoryRoot()
       .resolve("jni/lib/src/third_party/jni_bindings_generated.dart");
   final ffigenBindings = File.fromUri(ffigenBindingsPath);