常见的代码分层图
分层很明确,先说缺点
- service层可以依赖多个dao层
一个表肯定对应一个dao。如果一个service直接操作多张表(dao)也没问题,但是有可能所有表的操作都封闭在一个service中。
- 如果后期维护某一张表的时候你就得需要屡下所有调用此表的service,花费时间不说,还有可能漏掉。
- 如果对其中一个表进行别的业务复用的话,则需要把代码抽离出来,并且有可能开发人员不抽离,而是直接copy粘贴,导致代码原来越乱。
所以建议一个表对应一个dao和一个service,其中service只能操作自己的表(dao)。要是操作其他的表只能依赖其对应的service
- 上图没有明确表明哪些是可以互相依赖(service依赖其他service,dao可以依赖其他dao...),哪些不可以互相依赖。所以我们认为都是可以相互依赖的。互相依赖比较混乱。
dao专门负责管理sql,如果对一个实体的curd还涉及到另外其他的实体curd。那么这就显然属于业务范畴了,应该放在service。所以在dao这一层。我们不能让他操作多张表(不能有互相依赖)
- 没有强制的依赖校验。如果controller直接引用dao层也可以正常运行。会增加后期维护的困难性
代码依赖的强制校验
对于代码依赖校验,按照以上几个点来校验的话
- 首先得定义一个dao层,确保一个表的curd的sql不会乱出现别的dao地方。所以用到mybatisPlus的sql语法糖校验
- 其次不同层有不同的依赖规则。
- 如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 {}; }
|
注解校验的配置
- controller层
在controller包结构目录下新建一个packang-info.java。然后用此注解配置校验的内容
1 2 3
| @CodeVerify.PackageCheck(classMustExtendClass = BaseController.class , mustNamePattern = ".*Controller$" , notAllowDependPackageNames = {"mapper层的包名称", "dao层的包名称"})
|
- service层
在service包结构目录下新建一个packang-info.java。然后用此注解配置校验的内容
1 2 3 4
| @CodeVerify.PackageCheck(classMustExtendClass = BaseService.class , mustNamePattern = ".*Service(Impl)?$" , notAllowDependPackageNames = {"mapper层的包名称"} , dependOnly = BaseDao.class)
|
- dao层
建议Dao统一继承此BaseDao
一个表的所有sql按照规范写在同一个类中mybatisPlus的sql语法糖强制校验
1 2 3 4
| @CodeVerify.PackageCheck(classMustExtendClass = BaseDAO.class , mustNamePattern = ".*Dao$" , notAllowDependPackageNames = {} , interdependenceOnSamePackage = false)
|
开始校验
待容器启动后,所有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(); List<Package> checkPackageList = Arrays.stream(Package.getPackages()) .filter(t -> t.isAnnotationPresent(PackageCheck.class)) .collect(Collectors.toList());
String[] allBeanNames = beanFactory.getBeanDefinitionNames();
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)) { 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) .anyMatch(beanClass::isAssignableFrom); if (exclusionClass) { continue; }
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)中。这就叫充血模型。