Skip to content

Remove the Guava dependency - #352

Open
royteeuwen wants to merge 11 commits into
dreamhead:masterfrom
royteeuwen:remove-guava-dependency
Open

Remove the Guava dependency#352
royteeuwen wants to merge 11 commits into
dreamhead:masterfrom
royteeuwen:remove-guava-dependency

Conversation

@royteeuwen

Copy link
Copy Markdown

Removes the Guava dependency from all four modules. Rationale, sources and the open questions are in #351 — please read that first, since this PR is only worth reviewing if you want the change at all.

Short version of the why: Guava types appear in Moco's public signatures, and on AEM LTS / AEMaaCS com.google.common.* is no longer available as a public package. That already forced a workaround in a downstream Moco integration (wttech/aem-stubs#64), where a Groovy static-star import of Moco had to be replaced by a reflection loop because the static-star import triggers cross-bundle resolution of the Guava types in those signatures. Shading would not have helped; only taking Guava out of the API does.

Timing: 2.0.0 is unreleased and already breaking (socket API removal, Java 17 baseline), so this costs nothing extra in semver terms. After 2.0.0 ships it would need 3.0.0.

Result

  • No com.google.common reference anywhere in the sources; no Guava in any module's dependency tree.
  • Standalone jar 7,638,774 → 7,424,807 bytes (−214 KB). Uber jar 14,639,293 → 11,583,355 bytes (−3 MB). Guava classes in the shipped standalone jar: 178 → 0.
  • 830 tests green, and :moco-runner:proguardCheck passes.

Reviewing

11 commits, one per step, each compiling and passing the full suite on its own — so it is much easier to read commit by commit than as a single diff.

If the API change is the sticking point, it splits cleanly:

  • Commits 1–8 (added guava-free util scaffoldingreplaced remaining guava Iterables.isEmpty) are internals only, no public signature changes.
  • Commits 9–11 (replaced guava Immutable collections, removed the guava dependency, renamed the ordered map helpers) contain the API widening, the dependency removal, the docs and the release notes.

The first group stands on its own if you would rather not take the second. Happy to reopen this as two PRs on that boundary.

Worth knowing before you review

  • MediaType becomes com.github.dreamhead.moco.util.MediaType. Its toString() is the Content-Type on the wire, so it was differential-tested against Guava 33.6.0-jre and matches in 34 of 35 cases; the exception is that trailing whitespace is tolerated rather than rejected, which is documented and cannot introduce a throw where Guava succeeded. Non-charset parameters are preserved because a recorded multipart request round-trips its boundary through parse.
  • Map iteration order is load-bearing. Map.of/Map.copyOf randomise iteration order once per JVM, and headers and queries are rendered in that order — converting HttpRequestDumperTest to Map.of genuinely made it pass or fail per run. Those paths use Maps.orderedCopyOf/Maps.toOrderedMap, named deliberately unlike the JDK methods they must not be swapped for. There is a test pinning header and query order, and the suite was run four times in fresh JVMs.
  • Header constants keep Guava's canonical casing (Content-Length, not Netty's lower-case content-length), because lookups are exact-key and HttpMessage.hasContent() depends on one. Pinned by a test.
  • org.jspecify:jspecify was arriving only transitively via Guava and two @NonNull annotations need it; it is now declared explicitly as compileOnly. Open to dropping the annotations instead — see question 3 in Remove the Guava dependency in 2.0.0 #351.
  • -dontwarn com.google.** in proguard.pro was also masking json-path's optional Gson backends. Removing it un-suppressed 75 unresolved references and broke the ProGuard gate, so it is replaced with narrow rules for com.google.gson and org.jspecify.
  • A checkNoGuava task is wired into check, verified to fail when an import is reintroduced.

Breaking changes are written up in moco-doc/ReleaseNotes.md under a new 2.0.0 section, including the MocoMonitor @Subscribe removal, which fails silently for anyone registering a MocoMonitor on their own Guava EventBus.

Additive only: Guava is still on the classpath and no existing call site changed.

New MediaType replaces com.google.common.net.MediaType. Its toString() is a wire
value, so the rendering is pinned by tests against values captured from Guava
33.6.0-jre. Parameters other than charset are preserved because a recorded
multipart request round-trips its boundary back onto the wire.

Maps.copyOf and Maps.toUnmodifiableMap preserve insertion order. Map.copyOf is
not usable here: it randomises iteration order once per JVM run, and header and
query maps are rendered in iteration order. The collector also keeps Guava's
IllegalArgumentException on duplicate keys, reachable from a repeated form field.

HttpHeaders constants keep canonical casing rather than reusing Netty's
lower-case HttpHeaderNames, which would break HttpMessage.hasContent().
TypeToken.of(getClass()).getRawType() is just getClass(), verified to be
reference-identical including for anonymous subclasses.

Failover collects its statuses instead of using Set.of, which would reject the
repeated status in failover(file, 400, 400) where ImmutableSet.copyOf silently
de-duplicated.

nullToEmpty around String.replaceFirst was dead - replaceFirst never returns
null.

Removed @subscribe from MocoMonitor. Moco has had no EventBus since 8fa72e1
(2013), so this is inert internally. It is not inert for consumers: Guava's
EventBus resolves @subscribe across interfaces, so anyone registering a
MocoMonitor on their own bus had these four methods wired up, and after this
they are silently never registered. Undocumented and incoherent as an extension
point - onMessageArrived and onUnexpectedMessage both subscribe to Request, so
one post fires both - but it needs a release note.

StartTask logs elapsed milliseconds instead of Guava's auto-scaled Stopwatch
rendering.
Needed to stay a real type rather than inlined string building, because
DefaultHttpMessage and RestBaseSetting expose protected toStringHelper() and
subclasses extend the chain through super.toStringHelper().add(...).

Rendering verified byte-identical to Guava for empty, null, omitNullValues,
String[], int[], nested arrays, maps and chained cases.
checkNotNull becomes Objects.requireNonNull, which throws the same
NullPointerException with the same message. Only the two-argument form was ever
used, so no template/varargs overload is needed.

checkArgument and isNullOrEmpty move to the existing moco util classes, which
already followed this shim pattern.
Header constants keep Guava's canonical casing, pinned by a test. Netty's
HttpHeaderNames are lower-case and would have broken the exact-key lookups in
getHeader and HttpMessage.hasContent().

MediaType.toString() is the Content-Type on the wire, so it was differentially
tested against Guava 33.6.0-jre across the constants, parse round-trips,
type/subtype/charset accessors and withCharset. The only divergence is that
trailing whitespace is now tolerated rather than rejected, which is documented
and cannot introduce a throw where Guava succeeded.

FileContentType drops two toJavaUtil() calls now that charset() returns
java.util.Optional. HttpDumpers replaces Guava's is() with type and subtype
equality, which is what it reduced to for a parameterless FORM_DATA.
ByteStreams/CharStreams become InputStream.readAllBytes(); both the old and new
forms use the default charset. Files.asCharSink(APPEND) becomes
Files.writeString with CREATE/WRITE/APPEND. CharSource.wrap().readFirstLine()
becomes BufferedReader.readLine(), which also yields null for empty input.

Closeables.closeQuietly gives way to try-with-resources. Both call sites wrap a
ByteArrayInputStream from MessageContent.toInputStream(), whose close() is a
no-op, so swallowing the close failure was never doing anything.

util.Resources keeps Guava's lookup order - thread context class loader in
preference to this class's own - and throws rather than returning null. That
ordering matters because Moco runs embedded in other projects' test frameworks
and from the shrunk standalone jar.
…alents

Splitter needed care. String.split takes a regex, so Splitter.on('.') had to
become split("\\.", -1) rather than split("."), which matches any character. The
-1 limit is required everywhere because String.split drops trailing empty
strings while Guava's Splitter keeps them, and SSE parsing delimits events on
blank lines.

Multimaps become plain Map-of-collection fields. Semantics differ per site and
had to be preserved: ActionMonitor keeps List semantics so a repeated query
parameter is not collapsed, while ChannelSessionGroup and WatcherService keep
Set semantics. WatcherService also prunes a key when its last value goes,
because Guava did that and both the containsKey check and the isEmpty check that
stops the service depend on it. ChannelSessionGroup moves to ConcurrentHashMap
with newKeySet, which is safer than the previous synchronized view that was
iterated outside any lock.

Maps.transformEntries was a lazy view immediately copied, so the laziness was
unused; the anonymous EntryTransformer collapses into a stream collect.

HttpDumpers loses a pointless intermediate collect, and String.join replaces
Joiner - the only difference being that a null element now renders as "null"
instead of raising NullPointerException on an error path.
This is the breaking part of the migration. Public signatures widen from
ImmutableMap/ImmutableList to Map/List, which is binary-incompatible - the erased
return type changes - and source-breaking for anyone implementing HttpMessage,
HttpRequest, Content or SettingFetcher, or assigning getHeaders() to an
ImmutableMap variable.

Map.of and Map.copyOf are deliberately avoided on header, query, form, cookie and
template-variable paths. They randomise iteration order once per JVM run, and
those maps are rendered in iteration order. This was not theoretical: converting
HttpRequestDumperTest.should_dump_queries to Map.of made it pass or fail
depending on the run. It now builds a LinkedHashMap, and a companion test asserts
that header and query order survives. The suite was run four times in fresh JVMs
to confirm nothing else is salt-dependent.

Static lookup tables that are only ever read by key keep Map.of / Map.ofEntries,
since order genuinely does not matter there.

List.copyOf takes a Collection where ImmutableList.copyOf accepted any Iterable,
so CollectionHandler streams instead, and mount() uses List.of on its varargs
array.

Test helpers take List<Map.Entry<String, String>> rather than a Map, because some
tests deliberately send a repeated header name - of("foo", "bar", "foo", "bar2") -
which Map.of rejects.
Dropped from all four modules and from the version catalog. The standalone jar
shrinks by 214KB and the uber jar by about 3MB.

Two things surfaced only once the dependency was actually gone.

org.jspecify:jspecify was reaching moco-core transitively through Guava, and two
@nonnull annotations depended on it. It is now declared explicitly as compileOnly,
so it stays out of the published POM and the shrunk jar.

The proguard rule -dontwarn com.google.** was also covering json-path's optional
Gson backends, which have nothing to do with Guava. Deleting it outright
un-suppressed 75 unresolved references, so it is replaced with narrow rules for
com.google.gson and org.jspecify that say what they are for.

Added a checkNoGuava task wired into check, verified to fail when an import is
reintroduced, so this cannot regress one file at a time.
Maps.copyOf and Maps.toUnmodifiableMap shared their names with Map.copyOf and
Collectors.toUnmodifiableMap, which behave the opposite way: the JDK versions
randomise iteration order once per JVM run. Same-named helpers invite exactly the
substitution that must not happen, and that substitution is what made
HttpRequestDumperTest flaky earlier in this branch.

They are now orderedCopyOf and toOrderedMap, with the reasoning in the javadoc.

Audited every method on the util classes this migration added or extended: all of
them have production call sites, so nothing was left behind as dead code.
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.

1 participant