6聚合根与资源库 #

本系列包含以下文章:

  1. DDD入门
  2. DDD概念大白话
  3. 战略设计
  4. 代码工程结构
  5. 请求处理流程
  6. 聚合根与资源库(本文)
  7. 实体与值对象
  8. 应用服务与领域服务
  9. 领域事件
  10. CQRS

案例项目介绍 #

既然DDD是“领域”驱动,那么我们便不能抛开业务而只讲技术,为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云(不是马云,也不是码云)。如你已经在本系列的其他文章中了解过该案例,可跳过。

码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,并以该二维码为入口展开对“物品”的相关操作,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。

在使用码如云时,首先需要创建一个应用(App),一个应用包含了多个页面(Page),也可称为表单,一个页面又可以包含多个控件(Control),比如单选框控件。应用创建好后,可在应用下创建多个实例(QR)用于表示被管理的对象(比如机器设备)。每个实例均对应一个二维码,手机扫码便可对实例进行相应操作,比如查看实例相关信息或者填写页面表单等,对表单的一次填写称为提交(Submission);更多概念请参考码如云术语。

在技术上,码如云是一个无代码平台,包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发,其后端技术栈主要有Java、Spring Boot和MongoDB等。

码如云的源代码是开源的,可以通过以下方式访问:

码如云源代码:GitHub - mryqr-com/mry-backend: 本代码库为码如云后端代码。码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,手机扫码即可查看物品信息并发起相关业务操作,操作内容可由你自己定义,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。在技术上,码如云是一个无代码平台,全程采用DDD、整洁架构和事件驱动架构思想完成开发。

聚合根与资源库 #

在上一篇请求处理流程中我们讲到,领域模型是DDD的核心,而聚合根又是领域模型的核心。从某种意义上讲,DDD中其它组件均可看作是对聚合根的支撑或辅助。在本文中,我们将对聚合根以及与之密切相关的资源库(Repository)做详细的讲解。

聚合根是什么 #

在DDD概念大白话一文中,我们讲到了“什么是聚合根”,这里再重复一下。聚合根中的“聚合”即“高内聚,低耦合”中的“内聚”之意;而“根”则是“根部”的意思,也即聚合根是一种统领式的存在。事实上,并不存在一个教科书式的对聚合根的理论定义,你可以将聚合根理解为一个系统中最重要最显著的那些名词,这些名词是其所在的软件系统之所以存在的原因。为了给你一个直观的理解,以下是几个聚合根的例子:

  • 在一个电商系统中,一个订单(Order)对象表示一个聚合根
  • 在一个CRM系统中,一个客户(Customer)对象表示一个聚合根
  • 在一个银行系统中,一次交易(Transaction)对象表示一个聚合根

你可能会问,软件中的概念已经很多了,为什么还要搞出个聚合根的概念?我们认为这里至少有2点原因:

  1. 聚合根遵循了软件中“高内聚,低耦合”的基本原则
  2. 聚合根体现了一种模块化的原则,模块化思想是被各个行业所证明的可以降低系统复杂度的一种思想。所谓的DDD是“软件核心复杂性应对之道”,也即这个意思,它将软件系统在人脑中所呈现地更加有序和简单,让人可以更好地理解和管控软件系统。

在实际项目中识别聚合根时,我们需要对业务有深入的了解,因为只有这样你才知道到底哪些业务逻辑是内聚在一起的。这也是我们一直建议程序员和架构师们不要一味地埋头于技术而要多关注业务的原因。

事实上,如果让一个从来没有接触过DDD的人来建模,十有八九也能设计出上面的订单、客户和交易对象出来。没错,DDD绝非什么颠覆式的发明,依然只是在前人基础上的一种进步而已,这种进步更多的体现在一些设计原则上,对此我们将在下文进行详细阐述。

聚合根基类 #

在代码实现层面,一般的实践是将所有的聚合根都继承自一个公共基类AggregateRoot

//AggregateRoot@Getter
public abstract class AggregateRoot implements Identified {private String id;//聚合根IDprivate String tenantId;//租户IDprivate Instant createdAt;//创建时间private String createdBy;//创建人的MemberIdprivate String creator;//创建人姓名private Instant updatedAt;//更新时间private String updatedBy;//更新人MemberIdprivate String updater;//更新人姓名private List<DomainEvent> events;//临时存放领域事件private LinkedList<OpsLog> opsLogs;//操作日志@Version@Getter(PRIVATE)private Long _version;//版本号,实现乐观锁//...此处省略了AggregateRoot中行为方法@Overridepublic String getIdentifier() {return id;}
}

源码出处:com/mryqr/core/common/domain/AggregateRoot.java

AggregateRoot中,包含聚合根ID(id)、创建信息(createdAtcreatedBy)和更新信息(updatedAtupdatedBy)等数据。租户ID(tenantId)用于标定聚合根所在的租户(码如云是一个多租户系统)。另外,events用于临时性存放聚合根中所产生的领域事件,我们将在领域事件一文中对此所详细解释。

实际的聚合根继承自AggregateRoot,例如,在码如云中,分组(Group)聚合根的实现如下:

@Getter
@Document(GROUP_COLLECTION)
@TypeAlias(GROUP_COLLECTION)
@NoArgsConstructor(access = PRIVATE)
public class Group extends AggregateRoot {private String name;//名称private String appId;//所在的appprivate List<String> managers;//管理员private List<String> members;//普通成员private boolean archived;//是否归档private String customId;//自定义编号private boolean active;//是否启用private String departmentId;//由哪个部门同步而来//...此处省略了Group的行为方法
}

源码出处:com/mryqr/core/group/domain/Group.java

聚合根基本原则 #

从上面的代码例子可以看出,聚合根只是普通的Java对象而已,真正使之成为聚合根的是一些特定的设计原则。

内聚性原则 #

这个原则不用我们再细讲了吧,估计你在大学里就学过,只举个例子,对于上面的分组Group对象来说,管理员managers、普通成员members以及启用标志active均是Group不可分割的属性,这些属性独立于Group是无法存在的。

对外黑盒原则 #

对外黑盒原则讲的是,聚合根的外部(也即聚合根的调用方或客户方)不需要关心聚合根内部的实现细节,而只需要通过调用聚合根向外界暴露的共有业务方法即可。具体表现为,外部对聚合根的调用只能通过根对象完成,而不能调用聚合根内部对象上的方法。举个例子,在码如云中,管理员可以向分组(Group)中添加成员,具体的实现代码如下:

//Grouppublic void addMembers(List<String> memberIds, User user) {if (isSynced()) {throw new MryException(GROUP_SYNCED,"无法添加成员,已设置从部门同步。","groupId", this.getId());}this.members = concat(members.stream(), memberIds.stream()).distinct().collect(toImmutableList());addOpsLog("设置成员", user);
}

源码出处:com/mryqr/core/group/domain/Group.java

这里,外部在向分组中添加成员时,需要调用Group上的addMembers()方法,该方法知道将memberIds添加到自身的members字段中,这个过程对外部是不可见的。与之相对的另一种方式是,外部调用法先拿到Membermembers引用,然后由外部自行向members中添加memberIds

//外部调用方@Transactional
public void addGroupMembers(String groupId, List<String> memberIds, User user) {Group group = groupRepository.byIdAndCheckTenantShip(groupId, user);if (group.isSynced()) {throw new MryException(GROUP_SYNCED,"无法添加成员,已设置从部门同步。","groupId", group.getId());}List<String> members = group.getMembers();members.addAll(memberIds);groupRepository.save(group);log.info("Added members{} to group[{}].", memberIds, groupId);
}

源码出处:com/mryqr/core/group/command/GroupCommandService.java

这种方式是一种反模式,存在以下缺点:

  • 外部需要了解Group的内部结构,背离了对外黑盒原则,本例中,外部通过group.getMembers()获取到了Group内部的members属性
  • 聚合根内部的业务逻辑泄漏到了外部,背离了内聚性原则,本例中,对group.isSynced()的调用原本应该放在Group中的,结果却由外部承担了该职责

在对外黑盒原则的指导下,聚合根自然形成了一个边界,它站在这个边界上向外声明:“我所包围着的内部的所有均由我负责,如果谁想访问我的内部,直接访问是被禁止的,只能通过我这个“根”来访问。”

不变条件原则 #

不变条件(Invariants)表示聚合根需要保证其内部在任何时候均处于一种合法的状态(也即数据一致性需要得到保证),一个常见的例子是订单(Order)中有订单项(OrderItem)和订单价格(Price),当订单项发生变化时,其价格应该随之发生变化,并且这两种变化应该在订单的同一个业务方法中完成。这一点是好理解的,既然聚合根对外是一个黑盒,那么外界便不会负责给你聚合根擦屁股,你聚合根自己需要保证自身的正确性。

在码如云中,应用管理员可以向分组(Group)中添加分组管理员。这其中有层隐含意思是,既然分组管理员也是分组成员,那么在添加分组管理员的同时需要一并将其添加到分组成员中,具体实现代码如下:

//Grouppublic void addManager(String memberId, User user) {if (!this.members.contains(memberId)) {this.members = concat(members.stream(), Stream.of(memberId)).distinct().collect(toImmutableList());}this.managers = concat(this.managers.stream(), Stream.of(memberId)).distinct().collect(toImmutableList());raiseEvent(new GroupManagersChangedEvent(this.getId(), this.getAppId(), user));addOpsLog("添加管理员", user);
}

源码出处:com/mryqr/core/group/domain/Group.java

在本例的添加分组管理员addManager()方法中,我们除了向managers中添加成员外,还保证了该成员也出现在members中。这里的“分组管理员也是分组成员”即是一种不变条件,我们需要在聚合根内部保证不变条件不被破坏,因为不变条件往往意味着核心的业务逻辑。

通过ID引用其他聚合根原则 #

当一个聚合根需要引用另一个聚合根时,并不需要维持对另一聚合根的整体引用,而是只需通过ID进行引用即可。这个原则的出发点是:聚合根和聚合根之间是一种平级关系,并不是隶属关系,每个聚合根本身是一个相对独立的模块,其与其他聚合根的关系应该通过ID这种松耦合的方式进行引用,如果整体引用则更像是一种包含关系。

在码如云中,分组(Group)通过appId引用其所属的应用(App),通过departmentId引用所同步的部门(Department),而在managersmembers字段中,则是以memberId引用相应成员(Member):

@Getter
@Document(GROUP_COLLECTION)
@TypeAlias(GROUP_COLLECTION)
@NoArgsConstructor(access = PRIVATE)
public class Group extends AggregateRoot {private String name;//名称private String appId;//所在的appprivate List<String> managers;//管理员private List<String> members;//普通成员private boolean archived;//是否归档private String customId;//自定义编号private boolean active;//是否启用private String departmentId;//由哪个部门同步而来//...省略其他代码
}

源码出处:com/mryqr/core/group/domain/Group.java

与基础设施无关原则 #

既然整个领域模型与基础设施无关,那么位于领域模型之内的聚合根自然也不能与基础设施相关,这样好处是将业务复杂度与技术复杂度解耦开来,让业务模型可以独立于技术设施而完成自身的演变。比如,假设一个项目需要从Spring框架迁移到Guice框架,此时如果能够保证领域模型与基础设施的无关性,那么对领域模型的迁移过程讲变得非常简单,基本上无需修改任何代码直接拷贝到新的项目中即可。

事实上,码如云尚未完全做到这一点,从上面的例子中可以看到,AggregateRootGroup对Spring框架中的@Version@Document@TypeAlias3个与持久化相关的注解存在引用。如需解决这个问题,可以考虑在领域模型之外另建专门用于数据库访问的持久化对象(Persistence Model)。但是,引入持久化对象是有成本的,比如需要维护领域对象与持久化对象之间的相互转化等。在码如云,我们选择了妥协,一方面考虑到持久化对象的成本,另一方面我们也预见在将来要迁移出Spring框架的几率是非常小的。不过,除了前面提到的3个注解之外,码如云中的聚合根可以做到对基础设施没有任何其他引用。关于持久化对象,在Stackoverflow上有过非常有意义的讨论,读者可自行阅览。

跨聚合根用例 #

通常来讲,一个业务用例只会操作一个(或一种)聚合根。但有时,一个业务用例可能会导致多个(或多种)聚合根对象的更新,此时可分两种情况:

  1. 如果聚合根位于不同的进程空间(比如不同的微服务)中,那么解决方式一是可以使用事件驱动架构(EDA),二是通过全局事务(比如JTA)完成。基于全局事务的性能和效率低下等问题,DDD社区一般建议采用事件驱动架构,即在一个进程空间中只对其包含的聚合根进行操作,然后通过向其他进程空间发送事件通知的方式,使得其他进程空间做相应的聚合根更新。
  2. 如果聚合根位于同一个进程空间,此时依然可以选择事件驱动架构,但是另一种更简单实用的方式是直接同时更新多个聚合根,毕竟此时对所有聚合根的更新均处于同一个本地事务中。

码如云是一个单体系统,因此属于以上的第2种情况,我们根据聚合根之间的业务紧密程度的不同,在有些场景下选择了同时更新多个聚合根,在另一些场景下则选择通过事件驱动机制解决。比如,在“创建实例”的用例中,除了创建实例(QR)之外,还需要创建该实例对应的码牌(Plate),由于“有实例就必有码牌”,因此它们之间是紧密联系的,故在码如云中我们选择了在同一个本地事务中同时更新实例和码牌:

//QrCommandService@Transactional
public CreateQrResponse createQr(CreateQrCommand command, User user) {String name = command.getName();String groupId = command.getGroupId();Group group = groupRepository.cachedByIdAndCheckTenantShip(groupId, user);String appId = group.getAppId();App app = appRepository.cachedById(appId);PlatedQr platedQr = qrFactory.createPlatedQr(name, group, app, user);QR qr = platedQr.getQr();Plate plate = platedQr.getPlate();//同时保存QR和PlateqrRepository.save(qr);plateRepository.save(plate);log.info("Created qr[{}] of group[{}] of app[{}].",qr.getId(), groupId, appId);return CreateQrResponse.builder().qrId(qr.getId()).plateId(plate.getId()).groupId(groupId).appId(appId).build();
}

源码出处:com/mryqr/core/group/command/GroupCommandService.java

可以看到,在用例方法createQr()中,我们先后调用qrRepository.save(qr)plateRepository.save(plate)分别完成了对QRPlate的持久化。

如果你希望了解事件驱动架构相关的知识,请参考本系列的领域事件一文。

资源库 #

在DDD中,资源库(Repository)以聚合根为单位完成对数据库的访问。这里的重点是“以聚合根为单位”,也即只有聚合根才配得上拥有资源库(毕竟在DDD中大家都是围绕着聚合根转的嘛),其他对象(比如非聚合根实体)是没有对应资源库的,这也是资源库和DAO最大的区别。在编码实现时,资源库方法所接受的参数和返回的数据都应该是聚合根对象,例如,在码如云中,成员(Member)聚合根对应的资源库定义如下:

public interface MemberRepository {Member byId(String id); //返回聚合根Optional<Member> byIdOptional(String id); //返回聚合根Member byIdAndCheckTenantShip(String id, User user); //返回聚合根void save(Member member); //聚合根作为参数void delete(Member member); //聚合根作为参数
}

源码出处:com/mryqr/core/member/domain/MemberRepository.java

行业中这么一个现象,很多程序员在面对一个新的业务需求时,首先想到的是如何设计数据库的表结构,然后再编写业务代码。在DDD中,这是一种反模式,既然是“领域驱动”,那么我们首先应该关心的是如何业务建模,而不是数据库建模。事实上,正如Robert C. Martin在《整洁架构》一书中所说,数据库只是一个实现细节而已,不应该成为软件建模的主体。

资源库的作用,在于它在业务复杂度和技术复杂度之间做了一层很好的隔离,让我们可以独立地看待软件的业务模型而不受技术设施的影响。从本质上讲,资源库做的事情只是实现数据在内存和磁盘之间相互传输而已。在编程实现业务逻辑的时候,我们只需关心内存中的那个聚合根对象即可,当聚合根对象的状态由于业务操作发生了改变之后,再调用资源库将新的聚合根状态同步到磁盘中完成持久化,在调用时我们假设并相信资源库一定可以完成其自身的使命。

@Transactional
public void addGroupManager(String groupId, String memberId, User user) {Group group = groupRepository.byIdAndCheckTenantShip(groupId, user);group.addManager(memberId, user);groupRepository.save(group);log.info("Added manager[{}] to group[{}].", memberId, groupId);
}

源码出处:com/mryqr/core/group/command/GroupCommandService.java

在上例的“向分组中添加管理员”用例中,首先通过资源库GroupRepositorybyIdAndCheckTenantShip()方法得到聚合根Group对象,然后再完成后续操作。这里的addGroupManager()无需知道Group是如何加载的,甚至不用知道后台使用的是MySQL还是MongoDB或是其他,反正通过调用GroupRepository.byIdAndCheckTenantShip()可以得到一个完整合法的Group对象即可。

在资源库中,最重要的方法有以下3个:

public interface GroupRepository {Group byId(String id);void save(Group group);void delete(Group group);
}

源码出处:com/mryqr/core/member/domain/MemberRepository.java

其中,byId()用于根据ID获取指定聚合根,save()用于保存聚合根,delete()则用于删除聚合根。除此之外,资源库中还可以包含更多的查询方法,比如在GroupRepository中还包含以下方法:

//根据部门ID查找分组
List<Group> byDepartmentId(String departmentId);//根据ID查找分组,返回Optional
Optional<Group> byIdOptional(String id);//根据ID查找分组,同时检查租户
Group byIdAndCheckTenantShip(String id, User user);

源码出处:com/mryqr/core/member/domain/MemberRepository.java

需要注意的是,这里的查询方法指的是在实现业务逻辑的过程中需要做的查询操作,并不是为了前端显示那种纯粹的查询,因为纯粹的查询操作不见得一定要放到资源库中,而是可以作为一个单独的关注点通过CQRS解决。

在DDD项目中,通常将资源库分为接口类和实现类,将接口类放置在领域模型domain包中,而将实现类放置在基础设施infrastructure包中,这种做法有2点好处:

  1. 通过依赖反转,使得领域模型不依赖于基础设施
  2. 实现资源库的可插拔性,比如未来需要从MongoDB迁移到MySQL,那么只需创建新的实现类即可

总结 #

在本文中,我们讲到了作为DDD核心的聚合根的设计原则及实现,其中包含内聚原则、对外黑盒原则和不变条件原则等。此外,我们也对与聚合根密切相关的资源库做了讲解。在下一篇实体与值对象中,我们将讲到实体和值对象之间的区别,以及各自的典型编码实践。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/87424.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯打卡Day15天

文章目录 买不到的数目错误票据 一、买不到的数目OJ链接 本题思路:引理&#xff1a;给定a&#xff0c;b&#xff0c;若dgcd(a,b)>1 ,则一定不能凑出最大数。结论&#xff1a;如果 a,b均是正整数且互质&#xff0c;那么由 axby,x≥0,y≥0 不能凑出的最大数是 ab−a−b。 证…

从技术创新到应用实践,百度智能云发起大模型平台应用开发挑战赛!

大模型已经成为未来技术发展方向的重大变革&#xff0c;热度之下更需去虚向实&#xff0c;让技术走进产业场景。在这样的背景下&#xff0c;百度智能云于近期发起了“百度智能云千帆大模型平台应用开发挑战赛”。 挖掘大模型落地应用 千帆大模型平台应用开发挑战赛启动 在不久前…

基于 CPU 在docker 中部署PaddleOCR

1. 拉取镜像 docker pull registry.baidubce.com/paddlepaddle/paddle:2.4.0注&#xff1a;写该文章时&#xff0c;Paddle 最新版本为2.5.1&#xff0c;但是在实际安装中会出现与 PaddleHub 2.3.1版本的冲突&#xff0c;故采用2.4.0版本 2. 构建并进入容器 docker run --name…

4年从外包到外企,一个测试老鸟的自述

4年前&#xff0c;我拖着行李箱来到北京&#xff0c;成为了一名北漂&#xff0c;离开了校园的庇护&#xff0c;只身一人&#xff0c;想要在这片陌生的地方闯出一番名堂&#xff0c;可最后却不得人意&#xff0c;面临着所有北漂群体的共同困局&#xff0c;没有归属感&#xff0c…

轻量服务器是不是vps ?和vps有什么区别

​  轻量型服务器是介于云服务器和共享型服务器之间的一种解决方案。它提供较为独立的资源分配&#xff0c;但规模较小&#xff0c;适用于中小型网站和应用程序。轻量型服务器的硬件资源来源于大型的公有云集群的虚拟化技术。轻量型服务器的性能和带宽可能会稍逊于云服务器。…

初识C语言——详细入门(系统性学习day4)

目录 前言 一、C语言简单介绍、特点、基本构成 简单介绍&#xff1a; 特点&#xff1a; 基本构成&#xff1a; 二、认识C语言程序 标准格式&#xff1a; 简单C程序&#xff1a; 三、基本构成分类详细介绍 &#xff08;1&#xff09;关键字 &#xff08;2&#xf…

HDMI之HDCP 2.3

Authentication and Key Exchange Without Stored Km With Stored Km HDCP2Version DDC时序 协议截图 Bit2为1,可知DUT设备支持HDCP 2.2及以上版本 RxStatus DDC时序 协议截图 <

消息队列-rabbitMq

消息队列&#xff08;MQ&#xff09;到底能干什么&#xff1f; MQ全称为Message Queue&#xff0c;也就是消息队列&#xff0c;是应用程序和应用程序之间的通信方法。 在微服务盛行的当下&#xff0c;MQ被使用的也是越来越多&#xff0c;一般常用来进行业务异步解耦、解耦微服务…

Nginx 默认location index设置网站的默认首页

/斜杠代表location定位的路径&#xff0c;路径当中最重要的字段就是root。 root默认值就是html&#xff0c;这个就是nginx安装路径下面的html文件夹作为root的路径。默认不配置就是root下面的内容&#xff0c;index指定了主页的内容。 [rootjenkins html]# echo test > te…

MySQL学习笔记9

MySQL数据表中的数据类型&#xff1a; 在考虑数据类型、长度、标度和精度时&#xff0c;一定要仔细地进行短期和长远的规划&#xff0c;另外&#xff0c;公司制度和希望用户用什么方式访问数据也是要考虑的因素。开发人员应该了解数据的本质&#xff0c;以及数据在数据库里是如…

[linux] 过滤警告⚠️

如果你在Python脚本中输出和执行脚本文件时想要过滤掉警告信息&#xff0c;可以尝试以下方法&#xff1a; 使用warnings模块&#xff1a;导入warnings模块并设置warnings.filterwarnings("ignore")&#xff0c;这将会忽略所有的警告信息。在需要过滤警告的部分之前添…

PYQT制作动态时钟

所有代码&#xff1a; import sys from PyQt5.QtCore import Qt, QTimer, QRect from PyQt5.QtGui import QPixmap, QTransform, QPainter, QImage from PyQt5.QtWidgets import QApplication, QLabel from PyQt5 import uic import newdef adder():global iglobal angle_s, a…

拿到直播平台的rtmp地址和推流码之后,用 nodejs写一个循环读取视频文件内容,这个地址推流

拿到直播平台的rtmp地址和推流码之后&#xff0c;用 nodejs写一个循环读取视频文件内容&#xff0c;这个地址推流 ChatGPT 要使用Node.js将视频文件内容循环推流到给定的RTMP地址和推流码&#xff0c;您可以使用fluent-ffmpeg库来处理视频文件&#xff0c;并使用node-media-ser…

数据结构的奇妙世界:实用算法与实际应用

文章目录 数据结构和算法的基本概念数据结构数组链表栈队列树图 算法 常见的数据结构和算法排序算法快速排序示例 数据结构的应用数据库管理系统图像处理网络路由 数据结构和算法的性能分析时间复杂度空间复杂度 如何更好地编写代码避免常见错误结论 &#x1f389;欢迎来到数据…

java框架-Springboot-快速入门

文章目录 组件注册条件注解属性绑定自动装配原理自定义组件yaml属性配置日志日志级别日志分组文件输出文件归档与文件切割自定义配置切换日志组合 组件注册 Configuration、SpringBootConfigurationBean、ScopeController、Service、Repository、ComponentImportComponentScan…

学术团体的机器人相关分会和机器人相关大赛的说明

1. 中国机械工程学会 &#xff08;机器人分会&#xff09; 2017年成立&#xff0c;地点 华中科技大学 &#xff1a;中国机械工程学会机器人分会在汉成立 (huanqiu.com) 链接&#xff1a;中国机械工程学会 (cmes.org) 侧重点&#xff1a;工业机械臂、工厂和物流相关的移动机…

【前端设计模式】之代理模式

代理模式特性 代理模式是一种结构型设计模式&#xff0c;它通过创建一个代理对象来控制对另一个对象的访问。代理模式的主要特性包括&#xff1a; 代理对象与目标对象实现相同的接口或继承相同的基类&#xff0c;使得客户端可以透明地使用代理对象。代理对象持有对目标对象的…

用flex实现grid布局

1. css代码 .flexColumn(columns, gutterSize) {display: flex;flex-flow: row wrap;margin: calc(gutterSize / -2);> div {flex: 0 0 calc(100% / columns);padding: calc(gutterSize / 2);box-sizing: border-box;} }2.用法 .grid-show-item3 {width: 100%;display: fl…

【SpringBoot】-IDEA社区版SpringBoot项目的创建

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【Framework】 主要内容&#xff1a;IDEA下的springboot项目的创建&#xff0c;网页下springboot项目的创建。springboot目录结构的认识。 文章目录 一、什么是SpringBoot&#x…

【C++】STL之适配器---用deque实现栈和队列

目录 前言 一、deque 1、deque 的原理介绍 2、deque 的底层结构 3、deque 的迭代器 4、deque 的优缺点 4.1、优点 4.2、缺点 二、stack 的介绍和使用 1、stack 的介绍 2、stack 的使用 3、stack 的模拟实现 三、queue 的介绍和使用 1、queue 的介绍 2、queue 的使用 3、qu…