Java 开发项目模板 16 步快速搭建,拒绝重复性工作!
前言
在我的工作中,我从零开始搭建了不少软件项目,其中包含了基础代码框架和持续集成基础设施等,这些内容在敏捷开发中通常被称为“第 0 个迭代”要做的事情。但是,当项目运行了一段时间之后再来反观,我总会发现一些不足的地方,要么测试分类没有分好,要么基本的编码架子没有考虑周全。
另外,我在工作中也会接触到很多既有项目,公司内部和外部的都有,多数项目的编码实践我都是不满意的。比如,我曾经新加入一个项目的时候,前前后后请教了 3 位同事才把该项目在本地运行起来;又比如在另一项目中,我发现前端请求对应的 Java 类命名规范不统一,有被后缀为 Request 的,也有被后缀为 Command 的。
再者,工作了这么多年之后,我越来越发现基础知识以及系统性学习的重要性。诚然,技术框架的发展使得我们可以快速地实现业务功能,但是当软件出了问题之后有时却需要将各方面的知识融会贯通并在大脑里综合反应才能找到解决思路。
基于以上,我希望整理出一套公共性的项目模板出来,旨在尽量多地包含日常开发之所需,减少开发者的重复性工作以及提供一些最佳实践。对于后端开发而言,我选择了当前被行业大量使用的 Spring Boot,基于此整理出了一套公共的、基础性的实践方式,在结合了自己的经验以及其他项目的优秀实践之后,总结出本文以飨开发者。
本文以一个简单的电商订单系统为例,源代码请访问:
所使用的技术栈主要包括:Spring Boot、Gradle、MySQL、Junit 5、Rest Assured、Docker 等。
如果是缺乏练手项目的朋友可以领取一下我整理的比较经典的22个项目,源码和学习笔记都有。
好了,话不多说,坐稳扶好,发车喽!
第一步:从写好 README 开始
一份好的 README 可以给人以项目全景概览,可以使新人快速上手项目,可以降低沟通成本。同时,README 应该简明扼要,条理清晰,建议包含以下方面:
项目简介:用一两句话简单描述该项目所实现的业务功能;
技术选型:列出项目的技术栈,包括语言、框架和中间件等;
本地构建:列出本地开发过程中所用到的工具命令;
领域模型:核心的领域概念,比如对于示例电商系统来说有 Order、Product 等;
测试策略:自动化测试如何分类,哪些必须写测试,哪些没有必要写测试;
技术架构:技术架构图;
部署架构:部署架构图;
外部依赖:项目运行时所依赖的外部集成方,比如订单系统会依赖于会员系统;
环境信息:各个环境的访问方式,数据库连接等;
编码实践:统一的编码实践,比如异常处理原则、分页封装等;
FAQ:开发过程中常见问题的解答。
需要注意的是,README 中的信息可能随着项目的演进而改变(比如引入了新的技术栈或者加入了新的领域模型),因此也是需要持续更新的。虽然我们知道,软件文档的一个痛点便是无法与项目实际进展保持同步,但是就 README 这点信息来讲,还是建议开发者们不要吝啬那一点点敲键盘的时间。
此外,除了保持 README 的持续更新,一些重要的架构决定可以通过示例代码的形式记录在代码库中,新开发者可以通过直接阅读这些示例代码快速了解项目的通用实践方式以及架构选择,请参考 ThoughtWorks 的技术雷达。
一键式本地构建
为了避免诸如前文中所提到的“请教了 3 位同事才本地构建成功”的尴尬,为了减少“懒惰”的程序员们的手动操作,也为了为所有开发者提供一种一致的开发体验,我们希望用一个命令就可以完成所有的事情。这里,对于不同的场景我总结出了以下命令:
生成 IDE 工程:
idea.sh
,生成 IntelliJ 工程文件并自动打开 IntelliJ本地运行:
run.sh
,本地启动项目,自动启动本地数据库,监听调试端口 5005本地构建:
local-build.sh
,只有本地构建成功才能提交代码
以上 3 个命令基本上可以完成日常开发之所需,此时,对于新人的开发流程大致为:
拉取代码;
运行
idea.sh
,自动打开 IntelliJ;编写代码,包含业务代码和自动化测试;
运行
run.sh
,进行本地调试或必要的手动测试(本步骤不是必需);运行
local-build.sh
,完成本地构建;再次拉取代码,保证
local-build.sh
成功,提交代码。
事实上,这些命令脚本的内容非常简单,比如run.sh
文件内容为:
然而,这种显式化的命令却可以减少新人的恐惧感,因为他们只需要知道运行这 3 个命令就可以搞开发了。另外,一个小小的细节:本地构建的local-build.sh
命令本来可以重命名为更简单的build.sh
,但是当我们在命令行中使用 Tab 键自动补全的时候,会发现自动补全到了build
目录,而不是build.sh
命令,并不方便,因此命名为了local-build.sh
。细节虽小,但是却体现了一个宗旨,即我们希望给开发者一种极简的开发体验,我把这些看似微不足道的东西称作是对程序员的“人文关怀”。
目录结构
Maven 所提倡的目录结构当前已经成为事实上的行业标准,Gradle 在默认情况下也采用了 Maven 的目录结构,这对于多数项目来说已经足够了。此外,除了 Java 代码,项目中还存在其他类型的文件,比如 Gradle 插件的配置、工具脚本和部署配置等。无论如何,项目目录结构的原则是简单而有条理,不要随意地增加多余的文件夹,并且也需要及时重构。
在示例项目中,顶层只有 2 个文件夹,一个是用于放置 Java 源代码和项目配置的src
文件夹,另一个是用于放置所有 Gradle 配置的gradle
文件夹,此外,为了方便开发人员使用,将上文提到的 3 个常用脚本直接放到根目录下:
对于gradle
而言,我们刻意地将 Gradle 插件脚本与插件配置放到了一起,比如 Checkstyle:
事实上,在默认情况下 Checkstyle 插件会从项目根目录下的config
目录查找checkstyle.xml
配置文件,但是这一方面增加了多余的文件夹,另一方面与该插件相关的设施分散在了不同的地方,违背了广义上的内聚原则。
基于业务分包
早年的 Java 分包方式通常是基于技术的,比如与 domain 包平级的有 controller 包、service 包和 infrastructure 包等。这种方式当前并不被行业所推崇,而是应该首先基于业务分包。比如,在订单示例项目中,有两个重要的领域对象Order
和Product
(在 DDD 中称为聚合根),所有的业务都围绕它们展开,因此分别创建 order 包和 product 包,再分别在包下创建与之相关的各个子包。此时的 order 包如下:
可以看到,在 order 包下我们直接放置了OrderController
和OrderRepository
等类,而没有必要再为这些类划分单独的子包。而对于领域模型 Order 来讲,由于包含了多个对象,因此基于内聚性原则将它们归到 model 包中。但是这并不是一个必须,如果业务足够简单,我们甚至可以将所有类直接放到业务包下,product 包便是如此:
在编码实践中,我们总是基于一个业务用例来实现代码,在技术分包场景下,我们需要在分散的各包中来回切换,增加了代码导航的成本;另外,代码提交的变更内容也是散落的,在查看代码提交历史时,无法直观的看出该次提交是关于什么业务功能的。在业务分包下,我们只需要在单个统一的包下修改代码,减少了代码导航成本;另外一个好处是,如果哪天我们需要将某个业务迁移到另外的项目(比如识别出了独立的微服务),那么直接整体移动业务包即可。
当然,基于业务分包并不意味着所有的代码都必须囿于业务包下,这里的逻辑是:优先进行业务分包,然后对于一些不隶属于任何业务的代码可以单独分包,比如一些 util 类、公共配置等。比如我们依然可以创建一个 common 包,下面放置了 Spring 公共配置、异常处理框架和日志等子包:
自动化测试分类
在当前的微服务和前后端分离的开发模式下,后端项目仅提供纯粹的业务 API,而不包含 UI 逻辑,因此后端项目不会再包含诸如 WebDriver 的重量级端到端测试。同时,后端项目作为向外提供业务功能的独立运行单元,在 API 级别也应该有相应的测试。
此外,程序中有些框架性代码,要么是诸如 Controller 之类的技术性框架代码,要么是基于某种架构风格的代码(比如 DDD 实践中的 ApplicationService),这些代码一方面并不包含业务逻辑,一方面是很薄的一个抽象层(即实现相对简单),用单元测试来覆盖显得没有必要,因此笔者的观点是可以不为此编写单独的单元测试。再者,程序中有些重要的组件性代码,比如访问数据库的 Repository 或者分布式锁,使用单元测试实际上“测不到点上”,而使用 API 测试又显得在分类逻辑上不合理,为此我们可以专门创建一种测试类型谓之组件测试。
基于以上,我们可以对自动化测试做个分类:
单元测试:核心的领域模型,包括领域对象(比如 Order 类),Factory 类,领域服务类等;
组件测试:不适合写单元测试但是又必须测试的类,比如 Repository 类,在有些项目中,这种类型测试也被称为集成测试;
API 测试:模拟客户端测试各个 API 接口,需要启动程序。
Gradle 在默认情况下只提供src/test/java
目录用于测试,对于以上 3 种类型的测试,我们需要将它们分开以便于管理(也是职责分离的体现)。为此,可以通过 Gradle 提供的 SourceSets 对测试代码进行分类:
到此,3 种类型的测试可以分别编写在以下目录:
单元测试:
src/test/java
组件测试:
src/componentTest/java
API 测试:
src/apiTest/java
需要注意的是,这里的 API 测试更多强调的是对业务功能的测试,有些项目中可能还会存在契约测试和安全测试等,虽然从技术上讲都是对 API 的访问,但是这些测试都是单独的关注点,因此建议分开对待。
值得一提的是,由于组件测试和 API 测试需要启动程序,也即需要准备好本地数据库,我们采用了 Gradle 的docker-compose
插件(或者 jib 插件),该插件会在运行测试之前自动运行 Docker 容器(比如 MySQL):
更多的测试分类配置细节,比如 JaCoCo 测试覆盖率配置等,请参考本文的示例项目代码。对 Gradle 不熟悉的读者可以参考笔者的 Gradle 学习系列文章。
日志处理
在日志处理中,除了完成基本配置外,还有 2 个需要考虑的点:
在日志中加入请求标识,便于链路追踪。在处理一个请求的过程中有时会输出多条日志,如果每条日志都共享统一的请求 ID,那么在日志追踪时会更加方便。此时,可以使用 Logback 原生提供的 MDC(Mapped Diagnostic Context)功能,创建一个 RequestIdMdcFilter:
集中式日志管理,在多节点部署的场景下,各个节点的日志是分散的,为此可以引入诸如 ELK 之类的工具将日志统一输出到 ElasticSearch 中。本文的示例项目使用了 RedisAppender 将日志输出到 Logstash:
当然,统一日志的方案还有很多,比如 Splunk 和 Graylog 等。
异常处理
在设计异常处理的框架时,需要考虑以下几点:
向客户端提供格式统一的异常返回
异常信息中应该包含足够多的上下文信息,最好是结构化的数据以便于客户端解析
不同类型的异常应该包含唯一标识,以便客户端精确识别
异常处理通常有两种形式,一种是层级式的,即每种具体的异常都对应了一个异常类,这些类最终继承自某个父异常;另一种是单一式的,即整个程序中只有一个异常类,再以一个字段来区分不同的异常场景。层级式异常的好处是能够显式化异常含义,但是如果层级设计不好可能导致整个程序中充斥着大量的异常类;单一式的好处是简单,而其缺点在于表意性不够。
本文的示例项目使用了层级式异常,所有异常都继承自一个 AppException:
这里,ErrorCode
枚举中包含了异常的唯一标识、HTTP 状态码以及错误信息;而data
字段表示各个异常的上下文信息。
在示例系统中,在没有找到订单时抛出异常:
在返回异常给客户端时,通过一个 ErrorDetail 类来统一异常格式:
最终返回客户端的数据为:
可以看到,ORDER_NOT_FOUND
与data
中的数据结构是一一对应的,也即对于客户端来讲,如果发现了ORDER_NOT_FOUND
,那么便可确定data
中一定存在orderId
字段,进而完成精确的结构化解析。
后台任务与分布式锁
除了即时完成客户端的请求外,系统中通常会有一些定时性的例行任务,比如定期地向用户发送邮件或者运行数据报表等;另外,有时从设计上我们会对请求进行异步化处理。此时,我们需要搭建后台任务相关基础设施。Spring 原生提供了任务处理(TaskExecutor)和任务计划(TaskSchedulor)机制;而在分布式场景下,还需要引入分布式锁来解决并发冲突,为此我们引入一个轻量级的分布式锁框架 ShedLock。
启用 Spring 任务配置如下:
然后配置 Shedlock:
实现后台任务处理:
为了支持代码直接调用分布式锁,基于 Shedlock 的 LockProvider 创建 DistributedLockExecutor:
使用时在代码中直接调用:
本文的示例项目使用了基于 JDBC 的分布式锁,事实上任何提供原子操作的机制都可用于分布式锁,Shedlock 还提供基于 Redis、ZooKeeper 和 Hazelcast 等的分布式锁实现机制。
统一代码风格
除了 Checkstyle 统一代码格式之外,项目中有些通用的公共的编码实践方式也需要在整个开发团队中进行统一,包括但不限于以下方面:
客户端的请求数据类统一使用相同后缀,比如 Command
返回给客户端的数据统一使用相同后缀,比如 Represetation
统一对请求处理的流程框架,比如采用传统的 3 层架构或者 DDD 战术模式
提供一致的异常返回(请参考“异常处理”小节)
提供统一的分页结构类
明确测试分类以及统一的测试基础类(请参考“自动化测试分类”小节)
静态代码检查
静态代码检查主要包含以下 Gradle 插件,具体配置请参考本文示例代码:
Checkstyle:用于检查代码格式,规范编码风格
Spotbugs:Findbugs 的继承者
Dependency check:OWASP 提供的 Java 类库安全性检查
Sonar:用于代码持续改进的跟踪
健康检查
健康检查主要用于以下场景:
我们希望初步检查程序是否运行正常
有些负载均衡软件会通过一个健康检查 URL 判断节点的可达性
此时,可以实现一个简单的 API 接口,该接口不受权限管控,可以公开访问。如果该接口返回 HTTP 的 200 状态码,便可初步认为程序运行正常。此外,我们还可以在该 API 中加入一些额外的信息,比如提交版本号、构建时间、部署时间等。
启动本文的示例项目:
然后访问健康检查 API:http://localhost:8080/about,结果如下:
以上接口在示例项目中用了一个简单的 Controller 实现,事实上 Spring Boot 的 Acuator 框架也能够提供相似的功能。
API 文档
软件文档的难点不在于写,而在于维护。多少次,当我对照着项目文档一步一步往下走时,总得不到正确的结果,问了同事之后得到回复“哦,那个已经过时了”。本文示例项目所采用的 Swagger 在一定程度上降低了 API 维护的成本,因为 Swagger 能自动识别代码中的方法参数、返回对象和 URL 等信息,然后自动地实时地创建出 API 文档。
配置 Swagger 如下:
启动本地项目,访问 http://localhost:8080/swagger-ui.html:
数据库迁移
在传统的开发模式中,数据库由专门的运维团队或者 DBA 来维护,要对数据库进行修改需要向 DBA 申请,告之迁移内容,最后由 DBA 负责数据库变更实施。在持续交付和 DevOps 运动中,这些工作逐步提前到开发过程,当然并不是说不需要 DBA 了,而是这些工作可以由开发者和运维人员一同完成。另外,在微服务场景下,数据库被包含在单个服务的边界之内,因此基于内聚性原则(咦,这好像是本文第三次提到内聚原则了,可见其在软件开发中的重要性),数据库的变更最好也与项目代码一道维护在代码库中。
本文的示例项目采用了 Flyway 作为数据库迁移工具,加入了 Flyway 依赖后,在src/main/sources/db/migration
目录下创建迁移脚本文件即可:
迁移脚本的命名需要遵循一定的规则以保证脚本执行顺序,另外迁移文件生效之后不要任意修改,因为 Flyway 会检查文件的 checksum,如果 checksum 不一致将导致迁移失败。
多环境构建
在软件的开发流程中,我们需要将软件部署到多个环境,经过多轮验证后才能最终上线。在不同的阶段中,软件的运行态可能是不一样的,比如本地开发时可能将所依赖的第三方系统 stub 掉;持续集成构建时可能使用的是测试用的内存数据库等等。为此,本文的示例项目推荐采用以下环境:
local:用于开发者本地开发
ci:用于持续集成
dev:用于前端开发联调
qa:用于测试人员
uat:类生产环境,用于功能验收(有时也称为 staging 环境)
prod:正式的生产环境
CORS
在前后端分离的系统中,前端单独部署,有时连域名都和后端不同,此时需要进行跨域处理。传统的做法可以通过 JSONP,但这是一种比较“trick”的做法,当前更通用的实践是采用 CORS 机制,在 Spring Boot 项目中,启用 CORS 配置如下:
对于使用 Spring Security 的项目,需要保证 CORS 工作于 Spring Security 的过滤器之前,为此 Spring Security 专门提供了相应配置:
常用第三方类库
这里列出一些比较常见的第三方库,开发者们可以根据项目所需引入:
Guava:来自 Google 的常用类库
Apache Commons:来自 Apache 的常用类库
Mockito:主要用于单元测试的 mock
DBUnit:测试中管理数据库测试数据
Rest Assured:用于 Rest API 测试
Jackson 2:Json 数据的序列化和反序列化
jjwt:Jwt token 认证
Lombok:自动生成常见 Java 代码,比如 equals()方法,getter 和 setter 等;
Feign:声明式 Rest 客户端
Tika:用于准确检测文件类型
itext:生成 Pdf 文件等
zxing:生成二维码
Xstream:比 Jaxb 更轻量级的 XML 处理库
总结
本文通过一个示例项目谈及到了项目之初开发者搭建后端工程的诸多方面,其中的绝大多数实践均在笔者的项目中真实落地。读完本文之后你可能会发现,文中的很多内容都是很基础很简单的。没错,的确没有什么难的东西,但是要系统性地搭建好后端项目的基础框架却不见得是每个开发团队都已经做到的事情,而这恰恰是本文的目的。最后,需要提醒的是,本文提到的实践方式只是一个参考,一方面依然存在考虑不周的地方,另一方面示例项目中用到的技术工具还存在其他替代方案,请根据自己项目的实际情况进行取舍。
快领取我整理的比较经典的22个项目练起来吧,源码和笔记都整理好了,这还不敲?
原文地址:https://www.cnblogs.com/davenkin/p/spring-boot-template.html
版权声明: 本文为 InfoQ 作者【北游学Java】的原创文章。
原文链接:【http://xie.infoq.cn/article/ff43bcd18894f84caed6d85ae】。文章转载请联系作者。
评论