0%

COLA-复杂项目领域驱动设计框架源码和架构解析

COLA简介

可自行到github上查看md简介
https://github.com/alibaba/COLA

目录结构分析

COAL提供了一个很方便的maven生成模板,按照项目说明我们生成一个web项目,打开目录我们发现如下几个子模块

父项目
└── start   项目启动模块
└── app     项目应用服务模块(可以理解为客户端程序员在此编写代码)
└── client  提供给客户端程序员使用的二方包,为应用服务模块提供一下基础类
└── controller  控制层SpringMVC实现
└── domain  领域层
└── infrastructure  基础设施层,包含数据库持久化,一些其它基础设施调用代码放在此,如RPC调用,队列调用等基础设施代码

其项目依赖目录如下
start依赖controller,controller依赖app,app同时依赖client和infrastructure,infrastructure依赖domain

start
 └── controller
     └── app
         ├── client
         ├── infrastructure
             └── domain

从依赖中我们明显可以看到前文提到的六边形架构的影子,controller为输入端适配器,infrastructure为持久化输出适配器,而核心服务domain领域层作为软件的核心和最稳定的部分放在依赖的最底层

结合领域驱动设计分析目录结构

从目录结构当中划分其实这个框架应该分为两部分,app层之下(不包括)为领域部分,这部分由对领域知识理解较好的开发人员去建模,而app层(包括)之上则为客户端代码,可以由对领域知识不那么了解的开发人员去开发,作者之所以引入client模块一方面是提供了一些二方包和接口功能客户端调用,最主要的原因是反腐,为了反腐在client包中引入DTO,为了读写分离(在领域驱动中这点很重要)又有Command和Query两个实现类,以此完成写命令和读命令的区分,因为在写命令中需要完全遵守领域模型去完成,但是在查询中往往为了响应速度等的妥协而放弃领域模型,直接从基础设施层中读取查询对象返回给前端.

应用框架方面提供了一些注解如示例中的@Command注解,其需要作用在CommandExecutorI的实现类下,这部分内容放在app层代码由客户端人员去实现,这样可以将客户端的业务逻辑放在CommandExecutorI接口规定的方法下,客户端逻辑更加清晰,一个@Command代表着一个客户单操作,每个客户端操作的代码又放在不同的CommandExecutorI实现类下,而框架显示的将操作分为查询操作和命令操作来实现读写分离,实现了各个功能模块的解耦。

client包作为约束客户端(APP)层的重要模块,其下面定义了一个CustomerServiceI接口,此结构的实现类由app层去实现,这种依赖反转的方式很好的控制了app端能做什么也简化了开发。

domain包为领域开发人员所使用的包,领域开发人员将设计的领域模型都放在此提供app层和infrastructure层去调用

infrastructure包可以由数据库开发人员去实现,里面包含领域对象持久化内容和一些基础服务,并且其提供了一个SpringBean的配置文件负责整合了框架

核心源码解析

在demo app层中我们发现了被@Command注解的实现类

1
2
3
4
5
6
7
8
9
10
11
12
@Command
public class CustomerListByNameQryExe implements QueryExecutorI<MultiResponse<Customer>, CustomerListByNameQry> {

@Override
public MultiResponse<Customer> execute(CustomerListByNameQry cmd) {
List<Customer> customerList = new ArrayList<>();
Customer customer = new Customer();
customer.setCustomerName("Frank");
customerList.add(customer);
return MultiResponse.ofWithoutTotal(customerList);
}
}

在app服务类调用中仅仅是将customerListByNameQry对象发送给CommandBusI(CommandBus总线)即完成调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class CustomerServiceImpl implements CustomerServiceI {

@Autowired
private CommandBusI commandBus;

@Override
public Response addCustomer(CustomerAddCmd customerAddCmd) {
return (Response)commandBus.send(customerAddCmd);
}

@Override
public MultiResponse<Customer> listByName(CustomerListByNameQry customerListByNameQry) {
return (MultiResponse<Customer>)commandBus.send(customerListByNameQry);
}

}

我们使用编译器进入CommandBusI实现类发现CommandBusI下对应着一个CommandBus的实现类

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class CommandBus implements CommandBusI{

@Autowired
private CommandHub commandHub;

@Override
public Response send(Command cmd) {
return commandHub.getCommandInvocation(cmd.getClass()).invoke(cmd);
}

}

我们再进入CommandHub查看getCommandInvocation方法,发现其实CommandInvocation是从一个HashMap中来的,那么HashMap里的值是合适初始化的呢

1
2
3
4
5
6
7
8
private Map<Class/*CommandClz*/, CommandInvocation> commandRepository = new HashMap<>();

public CommandInvocation getCommandInvocation(Class cmdClass) {
CommandInvocation commandInvocation = commandRepository.get(cmdClass);
if (commandRepository.get(cmdClass) == null)
throw new ColaException(cmdClass + " is not registered in CommandHub, please register first");
return commandInvocation;
}

在infrastructure模块下我们发现框架的入口配置类ColaConfig

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ColaConfig {

@Bean(initMethod = "init")
public Bootstrap bootstrap() {
Bootstrap bootstrap = new Bootstrap();
List<String> packagesToScan = new ArrayList<>();
packagesToScan.add("com.liu");
bootstrap.setPackages(packagesToScan);
return bootstrap;
}
}

于是我们定位到框架的入口,顺着初始化代码在源码COLA-master的cola-framework下找到Bootstrap类,找到初始化方法init

1
2
3
4
public void init() {
Set<Class<?>> classSet = scanConfiguredPackages();
registerBeans(classSet);
}

看到这里我们就已经明白了scanConfiguredPackages()方法是扫描编译目录下的Class文件,registerBeans是注册bean对象
我们进入registerBeans方法,发现其遍历之前扫描的class文件,并且传入在registerFactory

1
2
3
4
5
6
7
8
9
private void registerBeans(Set<Class<?>> classSet) {
for (Class<?> targetClz : classSet) {
RegisterI register = registerFactory.getRegister(targetClz);
if (null != register) {
register.doRegistration(targetClz);
}
}

}

进入registerFactory.getRegister(targetClz)方法,发现代码在registerFactory中根据类上的注解返回对应的注册器,之前对应的@Command注解对应的就是为commandRegister注册器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public RegisterI getRegister(Class<?> targetClz) {
PreInterceptor preInterceptorAnn = targetClz.getDeclaredAnnotation(PreInterceptor.class);
if (preInterceptorAnn != null) {
return preInterceptorRegister;
}
PostInterceptor postInterceptorAnn = targetClz.getDeclaredAnnotation(PostInterceptor.class);
if (postInterceptorAnn != null) {
return postInterceptorRegister;
}
Command commandAnn = targetClz.getDeclaredAnnotation(Command.class);
if (commandAnn != null) {
return commandRegister;
}
Extension extensionAnn = targetClz.getDeclaredAnnotation(Extension.class);
if (extensionAnn != null) {
return extensionRegister;
}
EventHandler eventHandlerAnn = targetClz.getDeclaredAnnotation(EventHandler.class);
if (eventHandlerAnn != null) {
return eventRegister;
}
return null;
}

我们再看commandRegister中的doRegistration方法,现在结果一目了然,这里获取Spring容器中的CommandInvocation对象,并且调用commandHub.getCommandRepository().put(commandClz, commandInvocation)将之前被@Command注解的实现类传入commandRepository中也就是之前上面从HashMap中获取CommandInvocation的地方,代码中还提供了自定义AOP功能,但这部分并未开发接口,至此,整个框架的代码流程就已清晰。

1
2
3
4
5
6
7
8
9
@Override
public void doRegistration(Class<?> targetClz) {
Class<? extends Command> commandClz = getCommandFromExecutor(targetClz);
CommandInvocation commandInvocation = ApplicationContextHelper.getBean(CommandInvocation.class);
commandInvocation.setCommandExecutor((CommandExecutorI) ApplicationContextHelper.getBean(targetClz));
commandInvocation.setPreInterceptors(collectInterceptors(commandClz, true));
commandInvocation.setPostInterceptors(collectInterceptors(commandClz, false));
commandHub.getCommandRepository().put(commandClz, commandInvocation);
}