Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Rest Controller inheritance

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.

like image 621
Nikola Antić Avatar asked Feb 22 '19 09:02

Nikola Antić


1 Answers

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);
    }
}
like image 52
Fabien MIFSUD Avatar answered Oct 25 '22 13:10

Fabien MIFSUD