Skip to content

chore(build): Compile with a JDK 21 toolchain and upgrade Mockito and Error Prone - #6242

Draft
markushi wants to merge 13 commits into
mainfrom
build/jdk-21
Draft

markushi wants to merge 13 commits into
mainfrom
build/jdk-21

Conversation

@markushi

@markushi markushi commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

📜 Description

Switches the build and CI from JDK 17 to JDK 21, and upgrades the tools that the new JDK forces us to move: Mockito, Error Prone and NullAway. Compilation and tests run on a JDK 21 toolchain. The bytecode we publish still targets Java 8 (Spring Boot 4 / Spring 7 modules target 17).

Just changing to 21 has a few ripple effects, so there's quite a broad set of changes involved across the codebase.

JDK 21

  • All CI workflows now use JDK 21. CONTRIBUTING.md is updated.
  • The build fails fast with a clear message when Gradle requires a JDK older than 21.
  • A JDK 21 toolchain in the new io.sentry.jvm-targets convention plugin (build-logic) compiles the code and runs the tests, so a contributor on a newer JDK gets the same output as CI. The foojay resolver fetches a JDK 21 when no local one matches.
  • JVM library modules now compile with --release <target>. A newer JDK could otherwise let APIs such as List.getFirst() get into Java 8 bytecode without a compile error, reducing the risk of introducing breaking changes for Android.
  • The Java 8 and Java 17 pins apply to main compilation only, so test code - which is never published - simply follows the toolchain instead of being patched back up per task.

Mockito 4.8 → 5.24, mockito-kotlin 4.1 → 6.4

  • Mockito 4.8 cannot mock on JDK 21 unless we set net.bytebuddy.experimental.
  • mockito-inline is replaced with mockito-core. Mockito 5 uses the inline mock maker by default.
  • Mockito 5 and mockito-kotlin 5+ are built for Java 11. Thus test code (never published) now compiles for the toolchain's Java 21 rather than main's Java 8.
  • 6 tests use anyVararg(), because in Mockito 5 any() on a varargs parameter matches exactly one argument.

Error Prone 2.11 → 2.50, NullAway 0.9.5 → 0.14.2, Gradle plugin 3.0.1 → 5.1.1

  • The old versions crash on JDK 21: Error Prone 2.11 fails with NoSuchMethodError: TreeMaker.Select, and NullAway 0.9.5 cannot load against a newer Error Prone. Neither bump can be split out of this PR.
  • The new NullAway found 15 errors, fixed here:
    • Scope.executeBeforeBreadcrumb could throw an NPE. This happens when the callback returns null and the close() of the reentrancy guard then throws.
    • Spring 7 is annotated with JSpecify. Our Spring 7 advices dereferenced MethodInvocation.getThis(), which is null for static methods. They now pass a null target class to AopUtils.getMostSpecificMethod. invoke() is now @Nullable, the same as the Spring 7 contract.
    • SentryAppender (logback) passes an empty array instead of null varargs. The behavior is the same.
  • The new Error Prone version and the JDK 21 javac lints gave 114 new warnings:
    • Simple ones are fixed: pattern-matching instanceof (Java 17 modules only), Locale.ROOT for toUpperCase, explicit long → double casts, /** on non-Javadoc comments, and modifiers on effectively-private members.
    • The others are suppressed in the code, each with a one-line reason: identity comparisons with sentinels, rrweb enum ordinals, JdkObsolete replacements that need Java 10+, this-escape in public non-final classes, serial, and the vendored Gson.

Samples

  • The samples follow the toolchain too, with one exception: the five modules the Spring Boot 2.x matrix builds keep their published Java level (11, and 8 for sentry-samples-spring), as otherwise those would stop working because of Unsupported class file major version 65 errors.

Other

  • SentryEnvelopeItemTest."fromAttachment with file SecurityManager denies read access" installs a SecurityManager. Since JDK 18 this needs -Djava.security.manager=allow, which the sentry tests now set.
  • The Build job name no longer carries a Java version that goes stale at the next bump.

💡 Motivation and Context

Preparation for SDK 37 support (for example ActivityManager.registerAnrWarningListener). Robolectric 4.17 is the first version that supports SDK 37, and SDK 37 needs Java 21, so JDK 21 has to land first.

💚 How did you test it?

Tests

📝 Checklist

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

  • Robolectric 4.15 → 4.17 with the SDK 37 sandbox, including the 13 re-rendered snapshots
  • Scope the Java pins of the five samples outside the system tests (sentry-samples-android, -ktor-client, -openfeign, -servlet, -netflix-dgs) to main compilation, so every sample treats its test code the same way
  • Fix the flaky InternalSentrySdkTest, which reads session.json right after an asynchronous write

#skip-changelog

@sentry

sentry Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.60.0 (1) release

⚙️ sentry-android Build Distribution Settings

@markushi markushi changed the title build: Switch to JDK 21 and upgrade Mockito, Robolectric and Error Prone chore(build): Switch to JDK 21 and upgrade Mockito, Robolectric and Error Prone Oct 9, 2026

@runningcode runningcode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm all for this! i gave it a first round of reviews, ping me for another round!

Comment thread buildSrc/build.gradle.kts
Comment thread gradle/libs.versions.toml
Comment thread gradle/libs.versions.toml
try {
final double enqueuedTimeSeconds = Double.parseDouble(enqueuedTimeStr);
final double nowSeconds = DateUtils.millisToSeconds(System.currentTimeMillis());
final double nowSeconds = DateUtils.millisToSeconds((double) System.currentTimeMillis());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated to the PR what's going on here? why are we using a double? i don't see a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's super odd. I assume it's because the incoming enqueuedTimeStr is already in seconds, so we go along with this format here.
Many timestamps within the sentry universe are transmitted as floating point numbers: seconds since unix time (e.g. event.timestamp). I guess this is due to the python's time.time() giving you exactly that.

final @NotNull Object bean, @NotNull final String beanName) throws BeansException {
if (bean instanceof SentryOptions) {
final SentryOptions options = (SentryOptions) bean;
if (bean instanceof SentryOptions options) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool java syntax! does this at all affect the minimum java for this code? what even is the minimum java here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it does. But as this module is already pinned to Java 17 features anyway, it will fine here.
It all comes down to us specifying -Werror. Here's the full log:

SentryInitBeanPostProcessor.java:45: warning: [PatternMatchingInstanceof] This code can be simplified to use a pattern-matching instanceof.
error: warnings found and -Werror specified

id("com.apollographql.apollo") version "4.1.1"
}

// Not published. Mockito 5 and mockito-kotlin 5+ are built for Java 11.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it isn't published why not bump to 21?

Comment thread build.gradle.kts Outdated
// share the Java 8 target of the published code; it compiles for the build JDK instead.
// Registered once each plugin is applied, so this afterEvaluate runs after the plugins and the
// module have pinned the JVM target of all compile tasks.
listOf("java", "com.android.library", "com.android.application").forEach { pluginId ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't be adding any logic to the build.gradle.kts file. this should go in a convention plugin like we already have in build-logic

also don't we already have some code that sets this same type of logic to jdk 11 ? why do we set it to 21 here?

also: this seems like we should be using gradle's toolchains support here. https://docs.gradle.org/current/userguide/toolchains.html

Comment thread build.gradle.kts Outdated
targetCompatibility = JavaVersion.VERSION_21.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile>().matching(isTestTask).configureEach {
compilerOptions.jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, we should probably be using toolchains here: https://docs.gradle.org/current/userguide/toolchains.html

Comment thread build.gradle.kts Outdated
// JDK's, so newer JDK APIs (e.g. List.getFirst()) can't leak into published bytecode.
val javaExtension = the<JavaPluginExtension>()
tasks.named<JavaCompile>("compileJava") {
options.release.set(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread build.gradle.kts
Comment thread sentry/src/main/java/io/sentry/SynchronizedCollection.java
@markushi markushi changed the title chore(build): Switch to JDK 21 and upgrade Mockito, Robolectric and Error Prone chore(build): Compile with a JDK 21 toolchain and upgrade Mockito and Error Prone Oct 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants