阅读本文大概需要 16 分钟。
前言
在我的工作中,我从零开始搭建了不少软件项目,其中包含了基础代码框架和持续集成基础设施等,这些内容在敏捷开发中通常被称为“第0个迭代”要做的事情。但是,当项目运行了一段时间之后再来反观,我总会发现一些不足的地方,要么测试分类没有分好,要么基本的编码架子没有考虑周全。
另外,我在工作中也会接触到很多既有项目,公司内部和外部的都有,多数项目的编码实践我都是不满意的。比如,我曾经新加入一个项目的时候,前前后后请教了3位同事才把该项目在本地运行起来;又比如在另一项目中,我发现前端请求对应的Java类命名规范不统一,有被后缀为Request的,也有被后缀为Command的。
再者,工作了这么多年之后,我越来越发现基础知识以及系统性学习的重要性。诚然,技术框架的发展使得我们可以快速地实现业务功能,但是当软件出了问题之后有时却需要将各方面的知识融会贯通并在大脑里综合反应才能找到解决思路。
基于以上,我希望整理出一套公共性的项目模板出来,旨在尽量多地包含日常开发之所需,减少开发者的重复性工作以及提供一些佳实践。对于后端开发而言,我选择了当前被行业大量使用的Spring Boot,基于此整理出了一套公共的、基础性的实践方式,在结合了自己的经验以及其他项目的实践之后,总结出本文以飨开发者。
本文以一个简单的电商订单系统为例,源代码请访问:
git clone https://github.com/e-commerce-sample/order-backend
git checkout a443dace
所使用的技术栈主要包括:Spring Boot、Gradle、MySQL、Junit 5、Rest Assured、Docker等。
步:从写好README开始
一份好的README可以给人以项目全景概览,可以使新人快速上手项目,可以降低沟通成本。同时,README应该简明扼要,条理清晰,建议包含以下方面:
项目简介:用一两句话简单描述该项目所实现的业务功能; 技术选型:列出项目的技术栈,包括语言、框架和中间件等; 本地构建:列出本地开发过程中所用到的工具命令; 领域模型:核心的领域概念,比如对于示例电商系统来说有Order、Product等; 测试策略:自动化测试如何分类,哪些必须写测试,哪些没有必要写测试; 技术架构:技术架构图; 部署架构:部署架构图; 外部依赖:项目运行时所依赖的外部集成方,比如订单系统会依赖于会员系统; 环境信息:各个环境的访问方式,数据库连接等; 编码实践:统一的编码实践,比如异常处理原则、分页封装等; FAQ:开发过程中常见问题的解答。
https://www.thoughtworks.com/radar/techniques/lightweight-architecture-decision-records
一键式本地构建
生成IDE工程:idea.sh,生成IntelliJ工程文件并自动打开IntelliJ 本地运行:run.sh,本地启动项目,自动启动本地数据库,监听调试端口5005 本地构建:local-build.sh,只有本地构建成功才能提交代码
拉取代码; 运行idea.sh,自动打开IntelliJ; 编写代码,包含业务代码和自动化测试; 运行run.sh,进行本地调试或必要的手动测试(本步骤不是必需); 运行local-build.sh,完成本地构建; 再次拉取代码,保证local-build.sh成功,提交代码。
#!/usr/bin/env bash
./gradlew clean bootRun
目录结构
└── order-backend
├── gradle // 文件夹,用于放置所有Gradle配置
├── src // 文件夹,Java源代码
├── idea.sh //生成IntelliJ工程
├── local-build.sh // 提交之前的本地构建
└── run.sh // 本地运行
├── gradle
│ ├── checkstyle
│ │ ├── checkstyle.gradle
│ │ └── checkstyle.xml
基于业务分包
├── order
│ ├── OrderApplicationService.java
│ ├── OrderController.java
│ ├── OrderNotFoundException.java
│ ├── OrderRepository.java
│ ├── OrderService.java
│ └── model
│ ├── Order.java
│ ├── OrderFactory.java
│ ├── OrderId.java
│ ├── OrderItem.java
│ └── OrderStatus.java
└── product
├── Product.java
├── ProductApplicationService.java
├── ProductController.java
├── ProductId.java
└── ProductRepository.java
└── common
├── configuration
├── exception
├── loggin
└── utils
自动化测试分类
单元测试:核心的领域模型,包括领域对象(比如Order类),Factory类,领域服务类等; 组件测试:不适合写单元测试但是又必须测试的类,比如Repository类,在有些项目中,这种类型测试也被称为集成测试; API测试:模拟客户端测试各个API接口,需要启动程序。
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 组件测试:src/componentTest/java API测试:src/apiTest/java
apply plugin: 'docker-compose'
dockerCompose {
useComposeFiles = ['docker/mysql/docker-compose.yml']
}
bootRun.dependsOn composeUp
componentTest.dependsOn composeUp
apiTest.dependsOn composeUp
https://www.cnblogs.com/CloudTeng/p/3417762.html
日志处理
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)
throws ServletException, IOException {
//request id in header may come from Gateway, eg. Nginx
String headerRequestId = request.getHeader(HEADER_X_REQUEST_ID);
MDC.put(REQUEST_ID, isNullOrEmpty(headerRequestId) ? newUuid() : headerRequestId);
try {
filterChain.doFilter(request, response);
} finally {
clearMdc();
}
}
<appender name="REDIS" class="com.cwbase.logback.RedisAppender">
<tags>ecommerce-order-backend-${ACTIVE_PROFILE}</tags>
<host>elk.yourdomain.com</host>
<port>6379</port>
<password>whatever</password>
<key>ecommerce-ordder-log</key>
<mdc>true</mdc>
<type>redis</type>
</appender>
异常处理
向客户端提供格式统一的异常返回 异常信息中应该包含足够多的上下文信息,好是结构化的数据以便于客户端解析 不同类型的异常应该包含标识,以便客户端识别
public abstract class AppException extends RuntimeException {
private final ErrorCode code;
private final Map<String, Object> data = newHashMap();
}
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"
}
}
}
后台任务与分布式锁
@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(newScheduledThreadPool(10));
}
"shutdown") (destroyMethod =
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.setTaskDecorator(new LogbackMdcTaskDecorator());
executor.initialize();
return executor;
}
}
@Configuration
"PT30S") (defaultLockAtMostFor =
public class DistributedLockConfiguration {
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
public DistributedLockExecutor distributedLockExecutor(LockProvider lockProvider) {
return new DistributedLockExecutor(lockProvider);
}
}
@Scheduled(cron = "0 0/1 * * * ?")
@SchedulerLock(name = "scheduledTask", lockAtMostFor = THIRTY_MIN, lockAtLeastFor = ONE_MIN)
public void run() {
logger.info("Run scheduled task.");
}
为了支持代码直接调用分布式锁,基于Shedlock的LockProvider创建DistributedLockExecutor:
public class DistributedLockExecutor {
private final LockProvider lockProvider;
public DistributedLockExecutor(LockProvider lockProvider) {
this.lockProvider = lockProvider;
}
public <T> T executeWithLock(Supplier<T> supplier, LockConfiguration configuration) {
Optional<SimpleLock> lock = lockProvider.lock(configuration);
if (!lock.isPresent()) {
throw new LockAlreadyOccupiedException(configuration.getName());
}
try {
return supplier.get();
} finally {
lock.get().unlock();
}
}
}
public String doBusiness() {
return distributedLockExecutor.executeWithLock(() -> "Hello World.",
new LockConfiguration("key", Instant.now().plusSeconds(60)));
}
统一代码风格
客户端的请求数据类统一使用相同后缀,比如Command 返回给客户端的数据统一使用相同后缀,比如Represetation 统一对请求处理的流程框架,比如采用传统的3层架构或者DDD战术模式 提供一致的异常返回(请参考“异常处理”小节) 提供统一的分页结构类 明确测试分类以及统一的测试基础类(请参考“自动化测试分类”小节)
静态代码检查
Checkstyle:用于检查代码格式,规范编码风格 Spotbugs:Findbugs的继承者 Dependency check:OWASP提供的Java类库安全性检查 Sonar:用于代码持续改进的跟踪
健康检查
我们希望初步检查程序是否运行正常 有些负载均衡软件会通过一个健康检查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]"
}
API文档
@Configuration
2
"local", "dev"}) (value = {
public class SwaggerConfiguration {
public Docket api() {
return new Docket(SWAGGER_2)
.select()
.apis(basePackage("com.ecommerce.order"))
.paths(any())
.build();
}
}
数据库迁移
resources/
├── db
│ └── migration
│ ├── V1__init.sql
│ └── V2__create_product_table.sql
多环境构建
local:用于开发者本地开发 ci:用于持续集成 dev:用于前端开发联调 qa:用于测试人员 uat:类生产环境,用于功能验收(有时也称为staging环境) prod:正式的生产环境
CORS
@Configuration
public class CorsConfiguration {
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
// by default uses a Bean by the name of corsConfigurationSource
.cors().and()
...
}
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;
}
}
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处理库
总结
往期精彩回顾
IntelliJ IDEA 2019从入门到癫狂 图文教程!
朕已阅