split library jar into impl jar and api jar?

Viewed 285

Imagine we have a Lib .jar and two executable jars - App1 and App2 which both depend on the Lib .jar at compile time. Our current situation is that whenever we change the Lib's implementation (while keeping the signatures intact), we also need to change App1 and App2: while the source code in the executables themselves does not change at all, we are still required to update the Lib's version number within each executable's pom file, increase each executable version number, and redeploy them - hefty.

I imagine a better situation in which after changing Lib's implementation, the Lib's jar gets deployed and the executables only need to be restarted on the production environment to pick up those changes - no need to modify each executable's pom; no need to create new executable jars; no need to deploy new executable jars to the prod environment.

How can that better situation be orchestrated? We use Maven as our build system.

Notice that we would prefer an approach in which the lib is loaded in the same JVM as each of the executables - we don't want to have a web service architecture with the Lib's implementation running on another VM.

In addition, consider that we also maintain the executables and we develop them on our desktops i.e within an IDE, run maven commands on the command line, etc and we would like to preserve that ability.

Thanks.

1 Answers

Here is an attempt to split the lib into two jars - an api jar and an impl jar - and an app that at compile time only depends on the lib api hence it does not need to change when the lib impl changes - see ma?!:

  • the app is made to depend at compile time on the lib api (1)
  • the app's dependency on the lib impl is only stated on the app's pom for development purposes (2) and kept as a runtime dependency to avoid introducing undesired compile time dependencies (3)
  • at compile time the app only depends on the lib api and thus it does not know how to construct the implementation objects whose construction then becomes something else's responsibility e.g. Spring (4)
  • the api is based on interfaces (5). This helps with the definition of the signatures in a single place as well as avoiding Java's single inheritance limitation on the implementation classes.
  • to orchestrate the loading of the impl jar at runtime, I rely on a convention to include a sentinel class on the impl jar that is loaded initially as the app comes up (6) to force the loading of the entire impl jar into the app's JVM
  • what's left to do (not shown below) is to reference the impl jar at runtime on the prod environment via the classpath command line option

lib-api

public interface Foo { // 5
    void bar();
}

lib-impl

pom.xml

<dependencies>
    <dependency>
        <groupId>splitlib</groupId>
        <artifactId>lib-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId> <!-- 4 -->
    </dependency>
</dependencies>

ImplFoo.java

import org.springframework.stereotype.Component;

@Component
final class ImplFoo implements Foo {
    @Override
    public void bar() {
        System.out.println("hello world");
    }
}

Sentinel.java

public final class Sentinel {
    private Sentinel() {}
}

app

pom.xml

<profiles>
    <profile>
        <id>development</id> <!-- 2 -->
        <activation>
            <os><family>mac</family></os>

            <!-- or whatever -->

        </activation>
        <dependencies>
            <dependency>
                <groupId>splitlib</groupId>
                <artifactId>lib-impl</artifactId> <!-- 2 -->
                <scope>runtime</scope> <!-- 3 -->
            </dependency>
        </dependencies>
    </profile>
</profiles>

<dependencies>
    <dependency>
        <groupId>splitlib</groupId>
        <artifactId>lib-api</artifactId>    <!-- 1 -->
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId> <!-- 4 -->
    </dependency>
</dependencies>

Main.java

final class Main {
    static {
        try {
            // 6
            Class.forName(format("%s.Sentinel",  Foo.class.getPackage().getName()));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        // 4
        new AnnotationConfigApplicationContext(Foo.class.getPackage().getName())
                .getBean(Foo.class)
                .bar();
    }
}
Related