常见的代码分层图

代码分层图

分层很明确,先说缺点

  1. service层可以依赖多个dao层
    一个表肯定对应一个dao。如果一个service直接操作多张表(dao)也没问题,但是有可能所有表的操作都封闭在一个service中。
    • 如果后期维护某一张表的时候你就得需要屡下所有调用此表的service,花费时间不说,还有可能漏掉。
    • 如果对其中一个表进行别的业务复用的话,则需要把代码抽离出来,并且有可能开发人员不抽离,而是直接copy粘贴,导致代码原来越乱。

      所以建议一个表对应一个dao和一个service,其中service只能操作自己的表(dao)。要是操作其他的表只能依赖其对应的service

  2. 上图没有明确表明哪些是可以互相依赖(service依赖其他service,dao可以依赖其他dao...),哪些不可以互相依赖。所以我们认为都是可以相互依赖的。互相依赖比较混乱。

    dao专门负责管理sql,如果对一个实体的curd还涉及到另外其他的实体curd。那么这就显然属于业务范畴了,应该放在service。所以在dao这一层。我们不能让他操作多张表(不能有互相依赖)

  3. 没有强制的依赖校验。如果controller直接引用dao层也可以正常运行。会增加后期维护的困难性

代码依赖的强制校验

对于代码依赖校验,按照以上几个点来校验的话

  1. 首先得定义一个dao层,确保一个表的curd的sql不会乱出现别的dao地方。所以用到mybatisPlus的sql语法糖校验
  2. 其次不同层有不同的依赖规则。
    • 如controller不能依赖dao,
    • service不能依赖其他dao(表),只能依赖自己的dao
    • dao不能有相互依赖。

不同层有不用的配置,所以我们需要一个可配置的注解

校验依赖的注解

此注解只能放在package上。代表对此包以及子包里面spring管理的bean进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PackageCheck {

/**
* 当前包的校验排除子包
*/
String[] checkExcludeSubPackages() default "config";

/**
* 不能依赖此包的内容
*/
String[] notAllowDependPackageNames();

/**
* 必须命名正则的规则
*/
String mustNamePattern() default ".*";

/**
* 当前包所有的类必须继承的类
*/
Class<?> classMustExtendClass() default Object.class;

Class<?>[] exclusionClass() default {};

/**
* 当前包所有的类可以相互依赖
*/
boolean interdependenceOnSamePackage() default true;

/**
* 当前包所有的类依赖的类型有且只能有一个(如果指定的话)
*/
Class<?>[] dependOnly() default {};
}

注解校验的配置

  1. controller层 在controller包结构目录下新建一个packang-info.java。然后用此注解配置校验的内容
    1
    2
    3
    @CodeVerify.PackageCheck(classMustExtendClass = BaseController.class // 所有的controller必须要继承此类,可有可无
    , mustNamePattern = ".*Controller$" // controller命名必须匹配该正则
    , notAllowDependPackageNames = {"mapper层的包名称", "dao层的包名称"}) // controller不能直接依赖dao,也不能直接依赖mapper。(只能通过service调用)
  2. service层 在service包结构目录下新建一个packang-info.java。然后用此注解配置校验的内容
    1
    2
    3
    4
    @CodeVerify.PackageCheck(classMustExtendClass = BaseService.class// 所有的service必须要继承此类,可有可无
    , mustNamePattern = ".*Service(Impl)?$"// 该包下spring管理的bean命名必须匹配该正则
    , notAllowDependPackageNames = {"mapper层的包名称"} // 不能直接依赖mapper层,只能通过依赖dao。
    , dependOnly = BaseDao.class)//只能依赖一个Dao,一般都是一个表,一个mapper,一个dao,一个service
  3. dao层 建议Dao统一继承此BaseDao
    一个表的所有sql按照规范写在同一个类中mybatisPlus的sql语法糖强制校验
1
2
3
4
@CodeVerify.PackageCheck(classMustExtendClass = BaseDAO.class
, mustNamePattern = ".*Dao$"
, notAllowDependPackageNames = {}// 如果dao的java代码可以引用service不报错,可以配置不能依赖service包。因为我们项目是分模块,如果用service代码编译器会直接报错,所以写不写都没有必要
, interdependenceOnSamePackage = false) // 不允许有互相依赖,一个dao只能管理自己的表,如果涉及到其他表则应该放在service处理。

开始校验

待容器启动后,所有bean的依赖的关系已形成,我们就可以校验了。

可以参考springBoot容器启动流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
@Slf4j
public static class DependCheck implements ApplicationListener<ApplicationStartedEvent> {

@SneakyThrows
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();

// 所有需要校验的包。Package.getPackages()只有对应的package有真正的类才算是真正的一个package,只有Package-info.java是不行的
List<Package> checkPackageList = Arrays.stream(Package.getPackages())
.filter(t -> t.isAnnotationPresent(PackageCheck.class))// 把需要校验的过滤出来
.collect(Collectors.toList());

String[] allBeanNames = beanFactory.getBeanDefinitionNames(); // 获取所有的bean

List<String> errorMessageList = Lists.newArrayList();

log.info("check PackageNames:{}", checkPackageList);
for (Package checkPackage : checkPackageList) {
PackageCheck annotation = checkPackage.getAnnotation(PackageCheck.class);
log.info("DependCheck starting package:{}, configuration:{}", checkPackage.getName(), annotation);

// 校验的条件
String[] excludeSubPackages = annotation.checkExcludeSubPackages();
Class<?> mustExtendClass = annotation.classMustExtendClass();
String mustSuffixName = annotation.mustNamePattern();
String[] notAllowDependPackageNames = annotation.notAllowDependPackageNames();
boolean interdependenceOnSamePackage = annotation.interdependenceOnSamePackage();
Class<?>[] dependOnly = annotation.dependOnly();
Class<?>[] exclusionsClass = annotation.exclusionClass();


String currentPackage = checkPackage.getName();
for (String beanName : allBeanNames) {
BeanDefinition mergedBeanDefinition = beanFactory.getMergedBeanDefinition(beanName);
String beanClassName = mergedBeanDefinition.getResolvableType().getType().getTypeName();
if (!beanClassName.startsWith(currentPackage)) { // 只有这个package的包才会校验
continue;
}

boolean excludeSubPackage = Arrays.stream(excludeSubPackages)
.anyMatch(t -> beanClassName.substring(currentPackage.length()).contains(t)); // 排除子包的校验,如果有的话
if (excludeSubPackage) {
continue;
}

Class<?> beanClass = Class.forName(beanClassName);
boolean exclusionClass = Arrays.stream(exclusionsClass) // 排除class的校验,如果有的话
.anyMatch(beanClass::isAssignableFrom);
if (exclusionClass) {
continue;
}

// 当前bean的依赖
String[] beanDependenciesName = beanFactory.getDependenciesForBean(beanName);
List<Class<?>> beanDependenciesClass = Arrays.stream(beanDependenciesName)
.map(beanFactory::getMergedBeanDefinition)
.map(BeanDefinition::getResolvableType)
.map(ResolvableType::getRawClass)
.collect(Collectors.toList());


// 不允许依赖其他的包
if (ArrayUtils.isNotEmpty(notAllowDependPackageNames)) {

boolean match = Arrays.stream(notAllowDependPackageNames)
.anyMatch(notAllowDependPackageName ->
beanDependenciesClass.stream()
.map(Class::getName)
.anyMatch(className ->
className.startsWith(notAllowDependPackageName)
)
);

if (match) {
String msg = MessageFormat.format(
"类:{0},不能依赖{1}包的内容. \n\t目前依赖的有:{2}"
, beanClassName
, Arrays.toString(notAllowDependPackageNames)
, Arrays.toString(beanDependenciesName)
);
errorMessageList.add("依赖不规范:\n\t" + msg);
}
}

// 不能有相互依赖
if (!interdependenceOnSamePackage) {
boolean match = beanDependenciesClass.stream()
.map(Class::getName)
.anyMatch(dependTypeName -> dependTypeName.startsWith(currentPackage));
if (match) {
String msg = MessageFormat.format(
"类:{0},不能依赖同包及子包的类. \n\t目前依赖的有:{1}"
, beanClassName
, Arrays.toString(beanDependenciesName)
);
errorMessageList.add("依赖不规范:\n\t" + msg);
}
}

// 依赖的类型有且只能有一个
if (ArrayUtils.isNotEmpty(dependOnly)) {

boolean match = Arrays.stream(dependOnly)
.anyMatch(dependOnlyClass ->
beanDependenciesClass.stream()
.filter(dependOnlyClass::isAssignableFrom)
.count() > 1
);

if (match) {
String msg = MessageFormat.format(
"类:{0},依赖的类型有且只能有一个{1}, \n\t目前依赖的有:{2}"
, beanClassName
, Arrays.toString(dependOnly)
, Arrays.toString(beanDependenciesName)
);
errorMessageList.add("依赖不规范:\n\t" + msg);
}
}

boolean configBean = beanClass.isAnnotationPresent(ConfigurationProperties.class) || beanClass.isAnnotationPresent(Component.class);

if (!configBean && !mustExtendClass.isAssignableFrom(beanClass)) {
errorMessageList.add("类继承不规范:\n\t" + beanClassName + "必须继承" + mustExtendClass);
}
if (!configBean && !beanClassName.matches(mustSuffixName)) {
errorMessageList.add("名称不规范:\n\t" + beanClassName + "名称格式必须是:" + mustSuffixName);
}

}

}
if (!errorMessageList.isEmpty()) {
throw new RuntimeException("代码编写不规范\n" + String.join("\n", errorMessageList)) {
public Throwable fillInStackTrace() {
return this;
}
};
}
}
}

总结

由于每个团队,每个项目工程的规范都不同。所以我们根据注解的配置进行校验。
在结合mybatisPlus语法糖校验,保证sql只允许出现在一处,确保我们的项目curd不会过于混乱。
以本文的代码分层为规范的技术思想,不同于DDD领域驱动设计的是:按照本文设计的规范,在不用DDD(领取驱动设计)的前提下,应用过于庞大或复杂的情况时我们还能保证业务代码不会过于臃肿、林乱不堪。

引用知乎的DDD驱动设计的简介。

DDD解决的问题是单体应用过大过于复杂导致开发团队的成员没有人能够了解业务全貌,换句话说程序的复杂度失控了。 比如你有一个方法上千行,肯定难以维护,所以你要拆。但是一个应用你怎么拆?传统的拆分角度的出发点是基于技术,比如三层架构,比如前后分离。但是这样的拆分不能降低业务的复杂度。 DDD就是用来划分业务边界的。DDD不是架构思想,是统筹规划软件开发的思想。 很多架构模式应用到DDD设计的系统里。其实你用DDD拆分出来的服务用传统的代码组织方式(传统的分层,repository, service, controller)也完全没有问题

DDD就是用来划分业务边界的。但是DDD设计在普通的CURD应用开发中很难运用好,大部分都是分层的设计。但是我们可以吸取DDD的好处,并结合分层设计的思想来处理我们的业务代码
所以规范一张表对应一个实体,并且对应一个dao。然后我们保证一个dao只能由一个service操作,换句话说一个service只能操作一个dao,操作其他dao只能依赖其对应的service。
那么这种分层方式和DDD领取驱动设计的精髓有相似(重合)之处。在不用学习DDD的前提下还能保证我们的代码不会过于的混乱,也只有这种更精细的分层方式了。

贫血模型:普通bean的一些内置get|set毫无意义,这就叫贫血模型。
充血模型:由于DDD的设计思想就是把bean里面塞满各种各样的自身业务逻辑。使此bean所有的操作都能聚合在一个bean(domain)中。这就叫充血模型。