Как создать два метода на один и тот же URL в REST-контроллере?
У меня имеется эндпоинт /reports. Пользователь с ролью STUDENT делает запрос на данный эндпоинт и получает ответ. Изначально соответствующий метод в rest-контроллере выглядел вот так:
@GetMapping("/reports")
public ReportsTo getAll(@AuthenticationPrincipal AuthorizedUser user) {
return service.getToByStudentId(user.getProfile().getId());
}
Я добавил роль TUTOR (преподаватель). Пользователь с такой ролью тоже имеет доступ к методу getAll(). Как вы могли видеть, в предыдущем варианте я получал id студента из принципала, но теперь, когда я расширяю метод getAll(), такой способ уже не будет работать (так как преподаватель содержит свой id, а не студента), и теперь метод выглядит вот так:
@GetMapping("/reports")
public ReportsTo getAll(@RequestParam(value = "student-id") Integer studentId,
@RequestParam(value = "theme-id", required = false) Integer themeId,
@AuthenticationPrincipal AuthorizedUser user) {
return switch (user.getProfile().getRole()) {
case STUDENT -> {
checkIdCompliance(studentId, user.getProfile());
yield service.getToByStudentIdAndThemeId(studentId, themeId);
}
case TUTOR -> {
if (themeId == null) {
throw new ResourceIsNotAllowedException("Theme id can not be null");
}
yield service.getToByStudentId(studentId, themeId, pagination);
}
default -> throw new ResourceIsNotAllowedException(
"This resource is available only for users with the following roles: STUDENT, TUTOR");
};
}
Обратите внимание на параметры. Теперь мне приходится передавать в метод очень много лишних параметров. Например themeId не нужен студенту, как не нужен и studentId. В свою очередь для преподавателя лишним является получение принципала.
Я вижу здесь только одно решение: сделать два метода на один и тот же эндпоинт. Я представляю это вот так:
@GetMapping("/reports")
public ReportsTo getAll(@AuthenticationPrincipal AuthorizedUser user) {
return service.getToByStudentId(user.getProfile().getId());
}
@GetMapping("/reports")
public ReportsTo getAll(@RequestParam(value = "student-id") Integer studentId,
@RequestParam(value = "theme-id") Integer themeId) {
return service.getToByStudentIdAndThemeId(studentId, themeId);
}
Вот только это не будет работать, потому что Spring выдаст ошибку при наличии двух одинаковых эндпоинтов.
Проблема: разные роли могут иметь доступ к одному и тому же эндпоинту. При этом эндпоинт будет засоряться множеством параметров и проверок. Как эту проблему решить?
Ответы (3 шт):
Проблема тут в том, что это два разных ендпоинта, которые вы пробуете слить в один. Они делают разные вещи, один для текущего пользователя, второй для кого-то другого.
Они получают разные обязательные параметры. И клиент всегда будет точно знать какой из вариантов он вызывает.
Создать два одинаковых эндпоинта можно: укажите params
Суть в том, что при запуске Spring Boot приложения происходит регистрация всех эндопоинтов с использованием уникальной подписи. Подпись основана на следующих данных эндпоинта:
- URI (в моём случае
/reports) - HTTP-метод (в моём случае
GET) - produces (в моём случае
application/json) - consumes
- params
Разумеется, если у двух эндпоинтов окажется одинаковая подпись, то возникнет ошибка. Уникальную подпись для двух эндопоинтов с одинаковым путём можно создать, если вынести параметры запроса в свойство params. Это выглядит вот так:
@GetMapping("/reports")
public ReportsTo getAll(@AuthenticationPrincipal AuthorizedUser user) {
...
}
@GetMapping(value = "/reports", params = { "student-id", "theme-id" })
public ReportsTo getAll(@RequestParam(value = "student-id") Integer studentId,
@RequestParam(value = "theme-id") Integer themeId) {
...
}
Теперь эти два эндпоинта будут считаться разными, и ошибки не возникнет.
Конечно, это не является идеальным решением. Если у вас есть два эндпоинта /objects, и вы укажете параметр color для первого и size для второго, то при таком запросе: /objects?color=red&size=small возникнет конфликт. Данный конфликт можно предупредить, если создать дефолтный эндпоинт с обоими параметрами, при этом лишний параметр можно игнорировать:
@GetMapping(value = "/objects", params = { "color", "size" })
public ReportsTo get(@RequestParam("color") String color) {
...
}
Ответ на вопрос
Мой вопрос звучал так:
Проблема: разные роли могут иметь доступ к одному и тому же эндпоинту. При этом эндпоинт будет засоряться множеством параметров и проверок. Как эту проблему решить?
Несмотря на то, что я могу решить проблему, прибегнув к способу выше, я решил так не делать. @Roman Konoval подсказал более адекватное решение: эндпоинт для студента я назвал /my-reports, а эндпоинт для остальных ролей просто /reports.
Я бы даже на уровне контроллеров разделил.
StudentController с @RequestMapping("student")
и TutorController с @RequestMapping("tutor")
И тогда будет запрос идти на /tutor/reports, student/reports
А потом @PreAuthorize сразу на класс можно присобачить
А вообще вы же репорт не в контроллере генерируете а в сервисе каком-то верно ? У вас должно быть 2 разных метода в сервисе, и в контроллере вы решаете какой дергать на основе роли