目录

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,数据同步可以采用两种方案:

  1. 当数据库发生更改时,主动发送Event事件通知ES进行更新

  2. 直接监听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的味道),典型的处理流程是:

  1. 校验

  2. 协调领域模型或者领域服务

  3. 持久化

  4. 发布领域事件

在这一层可以使用流程编排,典型的流程也可以使用技术手段固化,比如抽象模板模式。

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)中,可以从以下几点考虑:

  1. 不是属于单个聚合根的业务或者需要多个聚合根配合的业务,放在领域服务中,注意是业务,如果没有业务,协调工作应该放到应用服务(Applicaction)中

  2. 静态方法放在领域服务中(Domain.Service)

  3. 需要通过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的定义不是在基础设施层吗,查询服务怎么可以访问到这些对象呢?我们有两个办法:

  1. 查询服务中定义一套一摸一样的DO,然后基础设施做转换,缺点是有点复杂,冗余了DO,优点是完美符合DIP原则:抽象在查询服务中,实现在基础设施

  2. 将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落地相关推荐

  1. 一文看懂 DDD(领域驱动设计)、CQRS和Event Souring与分层架构

    我最近开始学习领域驱动设计,CQRS和事件溯源. 到目前为止,我主要参与了使用"经典"N层/层架构和关系数据库的项目. 随着项目变得越来越复杂,我注意到这个模型并不总是很好. 不久 ...

  2. 领域驱动设计之CQRS

    1.概念 CQRS全称:Command Query Responsibility Segregation ,中文名:命令查询与职责分离 2.什么是CQRS CQRS 将系统中的操作分为两类,即「命令」 ...

  3. (一)初识DDD(领域驱动设计)

    初识DDD(领域驱动设计) 前言 01 基础概念 什么是DDD 02 DP(Domain Primitive) 案例一(用户登录) DP的引出 03 计划 前言 今天开始,更新领域驱动设计系统架构落地 ...

  4. 领域驱动设计DDD和CQRS落地

    DDD分层架构 Evans在它的<领域驱动设计:软件核心复杂性应对之道>书中推荐采用分层架构去实现领域驱动设计: image 其实这种分层架构我们早已驾轻就熟,MVC模式就是我们所熟知的一 ...

  5. [理论]领域驱动设计 DDD 是啥,cqrs是啥

    父文章 如何成为一名架构师,架构师成长之路_个人渣记录仅为自己搜索用的博客-CSDN博客_架构师成长之路 [落地版]领域驱动落地 [理论版]领域驱动设计DDD 代码框架 · 语雀 子文章 如何写可维护 ...

  6. 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

    前言 上一篇 基于ABP落地领域驱动设计-01.全景图 概述了DDD理论和对应的解决方案.项目组成.项目引用关系,以及基于ABP落地DDD的通用原则.从这本篇开始,会更加深入地介绍在基于 ABP Fr ...

  7. 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则

    dotNET兄弟会 专注.Net开源技术及跨平台开发!致力于构建完善的.Net开放技术文库!为.Net爱好者提供学习交流家园! 公众号 围绕DDD和ABP Framework两个核心技术,后面还会陆续 ...

  8. 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑

    系列文章 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则 基于ABP落地领域驱动设 ...

  9. ddd 企业应用架构模式_灵魂拷问:用了DDD分包就是落地了领域驱动设计吗?谈谈DDD本质...

    学习DDD的时候,作为开发,我们更关心它在技术层面的东西,尤其体现在DDD的分包方式.编码技巧等方面. 自然的,我们不禁发问,用了DDD的分包,就是实践落地了DDD了么? 不卖关子,直接说答案,并不是 ...

最新文章

  1. string.Format字符串格式化说明(转)
  2. s3c6140 UART驱动设计
  3. JVM垃圾回收的时候如何确定垃圾?什么是GC Roots?
  4. Java期末复习——ch02基本类型(进制转换,数据类型转换,汉字编码)
  5. HDU - 3966 Aragorn's Story(树链剖分+线段树)
  6. liunx 常用命令-cut
  7. 园林系统优秀党员推荐材料_园林绿化公司党员先进个人事迹材料
  8. decimal类型对象里面定义什么类型_奥斯塔罗 单身开启桃花雷达 现阶段的我适合什么类型的对象?...
  9. 《Unix环境高级编程》读书笔记 第5章-标准I/O流
  10. 启动hadoop输入jps显示:程序 ‘jps‘ 已包含在下列软件包中: * openjdk-7-jdk * openjdk-6-jdk 请尝试:sudo apt-get install ~
  11. 2017华为软挑——最小费用最大流(MCMF)
  12. JMeter接口测试工具基础 — Badboy工具
  13. STM8L IAP升级过程记录
  14. pycharm社区免费版如何创建Django项目
  15. 【已解决】【Selenium】请教大神,知乎的注册页面如何切换到登录页面?
  16. 养生年龄的早龄化一一朱乐睿教授
  17. element-ui手风琴自定义html,element-ui中el-table expand 手风琴效果,展开里面的内容或者ta...
  18. 机器学习从入门到创业手记-sklearn基础设计
  19. 大话赛宁云 | 训系列-如何构建网络空间的“练兵场”
  20. Android/Linux Kernel 内存管理-入门笔记

热门文章

  1. 现在上网的方式都有哪些啊?
  2. 用matlab实现harris角点检测,基于MatlabGUI的Harris角点检测程序
  3. CentOS下利用Docker部署Surging
  4. Kill杀死Linux中的defunct进程(僵尸进程)
  5. js时间戳​(timestamp)​与时间字符串相互转换
  6. 解决tomcat软件闪退问题
  7. 项目表格以及思路的设计-优惠券设计
  8. IE6location跳转问题
  9. [论文解析] Cones: Concept Neurons in Diffusion Models for Customized Generation
  10. html跑马灯 ie6,WOW Slider免费版