领域驱动设计和CQRS落地
目录
1. 前言
2. 领域驱动架构2.1 分层架构2.2 DIP改进分层架构2.3 六边形架构2.4 洋葱架构、整洁架构
3. 领域驱动模式3.1 SIDE-EFFECT-FREE3.2 CQRS
4. 领域驱动架构落地
5. 领域驱动代码落地5.1 组织代码5.2 落地用户界面5.3 落地应用服务5.4 落地领域模型5.5 落地领域服务5.6 落地基础设施5.7 落地查询服务5.8 落地MQ、Event、Cache5.9 落地RPC和防腐层
6. Cargo货物实例和源码
7. 参考资料
8. 总结
1. 前言
假定你已经初步了解过领域驱动设计(DDD)的基本概念,如果不了解,建议先阅读一些基础文章:
聚合根
实体
值对象
领域服务
领域事件
资源库
限界上下文
CQRS
本文重点讲述如何运用Java代码落地领域驱动设计。
2. 领域驱动架构
落地领域驱动的首要问题是选择何种架构去实现?
2.1 分层架构
Evans在它的《领域驱动设计:软件核心复杂性应对之道》书中推荐采用分层架构去实现领域驱动设计,架构图是这样的:
分层架构是一种常见的自上而下的依赖关系,其实我们早已驾轻就熟,MVC模式就是一种分层架构:我们尽可能去设计每一层,使其保持高度内聚性,让它们只对下层进行依赖,体现了高内聚低耦合的思想。
用户界面层:我们可以理解成web层的Controller,即对外暴露接口,显示界面;
应用层:和业务无关,它负责协调领域层进行工作;业务编排
领域层:领域驱动设计的业务核心,包含领域模型和领域服务,领域层的重点放在如何表达领域模型上,无需考虑显示和存储问题;
基础实施层:最底层,提供基础的接口和实现,领域层和应用服务层通过基础实施层提供的接口实现类如持久化、发送消息等功能。
阿里巴巴开源的整洁面向对向分层架构COLA采取了这样的分层架构来实现领域驱动,有兴趣可以去阅读下。
2.2 DIP改进分层架构
分层架构是一种可落地的架构,但是我们依然可以进行改进,Vernon在它的《实现领域驱动设计》一书中提到了采用依赖倒置原则改进的方案。
所谓的依赖倒置原则指的是:高层模块不应该依赖于低层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象。
这句话需要细细品味,正如架构图中看到的,基础实施层位于其他所有层的上方,接口定义在其它层,基础实施实现这些接口。或者可以这样来表述:领域层等其他层不应该依赖于基础实施层,两者都应该依赖于抽象。
这也就是意味着一个重要的落地指导原则: 所有依赖基础实施实现的功能,抽象和接口都应该定义在领域层或应用层中。
依赖倒置原则和分层架构的结合增强了高内聚低耦合的特性,每一层只依赖于抽象,因为具体的实现在基础实施层,无需关心。只要抽象不变,就无需改动那一层,实现如果需要改变,只需要修改基础实施层就可以了。
2.3 六边形架构
《实现领域驱动设计》一书中提到了DDD架构更深层次的变化,Vernon放弃了分层架构,采用了对称性架构:六边形架构,作者认为这是一种具有持久生命力的架构。
如图,在这种架构风格中,外部客户和内部系统的交互都会通过端口和适配器完成转换,这些外部客户之间是平等的,比如用户web界面和数据库持久化,当你需要一个新的外部客户时,只需要增加相应的适配器,比如当我们依赖外部一个RPC的服务时,只需要编写对应的适配器即可。
好吧,当将web界面和持久化统称在一起,没有前端和数据库后端之分的时候,这种观察架构的角度已经打动到了我。当你真正理解这种架构的时候,相信你也不得不佩服这种角度不同的设计。
怎么理解适配器呢,或者说适配器在各种外部客户的场景下是什么呢?
如果外部客户时HTTP请求,那么SpringMVC的注解和Controller构成了适配器,如果外部客户时MQ消息,那么适配器就是MQConsumer监听器,如果外部客户时数据库,那么适配器可能就是Mybatis的Mapper。
2.4 洋葱架构、整洁架构
随着架构的演化,后来又提出了洋葱架构和整洁架构,这些架构大同小异,它们都只允许外层依赖内层,不允许内层知道外层的细节,下图是整洁架构图,详细介绍这里就不作赘述,可以参考这篇文章The Clean Architecture:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
3. 领域驱动模式
当领域驱动设计突出了领域模型的地位,我们会使用一些优秀的设计模式与之结合。
3.1 SIDE-EFFECT-FREE
SIDE-EFFECT-FREE模式被称为无副作用模式,熟悉函数时编程的朋友都知道,严格的函数就是一个无副作用的函数,对于一个给定的输入,总是返回固定的结果,通常查询功能就是一个函数,命令功能就不是一个函数,它通常会执行某些修改。
根据这种模式,就有了CQRS的架构设计。
3.2 CQRS
在领域驱动架构中,通常会将查询和命令操作分开,我们称之为CQRS(命令查询的责任分离Command Query Responsibility Segregation)。这张图是来自Martin Fowler的文章CQRS:https://www.martinfowler.com/bliki/CQRS.html。
这张图读模块Query Model和写模块Command Model只是逻辑分离,物理层面还是使用了同一个数据库,我们可以将数据库改成读库和写库做到物理分离,这时候就需要同步都写库。
最终CQRS落地的方案我们选择了简单化处理,物理层面还是使用一个数据库,查询的时候部分数据直接从数据库读取,部分数据使用到了Elasticsearch,数据同步可以采用两种方案:
当数据库发生更改时,主动发送Event事件通知ES进行更新
直接监听Mysql的binlog更新ES
4. 领域驱动架构落地
根据上面的分析,最终落地的架构使用了对称性架构。
我们平等的看待Web、RPC、DB、MQ等外部服务 可以统一看做应用系统业务对于数据I/O的处理,如下图所示:
当一个命令Command请求过来时,会通过应用层的CommandService去协调领域层工作,而一个查询Query请求过来时,则直接通过基础实施的实现与数据库或者外部服务交互。再次强调,我们遵循依赖倒置原则,所有的抽象都定义在圆圈内部,实现都在基础设施。
在具体编写代码中我们发现,Query和Command的有一些数据和抽象服务是公用的,因此我们抽出了一个新的模块:Shared Data & Service,这个模块的功能为公用的数据对象和抽象接口。
5. 领域驱动代码落地
分析领域驱动架构的方法论有很多,但是落地到代码层面的方法论少之又少,这一小节我们将具体到DDD设计的每个小点来阐述如何代码落地。
5.1 组织代码
我们采用如下的package结构组织代码,每个package正好对应了我们的领域驱动架构。
用户界面Web放在了模块com.deepoove.cargo.web.controller
中,实现一些Controller,infrastructure放在了com.deepoove.cargo.infrastructure
中,抽象接口的实现,它们都依赖于应用服务和领域模型。
注意的是虽然架构平等的看待外部服务,但是我们依然将用户界面从基础设施抽取了出来,毕竟我们的项目是Web主导的。同理,如果你的项目不是个web项目,而是用来提供RPC服务的项目,那么我们可以创建一个新包去组织RPC适配器的代码:比如com.deepoove.cargo.remoting包。
5.2 落地用户界面
用户界面的代码放在com.deepoove.cargo.web.controller
包下面。Controller作为六边形架构中与HTTP端口的适配器,起到了适配请求,委托应用服务处理的任务。
这里我们有一个规范:所有查询的条件封装成XXXQry对象,所有命令的请求封装成XXXCommand对象。
package com.deepoove.cargo.web.controller;@RestController
@RequestMapping("/cargo")
public class CargoController {@AutowiredCargoQueryService cargoQueryService;@AutowiredCargoCmdService cargoCmdService;@RequestMapping(value = "/{cargoId}", method = RequestMethod.GET)public CargoDTO cargo(@PathVariable String cargoId) {return cargoQueryService.getCargo(cargoId);}@RequestMapping(method = RequestMethod.POST)public void book(@RequestBody CargoBookCommand cargoBookCommand) {cargoCmdService.bookCargo(cargoBookCommand);}@RequestMapping(value = "/{cargoId}/delivery", method = RequestMethod.PUT)public void modifydestinationLocationCode(@PathVariable String cargoId,@RequestBody CargoDeliveryUpdateCommand cmd) {cmd.setCargoId(cargoId);cargoCmdService.updateCargoDelivery(cmd);}}
对于参数校验我们的原则是:在用户界面层可以有请求参数的基本校验,但是 应用服务层和领域层的校验逻辑是必须存在的,校验和业务的耦合是紧密的,接下来我们就来看看如何落地应用服务层。
5.3 落地应用服务
com.deepoove.cargo.application.command
包里面是具体CommandService的抽象和实现,它将协调领域模型和领域服务完成业务功能,此处不包含任何逻辑。我们认为应用服务的每个方法与用例是一一对应的(好像嗅到了行为驱动测试BDD的味道),典型的处理流程是:
校验
协调领域模型或者领域服务
持久化
发布领域事件
在这一层可以使用流程编排,典型的流程也可以使用技术手段固化,比如抽象模板模式。
package com.deepoove.cargo.application.command.impl;@Service
public class CargoCmdServiceImpl implements CargoCmdService {@Autowiredprivate CargoRepository cargoRepository;@AutowiredDomainEventPublisher domainEventPublisher;@Overridepublic void bookCargo(CargoBookCommand cargoBookCommand) {// create CargoDeliverySpecification delivery = new DeliverySpecification(cargoBookCommand.getOriginLocationCode(),cargoBookCommand.getDestinationLocationCode());Cargo cargo = Cargo.newCargo(CargoDomainService.nextCargoId(), cargoBookCommand.getSenderPhone(),cargoBookCommand.getDescription(), delivery);// saveCargocargoRepository.save(cargo);// post domain eventdomainEventPublisher.publish(new CargoBookDomainEvent(cargo));}@Overridepublic void updateCargoDelivery(CargoDeliveryUpdateCommand cmd) {// validate// findCargo cargo = cargoRepository.find(cmd.getCargoId());// domain logiccargo.changeDelivery(cmd.getDestinationLocationCode());// savecargoRepository.save(cargo);}}
发布领域事件的动作放在了应用层没有放在领域层,而领域事件的定义是在领域层(紧接着会提到),这是为什么呢?
如果 不考虑持久化,发布领域事件的确应该在领域模型中,但是在代码落地时,考虑到持久化完成后才代表有了真实的事件,所以我们决定将触发事件的代码放到了资源库后面。
5.4 落地领域模型
业务核心领域模型的代码组织在com.deepoove.cargo.domain.aggregate
包中。我们采用了aggregate而不是model,是为了将聚合根的概念显现出来,每个聚合根单独成一个子包,在单个聚合根中包含所需要的值对象、领域事件的定义、资源库的抽象接口等。
领域事件的定义、资源库的抽象接口之所以放在相应聚合根的package中,是因为它更能体现这个领域模型,而且资源库的抽象和聚合根有着对应的关系(不大于聚合根的数量)。
package com.deepoove.cargo.domain.aggregate.cargo;import com.deepoove.cargo.domain.aggregate.cargo.valueobject.DeliverySpecification;public class Cargo {private String id;private String senderPhone;private String description;private DeliverySpecification delivery;public Cargo(String id) {this.id = id;}public Cargo() {}/*** Factory method:预订新的货物** @param senderPhone* @param description* @param delivery* @return*/public static Cargo newCargo(String id, String senderPhone, String description,DeliverySpecification delivery) {Cargo cargo = new Cargo(id);cargo.senderPhone = senderPhone;cargo.description = description;cargo.delivery = delivery;return cargo;}public void changeDelivery(String destinationLocationCode) {if (this.delivery.getOriginLocationCode().equals(destinationLocationCode)) { throw new IllegalArgumentException("destination and origin location cannot be the same."); }this.delivery.setDestinationLocationCode(destinationLocationCode);}public void changeSender(String senderPhone) {this.senderPhone = senderPhone;}}
关于聚合根对象的创建,特别提醒的是聚合根对象的创建不应该被Spring容器管理,也不应该在聚合根中注入其它对象。聚合根对象可以通过静态工厂方法来创建,下文还会介绍如何落地资源库进行聚合根的创建。
5.5 落地领域服务
领域服务的代码组织com.deepoove.cargo.domain.service
包中。
很多朋友无法判断业务逻辑什么时候该放在领域模型中(Domain.Aggregate),什么时候放在领域服务(Domain.Service)中,可以从以下几点考虑:
不是属于单个聚合根的业务或者需要多个聚合根配合的业务,放在领域服务中,注意是业务,如果没有业务,协调工作应该放到应用服务(Applicaction)中
静态方法放在领域服务中(Domain.Service)
需要通过rpc等其它外部服务处理业务的,放在领域服务中(Domain.Service)
package com.deepoove.cargo.domain.service;@Service
public class CargoDomainService {public static final int MAX_CARGO_LIMIT = 10;public static final String PREFIX_ID = "CARGO-NO-";/*** 货物物流id生成规则** @return*/public static String nextCargoId() {return PREFIX_ID + (10000 + new Random().nextInt(9999));}public void updateCargoSender(Cargo cargo, String senderPhone, HandlingEvent latestEvent) {if (null != latestEvent&& !latestEvent.canModifyCargo()) { throw new IllegalArgumentException("Sender cannot be changed after RECIEVER Status."); }cargo.changeSender(senderPhone);}}
5.6 落地基础设施
基础设施层的代码组织在com.deepoove.cargo.infrastructure
包中。
基础设施可以对抽象的接口进行实现,上文中说到资源库Repository的接口定义在领域层,那么在基础设施中就可以具体实现这个接口。
package com.deepoove.cargo.infrastructure.db.repository;@Component
public class CargoRepositoryImpl implements CargoRepository {@Autowiredprivate CargoMapper cargoMapper;@Overridepublic Cargo find(String id) {CargoDO cargoDO = cargoMapper.select(id);Cargo cargo = CargoConverter.deserialize(cargoDO);return cargo;}@Overridepublic void save(Cargo cargo) {CargoDO cargoDO = CargoConverter.serialize(cargo);CargoDO data = cargoMapper.select(cargoDO.getId());if (null == data) {cargoMapper.save(cargoDO);} else {cargoMapper.update(cargoDO);}}}
资源库Repository的实现就是将聚合根对象持久化,往往聚合根(Domain.Aggregate)的定义和数据库中定义的结构并不一致,我们将数据库的对象称为数据对象DO/PO/Entity,
当持久化时就需要将聚合根(Domain.Aggregate) 序列化 成数据库数据对象,通过资源库获取(构造)聚合根时,也需要 反序列化 数据库数据对象((Domain.Entity)
我们可以基于反射或其它技术手段完成序列化和反序列化操作,这样可以避免聚合根中编写过多的getter和setter方法。
5.7 落地查询服务
查询服务的代码组织在com.deepoove.cargo.application.query
包中。application应用服务包含了commond和query两个子包,query也可以提取出去和application平级,这两种做法没有对错,只是选择问题。
运用CQRS设计,查询服务不会调用应用服务,也不会调用领域模型和资源库Repository,它会直接查询数据库或者ES获取原始数据对象DO,然后组装成数据传输对象DTO给用户界面,这个组装的过程称为Assembler(转配),由于与用户界面有一定的对应关系,所以Assembler放在查询服务中。
那么问题来了,查询服务中如何获取DO呢?通常的做法是在查询模块中定义抽象接口,由基础设施实现从数据库获取数据 ,但是DO的定义不是在基础设施层吗,查询服务怎么可以访问到这些对象呢?我们有两个办法:
查询服务中定义一套一摸一样的DO,然后基础设施做转换,缺点是有点复杂,冗余了DO,优点是完美符合DIP原则:抽象在查询服务中,实现在基础设施
将DO放到shared Data & Service中去,这样就只要一套DO供查询服务和命令服务使用,查询服务定义接口,基础设施实现接口
具体落地我们发现方法1有点冗余,方法2和mybatis结合会产生疑惑,因为mybatis Mapper就是一个接口,何须在查询服务中再定义一套接口呢?
最终落地的方式仁者见仁智者见智,ddd-cargo示例项目中我选择了在查询服务和DB交互时 破坏了DIP原则,直接依赖Mapper读取数据对象进行组装。
我们来看看一个查询服务的实现,其中CargoDTOAssembler是一个组装器:
package com.deepoove.cargo.application.query.impl;@Service
public class CargoQueryServiceImpl implements CargoQueryService {@Autowiredprivate CargoMapper cargoMapper;@Autowiredprivate CargoDTOAssembler converter;@Overridepublic List<CargoDTO> queryCargos() {List<CargoDO> cargos = cargoMapper.selectAll();return cargos.stream().map(converter::apply).collect(Collectors.toList());}@Overridepublic List<CargoDTO> queryCargos(CargoFindbyCustomerQry qry) {List<CargoDO> cargos = cargoMapper.selectByCustomer(qry.getCustomerPhone());return cargos.stream().map(converter::apply).collect(Collectors.toList());}@Overridepublic CargoDTO getCargo(String cargoId) {CargoDO select = cargoMapper.select(cargoId);return converter.apply(select);}
}
是否需要将每个对象都转化成DTO返回给用户界面这个要看情况,个人认为当DO能满足界面需求时是可以直接返回DO数据的。
5.8 落地MQ、Event、Cache
毫无疑问,MQ、Event、Cache的实现都应该在基础设施层,它们接口的定义放在哪里呢?
一个方案是哪一层使用了抽象就在那一层定义接口,
另一个方案是放到一个共有的抽象包下,基础设施和对应层依赖这个共有的抽象包。
最终落地我选择将这些接口代码组织在了com.deepoove.cargo.shared
包下,这个包的定义就是共有的数据和抽象。
我们以领域事件为例来看看代码如何实现,首先定义抽象接口com.deepoove.cargo.shared.DomainEventPublisher
:
package com.deepoove.cargo.shared;public interface DomainEventPublisher {public void publish(Object event);
}
然后在基础实施中实现,具体实现采用guava的Eventbus方案:
package com.deepoove.cargo.infrastructure.event;@Component
public class GuavaDomainEventPublisher implements DomainEventPublisher {@AutowiredEventBus eventBus;public void publish(Object event) {eventBus.post(event);}}
发送事件的代码已经落地,那么监听事件的代码应该如何落地了呢?同样的,监听MQ的代码如何落地呢?按照架构图的指导,这些 监听器都应该充当着适配器的作用,所以它们的落地都应该放在基础设施层。
我们来看看具体监听器的实现:
package com.deepoove.cargo.infrastructure.event.comsumer;@Component
public class CargoListener {@Autowiredprivate CargoCmdService cargoCmdService;@Autowiredprivate EventBus eventBus;@PostConstructpublic void init(){eventBus.register(this);}@Subscribepublic void recordCargoBook(CargoBookDomainEvent event) {// invoke application service or domain service}
}
监听器的基本流程就是适配领域事件,然后调用应用服务去处理。
5.9 落地RPC和防腐层
前面提到过,当我们暴露一个RPC服务时和web层是平等对待的,比如暴露一个dubbo协议的服务就和暴露一个http的服务是平等的。这一小节我们将来探讨如何与第三方系统的RPC服务进行交互。
这里涉及到DDD中Bounded Context和Context Map的概念,在领域驱动设计中,限界上下文之间是不能直接交互的,它们需要通过Context Map进行交互,在微服务足够细致的年代,我们可以做到一个微服务就代表着一个限界上下文。
当我们与第三方系统RPC交互时,就要考虑如何设计Context Map,典型的模式有Shared Kernel共享内核模式和Anti-corruption防腐层模式,最终落地时我们选择了防腐层模式,它的结构如下图(图来自《实现领域驱动设计》一书)所示:
图中Adapter就是适配器,通用做法会再创建一个Translator实现上下文模型之间的翻译功能。
在看具体代码落地前还有一个问题需要强调,其它限界上下文的模型在我们系统中并不是一个模型实体,而是一个值对象,很显然Adapter应该放在基础设施层中,那么这个值对象存放在哪里呢?
我们可以将值对象和抽象接口定义在领域层,然后基础设施通过适配器和翻译器实现抽象接口,很明显这个做法是非常可取的。在具体落地时我们发现,这些值对象可能同时又被查询服务依赖,所以值对象和抽象接口定义在shared Data & Service中也是可取的,具体放在那里因看法而异。
接下来我们来看看适配器的基本实现,其中RemoteServiceTranslator
起到了模型之间翻译的作用。
package com.deepoove.cargo.infrastructure.rpc.salessystem;
@Component
public class RemoteServiceAdapter {@Autowiredprivate RemoteServiceTranslator translator;// @Autowired// remoteServicepublic UserDO getUser(String phone) {// User user = remoteService.getUser(phone);// return this.translator.toUserDO(user);return null;}public EnterpriseSegment deriveEnterpriseSegment(Cargo cargo) {// remote service// translatorreturn EnterpriseSegment.FRUIT;}}
6. Cargo货物实例和源码
落地代码实现了一个简单的货运系统,主要功能包括预订货物、修改货运信息、添加货运事件和追踪货运物流信息等,具体源码参见GitHub:https://github.com/Sayi/ddd-cargo
7. 参考资料
在整个落地过程中,每次阅读《领域驱动设计》和《实现领域驱动设计》这两本书都会给我带来新的想法,值得推荐。
The Clean Architecture
DDD, Hexagonal, Onion, Clean, CQRS
dddsample-core
8. 总结
所有的落地代码都是当前的想法,它一定会变化,架构和设计有魅力的地方就在于它会不断的变迁和升级,我们会不断丰富在领域驱动设计中的代码落地,也欢迎在下方留言与我探讨DDD相关的话题。
领域驱动设计和CQRS落地相关推荐
- 一文看懂 DDD(领域驱动设计)、CQRS和Event Souring与分层架构
我最近开始学习领域驱动设计,CQRS和事件溯源. 到目前为止,我主要参与了使用"经典"N层/层架构和关系数据库的项目. 随着项目变得越来越复杂,我注意到这个模型并不总是很好. 不久 ...
- 领域驱动设计之CQRS
1.概念 CQRS全称:Command Query Responsibility Segregation ,中文名:命令查询与职责分离 2.什么是CQRS CQRS 将系统中的操作分为两类,即「命令」 ...
- (一)初识DDD(领域驱动设计)
初识DDD(领域驱动设计) 前言 01 基础概念 什么是DDD 02 DP(Domain Primitive) 案例一(用户登录) DP的引出 03 计划 前言 今天开始,更新领域驱动设计系统架构落地 ...
- 领域驱动设计DDD和CQRS落地
DDD分层架构 Evans在它的<领域驱动设计:软件核心复杂性应对之道>书中推荐采用分层架构去实现领域驱动设计: image 其实这种分层架构我们早已驾轻就熟,MVC模式就是我们所熟知的一 ...
- [理论]领域驱动设计 DDD 是啥,cqrs是啥
父文章 如何成为一名架构师,架构师成长之路_个人渣记录仅为自己搜索用的博客-CSDN博客_架构师成长之路 [落地版]领域驱动落地 [理论版]领域驱动设计DDD 代码框架 · 语雀 子文章 如何写可维护 ...
- 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则
前言 上一篇 基于ABP落地领域驱动设计-01.全景图 概述了DDD理论和对应的解决方案.项目组成.项目引用关系,以及基于ABP落地DDD的通用原则.从这本篇开始,会更加深入地介绍在基于 ABP Fr ...
- 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则
dotNET兄弟会 专注.Net开源技术及跨平台开发!致力于构建完善的.Net开放技术文库!为.Net爱好者提供学习交流家园! 公众号 围绕DDD和ABP Framework两个核心技术,后面还会陆续 ...
- 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑
系列文章 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则 基于ABP落地领域驱动设 ...
- ddd 企业应用架构模式_灵魂拷问:用了DDD分包就是落地了领域驱动设计吗?谈谈DDD本质...
学习DDD的时候,作为开发,我们更关心它在技术层面的东西,尤其体现在DDD的分包方式.编码技巧等方面. 自然的,我们不禁发问,用了DDD的分包,就是实践落地了DDD了么? 不卖关子,直接说答案,并不是 ...
最新文章
- string.Format字符串格式化说明(转)
- s3c6140 UART驱动设计
- JVM垃圾回收的时候如何确定垃圾?什么是GC Roots?
- Java期末复习——ch02基本类型(进制转换,数据类型转换,汉字编码)
- HDU - 3966 Aragorn's Story(树链剖分+线段树)
- liunx 常用命令-cut
- 园林系统优秀党员推荐材料_园林绿化公司党员先进个人事迹材料
- decimal类型对象里面定义什么类型_奥斯塔罗 单身开启桃花雷达 现阶段的我适合什么类型的对象?...
- 《Unix环境高级编程》读书笔记 第5章-标准I/O流
- 启动hadoop输入jps显示:程序 ‘jps‘ 已包含在下列软件包中: * openjdk-7-jdk * openjdk-6-jdk 请尝试:sudo apt-get install ~
- 2017华为软挑——最小费用最大流(MCMF)
- JMeter接口测试工具基础 — Badboy工具
- STM8L IAP升级过程记录
- pycharm社区免费版如何创建Django项目
- 【已解决】【Selenium】请教大神,知乎的注册页面如何切换到登录页面?
- 养生年龄的早龄化一一朱乐睿教授
- element-ui手风琴自定义html,element-ui中el-table expand 手风琴效果,展开里面的内容或者ta...
- 机器学习从入门到创业手记-sklearn基础设计
- 大话赛宁云 | 训系列-如何构建网络空间的“练兵场”
- Android/Linux Kernel 内存管理-入门笔记
热门文章
- 现在上网的方式都有哪些啊?
- 用matlab实现harris角点检测,基于MatlabGUI的Harris角点检测程序
- CentOS下利用Docker部署Surging
- Kill杀死Linux中的defunct进程(僵尸进程)
- js时间戳​(timestamp)​与时间字符串相互转换
- 解决tomcat软件闪退问题
- 项目表格以及思路的设计-优惠券设计
- IE6location跳转问题
- [论文解析] Cones: Concept Neurons in Diffusion Models for Customized Generation
- html跑马灯 ie6,WOW Slider免费版