1. 概述
在本文中,我們將做一些新的事情。我們將演化一個現有的 REST Spring API,並使其使用命令查詢職責分離 – CQRS。
目標是清晰地分離服務層和控制器層,以便分別處理系統中的讀取 – 查詢和寫入 – 命令。
請記住,這只是邁向這種架構的早期步驟,而不是“終點”。儘管如此——我對它感到興奮。
最後,我們將使用的 API 發佈User資源,它是我們持續的 Reddit 應用案例研究的一部分,旨在説明其工作原理——當然,任何 API 都可以。
2. 服務層
我們先從簡單入手——只識別我們之前 User 服務中的讀取和寫入操作,並將它們拆分為 2 個單獨的服務:UserQueryService 和 UserCommandService:
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,你可以清楚地看到查詢服務正在執行所有讀取操作,並且 命令服務沒有讀取任何數據——所有 void 返回。
3. The Controller Layer
下一部分是控制器層。
3.1. The Query Controller
以下是我們的
@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. The Command Controller
現在,以下是命令控制器的實現:
@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. Separate Resource Representations
現在,讓我們快速瀏覽一下我們用户資源的各種表示形式,在將這些內容分離到命令和查詢之後:
public class UserQueryDto {
private Long id;
private String username;
private boolean enabled;
private Set<Role> roles;
private long scheduledPostsCount;
}
以下是我們的命令 DTO:
- UserRegisterCommandDto 用於表示用户註冊數據
public class UserRegisterCommandDto {
private String username;
private String email;
private String password;
}
- UserUpdatePasswordCommandDto 用於表示當前用户密碼更新數據
public class UserUpdatePasswordCommandDto {
private String oldPassword;
private String password;
}
- UserTriggerResetPasswordCommandDto 用於表示用户郵箱,用於通過發送包含重置密碼令牌的電子郵件來觸發重置密碼
public class UserTriggerResetPasswordCommandDto {
private String email;
}
- UserChangePasswordCommandDto 用於表示新的用户密碼——此命令在用户使用重置密碼令牌後調用
public class UserChangePasswordCommandDto {
private String password;
}
- UserUpdateCommandDto 用於表示新的用户數據——此 DTO 在修改後使用
public class UserUpdateCommandDto {
private Long id;
private boolean enabled;
private Set<Role> roles;
}
4. 結論
在本教程中,我們為 Spring REST API 建立了一個乾淨的 CQRS 實現的基礎。
下一步將通過識別一些單獨的職責(以及資源)並將其劃分為單獨的服務,以便更好地與以資源為中心的架構對齊。