Search code examples
javawiremockartifact

Why do "non-stand-alone" artifacts exist?


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:

  1. WireMock has a dependency on org.eclipse
  2. WireMock doesn't include this dependency in its artifact
  3. I have to provide it manually
<!-- 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?


Solution

  • In Normal Operation, Maven Calculates and Downloads Transitive Dependencies

    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.


    Vendoring Dependencies Sets Up Conflicts

    (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.


    Versioning Dependencies Prevents Security Updates

    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.