Quantcast
Viewing latest article 10
Browse Latest Browse All 55

Unveiling the Magic of Java ServiceLoader API: Building Modular and Extensible Applications

In the vast realm of Java programming, developers are often faced with the challenge of creating modular, extensible, and maintainable applications. As software systems grow in complexity, the need to decouple components and facilitate dynamic plugin loading becomes paramount. This is where the Java ServiceLoader API shines, offering a robust and elegant solution for achieving component-based architecture and runtime extensibility. While the ServiceLoader API has been part of Java since version 6, its popularity and user-friendliness truly soared with the advent of Java 9’s module system.

In this blog post, we will dive into the world of the ServiceLoader API by exploring a simple yet illustrative example involving a fictional “CoffeeVendor” service. We’ll demonstrate how to leverage the ServiceLoader API to effortlessly discover and incorporate different coffee provider implementations into our CoffeeVendor application. Through this example, we will showcase the power and simplicity of the ServiceLoader API and highlight its significance in building adaptable and pluggable Java applications.

So, grab your virtual coffee cup, and let’s embark on a journey to uncover the capabilities of the ServiceLoader API and how it can enhance the modularity and extensibility of your Java projects.

Defining the CoffeeVendor Service

To set the stage, we’ll introduce our central character: the CoffeeVendor service. This service, a coveted gem for client applications, adheres to a concise interface:

public interface CoffeeVendor {
    String getName();
    List<String> getVariants();
}

This integral interface resides within the “coffee.vendor” module:

module coffee.vendor {
    exports info.sanaulla.coffeevendor;
}

A Glimpse of the Module Structure

Before delving deeper, let’s take a quick glance at the module structure as envisioned within the IntelliJ IDE:

Enlisting the CoffeeVendor Client

With our stage well-set, it’s time to introduce, the “Client.” This class serves as the conduit to various implementations, aptly referred to as service providers, of the CoffeeVendor service. Leveraging the ServiceLoader.load() method, our Client eagerly loads all CoffeeVendor interface implementations available within the classpath/modulepath at runtime

public class Client {

    public static void main(String[] args) {
        ServiceLoader<CoffeeVendor> coffeeVendors = ServiceLoader.load(CoffeeVendor.class);
        System.out.println("Found the following coffee vendors");
        for(CoffeeVendor coffeeVendor : coffeeVendors){
            System.out.println(coffeeVendor.getName() + ". Variants: " + coffeeVendor.getVariants());
        }
    }
}

The Client’s home is the “client” module:

module client {
    requires coffee.vendor;
    uses info.sanaulla.coffeevendor.CoffeeVendor;
}

A noteworthy detail: adding the line “uses <name of the service interface>” within our module descriptor is vital for ServiceLoader’s operation. Without this inclusion, a runtime error occurs:

Image may be NSFW.
Clik here to view.

We can execute our client without providing any service provider. You will see just the below printed:

Found the following coffee vendors

Unveiling CoffeeVendor Service Providers

Let us provide two implementations for CoffeeVendor service: CostaCoffee and Starbucks.

CostaCoffee service provider implementation is given below:

public class CostaCoffee implements CoffeeVendor {

    public CostaCoffee(){
        System.out.println("Costa Coffee created");
    }
    @Override
    public String getName() {
        return "Costa Coffee";
    }

    @Override
    public List<String> getVariants() {

        return Arrays.asList("Latte", "Flat Black", "Cortado", "Espresso", "Mocha");
    }

}

It is part of the module costcoffee whose module definition is given below:

module costacoffee {
    requires coffee.vendor;
    provides info.sanaulla.coffeevendor.CoffeeVendor 
            with info.sanaulla.costacoffee.CostaCoffee;
}

The important piece of code above is provides ... with ... which shows that this module provides an implementation for the service CoffeeVendor.

Starbucks service provider implementation is given below:

public class Starbucks implements CoffeeVendor {
    public Starbucks(){
        System.out.println("Starbucks created");
    }
    @Override
    public String getName() {
        return "Starbucks";
    }

    @Override
    public List<String> getVariants() {
        return Arrays.asList("Latte", "Espresso", "Cappuccino", 
                "Caffe Americano", "Flat White" );
    }

}

It is part of the module starbucks whose module definition is given below:

module starbucks {
    requires coffee.vendor;
    provides info.sanaulla.coffeevendor.CoffeeVendor 
            with info.sanaulla.starbucks.Starbucks;
}

The modules directory structure looks something like below:

Conclusion

I am using IntelliJ IDE for this sample. So if I run my Client main class to my surprise I will not find any of the service providers we defined above. This is because these service provider modules are not part of the runtime dependency. So we need to add them as part of runtime dependency from the Module Settings as shown below:

Image may be NSFW.
Clik here to view.

If we run the main class now you will be able to see the following output:

The complete sample can be found in Github repo here.

The post Unveiling the Magic of Java ServiceLoader API: Building Modular and Extensible Applications appeared first on Experiences Unlimited.


Viewing latest article 10
Browse Latest Browse All 55

Trending Articles