Gunakan CQRS ke Spring REST API

REST Teratas

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS

1. Gambaran keseluruhan

Dalam artikel ringkas ini, kita akan melakukan sesuatu yang baru. Kami akan mengembangkan REST Spring API yang ada dan menggunakannya menggunakan Command Query Responsibility Segregation - CQRS.

Tujuannya adalah untuk memisahkan lapisan perkhidmatan dan pengawal dengan jelas untuk menangani Baca - Pertanyaan dan Penulisan - Perintah yang masuk ke dalam sistem secara berasingan.

Perlu diingat bahawa ini hanyalah langkah awal awal menuju jenis seni bina ini, bukan "tempat kedatangan". Yang dikatakan - Saya teruja dengan yang ini.

Akhirnya - contoh API yang akan kita gunakan adalah menerbitkan sumber Pengguna dan merupakan sebahagian daripada kajian kes aplikasi Reddit kami yang sedang berjalan untuk memberi contoh bagaimana ini berfungsi - tetapi tentu saja, API apa pun akan dilakukan.

2. Lapisan Perkhidmatan

Kami akan mula mudah - dengan hanya mengenal pasti operasi membaca dan menulis di perkhidmatan Pengguna kami sebelumnya - dan kami akan membahagikannya kepada 2 perkhidmatan berasingan - UserQueryService dan 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); }

Dengan membaca API ini, anda dapat melihat dengan jelas bagaimana perkhidmatan pertanyaan melakukan semua pembacaan dan perkhidmatan perintah tidak membaca data - semua pengembalian tidak sah .

3. Lapisan Pengawal

Selanjutnya - lapisan pengawal.

3.1. Pengawal Pertanyaan

Inilah UserQueryRestController kami :

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

Apa yang menarik di sini ialah pengawal pertanyaan hanya menyuntik perkhidmatan pertanyaan.

Apa yang lebih menarik ialah memotong akses pengawal ini ke perkhidmatan arahan - dengan meletakkannya dalam modul yang berasingan.

3.2. Pengawal Perintah

Sekarang, inilah pelaksanaan pengawal arahan kami:

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

Beberapa perkara menarik berlaku di sini. Pertama - perhatikan bagaimana setiap pelaksanaan API ini menggunakan perintah yang berbeza. Ini terutama untuk memberi kita asas yang baik untuk meningkatkan lagi reka bentuk API dan mengekstrak sumber yang berbeza ketika mereka muncul.

Sebab lain ialah ketika kita mengambil langkah seterusnya, menuju Event Sourcing - kita mempunyai set perintah yang bersih yang sedang kita kerjakan.

3.3. Perwakilan Sumber Berasingan

Sekarang mari kita melihat perwakilan sumber pengguna kita dengan cepat, setelah pemisahan ini menjadi perintah dan pertanyaan:

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

Berikut adalah DTO Perintah kami:

  • UserRegisterCommandD digunakan untuk mewakili data pendaftaran pengguna :
public class UserRegisterCommandDto { private String username; private String email; private String password; }
  • UserUpdatePasswordCommandDigunakan untuk mewakili data untuk mengemas kini kata laluan pengguna semasa:
public class UserUpdatePasswordCommandDto { private String oldPassword; private String password; }
  • UserTriggerResetPasswordCommandD digunakan untuk mewakili e-mel pengguna untuk mencetuskan tetapan semula kata laluan dengan menghantar e-mel dengan token tetapan semula kata laluan:
public class UserTriggerResetPasswordCommandDto { private String email; }
  • UserChangePasswordCommandD digunakan untuk mewakili kata laluan pengguna baru - perintah ini dipanggil setelah pengguna menggunakan token tetapan semula kata laluan.
public class UserChangePasswordCommandDto { private String password; }
  • UserUpdateCommandDuntuk digunakan untuk mewakili data pengguna baru setelah pengubahsuaian:
public class UserUpdateCommandDto { private Long id; private boolean enabled; private Set roles; }

4. Kesimpulan

Dalam tutorial ini, kami meletakkan dasar menuju implementasi CQRS yang bersih untuk Spring REST API.

Langkah seterusnya adalah untuk terus meningkatkan API dengan mengenal pasti beberapa tanggungjawab yang terpisah (dan Sumber) ke dalam perkhidmatan mereka sendiri sehingga kita lebih dekat dengan seni bina yang berpusatkan Sumber.

REST bawah

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS