Spring提供了同步事件机制:

    • 事件必须继承ApplicationEvent
    • 监听器实现ApplicationListener接口
    • 发布者注入ApplicationEventPublisher

    将BalanceChangedEvent改造:

    1. package com.lugew.domaindrivendesignwithspringboot.atm;
    2. import lombok.Getter;
    3. import org.springframework.context.ApplicationEvent;
    4. @Getter
    5. public class BalanceChangedEvent extends ApplicationEvent {
    6. public float delta;
    7. public BalanceChangedEvent(Object source, float delta) {
    8. super(source);
    9. this.delta = delta;
    10. }
    11. }

    监听器:

    1. package com.lugew.domaindrivendesignwithspringboot.management;
    2. import lombok.RequiredArgsConstructor;
    3. import org.springframework.context.ApplicationListener;
    4. import org.springframework.stereotype.Component;
    5. @Component
    6. @RequiredArgsConstructor
    7. public class BalanceChangedEventHandler implements
    8. ApplicationListener<BalanceChangedEvent> {
    9. private final HeadOfficeInstance headOfficeInstance;
    10. private final HeadOfficeRepository headOfficeRepository;
    11. @Override
    12. public void onApplicationEvent(BalanceChangedEvent domainEvent) {
    13. HeadOfficeDto headOfficeDto = headOfficeInstance.getInstance();
    14. HeadOffice headOffice = headOfficeDto.convertToHeadOffice();
    15. headOffice.changeBalance(domainEvent.getDelta());
    16. headOfficeRepository.save(headOffice.convertToHeadOfficeDto());
    17. }
    18. }

    注意,领域事件是定义在ATM限界上下文的,而处理器是位于Management限界上下文。这种结构反映了两者间的真实联系。ATM限界上下文产生事件,Management限界上下文消费事件并执行相应动作。接下来进行重构:

    1. package com.lugew.domaindrivendesignwithspringboot.atm;
    2. import lombok.RequiredArgsConstructor;
    3. import org.springframework.context.ApplicationEvent;
    4. import org.springframework.context.ApplicationEventPublisher;
    5. import org.springframework.stereotype.Controller;
    6. import org.springframework.web.bind.annotation.*;
    7. import java.util.ArrayList;
    8. import java.util.List;
    9. @Controller
    10. @RequestMapping(path = "/atms")
    11. @RequiredArgsConstructor
    12. public class AtmController {
    13. private final AtmRepository atmRepository;
    14. private final PaymentGateway paymentGateway;
    15. private final ApplicationEventPublisher applicationEventPublisher;
    16. private void dispatchEvents(AtmDto atmDto) {
    17. if (atmDto == null) return;
    18. for (ApplicationEvent domainEvent : atmDto.getDomainEvents()) {
    19. applicationEventPublisher.publishEvent(domainEvent);
    20. }
    21. atmDto.clearEvents();
    22. }
    23. @GetMapping()
    24. @ResponseBody
    25. public List<AtmDto> getAtms() {
    26. List<AtmDto> list = new ArrayList<>();
    27. atmRepository.findAll().forEach(list::add);
    28. return list;
    29. }
    30. @GetMapping("/{id}")
    31. @ResponseBody
    32. public AtmDto getAtm(@PathVariable("id") long id) {
    33. return atmRepository.findById(id).orElse(null);
    34. }
    35. @PutMapping("/{id}/{amount}")
    36. @ResponseBody
    37. public String takeMoney(@PathVariable("id") long id, @PathVariable("amount")
    38. float amount) {
    39. AtmDto atmDto = atmRepository.findById(id).orElse(null);
    40. Atm atm = atmDto.convertToAtm();
    41. if (!atm.canTakeMoney(amount).isEmpty()) return
    42. atm.canTakeMoney(amount);
    43. float amountWithCommission =
    44. atm.calculateAmountWithCommission(amount);
    45. paymentGateway.chargePayment(amountWithCommission);
    46. atm.takeMoney(amount);
    47. atmRepository.save(atm.convertToAtmDto());
    48. dispatchEvents(atmDto);
    49. return "You have withrawn amount : $" + amount;
    50. }
    51. }
    1. package com.lugew.domaindrivendesignwithspringboot.common;
    2. import lombok.Getter;
    3. import org.springframework.context.ApplicationEvent;
    4. import java.util.ArrayList;
    5. import java.util.List;
    6. /**
    7. * @author 夏露桂
    8. * @since 2021/6/22 10:07
    9. */
    10. @Getter
    11. public abstract class AggregateRoot extends Entity {
    12. private List<ApplicationEvent> domainEvents = new ArrayList<>();
    13. protected void addDomainEvent(ApplicationEvent newEvent) {
    14. domainEvents.add(newEvent);
    15. }
    16. public void clearEvents() {
    17. domainEvents.clear();
    18. }
    19. }
    1. package com.lugew.domaindrivendesignwithspringboot.atm;
    2. import com.lugew.domaindrivendesignwithspringboot.common.AggregateRoot;
    3. import com.lugew.domaindrivendesignwithspringboot.sharedkernel.Money;
    4. import lombok.Getter;
    5. import lombok.Setter;
    6. import static com.lugew.domaindrivendesignwithspringboot.sharedkernel.Money.None;
    7. @Getter
    8. @Setter
    9. public class Atm extends AggregateRoot {
    10. private static float commissionRate = 0.01f;
    11. private Money moneyInside = None;
    12. private float moneyCharged;
    13. public void loadMoney(Money money) {
    14. moneyInside = moneyInside.add(money);
    15. }
    16. public void takeMoney(float amount) {
    17. if (!canTakeMoney(amount).equals("")) {
    18. throw new IllegalStateException();
    19. }
    20. Money output = moneyInside.allocate(amount);
    21. moneyInside = moneyInside.substract(output);
    22. float amountWithCommission = calculateAmountWithCommission(amount);
    23. moneyCharged += amountWithCommission;
    24. addDomainEvent(new BalanceChangedEvent(null, amountWithCommission));
    25. }
    26. public String canTakeMoney(float amount) {
    27. if (amount <= 0f)
    28. return "Invalid amount";
    29. if (moneyInside.getAmount() < amount)
    30. return "Not enough money";
    31. if (!moneyInside.canAllocate(amount))
    32. return "Not enough change";
    33. return "";
    34. }
    35. public float calculateAmountWithCommission(float amount) {
    36. float commission = amount * commissionRate;
    37. float lessThanCent = commission % 0.01f;
    38. if (lessThanCent > 0) {
    39. commission = commission - lessThanCent + 0.01f;
    40. }
    41. return amount + commission;
    42. }
    43. public AtmDto convertToAtmDto() {
    44. AtmDto atmDto = new AtmDto();
    45. atmDto.setId(id);
    46. atmDto.setMoneyCharged(moneyCharged);
    47. atmDto.setOneCentCount(moneyInside.getOneCentCount());
    48. atmDto.setTenCentCount(moneyInside.getTenCentCount());
    49. atmDto.setQuarterCount(moneyInside.getQuarterCount());
    50. atmDto.setOneDollarCount(moneyInside.getOneDollarCount());
    51. atmDto.setFiveDollarCount(moneyInside.getFiveDollarCount());
    52. atmDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());
    53. atmDto.setDomainEvents(getDomainEvents());
    54. return atmDto;
    55. }
    56. }
    1. package com.lugew.domaindrivendesignwithspringboot.atm;
    2. import com.fasterxml.jackson.annotation.JsonIgnore;
    3. import com.lugew.domaindrivendesignwithspringboot.sharedkernel.Money;
    4. import lombok.Getter;
    5. import lombok.Setter;
    6. import org.springframework.context.ApplicationEvent;
    7. import javax.persistence.*;
    8. import java.util.List;
    9. @Entity
    10. @Getter
    11. @Setter
    12. public class AtmDto {
    13. @Id
    14. @GeneratedValue
    15. private long id;
    16. private float moneyCharged;
    17. private int oneCentCount;
    18. private int tenCentCount;
    19. private int quarterCount;
    20. private int oneDollarCount;
    21. private int fiveDollarCount;
    22. private int twentyDollarCount;
    23. @Transient
    24. private float amount;
    25. @Transient
    26. @JsonIgnore
    27. private List<ApplicationEvent> domainEvents;
    28. @PostLoad
    29. public void setAmount() {
    30. amount = oneCentCount * 0.01f + tenCentCount * 0.10f + quarterCount * 0.25f
    31. + oneDollarCount * 1f
    32. + fiveDollarCount * 5f + twentyDollarCount * 20f;
    33. }
    34. public void clearEvents() {
    35. domainEvents.clear();
    36. }
    37. public Atm convertToAtm() {
    38. Atm atm = new Atm();
    39. atm.setId(id);
    40. atm.setMoneyCharged(moneyCharged);
    41. atm.setMoneyInside(new Money(oneCentCount, tenCentCount, quarterCount,
    42. oneDollarCount, fiveDollarCount, twentyDollarCount));
    43. return atm;
    44. }
    45. }

    image.png
    上图是执行过程的描述。先是执行业务操作,操作触发了领域事件,随后进行校验,最后保存数据。事件的产生、消费和整个过程看起来不错,但是当中间环节出错时,领域事件的产生不能回滚。这就是上面先保存后发布领域事件的原因:

    1. atmRepository.save(atmDto);
    2. dispatchEvents(atmDto);