如何设计一个结构合理的java项目
程序员文章站
2022-06-24 17:59:57
...
## 1、前言
最近写一个Java处理工具,是一个springboot的非web项目,正好借这个机会总结一下自己的经验,当开发一个Java应用时,应该全局考虑哪些方面,包括如何划分功能包,如果建立对象关联,以及如果串起流程;另一方面在这个过程中,作为喜新厌旧的程序员,全面应用Jdk8的新特征,比如其中的语法糖还是蛮甜的。
## 2、类划分功能包
web项目是很多人接触最多的项目,与工具项目有相似点,也有不同点。一般的单体的web项目通常的包,表面上按这样划分:
多层次业务模块->controller->service->dao这么几层。而从实现了一个请求的单向处理过程看,服务端实际上完整过程是这样:通讯协议处理->filter->dispatchservlet->controller->service->dao。由于框架做了很多事情,开发人员很可能只需要固定的套路,而不需要深入研究,基本上技术是简单的CURD操作。
我这个工具过程功能比较少:解析excel->输出txt->合并txt三个步骤,虽然不多,但尽可能让结构合理,这样想增加功能也方便,也可以扩展出的很复杂功能。
## 3、类(包)之间关联
划包与包内组织好了,下面就是建立包之间调用关联了,主要是调用的对象之间,通过属性引用起来的。比如@autowired。还得不说spring容器的强大了。它可以按照你提供的配置蓝图与零件,给你组装生成出一个可以运转的机器。它的依赖注入,多个过程(比如前置,后置,生成,初始化,销毁等)中插入你的处理逻辑,比如factorybean产生代理对象,它的延时加载,条件加载,让你的这个配置蓝图可以非常的复杂。前面提到dubbo微核心,自己实现了一个小的容器,也有依赖注入,最有特色的是,用接口类型获取对象时,特殊注解的接口,它可以用代码动态生成接口的代理类对象给你,而这个代理对象根据请求参数不同,代理不同的实现类,具体代理哪个,又是从容器中取的。这就厉害了,一方面减少了spring容器的冗余功能,另一方面实现了自己特殊的需求,再着体现了作者的实力。
## 4、类(包)关系的变更
类的关系建好了,也不是一成不变,用户需求会变,功能要增加,低耦合的目的就是应对这种变化,如果事先考虑这种变化,可以有一定的机制,把变成留给用户,并规范用户的代码。如果没有,变化也可以限制在一定的变动量下。而用户插入的功能可能多个,所以经常发现用责任链模式串起来。
## 5、类的自我管理
大型项目,越看越感觉象生活中的场景,比如象构建一个医院,服务一个个病人。过程中有保安进门,有挂号产生一次服务链,每个科进行本科室的处理,人多了排队,再多限流。当然也有行政部门与科内部进行自我的管理,比如事先准备好设备,内部排班,整理统计数据等。发明面向对象编程的人太伟大了,贴合生活,易于理解并设计非常复杂的项目。
## 6、全面使用jdk8新特征
8已经老了,但很多项目更老,而且老程序员习惯不容易变。我却喜欢尽可能用点新的,了解新特征产生的原因,才能更好的用好。
- 多线程控制
多个地方用了CompletableFuture,使用这个和lambda表达式,由于可以把方法作为参数进行传递。编程思路要有一定的变化。这就是把大的处理过程,拆分成多个子过程,过程之间有串、并等组合关系,最后可以异步获取最终结果。下面这个是主线程等待多个子线程完成。
```java
```
上面这段是用传统的CountDownLatch,因为额外加了一个前置处理过程。
- stream
这是一个把配置的string转成map的语句,也是因为习惯使用springboot配置的方法,后来这个没用了,改成配置类了。不过这样的语句写的很简洁吧,也许有人认为不好理解,其实用几次,你就会喜欢上。
```
Map zone2provinceMap = Arrays.stream(zone2province.split(";")).filter(kv -> kv.contains("=")).map(kv -> kv.split("=")).collect(Collectors.toMap(x -> x[0], x -> x[1]));
```
其它简单的lambda表达式,optional就不提了,简单,好用,甜,上面代码里都有。
- 额外提一下spring的yml配置文件
以前一直用properties文件,因为这个工具有很多字典项,以前用DB存的多,现在用配置文件转map存了,而且有中文。所以开始用yml配置文件了。省的properties的中文还是unicode编码,不方便。
写的过程中,进一步体验并规范了项目的层次感。所有的业务字典map,全都在bizmap下。用一个统一的配置类解析持有,类很简单,增加一个map也很简单,如下,其它类用的地方就@Autowried一下。
```
```
## 7、总结
最近看到银行的大项目中的很多基础平台代码,总的来说,都逃不出上面总结的内容。当看过复杂的,著名的工程代码,掌握了上面的规律,再看这些都相对简单多了。这样可以很快掌握作者的思路,方便用好现有框架,也方便查问题,方便做功能完善。目前问题是大项目功能内容太多了,广度问题,比较花时间。
最近写一个Java处理工具,是一个springboot的非web项目,正好借这个机会总结一下自己的经验,当开发一个Java应用时,应该全局考虑哪些方面,包括如何划分功能包,如果建立对象关联,以及如果串起流程;另一方面在这个过程中,作为喜新厌旧的程序员,全面应用Jdk8的新特征,比如其中的语法糖还是蛮甜的。
## 2、类划分功能包
web项目是很多人接触最多的项目,与工具项目有相似点,也有不同点。一般的单体的web项目通常的包,表面上按这样划分:
多层次业务模块->controller->service->dao这么几层。而从实现了一个请求的单向处理过程看,服务端实际上完整过程是这样:通讯协议处理->filter->dispatchservlet->controller->service->dao。由于框架做了很多事情,开发人员很可能只需要固定的套路,而不需要深入研究,基本上技术是简单的CURD操作。
我这个工具过程功能比较少:解析excel->输出txt->合并txt三个步骤,虽然不多,但尽可能让结构合理,这样想增加功能也方便,也可以扩展出的很复杂功能。
- 1. 为什么分层分包,而不是铁板一块?因为把不变的和可变的进行区分,有利于修改,有利于重用,有利于加塞...缺点嘛,要建立关联关系,把分散的东西有机结合起来,这个难度略大,不理解的人不方便定位功能点,更不好修改,易打乱结构。
- 2. 为什么做软件强调抽象思维,这个过程就是有效的划分块。实现高聚合低耦合软件的也必要能力。
- 3. 通常写web的,需要的包只是处理框架的一部分,所以不用不完整的过程。自己写工具就是全过程了,所以写web不利于提高java开发水平,重点是业务理解了。
- 4. 看到过有代码,把web层的东西写到service中,比如httpServletRequest传了过去,是对分层缺少理解。危害是什么呢?如果换了通讯协议,比如http换grpc,service也要改,本来可以不改的。大多数情况下不会换,但可能service会组合,也是潜在问题。VO/DTO/BO这样的对象区别,这在于它们处于不同的层次。
- 5. 同一层次的东西放在一个包里,包里面一般分两层,包中的根目录放接口,抽象类与参数对象等。包里的其它目录一般是接口的多种实现类。web项目的controller层中,接口在框架里了,都是实现类,所以一个业务模块只一层。
- 6. 包里可以有包,类似组织架构,那调用层与被调用层是同级还是内部?service为何不放controller包里,dao为何也独立出来?一是因为service理论上可以被其它协议调用,不用controller这个http框架。dao可能因为调整多,放太深也不好。
- 7. 包划分示例:常见的dubbo,划分了很多层,最核心的远程rpc功能只有rpc通讯层,remote传输层两个。其它是辅助或者是另外的功能块,比如serialize,monitor是公共的,registry是另一功能了。理解源码就要先有全局思路,比如理解功能与多层次目录的划分,就知道作者怎么想的,各个功能在点哪点。remote层里有多种传输,比如http/p2p/telnet,也有自己的。自己的长连接还要心跳机制,也有重连机制,所以里面还有head处理等更深的层次。一个大项目,排放的条理性,科学性很重要,写起来思路连贯,找起来一歩定位。
- 8. 再比如rocketmq,模块有业务块与name块,最核心的业务功能是接收消息并存下来,所以有broker包与store包。其它还有remote块,通讯肯定是公共的。
- 9. 包内组织示例:dubbo为例,它是微核心,几乎所有的东西都是接口化,甚至对象仓库也是,有两种仓库,一种是spring库,一种是自己写的。前面说了,包内分接口公共类与各个实现的子包,有些实现是自己的,有些是用第三方的。每种方式就一个目录,第三方功能所在的目录里一般都是对第三方接口类的包装类,包装过程也是对多种第三方实现的的抽象过程,因为第三方功能可能差别不少,要兼容适配。这里面可能用到代理、装饰,门面等设计模式。
## 3、类(包)之间关联
划包与包内组织好了,下面就是建立包之间调用关联了,主要是调用的对象之间,通过属性引用起来的。比如@autowired。还得不说spring容器的强大了。它可以按照你提供的配置蓝图与零件,给你组装生成出一个可以运转的机器。它的依赖注入,多个过程(比如前置,后置,生成,初始化,销毁等)中插入你的处理逻辑,比如factorybean产生代理对象,它的延时加载,条件加载,让你的这个配置蓝图可以非常的复杂。前面提到dubbo微核心,自己实现了一个小的容器,也有依赖注入,最有特色的是,用接口类型获取对象时,特殊注解的接口,它可以用代码动态生成接口的代理类对象给你,而这个代理对象根据请求参数不同,代理不同的实现类,具体代理哪个,又是从容器中取的。这就厉害了,一方面减少了spring容器的冗余功能,另一方面实现了自己特殊的需求,再着体现了作者的实力。
- 1. 一般就不要自己处理引用了,就用spring的,除非比较简单的。如果是引用多个实现,可以@Autowired Map,用的时候可以按名字取真正的实现类。我的项目中启动类就是引用了不同excel的解析类,根据特征名字选择调用。
- 2. 引用可以是单向的,也可以是双向的。用@Autowired不用管了,自己写一般就是构造函数或者Init()方法中引入被调用对象,双向就是之后再把自己(this)传给调用对象的设置方法。
- 3. 引用还可以是间接的,间接时就要有人穿针引线,通常就是listenner对象。双向引用就是方便反馈结果,用间接则增加了灵活性,反馈可以给自己,也可以给别人,也可以广播,更灵活。至于反馈给谁,调用方产生监听器时会指明的。比如领导调用员工,双向引用(留下手机号)就可以直接反馈结果给领导,也可以间接方式,把秘书手机给员工,并明确秘书收到结果反馈自己和另一领导。
- 4. 我的工具中,【解析类】处理时,使用了easyexcel包。先产生一个配置【数据收集类】的【监听器】,把一个【监听器】给easyexcel,监听到数据时,会通知【数据收集类】存起来。
- 5. 这里说的关联关系,都是spring默认的singleton类。至于【监听器】,虽然可能也只是一个,但一般不当成@Component,可以说是它的地位太低吧。而是用的时候new一个,复杂的用工厂临时造一个对象。【监听器】通知谁,不是自己@Autowired的,而是由安排它的类指派的。
- 6. 还有一种业务关联关系,类之间本身没有任何联系,而是由业务配置联系起来的,这个最典型的就是工作流了,特别是条件节点,根据特征数据不同,调用不同的处理类。还有就是责任链模式,一个个独立的处理节点类,是由另外的数组或者链表前面关联起来的,最常见的是filterChain。特别注意的是filterChain不是单例,每一个处理建一个新的filterChain,因为内部要记录当前起到哪一步了,而链条上的节点filter是单例。这个生活中的例子就是大家参加体检,每人一个体检单chain,而各个科室就是节点,每个人的体检单上打过勾的都可能不同。更复杂的后面的节点,由前面的节点根据情况产生。
- 7. 责任链的例子非常多,除了servelet的filter外,druid中的监控功能也是,所以相关的sql类都被代理,代理蹭就插入了一个短chain,其中一个节点功能就是输出监控信息。springaop中也在Invocation中建了一个链,因为一个方法上可以有多个advise,要组织成链条。我以前也用过,一个请求要经过多次处理,如果中间因故断了,还可以再次请求,找到后续处理的索引,继续后续的处理。
- 8. 有了spring,类的引用太容易了,但要避免网状引用。而应该用树状引用。最典型的就是rocketmq中,非常多的类都是通过核心协调的controller类调用的,这个不是web中的controller。部门间调用都通过领导。
## 4、类(包)关系的变更
类的关系建好了,也不是一成不变,用户需求会变,功能要增加,低耦合的目的就是应对这种变化,如果事先考虑这种变化,可以有一定的机制,把变成留给用户,并规范用户的代码。如果没有,变化也可以限制在一定的变动量下。而用户插入的功能可能多个,所以经常发现用责任链模式串起来。
- 1. filter机制就是插在http的请求中的过程,可有可无,用户自己来实现具体的filter。但是这限制在特定协议中的灵活,所以spring有自己的interceptor机制。我参与的项目中,平台框架自己弄了一个hander机制。
- 2. 如果没有考虑到这个变化,spring提供了aop机制。
- 3. 如果原有的关系中插一层关系,而且没有spring,就要用代理的思路,比如druid把所有的数据库操作都包装了一下。中间插入自己的日志处理chain节点,想增加也是很方便的。
- 4. 比如我现在做的多个excel解析,可以进一步搞的很复杂,前面可以增加多种协议处理远程excel,也可以解析其它格式文件,解析好的数据可以放内存,也可以放数据文件暂存,输出片断可以是txt,也可以是其它类型。想要日志的话,可以找个地方插入责任链。想切换easyexcel到poi,也可以分别包装一下,上层调用包装接口,具体实现可以在配置中写,也可以从参数中拿。如果要增加加解密操作,可以增加处理层,也可以包装已有的加密方式。如果要增加数据查询,可以在暂存层上増加查询功能,而查询可以是http的请求方式,也可以弄成部分支持sql格式的查询。总之,高聚合的东西不太变,低耦合的设计就是考虑变化的。
- 5. 设想,如果没有很好的抽象思维,合理科学的分层 ,改动量与改动难度就越来越大。当然大多数工作都是简单的,专注于业务的,所以乱一点问题不大。这也就是可能做过很多业务,但真正的技术水平还不高的原因。
## 5、类的自我管理
大型项目,越看越感觉象生活中的场景,比如象构建一个医院,服务一个个病人。过程中有保安进门,有挂号产生一次服务链,每个科进行本科室的处理,人多了排队,再多限流。当然也有行政部门与科内部进行自我的管理,比如事先准备好设备,内部排班,整理统计数据等。发明面向对象编程的人太伟大了,贴合生活,易于理解并设计非常复杂的项目。
- 1. spring中产生一个新的对象,有init(),有distroy()进行统一处理,可以做准备动作,也可以优雅结束,说明对象有完整的生命周期。前面提到的业务对象,一般是用完自己销毁了,不考虑 这个。
- 2. 一般的自我管理是通过内部的管理线程进行的,比如线程池内处理线程的增加与减少。连接池内连接数的增加与减少。微服务中可用服务的从zookeeper上更新等等。远程连接的重连机制,心跳机制。
- 3. 这个小工具没有这方面的管理需求,只是init()时,把配置文件中的数据,设置给static变量。因为之前很多地方按static中取的,不想改。
## 6、全面使用jdk8新特征
8已经老了,但很多项目更老,而且老程序员习惯不容易变。我却喜欢尽可能用点新的,了解新特征产生的原因,才能更好的用好。
- 多线程控制
多个地方用了CompletableFuture,使用这个和lambda表达式,由于可以把方法作为参数进行传递。编程思路要有一定的变化。这就是把大的处理过程,拆分成多个子过程,过程之间有串、并等组合关系,最后可以异步获取最终结果。下面这个是主线程等待多个子线程完成。
```java
List<CompletableFuture<Void>> outputCfList = new ArrayList<CompletableFuture<Void>>(); txtFileMap.forEach((k,v)->{ outputCfLst.add(CompletableFuture.runAsync(()->v.createTxt(filePath)));//甜甜的语法糖 }); CompletableFuture.allOf(outputCfList.toArray(new CompletableFuture[txtFileMap.size()])).join(); ``` ```java new Thread(() -> { String[] fileList = argsFile.list((dir, name) -> name.toUpperCase().endsWith("XLSX"));//甜甜的语法糖 LOGGER.info("the directory's total xlsx files number is [{}]", fileList.length); String rawExcelFile = ""; for (String fileName : fileList) { 。。。。。。。。。。。 } if (StringUtils.isNotEmpty(rawExcelFile)) { LOGGER.info("find [Ma wenjie] raw excel file and begin generate new excel file..."); convertMap.get("rawConverter").read(argsFile.getPath() + File.separator + rawExcelFile); } dealRawExcelLock.countDown(); }).start(); // 主线程等待前一步生成excel dealRawExcelLock.await();
```
上面这段是用传统的CountDownLatch,因为额外加了一个前置处理过程。
- stream
这是一个把配置的string转成map的语句,也是因为习惯使用springboot配置的方法,后来这个没用了,改成配置类了。不过这样的语句写的很简洁吧,也许有人认为不好理解,其实用几次,你就会喜欢上。
```
Map zone2provinceMap = Arrays.stream(zone2province.split(";")).filter(kv -> kv.contains("=")).map(kv -> kv.split("=")).collect(Collectors.toMap(x -> x[0], x -> x[1]));
```
其它简单的lambda表达式,optional就不提了,简单,好用,甜,上面代码里都有。
- 额外提一下spring的yml配置文件
以前一直用properties文件,因为这个工具有很多字典项,以前用DB存的多,现在用配置文件转map存了,而且有中文。所以开始用yml配置文件了。省的properties的中文还是unicode编码,不方便。
写的过程中,进一步体验并规范了项目的层次感。所有的业务字典map,全都在bizmap下。用一个统一的配置类解析持有,类很简单,增加一个map也很简单,如下,其它类用的地方就@Autowried一下。
```
@Configuration @Data @ConfigurationProperties(prefix="bizmap") public class ConfigBizMap { public Map<String,String> zone2province; public Map<String,String> ...; public Map<String,String> ...; public Map<String,String> ...; public Map<String,String> ...; public Map<String,String> ...; }
```
## 7、总结
最近看到银行的大项目中的很多基础平台代码,总的来说,都逃不出上面总结的内容。当看过复杂的,著名的工程代码,掌握了上面的规律,再看这些都相对简单多了。这样可以很快掌握作者的思路,方便用好现有框架,也方便查问题,方便做功能完善。目前问题是大项目功能内容太多了,广度问题,比较花时间。
上一篇: Java实战花店商城系统的实现流程