Wenden Sie CQRS auf eine Spring REST-API an

Wenden Sie CQRS auf eine Spring REST-API an

1. Überblick

In diesem kurzen Artikel werden wir etwas Neues tun. Wir werden eine vorhandene REST Spring-API weiterentwickeln und die Verwendung der Segregation für die Verantwortung für Befehlsabfragen verwenden -CQRS.

Das Ziel ist es,clearly separate both the service and the controller layers für Lesevorgänge - Abfragen und Schreibvorgänge - Befehle zu verwenden, die separat in das System eingehen.

Denken Sie daran, dass dies nur ein früher erster Schritt in Richtung dieser Art von Architektur ist und kein „Ankunftsort“. Davon abgesehen - ich bin begeistert von diesem.

Schließlich veröffentlicht die Beispiel-API, die wir verwenden werden, die Ressourcen vonUserund ist Teil unserer laufendenReddit app case study, um zu veranschaulichen, wie dies funktioniert - aber natürlich wird jede API dies tun.

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 undUserCommandService:

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 Lesen dieser API können Sie deutlich sehen, wie der Abfragedienst alle Lesevorgänge undthe command service isn’t reading any data – all void returns ausführt.

3. Die Controller-Schicht

Als nächstes - die Controller-Schicht.

3.1. Der Abfrage-Controller

Hier sind unsereUserQueryRestController:

@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 injiziert.

Noch interessanter wärecut off the access of this controller to the command services - indem diese in ein separates Modul gestellt werden.

3.2. Der Command Controller

Hier ist unsere Befehlssteuerungsimplementierung:

@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, dass jede dieser API-Implementierungen einen anderen Befehl verwendet. Dies dient vor allem dazu, eine gute Grundlage für die weitere Verbesserung des API-Designs und das Extrahieren unterschiedlicher Ressourcen zu schaffen.

Ein weiterer Grund ist, dass wir beim 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 der Benutzerregistrierungsdaten:

public class UserRegisterCommandDto {
    private String username;
    private String email;
    private String password;
}
  • UserUpdatePasswordCommandDto zur Darstellung von Daten zur Aktualisierung des aktuellen Benutzerkennworts:

public class UserUpdatePasswordCommandDto {
    private String oldPassword;
    private String password;
}
  • UserTriggerResetPasswordCommandDtowird 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 Kennwort zum Zurücksetzen des Kennworts verwendet hat.

public class UserChangePasswordCommandDto {
    private String password;
}
  • UserUpdateCommandDtowird 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 die Grundlagen 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) für ihre eigenen Dienste identifiziert werden, damit wir uns enger an einer ressourcenzentrierten Architektur ausrichten können.