Автор оригинала: Eugen Paraschiv.
1. Обзор
В этой краткой статье мы собираемся сделать что-то новое. Мы собираемся разработать существующий API REST Spring и заставить его использовать разделение ответственности за командные запросы – CQRS .
Цель состоит в том, чтобы четко разделить уровни службы и контроллера для обработки запросов на чтение и команд на запись, поступающих в систему отдельно.
Имейте в виду, что это всего лишь ранний первый шаг к такого рода архитектуре, а не “точка прибытия”. Тем не менее, я в восторге от этого.
Наконец, пример API, который мы собираемся использовать, – это публикация User ресурсов и является частью нашего текущего Reddit app case study , чтобы проиллюстрировать, как это работает – но, конечно, подойдет любой API.
2. Уровень Обслуживания
Мы начнем с простого – просто определив операции чтения и записи в нашей предыдущей службе пользователей – и разделим их на 2 отдельные службы – UserQueryService и UserCommandService :
public interface IUserQueryService { ListgetUsersList(int page, int size, String sortDir, String sort); String checkPasswordResetToken(long userId, String token); String checkConfirmRegistrationToken(String token); long countAllUsers(); }
public interface IUserCommandService { void registerNewUser(String username, String email, String password, String appUrl); void updateUserPassword(User user, String password, String oldPassword); void changeUserPassword(User user, String password); void resetPassword(String email, String appUrl); void createVerificationTokenForUser(User user, String token); void updateUser(User user); }
Из чтения этого API вы можете ясно видеть, как служба запросов выполняет все чтение, и служба команд не читает никаких данных – все void возвращает .
3. Уровень Контроллера
Далее – уровень контроллера.
3.1. Контроллер Запросов
Вот наш Пользовательский запрос RestController :
@Controller @RequestMapping(value = "/api/users") public class UserQueryRestController { @Autowired private IUserQueryService userService; @Autowired private IScheduledPostQueryService scheduledPostService; @Autowired private ModelMapper modelMapper; @PreAuthorize("hasRole('USER_READ_PRIVILEGE')") @RequestMapping(method = RequestMethod.GET) @ResponseBody public ListgetUsersList(...) { PagingInfo pagingInfo = new PagingInfo(page, size, userService.countAllUsers()); response.addHeader("PAGING_INFO", pagingInfo.toString()); List users = userService.getUsersList(page, size, sortDir, sort); return users.stream().map( user -> convertUserEntityToDto(user)).collect(Collectors.toList()); } private UserQueryDto convertUserEntityToDto(User user) { UserQueryDto dto = modelMapper.map(user, UserQueryDto.class); dto.setScheduledPostsCount(scheduledPostService.countScheduledPostsByUser(user)); return dto; } }
Что здесь интересно, так это то, что контроллер запросов только вводит службы запросов.
Что было бы еще более интересно, так это отрезать доступ этого контроллера к командным службам – поместив их в отдельный модуль.
3.2. Командный Контроллер
Теперь, вот наша реализация контроллера команд:
@Controller @RequestMapping(value = "/api/users") public class UserCommandRestController { @Autowired private IUserCommandService userService; @Autowired private ModelMapper modelMapper; @RequestMapping(value = "/registration", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void register( HttpServletRequest request, @RequestBody UserRegisterCommandDto userDto) { String appUrl = request.getRequestURL().toString().replace(request.getRequestURI(), ""); userService.registerNewUser( userDto.getUsername(), userDto.getEmail(), userDto.getPassword(), appUrl); } @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/password", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) public void updateUserPassword(@RequestBody UserUpdatePasswordCommandDto userDto) { userService.updateUserPassword( getCurrentUser(), userDto.getPassword(), userDto.getOldPassword()); } @RequestMapping(value = "/passwordReset", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void createAResetPassword( HttpServletRequest request, @RequestBody UserTriggerResetPasswordCommandDto userDto) { String appUrl = request.getRequestURL().toString().replace(request.getRequestURI(), ""); userService.resetPassword(userDto.getEmail(), appUrl); } @RequestMapping(value = "/password", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void changeUserPassword(@RequestBody UserchangePasswordCommandDto userDto) { userService.changeUserPassword(getCurrentUser(), userDto.getPassword()); } @PreAuthorize("hasRole('USER_WRITE_PRIVILEGE')") @RequestMapping(value = "/{id}", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) public void updateUser(@RequestBody UserUpdateCommandDto userDto) { userService.updateUser(convertToEntity(userDto)); } private User convertToEntity(UserUpdateCommandDto userDto) { return modelMapper.map(userDto, User.class); } }
Здесь происходит несколько интересных вещей. Во – первых, обратите внимание, как каждая из этих реализаций API использует другую команду. Это в основном для того, чтобы дать нам хорошую основу для дальнейшего совершенствования дизайна API и извлечения различных ресурсов по мере их появления.
Другая причина заключается в том, что, когда мы делаем следующий шаг в направлении поиска событий, у нас есть чистый набор команд, с которыми мы работаем.
3.3. Отдельные представления ресурсов
Теперь давайте быстро рассмотрим различные представления нашего пользовательского ресурса, после этого разделения на команды и запросы:
public class UserQueryDto { private Long id; private String username; private boolean enabled; private Setroles; private long scheduledPostsCount; }
Вот наши командные DTO:
- Команда регистрации пользователя D to используется для представления регистрационных данных пользователя :
public class UserRegisterCommandDto { private String username; private String email; private String password; }
- UserUpdatePasswordCommandDto используется для представления данных для обновления текущего пароля пользователя:
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
- Команда сброса пароля триггера пользователя D to используется для представления электронной почты пользователя, чтобы вызвать сброс пароля, отправив электронное письмо с маркером сброса пароля:
public class UserTriggerResetPasswordCommandDto { private String email; }
- UserChangePasswordCommandDto используется для представления нового пароля пользователя – эта команда вызывается после того, как пользователь использует токен сброса пароля.
public class UserChangePasswordCommandDto { private String password; }
- Команда обновления пользователя D to используется для представления данных нового пользователя после внесения изменений:
public class UserUpdateCommandDto { private Long id; private boolean enabled; private Setroles; }
4. Заключение
В этом уроке мы заложили основу для чистой реализации CQRS для API Spring REST.
Следующим шагом будет дальнейшее совершенствование API путем определения некоторых отдельных обязанностей (и ресурсов) в их собственных службах, чтобы мы более тесно согласовывались с архитектурой, ориентированной на ресурсы.