在我的工作中,我从零开始搭建了不少软件项目,其中包含了基础代码框架和持续集成基础设施等,这些内容在敏捷开发中通常被称为“第0个迭代”要做的事情。但是,当项目运行了一段时间之后再来反观,我总会发现一些不足的地方,要么测试分类没有分好,要么基本的编码架子没有考虑周全。
基于以上,我希望整理出一套公共性的项目模板出来,旨在尽量多地包含日常开发之所需,减少开发者的重复性工作以及提供一些最佳实践。对于后端开发而言,我选择了当前被行业大量使用的Spring Boot,基于此整理出了一套公共的、基础性的实践方式,在结合了自己的经验以及其他项目的优秀实践之后,总结出本文以飨开发者。
所使用的技术栈主要包括:Spring Boot、Gradle、MySQL、Junit 5、Rest Assured、Docker等。
技术选型:列出项目的技术栈,包括语言、框架和中间件等;
- 测试策略:自动化测试如何分类,哪些必须写测试,哪些没有必要写测试;
- 外部依赖:项目运行时所依赖的外部集成方,比如订单系统会依赖于会员系统;
- FAQ:开发过程中常见问题的解答。
一键式本地构建
本地运行:run.sh,本地启动项目,自动启动本地数据库,监听调试端口5005
- 拉取代码;
- 运行run.sh,进行本地调试或必要的手动测试(本步骤不是必需);
- 事实上,这些命令脚本的内容非常简单,比如run.sh文件内容为:
然而,这种显式化的命令却可以减少新人的恐惧感,因为他们只需要知道运行这3个命令就可以搞开发了。另外,一个小小的细节:本地构建的local-build.sh命令本来可以重命名为更简单的build.sh,但是当我们在命令行中使用Tab键自动补全的时候,会发现自动补全到了build目录,而不是build.sh命令,并不方便,因此命名为了local-build.sh。
目录结构
└── order-backend
├── gradle // 文件夹,用于放置所有Gradle配置
├── src // 文件夹,Java源代码
├── idea.sh //生成IntelliJ工程
├── local-build.sh // 提交之前的本地构建
└── run.sh // 本地运行
├── gradle
│ ├── checkstyle
│ │ ├── checkstyle.gradle
│ │ └── checkstyle.xml
比如,在订单示例项目中,有两个重要的领域对象Order和Product(在DDD中称为聚合根),所有的业务都围绕它们展开,因此分别创建order包和product包,再分别在包下创建与之相关的各个子包。此时的order包如下:
可以看到,在order包下我们直接放置了OrderController和OrderRepository等类,而没有必要再为这些类划分单独的子包。而对于领域模型Order来讲,由于包含了多个对象,因此基于内聚性原则将它们归到model包中。但是这并不是一个必须,如果业务足够简单,我们甚至可以将所有类直接放到业务包下,product包便是如此:
在编码实践中,我们总是基于一个业务用例来实现代码,在技术分包场景下,我们需要在分散的各包中来回切换,增加了代码导航的成本;另外,代码提交的变更内容也是散落的,在查看代码提交历史时,无法直观的看出该次提交是关于什么业务功能的。
└── common
├── configuration
├── exception
├── loggin
└── utils
自动化测试分类 再者,程序中有些重要的组件性代码,比如访问数据库的Repository或者分布式锁,使用单元测试实际上“测不到点上”,而使用API测试又显得在分类逻辑上不合理,为此我们可以专门创建一种测试类型谓之组件测试。 组件测试:不适合写单元测试但是又必须测试的类,比如Repository类,在有些项目中,这种类型测试也被称为集成测试;
sourceSets {
componentTest {
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
apiTest {
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
}
单元测试:src/test/java
- 需要注意的是,这里的API测试更多强调的是对业务功能的测试,有些项目中可能还会存在契约测试和安全测试等,虽然从技术上讲都是对API的访问,但是这些测试都是单独的关注点,因此建议分开对待。
https://www.cnblogs.com/CloudTeng/p/3417762.html
在日志处理中,除了完成基本配置外,还有2个需要考虑的点:
异常处理
异常信息中应该包含足够多的上下文信息,最好是结构化的数据以便于客户端解析
- 层级式异常的好处是能够显式化异常含义,但是如果层级设计不好可能导致整个程序中充斥着大量的异常类;单一式的好处是简单,而其缺点在于表意性不够。
public class OrderNotFoundException extends AppException { public OrderNotFoundException(OrderId orderId) { super(ErrorCode.ORDER_NOT_FOUND, ImmutableMap.of("orderId", orderId.toString())); } }
public final class ErrorDetail { private final ErrorCode code; private final int status; private final String message; private final String path; private final Instant timestamp; private final Map<String, Object> data = newHashMap(); }
{ requestId: "d008ef46bb4f4cf19c9081ad50df33bd", error: { code: "ORDER_NOT_FOUND", status: 404, message: "没有找到订单", path: "/order", timestamp: 1555031270087, data: { orderId: "123456789" } } }
启用Spring任务配置如下:
然后配置Shedlock:
实现后台任务处理:
使用时在代码中直接调用:
本文的示例项目使用了基于JDBC的分布式锁,事实上任何提供原子操作的机制都可用于分布式锁,Shedlock还提供基于Redis、ZooKeeper和Hazelcast等的分布式锁实现机制。
除了Checkstyle统一代码格式之外,项目中有些通用的公共的编码实践方式也需要在整个开发团队中进行统一,包括但不限于以下方面:
- 统一对请求处理的流程框架,比如采用传统的3层架构或者DDD战术模式
- 明确测试分类以及统一的测试基础类(请参考“自动化测试分类”小节)
静态代码检查主要包含以下Gradle插件,具体配置请参考本文示例代码:
- Dependency check:OWASP提供的Java类库安全性检查
- 健康检查
有些负载均衡软件会通过一个健康检查URL判断节点的可达性
./run.sh
{
requestId: "698c8d29add54e24a3d435e2c749ea00",
buildNumber: "unknown",
buildTime: "unknown",
deployTime: "2019-04-11T13:05:46.901+08:00[Asia/Shanghai]",
gitRevision: "unknown",
gitBranch: "unknown",
environment: "[local]"
}
配置Swagger如下:
启动本地项目,访问http://localhost:8080/swagger-ui.html:
在传统的开发模式中,数据库由专门的运维团队或者DBA来维护,要对数据库进行修改需要向DBA申请,告之迁移内容,最后由DBA负责数据库变更实施。在持续交付和DevOps运动中,这些工作逐步提前到开发过程,当然并不是说不需要DBA了,而是这些工作可以由开发者和运维人员一同完成。
resources/
├── db
│ └── migration
│ ├── V1__init.sql
│ └── V2__create_product_table.sql
local:用于开发者本地开发
- qa:用于测试人员
@Configuration
public class CorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// by default uses a Bean by the name of corsConfigurationSource
.cors().and()
...
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
这里列出一些比较常见的第三方库,开发者们可以根据项目所需引入:
- Mockito:主要用于单元测试的mock
- Jackson 2:Json数据的序列化和反序列化
- Feign:声明式Rest客户端
- zxing:生成二维码
- 总结
End