最近帮忙起个新东西,代码层面上想有些变化,针对原有的痛点做些修改。
之前在项目组中写业务逻辑,也有很多时候感觉写着或者用着难受,趁这次机会,把一些思考成熟的东西贡献出来,抛砖引玉,以期得到更好的解决方案。
以下都是比较碎的点,无所谓先后。无论是技术本身,还是行文方式,欢迎不留情面拍砖。
1.数据库版本控制/db migration —— flyway
之前项目组对数据库脚本的管理完全手工,开发阶段记录下数据库变更,发布增量版本时择出变更部分,投产时嘱咐运维同事先执行变更部分的数据库脚本。
痛点在于:
- 机械单调劳动
- 易出错
为了解决这些痛点,引入Flyway
,其官网链接
使用Flyway
,开发阶段将数据库变更写入若干编号的脚本文件,发布版本时可带上所有历史脚本,投产时由flyway自行比较并先执行新脚本。
举例来说,脚本编号越大越新,开发环境数据库执行到了脚本4,功能测试环境到了2,生产环境到了1。
版本 | 开发 | 功能测试 | 生产 |
---|---|---|---|
V1 | √ | √ | √ |
V2 | √ | √ | |
V3 | √ | ||
V4 | √ |
使用flyway
开发结束后打包无需手工择出脚本3&4,无需运维同事在功能测试环境执行这两个脚本。打包中包含有4个脚本,程序在功能测试环境启动时,flyway
发现当前环境db到版本2,自动执行包中2之后的所有脚本,即3和4。
flyway
默认的脚本位置为classpath:db/migration
,脚本文件名模式为V版本号__描述.sql
(版本号可以是1_2,描述也可用“”分隔,版本号和描述之间为两个“”),更多配置可见org.flywaydb.core.Flyway
类。
若使用spring-boot
,flyway
已在<dependencyManagement>
中,自动配置类为org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
引入工具后,同时也要注意纪律,开发阶段试验好再提交,否则需要回退版本
2.检查型异常传递错误
2.1.使用包装有返回码、返回信息和返回数据的包装对象的问题
这个方案,包装对象的代码类似于
public class SomeWrapper<T> {
private int code;
private String msg;
private T data;
// 略
}
使用十分不方便
SomeWrapper<SomeResponse> result = someService.doSomething(someInput);
if (result != null) {
if (SUCCESS_CODE.equals(result.getCode())) {
SomeResponse data= result.getData();
if (data != null) {
// 略,成功
} else {
// 略,防御性编程,一般意味着系统失败,异常等
}
} else {
// 略,处理业务失败
}
}else{
// 略,防御性编程,一般意味着系统失败,异常等
}
代码显得啰嗦,4个分支中,只有1个在处理成功的情况。主要考量及缺点如下:
- 不敢直接使用
data
,应该先判code
是否成功 - 不敢直接使用
SomeWrapper
,应该判null
- 冗余,成功时,
msg
没用;失败时,data
没用
问题尤为严重的是每使用一个返回前都要判返回码code
,完全口耳相传+自觉,万一真有谁忘了判而直接使用,就是灾难,错都不知道怎么错的。
而且本质上,java
中一个函数的return
,一定要返回一个确定的类型。这个return
既需要能返回正确的数据,又需要兼顾错误的信息。
2.2.使用包装有错误码、错误信息的异常传递错误
核心思想主要为:
- 正常情况直接用即可,出错时在
catch
语句块中处理 - 不使用非检查型异常,而是使用检查型;编译器强制要求用户处理错误情况
可见nacos中的com.alibaba.nacos.api.exception.NacosException
,其源码片段如下
public class NacosException extends Exception {
private int errCode;
private String errMsg;
// 略
}
例如某服务的接口定义为
public interface SomeService {
SomeResponse doSomething(SomeRequest someRequest) throws ServiceException;
}
使用起来
try {
SomeResponse = someService.doSomething(someRequest);
if(dateBO != null){
// 略,成功
}else{
// 略
}
} catch(ServiceException e){
// 略
}
可见和包装类方案相比,优点至少有:
- 无需对包装类判
null
- 无需想着判断返回码,直接使用时一定意味着没有错
- 编译器强制要求处理业务错误,在
catch
中写处理逻辑,或者向上抛出
推荐一篇Joe Duffy写的专门讨论如何传递错误的博客——The Error Model,保证收获满满
3.严格区分main和test
打包时需要拆出某些文件不进入jar包,而是放入单独的目录。
之前的做法为将配置文件和脚本文化部放入test,打包时必须跑test
,assembly
中额外处理一下test-classes
文件夹下的配置文件和脚本文件。
缺点很明显:
- 混淆main和test,给人疑惑
- 打包必须跑test
- 开发环境要想启动工程,一定要在test中再写一个同样的类;如果启动main中的主类,则用不了test中的配置文件
主要涉及打包的改变。
之前方案要解决的问题主要是配置文件的位置,将这些文件移出jar包。新方案不需要把这些文件放入test
,而是调整maven-jar-plugin
,将这些文件排除。pom.xml
中配置片段如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/application*.yml</exclude>
<exclude>**/application*.yaml</exclude>
<exclude>**/*.properties</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
与之配合,assembly.xml
中把这些文件放入指定文件夹中
<fileSets>
<fileSet>
<directory>${project.build.directory}/classes</directory>
<outputDirectory>conf</outputDirectory>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/*.properties</include>
</includes>
<fileMode>0644</fileMode>
</fileSet>
</fileSets>
使用spring-boot
的话,jar包外的配置可覆盖jar包内的配置,无需使用启动脚本,上述两个问题均解决。
可见spring-boot官网文档中外化配置的优先级
另外,对于配置外化,可考虑使用配置中心,几个开源产品及官网为:spring-cloud-config,nacos,apollo
为了向未来可能使用的docker
平滑迁移,也建议尽早换用配置中心。
未完待续
先告一段落,后续会介绍如下几点:
- 单元/集成测试
- 切面优于父类
- 新线程中使用spring基础施设
- cache应清空
- 【争议较大】状态机+轮询批量,应用mq做解耦
- 理解spring的思想,pojo,ioc
- 不想写sql语句