Spring提供了同步事件机制:
- 事件必须继承ApplicationEvent
- 监听器实现ApplicationListener接口
- 发布者注入ApplicationEventPublisher
将BalanceChangedEvent改造:
package com.lugew.domaindrivendesignwithspringboot.atm;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
@Getter
public 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
@RequiredArgsConstructor
public class BalanceChangedEventHandler implements
ApplicationListener<BalanceChangedEvent> {
private final HeadOfficeInstance headOfficeInstance;
private final HeadOfficeRepository headOfficeRepository;
@Override
public 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")
@RequiredArgsConstructor
public 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()
@ResponseBody
public List<AtmDto> getAtms() {
List<AtmDto> list = new ArrayList<>();
atmRepository.findAll().forEach(list::add);
return list;
}
@GetMapping("/{id}")
@ResponseBody
public AtmDto getAtm(@PathVariable("id") long id) {
return atmRepository.findById(id).orElse(null);
}
@PutMapping("/{id}/{amount}")
@ResponseBody
public 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()) return
atm.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
*/
@Getter
public 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
@Setter
public 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
@Setter
public class AtmDto {
@Id
@GeneratedValue
private long id;
private float moneyCharged;
private int oneCentCount;
private int tenCentCount;
private int quarterCount;
private int oneDollarCount;
private int fiveDollarCount;
private int twentyDollarCount;
@Transient
private float amount;
@Transient
@JsonIgnore
private List<ApplicationEvent> domainEvents;
@PostLoad
public 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);