在项目搭建完成进行了N个迭代之后,往往因为需求的变化以及设计的缺陷导致领域模型、接口、数据库设计等和最开始的时候大为不同,架构设计需要保鲜会花去大量的时间。有没有一种办法可以实时反应项目的各种技术设计呢?就像Swagger一样,代码自动生成文档。我们采用了静态代码扫描的方案,通过代码来反应真实的技术设计,这就是《云巧工坊-应用素描》的功能。
《应用素描》提供了领域模型、数据模型、接口信息的详细信息查看功能,还提供了模型与模型之间的关系,表与表之间的关系图,是一个快速了解现有项目设计的好帮手。
使用方式
一、项目引入
<dependencies> ... <dependency><groupId>com.aliyun.gts.yunqiao</groupId><artifactId>yunqiao-boot-starter-arch-annotation</artifactId><version>${yunqiao-boot.version}</version></dependency> ... </dependencies>x
二、注解使用
2.1 领域模型注解
根据 领域驱动开发的理论(DDD),领域模型分为3中类型:
- ValueObject(值对象):没有唯一标识只是代表一些列属性的对象,比如
importcom.aliyun.gts.yunqiao.arch.annotation.domain.ValueObject; /*** 表示一个点*/publicclassPoint { /*** x 坐标*/privatedoublex; /*** y 坐标*/privatedoubley; }
- Entity(实体):有唯一标识,可以根据唯一标识获取到的唯一的对象,比如
importcom.aliyun.gts.yunqiao.arch.annotation.domain.Entity; /*** 地址*/identity="addressCode") (publicclassAddress { /*** 地址编码*/privateStringaddressCode; /*** 省*/privateStringprovince; /*** 市*/privateStringcity; /*** 区*/privateStringarea; }
- Aggregate(聚合根):聚合根是一种特殊的实体,它除了有唯一标识,还聚合了很多其他实体和值对象,相当于数据库表中的主表,比如
importcom.aliyun.gts.yunqiao.arch.annotation.domain.Aggregate; /*** 用户*/identity="userId") (publicclassUser { /*** 用户ID*/privateStringuserId; /*** 用户名*/privateStringname; /*** 年龄*/privateintage; /*** 用户住址*/privateAddressaddress; }
- Context(限界上下文):把高内聚相关的一些聚合根、实体、值对象划分到一起的一种分类,一般在java里就是一个package(包)。
// package-info.javacode="user", name="用户域") (packagecom.aliyun.gts.x.user; importcom.aliyun.gts.yunqiao.arch.annotation.domain.Context;
- DomainService(领域服务):把不太适合在领域模型中编写的,但又和领域模型相关的一些方法放到独立的对象中,称之为领域服务,在贫血模型中,所有方法都是在领域服务中的,比如:
importcom.aliyun.gts.yunqiao.arch.annotation.domain.DomainService; /*** 用户领域服务*/User.class) (publicclassUserDomainService { /*** 创建用户*/publicUsercreateUser(...) {...} } 6.DomainRepository(领域仓储):持久化领域模型的对象,一般为数据库操作对象,称之为领域仓储,比如importcom.aliyun.gts.yunqiao.arch.annotation.domain.DomainRepository; /*** 用户领域仓储*/User.class) (publicinterfaceIUserDomainRepository { /*** 保存用户*/voidsave(Useruser); }
把 yunqiao-boot-starter-arch-annotation 引入项目后,就可以使用它提供的注解对领域模型进行标记,上面示例中的
- @ValueObject 对值对象进行标记
- @Entity(identity="") 对实体进行标记,其中 identity 标识该实体的唯一标识的属性名称
- @Aggregate(identity="") 对聚合根进行标记,其中 identity 标识该实体的唯一标识的属性名称
- @Context(code="", name="") 在package-info.java文件中对package进行标记,其中code为该限界上下文的唯一标识,name为该限界上下文的名称。
- @DomainService(value=Class) 对领域服务进行标记,其中value为聚合根的类,表示该服务为哪一个聚合根服务。
- @DomainRepository(value=Class) 对领域仓储进行标记,其中value为聚合根的类,表示该仓储为哪一个聚合根进行持久化操作。
2.2 数据模型注解
复用 Mybatis-Plus 的 @TableField、@TableId、@TableName 用来标记 Data Object。除此之外,添加了以下注解,以更全面的描述数据模型的结构。
ElementType.TYPE}) ({RetentionPolicy.RUNTIME) (public@interfaceTableExt { Index[] indexes() default {}; PrimaryKeyprimaryKey() default (columns= {}); Stringcharset() default"utf8"; } ElementType.FIELD}) ({RetentionPolicy.RUNTIME) (public@interfaceTableFieldExt { booleanunique() defaultfalse; booleannotNull() defaultfalse; booleanautoIncrement() defaultfalse; StringdefaultValue() default""; intlength() default-1; } ({}) RetentionPolicy.RUNTIME) (public@interfaceIndex { IndexColumn[] columns(); Stringname(); booleanunique() defaultfalse; } ({}) RetentionPolicy.RUNTIME) (public@interfacePrimaryKey { IndexColumn[] columns(); } ({}) RetentionPolicy.RUNTIME) (public@interfaceIndexColumn { enumIndexColumnOrder { UNDEFINED, ASC, DESC, } Stringname(); IndexColumnOrderorder() defaultIndexColumnOrder.UNDEFINED; intlength() default-1; }
Example
charset="gb2312", primaryKey= ( (columns= ("id") ), indexes= { name="idx_1", columns= ("field1")), (name="uk_1", columns= { (value="field2", length=100), (value="field1", order=IndexColumn.IndexColumnOrder.DESC)}) ( }) value="tableName", schema="schemaName") (publicclassMybatisPlusDataObject { privatestaticfinalStringDUMMY="DUMMY"; value="ID", type=IdType.AUTO) (privateLongid; exist=false) (privateStringnotExistField; unique=true, notNull=true, autoIncrement=true, defaultValue="1234", length=1024) (privateStringfield1; "the_second_field") (privateStringfield2; }
2.3 其他支持的注解
控制器(Rest API)标记
兼容 Spring MVC。通过 @Controller 或 @RestController 标记类是一个控制器类;通过 @RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping,标记方法是一个对外的 API。
AppService 标记
通过 @AppService 标记类是一个应用服务类(此注解继承了 Spring 的 @Service 注解,故无需再重复使用 Spring 的注解);应用服务类下所有的 public 方法为应用服务的行为。
Mapper 标记
复用 Mybatis-Plus 的 Mapper<T> 接口(BaseMapper 扩展了 Mapper),用以标记 mapper 以及其服务的数据模型;mapper 接口的所有方法为 mapper 的行为。
实现原理
Java语法解析使用的是开源库 javaparser,javaparser 可以把Java源代码解析成一颗语法树,然后提供各种回调接口来有序遍历这棵树。例如遍历一个文件里所有定义的类和获取类里面的方法定义:
voiditerateClassOrInterface(CompilationUnitcompilationUnit) { compilationUnit.accept(newVoidVisitorAdapter<Void>() { publicvoidvisit(ClassOrInterfaceDeclarationn, Voidarg) { super.visit(n, arg); List<MethodDeclaration>methods=c.getMethods(); } }, null); }
javaparser 解析一个文件成为语法树是非常消耗性能的,所以不能频繁的去调用 parse 方法去解析文件,但是也不能把所有的语法分析逻辑写在一个回调里,那么就非常适合使用责任链模式来对不同业务的分析逻辑进行解耦
最后把解析后的结构化数据交给前端页面进行可视化渲染展示,就可以实现代码即文档了。