0

I have an interface A with a method Result doAction(Param param). I have a Spring application that will use implementations of the interface and call doAction() on it.

But the application does not define an implementation itself. The idea is that other people can provide their own implementations of the interface in JARs (plugins), the main application will pull those in as dependencies, and call doAction() on the JAR's implementation.

Any idea how I can do this in practice? The ideas I had were:

  • Try to autowire the implementation through Spring Boot, but for that I would need to know its package and add it to the component scan. So it would mean putting requirements on the naming of the "plugin" jar. Something I would prefer not to do.

  • With plain Java my first idea was to keep a registry of implementations (e.g. a Set<Interface A>), but the plugin wouldn't be able to access the registry -- it would be a dependency cycle.

What I'm doing right now is defining a Rest API that the "plugin" needs to implement, deploy the plugin in the same environment and the main application just makes the calls through the Rest API. But for performance reasons I'm looking for a solution with more direct calls that doesn't involve communication over the network. Any suggestions?

devil0150
  • 1,350
  • 3
  • 13
  • 36

1 Answers1

0

What you need is Service Provider Interface (SPI).

Assume you have a 4-module project with the following structure:

.
├── app
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── java
│               └── com/example/app/App.java
├── plugin
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── java
│               └── com/example/plugin/Animal.java
├── plugin-cat
│   ├── build.gradle.kts
│   └── src
│       └── main
│           ├── java
│           │   └── com/example/cat/Cat.java
│           └── resources
│               └── META-INF/services/com.example.plugin.Animal
├── plugin-dog
│   ├── build.gradle.kts
│   └── src
│       └── main
│           ├── java
│           │   └── com/example/dog/Dog.java
│           └── resources
│               └── META-INF/services/com.example.plugin.Animal
└── settings.gradle.kts

The interface looks like:

// Animal.java
public interface Animal {
    String kind();
}

and its two implementations:

// Cat.java
public class Cat implements Animal {
    @Override
    public String kind() {
        return "cat";
    }
}
// Dog.java
public class Dog implements Animal {
    @Override
    public String kind() {
        return "dog";
    }
}

Then put a file named com.example.plugin.Animal under each implementation plugin's resources folder. The file contains the canonical name of their implementation class in one line:

plugin-cat/src/main/resources/META-INF/services/com.example.plugin.Animal:

com.example.cat.Cat

plugin-dog/src/main/resources/META-INF/services/com.example.plugin.Animal:

com.example.dog.Dog

You can choose which dependency to include in app/build.gradle.kts:

dependencies {
    implementation(project(":plugin"))
    implementation(project(":plugin-dog"))
    // implementation(project(":plugin-cat"))
}

For this config, the main class App.java:

public class App {
    public Animal getAnimal() {
        var animal = ServiceLoader.load(Animal.class).findFirst();
        assert animal.isPresent();
        return animal.get();
    }

    public static void main(String[] args) {
        System.out.println(new App().getAnimal().kind());
    }
}

will print dog to the console.

Here is the project for this demo: https://github.com/chehsunliu/stackoverflow/tree/main/a.2023-05-10.gradle.75359691.


References

chehsunliu
  • 1,559
  • 1
  • 12
  • 22