Spring提供了同步事件机制:
- 事件必须继承ApplicationEvent
- 监听器实现ApplicationListener接口
- 发布者注入ApplicationEventPublisher
将BalanceChangedEvent改造:
package com.lugew.domaindrivendesignwithspringboot.atm;import lombok.Getter;import org.springframework.context.ApplicationEvent;@Getterpublic class BalanceChangedEvent extends ApplicationEvent {public float delta;public BalanceChangedEvent(Object source, float delta) {super(source);this.delta = delta;}}
监听器:
package com.lugew.domaindrivendesignwithspringboot.management;import lombok.RequiredArgsConstructor;import org.springframework.context.ApplicationListener;import org.springframework.stereotype.Component;@Component@RequiredArgsConstructorpublic class BalanceChangedEventHandler implementsApplicationListener<BalanceChangedEvent> {private final HeadOfficeInstance headOfficeInstance;private final HeadOfficeRepository headOfficeRepository;@Overridepublic void onApplicationEvent(BalanceChangedEvent domainEvent) {HeadOfficeDto headOfficeDto = headOfficeInstance.getInstance();HeadOffice headOffice = headOfficeDto.convertToHeadOffice();headOffice.changeBalance(domainEvent.getDelta());headOfficeRepository.save(headOffice.convertToHeadOfficeDto());}}
注意,领域事件是定义在ATM限界上下文的,而处理器是位于Management限界上下文。这种结构反映了两者间的真实联系。ATM限界上下文产生事件,Management限界上下文消费事件并执行相应动作。接下来进行重构:
package com.lugew.domaindrivendesignwithspringboot.atm;import lombok.RequiredArgsConstructor;import org.springframework.context.ApplicationEvent;import org.springframework.context.ApplicationEventPublisher;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.*;import java.util.ArrayList;import java.util.List;@Controller@RequestMapping(path = "/atms")@RequiredArgsConstructorpublic class AtmController {private final AtmRepository atmRepository;private final PaymentGateway paymentGateway;private final ApplicationEventPublisher applicationEventPublisher;private void dispatchEvents(AtmDto atmDto) {if (atmDto == null) return;for (ApplicationEvent domainEvent : atmDto.getDomainEvents()) {applicationEventPublisher.publishEvent(domainEvent);}atmDto.clearEvents();}@GetMapping()@ResponseBodypublic List<AtmDto> getAtms() {List<AtmDto> list = new ArrayList<>();atmRepository.findAll().forEach(list::add);return list;}@GetMapping("/{id}")@ResponseBodypublic AtmDto getAtm(@PathVariable("id") long id) {return atmRepository.findById(id).orElse(null);}@PutMapping("/{id}/{amount}")@ResponseBodypublic String takeMoney(@PathVariable("id") long id, @PathVariable("amount")float amount) {AtmDto atmDto = atmRepository.findById(id).orElse(null);Atm atm = atmDto.convertToAtm();if (!atm.canTakeMoney(amount).isEmpty()) returnatm.canTakeMoney(amount);float amountWithCommission =atm.calculateAmountWithCommission(amount);paymentGateway.chargePayment(amountWithCommission);atm.takeMoney(amount);atmRepository.save(atm.convertToAtmDto());dispatchEvents(atmDto);return "You have withrawn amount : $" + amount;}}
package com.lugew.domaindrivendesignwithspringboot.common;import lombok.Getter;import org.springframework.context.ApplicationEvent;import java.util.ArrayList;import java.util.List;/*** @author 夏露桂* @since 2021/6/22 10:07*/@Getterpublic abstract class AggregateRoot extends Entity {private List<ApplicationEvent> domainEvents = new ArrayList<>();protected void addDomainEvent(ApplicationEvent newEvent) {domainEvents.add(newEvent);}public void clearEvents() {domainEvents.clear();}}
package com.lugew.domaindrivendesignwithspringboot.atm;import com.lugew.domaindrivendesignwithspringboot.common.AggregateRoot;import com.lugew.domaindrivendesignwithspringboot.sharedkernel.Money;import lombok.Getter;import lombok.Setter;import static com.lugew.domaindrivendesignwithspringboot.sharedkernel.Money.None;@Getter@Setterpublic class Atm extends AggregateRoot {private static float commissionRate = 0.01f;private Money moneyInside = None;private float moneyCharged;public void loadMoney(Money money) {moneyInside = moneyInside.add(money);}public void takeMoney(float amount) {if (!canTakeMoney(amount).equals("")) {throw new IllegalStateException();}Money output = moneyInside.allocate(amount);moneyInside = moneyInside.substract(output);float amountWithCommission = calculateAmountWithCommission(amount);moneyCharged += amountWithCommission;addDomainEvent(new BalanceChangedEvent(null, amountWithCommission));}public String canTakeMoney(float amount) {if (amount <= 0f)return "Invalid amount";if (moneyInside.getAmount() < amount)return "Not enough money";if (!moneyInside.canAllocate(amount))return "Not enough change";return "";}public float calculateAmountWithCommission(float amount) {float commission = amount * commissionRate;float lessThanCent = commission % 0.01f;if (lessThanCent > 0) {commission = commission - lessThanCent + 0.01f;}return amount + commission;}public AtmDto convertToAtmDto() {AtmDto atmDto = new AtmDto();atmDto.setId(id);atmDto.setMoneyCharged(moneyCharged);atmDto.setOneCentCount(moneyInside.getOneCentCount());atmDto.setTenCentCount(moneyInside.getTenCentCount());atmDto.setQuarterCount(moneyInside.getQuarterCount());atmDto.setOneDollarCount(moneyInside.getOneDollarCount());atmDto.setFiveDollarCount(moneyInside.getFiveDollarCount());atmDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());atmDto.setDomainEvents(getDomainEvents());return atmDto;}}
package com.lugew.domaindrivendesignwithspringboot.atm;import com.fasterxml.jackson.annotation.JsonIgnore;import com.lugew.domaindrivendesignwithspringboot.sharedkernel.Money;import lombok.Getter;import lombok.Setter;import org.springframework.context.ApplicationEvent;import javax.persistence.*;import java.util.List;@Entity@Getter@Setterpublic class AtmDto {@Id@GeneratedValueprivate long id;private float moneyCharged;private int oneCentCount;private int tenCentCount;private int quarterCount;private int oneDollarCount;private int fiveDollarCount;private int twentyDollarCount;@Transientprivate float amount;@Transient@JsonIgnoreprivate List<ApplicationEvent> domainEvents;@PostLoadpublic void setAmount() {amount = oneCentCount * 0.01f + tenCentCount * 0.10f + quarterCount * 0.25f+ oneDollarCount * 1f+ fiveDollarCount * 5f + twentyDollarCount * 20f;}public void clearEvents() {domainEvents.clear();}public Atm convertToAtm() {Atm atm = new Atm();atm.setId(id);atm.setMoneyCharged(moneyCharged);atm.setMoneyInside(new Money(oneCentCount, tenCentCount, quarterCount,oneDollarCount, fiveDollarCount, twentyDollarCount));return atm;}}

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