Вопрос или проблема
Это кажется обычным случаем, но я всё ещё не могу найти подходящее решение.
У меня есть сервис, который сильно связан с текущим аутентифицированным пользователем. Он должен получать userId
из более высокого контекста (Контроллер) так или иначе. Сервис не может получить userId
из любого доступного глобально контекста ‘auth’, он должен быть получен в качестве аргумента.
Сервис также должен быть бином (@Service), поэтому он должен автоматически инициализироваться Spring.
Один из способов достижения этого следующий:
- Инициализировать сервис без
userId
- Затем передавать
userId
в каждый метод, который вызывается на сервисе:
@Service
class MyService
public void method1(int userId) {
// что-то с userId
}
public void method2(int userId) {
// что-то с userId
}
end
// Использование
myService = new MyService(); // Это делает Spring (без проблем).
myService.method1(getUserIdFromAnyAuthContext())
myService.method2(getUserIdFromAnyAuthContext())
Вот как я бы хотел, чтобы мой сервис работал (если бы это не был бин):
@Service
class MyService
private int currentUserId;
public MyService(int currentUserId) {
this.currentUserId = currentUserId;
}
public void method1() {
// что-то с currentUserId
}
public void method2() {
// что-то с currentUserId
}
end
// Использование
myService = new MyService(getUserIdFromAnyAuthContext()); // Я хочу, чтобы Spring мог это сделать и сделать MyService бин.
myService.method1()
myService.method2()
Приведённый выше пример показывает, как бы я сделал это, если бы это не был бин. Суть в том, чтобы не передавать userId
в каждый метод. Он должен быть инъецирован в сервис каким-то образом с каждым запросом, но в то же время это должно выглядеть так, как будто я передал его стандартным способом Java, чтобы его можно было тестировать независимо от внешнего контекста.
Как сделать сервис бином и инициализировать его данными ‘auth’?
Обновлено:
В случае, если это имеет значение (в ответ на комментарий @Anish B.): я собираюсь использовать Spring Security в качестве провайдера аутентификации. Но, пожалуйста, обратите внимание, что сам сервис не может получать доступ к любому глобально доступному контексту ‘auth’, он должен быть независим от внешнего контекста.
Spring Security создает бин Authentication, который содержит информацию о текущем пользователе. Если вы хотите избавиться от зависимости от Spring Security, просто используйте интерфейс Principal, который является родителем Authentication.
Ответ или решение
Для решения задачи инициализации сервиса Spring с идентификатором аутентифицированного пользователя, при этом избегая глобальных контекстов, необходимо рассмотреть несколько подходов. В этом ответе будет представлено решение, которое соответствует требованиям, а также учитывает использование Spring Security.
Концепция
Сервис должен получать userId
как параметр на уровне метода, однако он также должен быть инкапсулирован в качестве внутреннего состояния класса без зависимостей от глобальных контекстов для обеспечения тестируемости и модульности.
Решение
-
Создание пользовательского контекста:
Создадим интерфейсUserContext
, который будет хранить информацию о текущем пользователе и передавать ее в сервис.public interface UserContext { int getCurrentUserId(); }
-
Реализация UserContext:
Реализуем этот интерфейс, чтобы получать данные из Spring Security.@Component public class SecurityUserContext implements UserContext { @Override public int getCurrentUserId() { // Получение идентификатора пользователя из Spring Security Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { return ((CustomUserDetails) authentication.getPrincipal()).getUserId(); } throw new IllegalStateException("Пользователь не аутентифицирован"); } }
-
Создание сервиса:
Теперь можем создать сервис, который будет принимать в конструктореUserContext
.@Service public class MyService { private final int currentUserId; public MyService(UserContext userContext) { this.currentUserId = userContext.getCurrentUserId(); } public void method1() { // Логика с использованием currentUserId } public void method2() { // Логика с использованием currentUserId } }
-
Обработка контекста пользователя в контроллере:
Контроллер будет получать экземплярUserContext
и использовать его для инициализации сервиса. Это может быть также сделано с помощью внедрения зависимостей.@RestController public class UserController { private final UserContext userContext; private final MyService myService; public UserController(UserContext userContext, MyService myService) { this.userContext = userContext; this.myService = myService; } @GetMapping("/some-endpoint") public ResponseEntity<?> someMethod() { myService.method1(); return ResponseEntity.ok("Успех"); } }
Преимущества предложенного подхода
-
Отделение от глобального контекста: Сервис не зависит от статических методов или глобальных переменных, он полностью изолирован и может быть протестирован независимо от состояния аутентификации.
-
Легкость тестирования: Можно легко создавать мок-объекты
UserContext
для написания юнит-тестов, что упрощает процесс тестирования. -
Поддержка Spring: Использование Spring для управления жизненным циклом бинов и инъекций.
Заключение
Данный подход позволяет эффективно инициализировать сервис с информацией о пользователе, не полагаясь на глобальный контекст аутентификации. Он обеспечивает тестируемость и модульность, что является важным аспектом современного разработки программного обеспечения.