Как создать два метода на один и тот же 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 шт):

Автор решения: Roman Konoval

Проблема тут в том, что это два разных ендпоинта, которые вы пробуете слить в один. Они делают разные вещи, один для текущего пользователя, второй для кого-то другого.

Они получают разные обязательные параметры. И клиент всегда будет точно знать какой из вариантов он вызывает.

→ Ссылка
Автор решения: Zhenyria

Создать два одинаковых эндпоинта можно: укажите params

Суть в том, что при запуске Spring Boot приложения происходит регистрация всех эндопоинтов с использованием уникальной подписи. Подпись основана на следующих данных эндпоинта:

  1. URI (в моём случае /reports)
  2. HTTP-метод (в моём случае GET)
  3. produces (в моём случае application/json)
  4. consumes
  5. 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.

→ Ссылка
Автор решения: roma2341

Я бы даже на уровне контроллеров разделил. StudentController с @RequestMapping("student") и TutorController с @RequestMapping("tutor") И тогда будет запрос идти на /tutor/reports, student/reports А потом @PreAuthorize сразу на класс можно присобачить

А вообще вы же репорт не в контроллере генерируете а в сервисе каком-то верно ? У вас должно быть 2 разных метода в сервисе, и в контроллере вы решаете какой дергать на основе роли

→ Ссылка