17

I have a typed abstract RestController that contains some common logic for processing of all objects of the type. The service for processing is provided through the constructor.

During the bean instantiation of the subclass, both constructors are called with non-null parameters and the superclass non-null assertion successfully passed.

Calling the API endpoint (URI path is a composition of the subclass and superclass paths) calls the correct method, with correctly identified parameters. However, the endpoint method throws a null pointer exception because the provided service (the one that passed the non-null assertion) was null. Upon inspection all properties of both subclass and superclass of the bean whose method was called report all properties to be null.

Here is a simplified example:

Model:

public class Cookie {
    public long id;
}

public class ChocolateCookie extends Cookie {
    public long chipCount;
}

Service:

public interface CookieService<T extends Cookie> {
    T findCookie(long cookieId);
    void eatCookie(T cookie);
}

@Service
public class ChocolateCookieService implements CookieService<ChocolateCookie> {

    @Override
    public ChocolateCookie findCookie(long cookieId) {
        // TODO Load a stored cookie and return it.
        return new ChocolateCookie();
    }

    @Override
    public void eatCookie(ChocolateCookie cookie) {
        // TODO Eat cookie;
    }
}

Rest Controllers:

public abstract class CookieApi<T extends Cookie> {

    private final CookieService<T> cookieService;

    public CookieApi(CookieService<T> cookieService) {
        this.cookieService = cookieService;
        Assert.notNull(this.cookieService, "Cookie service must be set.");
    }

    @PostMapping("/{cookieId}")
    public ResponseEntity eatCookie(@PathVariable long cookieId) {
        final T cookie = cookieService.findCookie(cookieId); // Cookie service is null
        cookieService.eatCookie(cookie);
        return ResponseEntity.ok();
    }
}

@RestController
@RequestMapping("/chocolateCookies")
public class ChocolateCookieApi extends CookieApi<ChocolateCookie> {

    @Autowired
    public ChocolateCookieApi(ChocolateCookieService cookieService) {
        super(cookieService);
    }

    @PostMapping
    public ResponseEntity<ChocolateCookie> create(@RequestBody ChocolateCookie dto) {
        // TODO Process DTO and store the cookie
        return ResponseEntity.ok(dto);
    }
}

As a note, if instead of providing a service object to the superclass I defined an abstract method for getting the service on demand and implemented it in the subclass, the superclass would function as intended.

The same principle works in any case where @RestController and @RequestMapping are not included in the equation.

My question is two-fold:

  1. Why is the happening?
  2. Is there a way to use the constructor, or at least to not have to implement getter methods for each subclass and each service required by the superclass?

EDIT 1:

I tried recreating the issue, but the provided code was working fine as people suggested. After tampering with the simplified project, I finally managed to reproduce the issue. The actual condition for reproducing the issue is that the endpoint method in the superclass must be inaccessible by the subclass (example: Classes are in different packages and the method has package visibility). This causes spring to create an enhancerBySpringCGLIB proxy class with zero populated fields.

Modifying the superclass methods to have protected/public visibility resolved the issue.

Nick
  • 218
  • 1
  • 2
  • 12
  • Very interesting question and the matter is very well exposed. – davidxxx Feb 22 '19 at 09:34
  • @davidxxx though a repo reproducing the problem would've been nice as well, as apparently people are unable to reproduce the issue – eis Feb 26 '19 at 21:10
  • @eis I agree according to the actual answers/comment. Nikola Antic you should indeed add more information or provide a repo with a minimal project that reproduces the issue. – davidxxx Feb 27 '19 at 19:38
  • Well, after reading your last edited comment it totally makes sense, in your question you missed to mention those details and as all methods of your classes posted are public we did not catch it. Good thing you found out what happened. Happy coding! – Karl Feb 27 '19 at 21:44
  • you could add your own answer and accept that, so this question would at least be marked as answered. Either that or accept one of the answers that are essentially saying that the code you've posted works just fine. – eis Feb 28 '19 at 04:52

3 Answers3

2

Nikola,

I'm not sure why your code is not working in your system, I created same classes in a project and it is working fine, I even added another Cookie type, service and api classes.

SpringBoot log (you can see 4 end points initialized):

2019-02-26 14:39:07.612  INFO 86060 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/chocolateCookies],methods=[POST]}" onto public org.springframework.http.ResponseEntity<cookie.ChocolateCookie> cookie.ChocolateCookieApi.create(cookie.ChocolateCookie)
2019-02-26 14:39:07.613  INFO 86060 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/chocolateCookies/{cookieId}],methods=[POST]}" onto public org.springframework.http.ResponseEntity<?> cookie.CookieApi.eatCookie(long)
2019-02-26 14:39:07.615  INFO 86060 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/oatmeal-raisin-cookie],methods=[POST]}" onto public org.springframework.http.ResponseEntity<cookie.OatmealRaisinCookie> cookie.OatmealRaisingCookieApi.create(cookie.OatmealRaisinCookie)
2019-02-26 14:39:07.615  INFO 86060 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/oatmeal-raisin-cookie/{cookieId}],methods=[POST]}" onto public org.springframework.http.ResponseEntity<?> cookie.CookieApi.eatCookie(long)

Testing controllers in postman enter image description here

enter image description here

As @Domingo mentioned, you may have some configuration problems in your application because from OOP and Spring IoC perspectives your code looks fine and runs with no problems.

NOTE: I'm running these controllers using SpringBoot 2.0.5, Java 8, Eclipse

I posted my project in GitHub for your reference. https://github.com/karl-codes/cookie-monster

Cheers!

Karl
  • 776
  • 6
  • 15
  • I reproduced the same project without using sprign boot and it works; i agree with others. It seems to a configuration problem – Angelo Immediata Feb 27 '19 at 13:25
  • 4
    The provided code was working fine as people suggested. The actual condition for reproducing the issue is that the endpoint method in the superclass must be inaccessible by the subclass (example: Classes are in different packages and the method has package visibility). This causes spring to create an enhancerBySpringCGLIB proxy class with zero populated fields. Modifying the superclass methods to have protected/public visibility resolved the issue. Accepting this answer as it technically was correct and the effort ultimately led me to the final solution. – Nick Feb 28 '19 at 07:30
1

you can define an abstract method in your abstract class and autowire the correct service on each implementation :

public abstract class CookieApi<T extends Cookie> {

    protected abstract CookieService<T> getCookieService();

    @RequestMapping("/cookieId")
    public void eatCookie(@PathVariable long cookieId) {
        final T cookie = cookieService.findCookie(cookieId); // Cookie service is null
        this.getCookieService().eatCookie(cookie);
    }
}

@RestController
@RequestMapping("/chocolateCookies")
public class ChocolateCookieApi extends CookieApi<ChocolateCookie> {

    @Autowired
    private ChocolateCookie chocolateCookie;

    @Override
    protected CookieService<T> getCookieService() {
        return this.chocolateCookie;
    }

    @PostMapping
    public ResponseEntity<ChocolateCookie> create(@RequestBody ChocolateCookie dto) {
        // TODO Process DTO and store the cookie
        return ResponseEntity.ok(dto);
    }
}
Fabien MIFSUD
  • 335
  • 5
  • 14
  • 1
    I did try this approach before and it works. My goal is to avoid implementing getXXXService() in each subclass. There are around 10 subclasses that inherit the problematic superclass and the superclass has 3-4 services that it needs. This amounts to a lot of unnecessary method implementations. – Nick Feb 22 '19 at 10:16
0

your sample in general looks alright and dependency injection should work with spring.

If you want to access service which reference is in parent abstract class the reference should be not private but protected.

--

Spring initializes all RestControllers as Singleton Beans into application context injecting existing Services Beans, if there is no bean to inject application startup will fail. If calling rest endpoint access the controller that has no service reference in it, it is not the same one that was initialized with service in it(which I don't know how could happen) or something is wrong with your config.

Put that on git hub.

domingo
  • 167
  • 8