添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

JPA DDD 实践

充血、涨血模型的讨论不在此篇文章的讨论,DDD具体如何拆分业务也不在此讨论范围,我只是说一下我用JPA是如何进行DDD编码的。

首先需要明确一点,你到底需不需要PO(Persistent Object)?

我这里的实践是将PO和Domain合并了,拆分PO有好处有坏处,好处就是职责清晰,相对编码逻辑清楚点,坏处就是多了一个莫名其妙的PO,你会感到有一点点的不爽。

其次,你需不需要Service层?我建议你最好加个Service层,如果连Service层都没有,你这玩意儿跨度有点大,我个人不是很建议。

我个人的架构的习惯就是:接入层(例如Controller)负责参数的接受、校验以及返回结果,Service层负责事务,Domain层负责细节逻辑处理,Repository层负责持久化的抽象实现。

废话不多说,直接看领域模型,我这里用账号举例。

@Slf4j
@Entity
@Table(name = "uaa_account", uniqueConstraints = {
        @UniqueConstraint(name = "UK_username", columnNames = {"username"})
@SQLDelete(sql = "UPDATE uaa_account SET deleted = true WHERE id=?")
@Where(clause = "deleted=false")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {
    /* 状态 */
    @GeneratedValue(generator = "generator")
    @GenericGenerator(name = "generator",
            strategy = "com.esteel.uaa.manager.IdGenerator")
    @Getter
    private Long id;
    @Getter
    private String username;
    @Getter
    private String password;
    @Getter
    private String name;
    @Getter
    private boolean locked = false;
    @Getter
    private boolean enabled = true;
    @Getter
    private boolean expired = false;
    @ManyToMany
    @JoinTable(name = "uaa_rel_account_role",
            joinColumns = @JoinColumn(name = "account_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private final Set<Role> roles = new HashSet<>();
    public Set<Role> getRoles() {
        return Collections.unmodifiableSet(roles);
    public void lock() {
        this.locked = true;
    public void unlock() {
        this.locked = false;
    public void enable() {
        this.enabled = true;
    public void disable() {
        this.enabled = false;
    public void expire() {
        this.expired = true;
     * 为账号添加角色,该角色必须已经被持久化,否则会抛出 {@link AccountAddRoleException}
     * @param role 角色
    public void addRole(Role role) {
        if (role.getId() == null) {
            throw new AccountAddRoleException();
        this.roles.add(role);
     * 移除一个角色
     * @param role 角色
    public void removeRole(Role role) {
        this.roles.remove(role);
     * 修改用户名,如果用户名已经存在,将会抛出 {@link AccountUsernameExistsException}
     * 如果用户名不符合要求,则会抛出{@link AccountUsernameInvalidException}
     * @param username 用户名
    public void changeUsername(String username) {
        if (repository.existsByUsername(username)) {
            throw new AccountUsernameExistsException();
        if (!StringUtils.hasText(username)) {
            throw new AccountUsernameInvalidException();
        this.username = username;
     * 修改密码
     * @param password 新密码
    public void resetPassword(String password) {
        this.password = passwordEncoder.encode(password);
     * 修改姓名





    
     * @param name 姓名
    public void changeName(String name) {
        this.name = name;
    public void save() {
        repository.save(this);
    public void destroy() {
        repository.delete(this);
    // ===============构造方法区================
    @Transient
    private PasswordEncoder passwordEncoder;
    @Transient
    private AccountRepository repository;
    protected Account(String username, String password) {
        this.init();
        this.username = username;
        this.password = passwordEncoder.encode(password);
    @PostLoad
    private void init() {
        passwordEncoder = ApplicationContextProvider.getBean(PasswordEncoder.class);
        repository = ApplicationContextProvider.getBean(AccountRepository.class);
        log.info(repository + "");
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return Objects.equals(id, account.id);
    @Override
    public int hashCode() {
        return Objects.hash(id);

首先,我们一段代码一段代码去看。

@Slf4j
@Entity
@Table(name = "uaa_account", uniqueConstraints = {
        @UniqueConstraint(name = "UK_username", columnNames = {"username"})
@SQLDelete(sql = "UPDATE uaa_account SET deleted = true WHERE id=?")
@Where(clause = "deleted=false")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

声明式的编程,JPA避免不了,这里简单做了几件事情:

  1. 声明这是个Entity
  2. 这个Entity指向的表
  3. 逻辑删除
  4. 一个受保护的构造方法给JPA反序列化使用

这里其实是有问题的,受保护的构造方法其实是对JPA的妥协,理论上不应该有,按照代码的设计来讲,应该是一个私有无参的构造方法来保证必填参数的逻辑校验。

接着,我们看第二段代码:

    @Id
    @GeneratedValue(generator = "generator")
    @GenericGenerator(name = "generator",
            strategy = "com.esteel.uaa.manager.IdGenerator")
    @Getter
    private Long id;
    @Getter
    private String username;
    @Getter
    private String password;
    @Getter
    private String name;
    @Getter
    private boolean locked = false;
    @Getter
    private boolean enabled = true;
    @Getter
    private boolean expired = false;
    @ManyToMany
    @JoinTable(name = "uaa_rel_account_role",
            joinColumns = @JoinColumn(name = "account_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private final Set<Role> roles = new HashSet<>();
    public Set<Role> getRoles() {
        return Collections.unmodifiableSet(roles);
    }

这里是状态代码区,里面声明了一些状态,全是只读状态,因为我这边对Account的定义就是一旦被创建,就不允许通过简单的Setter方法来修改状态,我是需要进行逻辑控制的。

包括多对多的一个集合Roles,我这边给出的是一个不可变的集合,意味着我只允许你读,但是不允许你 随便 写,想写可以,必须通过我提供的函数进行操作。

第三段是对象行为,我这边其实比较简单,看上去与普通的setter没啥区别:

    public void lock() {
        this.locked = true;
    public void unlock() {
        this.locked = false;
    public void enable() {
        this.enabled = true;
    public void disable() {
        this.enabled = false;
    public void expire() {
        this.expired = true;
     * 为账号添加角色,该角色必须已经被持久化,否则会抛出 {@link AccountAddRoleException}
     * @param role 角色
    public void addRole(Role role) {
        if (role.getId() == null) {
            throw new AccountAddRoleException();
        this.roles.add(role);
     * 移除一个角色
     * @param role 角色
    public void removeRole(Role role) {
        this.roles.remove(role);
     * 修改用户名,如果用户名已经存在,将会抛出 {@link AccountUsernameExistsException}
     * 如果用户名不符合要求,则会抛出{@link AccountUsernameInvalidException}
     * @param username 用户名
    public void changeUsername(String username) {
        if (repository.existsByUsername(username)) {
            throw new AccountUsernameExistsException();
        if (!StringUtils.hasText(username)) {
            throw new AccountUsernameInvalidException();
        this.username = username;
     * 修改密码
     * @param password 新密码
    public void resetPassword(String password) {
        this.password = passwordEncoder.encode(password);
     * 修改姓名
     * @param name 姓名
    public void changeName(String name) {
        this.name = name;
    public void save() {
        repository.save(this);
    public void destroy() {
        repository.delete(this);
    }

这段代码看似平淡无奇,但是有两点需要注意:

第一点是:我在改变状态的时候加了逻辑处理,例如:

    /**
     * 修改用户名,如果用户名已经存在,将会抛出 {@link AccountUsernameExistsException}
     * 如果用户名不符合要求,则会抛出{@link AccountUsernameInvalidException}
     * @param username 用户名
    public void changeUsername(String username) {
        if (repository.existsByUsername(username)) {
            throw new AccountUsernameExistsException();
        if (!StringUtils.hasText(username)) {
            throw new AccountUsernameInvalidException();
        this.username = username;
    }

第二点是:我依赖了持久化层Repository和外部工具PasswordEncounter,例如:

repository.save(this);

以及

this.password = passwordEncoder.encode(password);

这里,就引申出一个问题,这俩货到底是怎么被注入进来的?

我们接着看下面的代码:

    // ===============构造方法区================
    @Transient
    private PasswordEncoder passwordEncoder;
    @Transient
    private AccountRepository repository;
    protected Account(String username, String password) {
        this.init();
        this.username = username;
        this.password = passwordEncoder.encode(password);
    @PostLoad
    private void init() {
        passwordEncoder = ApplicationContextProvider.getBean(PasswordEncoder.class);
        repository = ApplicationContextProvider.getBean(AccountRepository.class);
        log.info(repository + "");
    }

我们在构造这个类的时候,在构造函数中做了一个动作,就是 @PostLoad 所标记的方法,我在这里初始化了这两依赖。

@PostLoad 大家都知道,就是JPA从数据库中加载这个对象的时候,会执行这个代码。

这样,我就可以为这个对象赋予了更多 实际行为。

什么叫 实际行为

我们对于DDD的理解,往往都是只关注实现而不关注如何落地。

这也是争论比较多的地方。

如果只关注如何实现的话,Domain层不应当依赖任何持久化或外部工具,应当独立的完成状态变更,由外部控制器来决定如何落地。

这样有好处,即领域脱离框架。

但是也有坏处,就是过于理想主义,因为毕竟你的最终目标是如何落地,就如同我这里所讲的是如何基于JPA进行DDD开发一样。

不过落地有两种方式。

第一种方式就是状态改变即save:

    public void resetPassword(String password) {
        this.password = passwordEncoder.encode(password);
        this.save();
    }

即对象状态发生改变的时候,就立刻进行save,这样就不需要在控制器中显示的调用save,其实JPA管理的很好,你就算立刻进行sava,它也是在事务执行完毕之后再提交。

但是如果不再事务控制中,就容易出殡。

所以,第二种方式就是将save方法暴露出去,由外部控制器来决定何时进行save。

如果我是一个人写代码,我会选择直接save,如果是多人配合,我还是习惯将这个save操作暴露给操作者让他自己选择。


到这里,领域的定义已经完成, 基于JPA做DDD唯一的缺点就是,JPA的Entity【 建议】 你要有一个无参且最低级别为Protected的构造函数。

如果能够定义private,那就完美了,Protected的构造函数还是容易出殡,但是没办法。


后记:

如果你使用aspectj做高级AOP,利用 @Configurable 做注入我是非常不建议的,因为这玩意儿过于 高级 ,多团队配合的时候容易出殡。

ApplicationContextProvidder 是一个套路玩法,具体代码如下:

@Slf4j
@Component
public final class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;
    private static void setContext(ApplicationContext ctx) {
        ApplicationContextProvider.applicationContext = ctx;
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        setContext(ctx);