0

I am starting a new Spring Boot web project and want to nest @RestController inside of each other to have a structure for my v1, v2, etc, API and other endpoints that may come along and have the child obtain the path from their parent. For example:

@RestController
@RequestMapping("/api")
public class RootAPIController {
}
@RestController
@RequestMapping("/v1")
public class RootV1Controller extends RootAPIController {
}

to the final path like this:

@RestController
@RequestMapping("/card")
public class CardController extends RootV1Controller {

    @GetMapping(value = "/all", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getCards() {
        return new ResponseEntity<>("", HttpStatus.OK);
    }
}

But Spring doesn't seem to be exposing my URL's at all, all I'm getting is a 404 back when trying any of them.

@SpringBootApplication
public class BackendServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(BackendServiceApplication.class, args);
    }

}

All of my classes are in children under the main package that the SpringBootApplication sits at, from what I have read it should pick those up if they are under it, but it just won't for the ones that are outside of it. What am I missing here?

Thanks!

trever
  • 961
  • 2
  • 9
  • 28
  • So where is your endpoint? I see 2 controllers without an endpoint definition. – Berk Kurkcuoglu Jul 17 '23 at 07:35
  • All I am really trying to do at this point is set up the structure so when I get to my actionable controller it will be in the format of `/api/v1/xxx/` endpoint. I've tried going to the very end and setting up one to test with, didn't work. For example: `/api/v1/card/all` also gives me a 404. – trever Jul 17 '23 at 07:41
  • 1
    Consider adding a common prefix for all rest controllers in the system instead of the hierarchy: https://stackoverflow.com/questions/28006501/how-to-specify-prefix-for-all-controllers-in-spring-boot – Mark Bramnik Jul 17 '23 at 07:49

2 Answers2

0

Spring request mappings don't work with inheritance like that. See for example: Spring MVC @RequestMapping Inheritance

The Spring docs don't even mention inheritance in this context.

I can't find a reference right now, but I believe Java itself is preventing it to work like this. The annotation on the subclass overwrites the annotation on the superclass, so that reflection can "see" the original annotation.

RoToRa
  • 37,635
  • 12
  • 69
  • 105
0

As others have said, Spring does not support this way of inheriting RequestMapping through classes. If you want to implement nested access paths, you can customize an annotation and use SPEL to achieve this function. Here is a simple example:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping
public @interface NestingPath {
    @AliasFor(annotation = RequestMapping.class)
    String value() default "";
}
@Component("nph")
public class NestingPathHandler {

    public String eval(Class<?> clazz, String path) throws ClassNotFoundException {
        Deque<String> pathList = new LinkedList<>();
        pathList.push(path);
        findNestingPath(clazz.getSuperclass(), pathList);
        StringBuilder sb = new StringBuilder();
        while (!pathList.isEmpty()) {
            sb.append(pathList.pop());
        }
        return sb.toString();
    }

    private void findNestingPath(Class<?> clazz, Deque<String> pathList) throws ClassNotFoundException {
        if (clazz == Object.class) {
            return;
        }
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == NestingPath.class) {
                String value = ((NestingPath) annotation).value();
                if (value.contains("@nph.eval")) {
                    int i1 = value.lastIndexOf("'");
                    int j1 = value.lastIndexOf("'", i1 - 1);
                    String path = value.substring(j1 + 1, i1);

                    int i2 = value.lastIndexOf("T(");
                    int j2 = value.lastIndexOf(")", j1 + 1);
                    String className = value.substring(i2 + 2, j2);

                    pathList.push(eval(Class.forName(className), path));
                } else {
                    pathList.push(value);
                    findNestingPath(clazz.getSuperclass(), pathList);
                }
                break;
            }
        }
    }
}
@RestController
@NestingPath("#{@nph.eval(T(org.test.controller.RootAPIController),'/root')}")
public class RootAPIController {
}

@RestController
@NestingPath("#{@nph.eval(T(org.test.controller.RootV1Controller),'/v1')}")
public class RootV1Controller extends RootAPIController {
}

@RestController
@NestingPath("#{@nph.eval(T(org.test.controller.CardController),'/card')}")
public class CardController  extends RootV1Controller {
    @GetMapping(value = "/all", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getCards() {
        return new ResponseEntity<>("card", HttpStatus.OK);
    }
}

But the implementation I provided is not concise, so it is not a good choice. Maybe others will provide a better implementation.

cli ash
  • 27
  • 4