知識庫 / Spring RSS 訂閱

將 CQRS 應用於 Spring REST API

REST,Spring
HongKong
4
03:59 AM · Dec 06 ,2025

1. 概述

在本文中,我們將進行一項新的嘗試。我們將演化一個現有的 REST Spring API,並使其採用命令查詢職責分離(CQRS)模式——CQRS

目標是明確分離服務層和控制器層,以便分別處理系統中的讀取(查詢)和寫入(命令)請求。

請注意,這只是邁向這種架構的早期步驟,而不是“終點”。儘管如此,我對這個方向感到興奮。

最後,我們將使用的 API 是發佈User資源的示例,並且是我們在持續的 Reddit 應用案例研究的一部分,旨在説明這種模式的工作原理——當然,任何 API 都可以。

2. 服務層

我們將從簡單入手——通過識別我們先前 User 服務的讀寫操作,並將這些操作拆分為 2 個獨立的服務:<em >UserQueryService</em><em >UserCommandService</em>

public interface IUserQueryService {

    List<User> 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);

}

從閲讀此API,您可以清楚地看到查詢服務正在執行所有的讀取操作,而命令服務並未進行任何數據讀取——所有返回值為空。

3. 控制器層

接下來是控制器層。

3.1 查詢控制器

以下是我們使用的 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<UserQueryDto> getUsersList(...) {
        PagingInfo pagingInfo = new PagingInfo(page, size, userService.countAllUsers());
        response.addHeader("PAGING_INFO", pagingInfo.toString());
        
        List<User> 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 Set<Role> roles;

    private long scheduledPostsCount;
}

以下是我們的 Command DTO:

  • UserRegisterCommandDto 用於表示用户註冊數據:
public class UserRegisterCommandDto {
    private String username;
    private String email;
    private String password;
}
<ul>
 <li><em>UserUpdatePasswordCommandDto</em> 用於表示更新當前用户密碼的數據:</li>
</ul>
public class UserUpdatePasswordCommandDto {
    private String oldPassword;
    private String password;
}
<ul>
 <li><em>UserTriggerResetPasswordCommandDto</em> 用於表示用户郵箱,通過發送包含重置密碼令牌的郵件來觸發重置密碼流程:</li>
</ul>
public class UserTriggerResetPasswordCommandDto {
    private String email;
}
<ul>
 <li><em>UserChangePasswordCommandDto</em> 用於表示新用户密碼 – 此命令在用户使用密碼重置令牌後調用。</li>
</ul>
public class UserChangePasswordCommandDto {
    private String password;
}
  • UserUpdateCommandDto 用於表示修改後新用户的元數據:
public class UserUpdateCommandDto {
    private Long id;

    private boolean enabled;

    private Set<Role> roles;
}

4. 結論

在本教程中,我們為 Spring REST API 建立了一個乾淨的 CQRS 實現的基礎。

下一步將通過識別一些獨立的職責(以及資源)並將其轉換為單獨的服務,從而更緊密地與以資源為中心的架構對齊。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.