欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何设计一个结构合理的java项目

程序员文章站 2022-03-11 12:37:10
...
## 1、前言

最近写一个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