0

I have a polymorphic service, like this:

public abstract class PaymentService {
    abstract boolean processPayment(Payment payment);
}
@Service
public class CreditPaymentService extends PaymentService {
    @Override
    public boolean processPayment(Payment payment) {
        // method implementation
    }
}
@Service
public class CashPaymentService extends PaymentService {
    @Override
    public boolean processPayment(Payment payment) {
        // method implementation
    }
}

And in my PaymentController, i receive a Payment and i must send it to the right service to process it, depending on the type of the payment.

@Controller
public class PaymentController {

    @Autowired
    private PaymentService service;

    @Override
    @PostMapping
    public ResponseEntity<Payment> processPayment(@RequestBody Payment payment) {
        service.processPayment(payment);
        return noContent().build();
    }
}

How would spring know which service to instantiate?

  • See also https://stackoverflow.com/questions/51766013/spring-boot-autowiring-an-interface-with-multiple-implementations – David Conrad May 12 '20 at 00:51
  • @David Conrad is right. Also, what's the harm in just injecting the subclass? – mre May 12 '20 at 01:12

3 Answers3

2

It wouldn't. You have to use a @Qualifier and autowire both of them, and then determine which one to dispatch the payment to, presumably by looking in the request body.

David Conrad
  • 15,432
  • 2
  • 42
  • 54
2

Another approach would be use bean name as follows. This expects a mode of payment specified in the request.

example : Payment

public class Payment {
    private String mode;

    public String getMode() {
        return this.mode;
    }

    //.. rest of the class
}

Now define a constants class PaymentModes

public final class PaymentModes {
    private PaymentModes() {
    }

    public static final String CASH = "cash";
    public static final String CREDIT = "credit";
}

And provide bean names for the services

@Service(value = PaymentModes.CASH)
public class CashPaymentService extends PaymentService {

    @Override
    public boolean processPayment(Payment payment) {
        //Implementation
    }
}

and

@Service(value = PaymentModes.CREDIT)
public class CreditPaymentService extends PaymentService {
    @Override
    public boolean processPayment(Payment payment) {
        // Implementation
    }
}

This would register both the beans to the application context with the bean names specified during component scan.

From the documentation

An autowired Map instance’s values consist of all bean instances that match the expected type, and the Map instance’s keys contain the corresponding bean names.

So if you autowire as follows

@Autowired
Map<String,PaymentService> serviceMap;

the map would be something as follows :

{cash=rg.so.q61741320.CashPaymentService@1e74829, credit=rg.so.q61741320.CreditPaymentService@16f416f}

where key is the bean name configured and value is the bean instance.

and the following logic can be used to lookup the required service instance from the map.

@Override
@PostMapping
private ResponseEntity<Payment> processPayment(@RequestBody Payment payment) {
    String paymentMode = payment.getMode();// get payment mode;
    if(serviceMap.containsKey(paymentMode)) {
        serviceMap.get(paymentMode).processPayment(payment);
    }
    return noContent().build();
}

Note : The overridden methods should be public to allow it to be called from other classes , but given as private in the code with the question. Appears to be a typo .

Hope this helps

R.G
  • 6,436
  • 3
  • 19
  • 28
0

There is a workaround to this, by using a factory for your paymentServices.

First of all create a factory that will get you the right Service based on paymentType (You could also use Enums for this).

Then whenever you want to get a paymentService, you get it through the factory.

    @Component
    public class PaymentServiceFactory {

        @Autowired
        ApplicationContext context;

        public PaymentService getPaymentService(String paymentType) {

            // paymentType: Could be cash or credit
            return (PaymentService) context.getBean(paymentType + "PaymentService");
        }
    }

    @Controller
    public class PaymentController {
        @Autowired
        PaymentServiceFactory paymentServiceFactory;

        @PostMapping
        private ResponseEntity<Payment> processPayment(@RequestBody Payment payment) {

            PaymentService service = paymentServiceFactory.getPaymentService(payment.type);
            service.processPayment(payment);
            return noContent().build();
        }
    }

I hope this helps you.

youness.bout
  • 343
  • 3
  • 9