I started to learn WireMock. My first experience is not very positive. Here's a failing MRE:
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.Test;
public class GenericTest {
@Test
void test() {
new WireMockServer(8090);
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>3.5.1</version>
<scope>test</scope>
</dependency>
java.lang.NoClassDefFoundError: org/eclipse/jetty/util/thread/ThreadPool
I debugged it a little:
public WireMockServer(int port) {
this(/* -> this */ wireMockConfig() /* <- throws */.port(port));
}
// WireMockConfiguration
// ↓ throwing inline
private ThreadPoolFactory threadPoolFactory = new QueuedThreadPoolFactory();
public static WireMockConfiguration wireMockConfig() {
return /* implicit no-args constructor */ new WireMockConfiguration();
}
package com.github.tomakehurst.wiremock.jetty;
import com.github.tomakehurst.wiremock.core.Options;
import com.github.tomakehurst.wiremock.http.ThreadPoolFactory;
// ↓ package org.eclipse does not exist, these lines are in red
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
public class QueuedThreadPoolFactory implements ThreadPoolFactory {
@Override
public ThreadPool buildThreadPool(Options options) {
return new QueuedThreadPool(options.containerThreads());
}
}
My conclusions:
org.eclipse
<!-- like so -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>12.0.7</version>
</dependency>
I even visited their GitHub to see for myself if the dependency is marked as provided
, but they use Gradle, and I don't know Gradle
But that's not all! You'll also have to include (at least) com.github.jknack.handlebars
and com.google.common.cache
(see com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine
)
Luckily, I found this "stand-alone" artifact that doesn't require any manual props
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.5.1</version>
<scope>test</scope>
</dependency>
My question: Why aren't all artifacts "stand-alone"? Why do artifacts that don't work unless propped up by manually declared dependencies even exist, what are their advantages?
There's a presumption in the question that it was normal and expected by Wiremock's development team that you would manually add jetty-util as an explicit dependency when not using the standalone artifact. That's not true: This was a bug.
The alternative to standalone jars is not needing to add dependencies yourself. The alternative to standalone JARs is having the POM for a project specify compatibility constraints on all its dependencies, so that Maven can combine all the constraints of all the dependencies and try to calculate a dependency set that satisfies all those constraints.
(A quick terminology note: To "vendor" a dependency is to redistribute it yourself; this is what "standalone" JARs do).
Let's say that you're using library A, which requires C version 1.2.3 or newer (but not past 1.4.99); and library B, which requires C version 1.3.0 or newer (but not 2.0+).
When Maven is calculating a dependency set, this is easy: it knows that the overall project requires a version of C that's at least 1.3.0, and not 1.5+. It thus might pick, say, 1.4.15.
By contrast, if library A bundled in C==1.2.3 to a standalone jar, that version of C is on the overall project's classpath, so library B can end up getting C 1.2.3 even though it doesn't work with any version of C older than 1.3.0.
There are mechanisms like OSGi to automate setup of multiple classloaders with different dependency chains active within the same JVM at the same time to allow concurrent use of conflicting library versions, but these add a great deal of complexity on their own -- it's very much best avoided.
You're using Library A in standalone mode, which vendors C 1.2.3.
There's a major vulnerability discovered in C releases older than 1.4.13. Now what?
If you weren't operating in standalone mode, Maven could resolve to the newest available version that was compatible with everything in your dependency chain; but because you're letting your dependencies pull in versions of transitive dependencies they packaged at build time, now you need to wait until library A publishes a new release.
If they only publish that new release on a branch you aren't compatible with -- too bad, so sad, nothing you can do about it without a bunch of build engineering work to pull content out of the jar they distributed at build time.