Wenden Sie CQRS auf eine Spring REST-API an

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

In diesem kurzen Artikel werden wir etwas Neues machen. Wir werden eine vorhandene REST Spring-API weiterentwickeln und die Verwendung der CQRS (Command Query Responsibility Segregation) verwenden.

Ziel ist es, sowohl die Service- als auch die Controller-Ebene klar voneinander zu trennen , um Lesevorgänge - Abfragen und Schreibvorgänge - Befehle, die separat in das System eingehen, zu verarbeiten.

Denken Sie daran, dass dies nur ein früher erster Schritt in Richtung dieser Art von Architektur ist und kein „Ankunftspunkt“. Davon abgesehen - ich freue mich über diesen einen.

Schließlich - die Beispiel-API, die wir verwenden werden, veröffentlicht Benutzerressourcen und ist Teil unserer laufenden Reddit-App-Fallstudie, um zu veranschaulichen, wie dies funktioniert - aber natürlich wird jede API funktionieren.

2. Die Serviceschicht

Wir beginnen einfach - indem wir nur die Lese- und Schreibvorgänge in unserem vorherigen Benutzerdienst identifizieren - und teilen diese in zwei separate Dienste auf - UserQueryService und UserCommandService :

public interface IUserQueryService { List getUsersList(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); }

Durch das Lesen dieser API können Sie deutlich sehen, wie der Abfragedienst das gesamte Lesen ausführt und der Befehlsdienst keine Daten liest - alle ungültigen Rückgaben .

3. Die Controller-Schicht

Als nächstes - die Controller-Schicht.

3.1. Der Abfrage-Controller

Hier ist unser UserQueryRestController :

@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 List getUsersList(...) { 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; } }

Interessant ist hier, dass der Abfrage-Controller nur Abfragedienste einfügt.

Noch interessanter wäre es, den Zugriff dieses Controllers auf die Befehlsdienste zu sperren - indem Sie diese in einem separaten Modul platzieren.

3.2. Der Command Controller

Hier ist unsere Implementierung des Befehlscontrollers:

@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); } }

Hier passieren ein paar interessante Dinge. Beachten Sie zunächst, wie jede dieser API-Implementierungen einen anderen Befehl verwendet. Dies dient hauptsächlich dazu, uns eine gute Basis zu bieten, um das Design der API weiter zu verbessern und verschiedene Ressourcen zu extrahieren, sobald sie entstehen.

Ein weiterer Grund ist, dass wir im nächsten Schritt in Richtung Event Sourcing über einen sauberen Befehlssatz verfügen, mit dem wir arbeiten.

3.3. Separate Ressourcendarstellungen

Lassen Sie uns nun nach dieser Trennung in Befehle und Abfragen schnell die verschiedenen Darstellungen unserer Benutzerressource durchgehen:

public class UserQueryDto { private Long id; private String username; private boolean enabled; private Set roles; private long scheduledPostsCount; }

Hier sind unsere Befehls-DTOs:

  • UserRegisterCommandDto zur Darstellung von Benutzerregistrierungsdaten :
public class UserRegisterCommandDto { private String username; private String email; private String password; }
  • UserUpdatePasswordCommandDto wird verwendet, um Daten zum Aktualisieren des aktuellen Benutzerkennworts darzustellen:
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
  • UserTriggerResetPasswordCommandDto wird verwendet, um die E-Mail des Benutzers darzustellen, um das Zurücksetzen des Passworts durch Senden einer E-Mail mit dem Token zum Zurücksetzen des Passworts auszulösen:
public class UserTriggerResetPasswordCommandDto { private String email; }
  • UserChangePasswordCommandDto zur Darstellung eines neuen Benutzerkennworts - Dieser Befehl wird aufgerufen, nachdem der Benutzer das Token zum Zurücksetzen des Kennworts verwendet hat.
public class UserChangePasswordCommandDto { private String password; }
  • UserUpdateCommandDto wird verwendet, um die Daten neuer Benutzer nach Änderungen darzustellen:
public class UserUpdateCommandDto { private Long id; private boolean enabled; private Set roles; }

4. Fazit

In diesem Tutorial haben wir den Grundstein für eine saubere CQRS-Implementierung für eine Spring REST-API gelegt.

Der nächste Schritt wird darin bestehen, die API weiter zu verbessern, indem einige separate Verantwortlichkeiten (und Ressourcen) in ihren eigenen Diensten identifiziert werden, damit wir uns enger an einer ressourcenzentrierten Architektur ausrichten können.

REST unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs