16

I have a class as follows

@Component
public abstract class NotificationCenter {
    protected final EmailService emailService;
    protected final Logger log = LoggerFactory.getLogger(getClass());

    protected NotificationCenter(EmailService emailService) {
        this.emailService = emailService;
    }

    protected void notifyOverEmail(String email, String message) {
        //do some work
        emailService.send(email, message);
    }
}

EmailService is a @Service and should be auto-wired by constructor injection.

Now I have a class that extends NotificationCenter and should also auto-wire components

@Service
public class NotificationCenterA extends NotificationCenter {    
    private final TemplateBuildingService templateBuildingService;

    public NotificationCenterA(TemplateBuildingService templateBuildingService) {
        this.templateBuildingService = templateBuildingService;
    }
}

Based on the above example the code won't compile because there is no default constructor in the abstract class NotificationCenter unless I add super(emailService); as the first statement to NotificationCenterA constructor but I don't have an instance of the emailService and I don't intend to populate the base field from children.

Any idea what's the proper way to handle this situation? Maybe I should use field injection?

prettyvoid
  • 3,446
  • 6
  • 36
  • 60
  • 1
    You would need both, right? Inject a compatible instance of `EmailService` and `TemplateBuildingService`, then call super for `EmailService`. I do not see any other way(s). – x80486 Oct 29 '18 at 16:31

4 Answers4

10

NotificationCenter is not a real class but an abstract class, so you can't create the instance of it. On the other hand, it has a field (final field!) EmailService that has to be initialized in constructor! Setter won't work here, because the final field gets initialized exactly once. It's Java, not even Spring.

Any class that extends NotificationCenter inherits the field EmailService because this child "is a" notification center

So, you have to supply a constructor that gets the instance of email service and passes it to super for initialization. It's again, Java, not Spring.

public class NotificationCenterA extends NotificationCenter {    
    private final TemplateBuildingService templateBuildingService;

    public NotificationCenterA(EmailService emailService, TemplateBuildingService templateBuildingService) {
       super(emailService);
       this.templateBuildingService = templateBuildingService;
    }
} 

Now spring manages beans for you, it initializes them and injects the dependencies. You write something that frankly I don't understand:

...as the first statement to NotificationCenterA constructor but I don't have an instance of the emailService and I don't intend to populate the base field from children.

But Spring will manage only a NotificationCenterA bean (and of course EmailService implementation), it doesn't manage the abstract class, and since Java puts the restrictions (for a reason) described above, I think the direct answer to your question will be:

  1. You can't use setter injection in this case (again, because of final, it is Java, not because of Spring)
  2. Constructor injection, being in a general case better than setter injection can exactly handle your case
Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
  • Thanks for the clarification. One question, what if I switch the abstract class to a concrete class, do I still have to populate emailService from children? – prettyvoid Oct 29 '18 at 16:58
  • Yes, if you create a child class, of course now you can create a parent class "on its own", but in this case, child class does not come into consideration at all – Mark Bramnik Oct 29 '18 at 17:04
  • If I make `NotificationCenter` a class instead of abstract class, I'd still have to call super constructor supplying it with emailService.. so the main thing that I'm trying to avoid is not averted. Could you clarify why in this case child class doesn't come into consideration at all? – prettyvoid Oct 29 '18 at 17:11
  • Parent class can be created regardless the child class (as long as its not abstract). Child class has to take parent's state (fields) into consideration... – Mark Bramnik Oct 29 '18 at 17:12
7

First point :

@Component is not designed to be used in abstract class that you will explicitly implement. An abstract class cannot be a component as it is abstract.
Remove it and consider it for the next point.

Second point :

I don't intend to populate the base field from children.

Without Spring and DI, you can hardcoded the dependency directly in the parent class but is it desirable ? Not really. It makes the dependency hidden and also makes it much more complex to switch to another implementation for any subclass or even for testing.
So, the correct way is injecting the dependency in the subclass and passing the injected EmailService in the parent constructor :

@Service
public class NotificationCenterA extends NotificationCenter {    
    private final TemplateBuildingService templateBuildingService;    

    public NotificationCenterA(TemplateBuildingService templateBuildingService, EmailService emailService) {
        super(emailService);
        this.templateBuildingService = templateBuildingService;
    }
}

And in the parent class just remove the useless @Component annotation.

Any idea what's the proper way to handle this situation? Maybe I should use field injection?

Not it will just make your code less testable/flexible and clear.

davidxxx
  • 125,838
  • 23
  • 214
  • 215
0

using field injection would be the way to go since you mentioned you don't want to have the emailService in child class.

The other way you can try is to inject the EmailService bean into NotificationCenterA constructor, and then pass it to super(emailService).

So, it would be something like:

        @Autowired
        public NotificationCenterA(EmailService emailService, TemplateBuildingService templateBuildingService) {
            super(emailService);
            this.templateBuildingService = templateBuildingService;
        }
Nitin1706
  • 621
  • 1
  • 11
  • 21
0

You can also achieve this by using @Lookup annotation.

public abstract class NotificationCenter {
    protected final Logger log = LoggerFactory.getLogger(getClass());

    @Lookup
    protected EmailService getEmailService() {
        return null; // Spring will implement this @Lookup method using a proxy
    }

    protected void notifyOverEmail(String email, String message) {
        //do some work
        EmailService emailService = getEmailService(); // This is return the service bean.
        emailService.send(email, message);
    }
}
AMagic
  • 2,690
  • 3
  • 21
  • 33