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 實現的基礎。
下一步將通過識別一些獨立的職責(以及資源)並將其轉換為單獨的服務,從而更緊密地與以資源為中心的架構對齊。