How to implement multiton pattern using Spring Framework

308 Views Asked by At

How does one implement multiton pattern using Spring Framework's facilities?

https://en.wikipedia.org/wiki/Multiton_pattern

I want to write a factory which takes a pair of client and supplier as arguments. The factory should always return a bean of type T. For a given pair of client and supplier, the instance of T return should be a singleton, but for a different pair of client and supplier, it will be a different instance of T. Please suggest a way to implement this without implementing boilerplate code that Spring may already provide.

Interface ClientSdk {
    sendRequestToClient();
}

class ClientASdk implements ClientSdk {
}

class ClientBSdk implements ClientSdk {
}

enum Client {
    ClientA,
    ClientB;
}

enum Supplier {
    SupplierA,
    SupplierB;
}    

class ClientSupplier {
    private Client client;
    private Supplier supplier;
}

class SdkFactory {
    public ClientSdk getClientSdk(ClientSupplier clientSupplier) {
        //For a given ClientSupplier, always return the same 
        //ClientSupplier instance 
    }
}

@Service
class ClientRequestService {
    public sendRequestToClient(ClientSupplier clientSupplier) {
          ClientSdk clientSdk = SdkFactory.getSdk(clientSupplier);
          clientSdk.sendRequestToClient();
    }
}
1

There are 1 best solutions below

3
On

Here's a solution to your problem. It does make SdkFactory a bean as @crizzis suggests, but it also creates bean instances for each ClientSdk instance so that each of them can be autowired or otherwise helped out by Spring. Note that I added an ident() method to the ClientSdk interface just to show that the MyClientSdk beans have in fact been autowired with the Spring Environment:

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

interface ClientSdk {
    void sendRequestToClient();
}

// This class is instantiated via a @Bean method inside SdkFactory. Since it is annotated as a Prototype bean,
// multiple instances of this class can be created as beans.
class MyClientSdk implements ClientSdk {

    @Autowired
    Environment environment;

    private final String clientSupplier;

    MyClientSdk(String clientSupplier) {
        this.clientSupplier = clientSupplier;
        System.out.printf("@@@ Created MyClientSdk for: %s\n", clientSupplier);
    }

    public void sendRequestToClient() {
        System.out.printf("Sending request for client: %s\n", clientSupplier);
        System.out.printf("CS: %s  Environment Prop: %s\n", clientSupplier, environment.getProperty("spring.application.name"));
    }
}

@Component
class SdkFactory implements BeanFactoryAware {

    private Map<String, ClientSdk> sdks = new HashMap<>();
    private BeanFactory beanFactory;

    // Here's the key logic to insure that we get just one instance of ClientSdk per clientSupplier value.
    ClientSdk getClientSdk(String clientSupplier) {
        if (!sdks.containsKey(clientSupplier))
            sdks.put(clientSupplier, beanFactory.getBean(ClientSdk.class, clientSupplier));
        return sdks.get(clientSupplier);
    }

    // This is probably the most important bit.  This creates a Spring Bean unique to a particular 'clientSupplier'
    // value, but only does so when requested so that the factory can control when these beans are created, creating
    // only one per a particular `clientSupplier` value.
    @Bean
    @Scope("prototype")
    ClientSdk createSdk(String clientSupplier) {
        return new MyClientSdk(clientSupplier);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
}

@Service
class ClientRequestService {

    @Autowired
    SdkFactory factory;

    public void sendRequestToClient(String clientSupplier) {
        ClientSdk clientSdk = factory.getClientSdk(clientSupplier);
        clientSdk.sendRequestToClient();
    }
}


@SpringBootApplication
public class HarmonyTestApp implements CommandLineRunner {

    @Autowired
    ClientRequestService service;

    public static void main(String[] args) {
        try {
            ApplicationContext applicationContext = new SpringApplication(new Class<?>[]{HarmonyTestApp.class}).run(args);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run(String... args) {
        service.sendRequestToClient("client1");
        service.sendRequestToClient("client2");
        service.sendRequestToClient("client1");
        service.sendRequestToClient("client1");
        service.sendRequestToClient("client2");
    }
}

Result:

@@@ Created MyClientSdk for: client1
Sending request for client: client1
CS: client1  Environment Prop: TestApp
@@@ Created MyClientSdk for: client2
Sending request for client: client2
CS: client2  Environment Prop: TestApp
Sending request for client: client1
CS: client1  Environment Prop: TestApp
Sending request for client: client1
CS: client1  Environment Prop: TestApp
Sending request for client: client2
CS: client2  Environment Prop: TestApp

Note that per the output, each of client1's and client2's ClientSdk objects are only created once, even though they're used multiple times. Also notice that since the call to ident() in sendRequestToClient prints the value of a property obtained by an autowired Environment instance, autowiring of each ClientSdk object has worked.

I do realize that I used a String instead of a ClientSupplier object as the identifying key for each ClientSdk object. I did that just to keep the example as simple as I could. I expect you can expand the example to replace the clientSupplier String with an instance of ClientSupplier and somehow use that object as the key/identifier to insure that just one ClientSdk instance is created per ClientSupplier. That detail isn't really germain to the basic idea here.

Also, please note that the question itself changed while I was working on my implementation. Given that there are now exactly two subclasses of ClientSdk, you could simply make those regular @Component Spring beans. Having a small static number of those makes this problem less interesting. The technique I demonstrate here allows for an unlimited number of bean instances of ClientSdk class without having to define a unique class for each of them. This requires that Spring create arbitrary instances of them based on runtime information. This was what the original form of the question seemed to be asking for.