背景
本规范文档是信息系统的技术标准,明确总体的信息技术的规范性、完整性、一致性。规范建立是逐步完善、提高并演进成熟的过程。通过系统建设确定应用系统的功能边界、系统间边界以及应遵守建设的规则,形成技术标准,再更新和完善架构规范。中科金所有系统的建设必须严格遵循规范制定的原则。
适用范围: 本文档适用于中科金技术部所有技术人员。
目标
-
统一技术架构设计原则:我们在做技术落地时,需要遵守的原则。
-
确定中间件选型的选型。
-
代码规范。
各类架构的关系
下图展示了各种架构的关系,技术架构处在整体技术体系的最底层,为上层提供技术支撑。
技术架构设计原则
-
高可用性: 整体可用性达到4个9,全年不可用时间累积不超过50分钟,单个系统可用性达到5个9,全年不可用性累计不超过5分钟。
-
易扩展性: 在较低的运维成本的基础上,实现负载能力的水平扩展,同时要保证数据的一致性。
-
低耦合性:功能模块之间要尽量减少依赖,耦合性低意味着迭代成本的降低。
-
可管理性: 功能模块要实现可管理性。分为数据面和控制面。
-
可适配性: 为了满足与客户的系统或外部系统互通的需求,服务要有高效的方法实现适配。
中间件选型
下图是对中间件的选型以及各个中间件在整体技术栈的位置和功能。
-
流量网关层:采用Ngnix + LUA 作为流量网关。用于验证请求的合法性,流控等
-
分布式缓存:redis。缓存
-
消息队列: Kafka(适用于业务复杂度低,消息主题数量少,单主题消息量巨大的场景)。Rocketmq(适用于复杂的业务场景,适合主题数量多的场景,同时提供事务消息和定时消息)
-
服务治理-配置中心:Nacos。配置信息组件。用于保存配置信息,常用于开关,实时更新配置信息等。
-
服务治理-注册中心:Nacos。用于服务的注册和订阅。
-
服务治理-流控:Sentinel。通过事先设定限流规则来防范过高流量把服务打死。
-
服务治理-熔断:Sentinel。用于保护下游服务避免被过高的流量打死。事后防范。
-
服务治理-降级:Sentinel。在高峰期下线非核心服务的功能。或当被调用的服务已经不能提供正常的服务,需要主动关闭对这个服务的调用。目的是避免了服务调用的超时和不必要的异常从而影响主流程。
-
服务治理-RPC: Dubbo。服务间的调用采用 Dubbo RPC。
-
服务治理-分布式事务服务:Seata。提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
-
分布式DAL(分布式数据访问层):ShardingJDBC。用于分库分表的场景。
-
可监控平台-日志中心:ELK。包括Elasticsearch、Logstash、Kibana。
-
可监控平台-全链路跟踪服务:Skywalking。用于全链路跟踪服务。
-
可监控平台-指标监控与警告:Prometheus。用于运行指标的监控与警告。
-
数据层-OLTP: mysql,postgreSQL
-
数据层-搜索:Elastic Search
开发规范
Java开发规范
-
命名风格
-
【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
-
【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
-
【强制】类名使用UpperCamelCase风格,但以下情形例外:DO / BO / DTO / VO / AO / PO等。
-
【强制】方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格,必须遵从驼峰形式。
-
【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
-
【强制】抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类名开始,以Test结尾
-
【强制】类型与中括号紧挨相连来定义数组。正例:定义整形数组int[] arrayDemo;反例:在main参数中,使用String args[]来定义
-
【强制】POJO类中布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。
-
【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
-
【强制】杜绝完全不规范的缩写,避免望文不知义。
-
【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。正例:从远程仓库拉取代码的类命名为PullCodeFromRemoteRepository。反例:变量int a; 的随意命名方式。
-
【推荐】如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。
-
【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
-
【参考】各层命名规约:
- Service/DAO层方法命名规约
- 获取单个对象的方法用get作前缀
- 获取多个对象的方法用list作前缀
- 获取统计值的方法用count作前缀
- 插入的方法用save/insert作前缀
- 删除的方法用remove/delete作前缀
- 修改的方法用update作前缀
-
领域模型命名规约
- 数据对象:xxxDO,xxx即为数据表名
- 数据传输对象:xxxDTO,xxx为业务领域相关的名称
- 展示对象:xxxVO,xxx一般为网页名称
- POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO
-
常量定义
-
【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中
-
【推荐】不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。 说明:大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。正例:缓存相关常量放在类CacheConsts下;系统配置相关常量放在类ConfigConsts下
-
【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量
- 跨应用共享常量:放置在二方库中,通常是client.jar中的constant目录下
- 应用内共享常量:放置在一方库中,通常是子模块中的constant目录下
-
【推荐】如果变量值仅在一个固定范围内变化用enum类型来定义
-
【推荐】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开
-
代码格式
-
【强制】if/for/while/switch/do等保留字与括号之间都必须加空格
-
【强制】任何二目、三目运算符的左右两边都需要加一个空格。 说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等
-
【强制】采用4个空格缩进,禁止使用tab字符
-
【强制】IDE的text file encoding设置为UTF-8; IDE中文件的换行符使用Unix格式,不要使用Windows格式
-
【推荐】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:1)第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进。 2)运算符与下文一起换行。 3)方法调用的点符号与下文一起换行。4)方法调用中的多个参数需要换行时,在逗号后进行换行。
-
【推荐】在项目代码中提供.EditorConfig 文件,项目组开发人员在进行项目开发前,先在IDE配置中打开使用本地代码的.EditorConfig,保持项目中代码格式统一(提供公司统一格式)。EditorConfig配置文件:
-
【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性
-
OOP规约
-
【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可
-
【强制】所有的覆写方法,必须加@Override注解
-
【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么
-
【强制】不能使用过时的类或方法
-
【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。在日期格式字符串中需区分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。标准格式为:"yyyy-MM-dd HH:mm:ss"
-
【强制】获取当前毫秒数:System.currentTimeMillis(),而不是 new Date().getTime()
-
【强制】所有整型包装类对象(Short、Intger、Long、Float)之间值的比较,全部使用 equals 方法比较。
-
【强制】浮点数之间的等值判断,基本数据类型不能使用 == 进行比较,包装数据类型不能使用 equals 进行判断。(1)指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。说明:float a = 1.0F - 0.9F; float b = 0.9F - 0.8F; float diff = 1e-6F; if (Math.abs(a - b) < diff) { System.out.println("true"); }
-
【强制】BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。原因:equals() 方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo() 则会忽略精度
-
【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象(字符串)
-
关于基本数据类型与包装数据类型的使用标准如下:
- 【强制】所有的POJO类属性必须使用包装数据类型
- 【强制】RPC方法的返回值和参数必须使用包装数据类型
- 【推荐】所有的局部变量使用基本数据类型
-
【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中
-
【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读
-
【推荐】循环体内,字符串的连接方式,使用StringBuilder的append方法进行扩展
-
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
-
【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯快速定位代码
-
【强制】线程池不允许使用 Executors去创建,而是手动通过 ThreadPoolExecutor 创建线程池的方式,同时必须明确指定线程池的RejectRejectExecutionHandler策略
-
【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区代码块,就不要锁整个方法体;能用对象锁,就不要用类锁
-
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
-
【强制】在使用阻塞等待获取锁的方式时,必须在try业务代码块之外执行lock方法获取锁,在代码块的finally中执行unlock方法来释放锁。说明:
Lock lock = new xxxLock()
…
lock.lock()
try {
doBiz()
…
} finally {
lock.unlock()
}
- 【强制】在使用尝试机制来获取锁的方式时,必须在try业务代码块之前,先判断当前线程是否成功获取锁。在代码块的finally中执行unlock方法来释放锁。说明:
Lock lock = new xxxLock()
…
boolean lockResult = lock.tryLock()
if (lockResult) {
try {
doBiz()
…
} finally {
lock.unlock()
}
}
- 控制语句
- 【强制】在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使空代码。说明:break是退出switch语句块,而return是退出方法体
- 【强制】在if/else/for/while/do语句中必须使用大括号。即使只有一行代码,也需避免采用单行的编码方式:if (condition) statements
- 【推荐】避免采用取反逻辑运算符。说明:if(!(x >= 628)) 用于表达 x 小于 628,就可以改成 (x < 628)
- 注释规约
- 【强制】类、类属性、类方法的注释必须使用Javadoc规范,使用/*内容/格式,不得使用// xxx方式
- 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是Javadoc注释中参数、返回值、异常等的修改
- 【强制】所有类都必须添加创建者和创建日期
- 【参考】谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除
- 【参考】对于注释的要求:第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息
应用结构规范
-
【推荐】推荐新建立的项目使用Gradle进行构建,对依赖的管理和项目的构建都优于Maven
-
-
应用子模块结构规范
-
-
xxx表示当前应用名称,如meds
-
【强制】xxx-sdk - 服务SDK,存放对外提供的SDK代码
-
【强制】xxx-web - API/Controller,存放对外的REST服务代码
-
【强制】xxx-common - 通用组件,存放公共的组件代码
-
【强制】xxx-dal/repository - 数据访问,存放数据库访问相关的代码
-
【强制】xxx-domain - 领域对象,存放数据交换VO、DTO相关实体类代码
-
【强制】xxx-service - 服务层,存放具体的业务服务代码
-
【强制】xxx-demo – 实例模块,存放相关的样例代码
-
【推荐】xxx-main –打包启动模块,一般存放相关自动配置类以及启动主函数,一个项目存在多个打包启动模块可灵活配置
-
应用包规范
-
【强制】以com.zkj.xxx.yyy命名,xxx为当前项目的主应用名,yyy为当前项目所在的子模块名
-
【强制】单元测试包名规范:com.zkj.xxx.yyy.test
-
-
应用资源路径规范
-
-
-
【强制】src/test/java - 集成测试,单元测试目录,所有测试类以xxxTest结尾
-
【强制】src/main/java - java源码所在路径
-
【强制】src/main/resources/config - 配置文件,存放相关属性文件/yaml文件
-
【强制】src/main/resources/static - 静态资源,存放静态的资源文件
-
【强制】src/main/resources/templates - 静态模版资源,存放模版语言代码
-
SpringCloud项目目录结构规范
- 【强制】新创建的应用,必须是基于SpringBoot的SpringCloud应用,需要满足以下项目结构:
| ├── pom.xml└── src├── main│ ├── java│ │ └── com│ │ └── zkj│ │ └── demo│ │ ├── DemoMain.java│ └── resources│ ├──bootstrap.yml│ ├── config│ │ ├── config.properties│ ├──logback-demo.xml│ └──server.jks└── test |
|---|
- DemoMain.java
springboot启动的主函数
- resources/confg/config.properties
spring.profiles.active=local时读取的本地配置,本地启动调试时可用
- logback-demo.xml
格式为logback-${spring.application.name}.xml,注意开启,可用于动态修改日志级别
- server.jks
存放配置解密的公私钥信息,默认读取此文件,可在 bootstrap.yml中配置,适配不同的业务环境,配置属性为:encrypt.keyStore.location、encrypt.keyStore.password、encrypt.keyStore.secret、encrypt.keyStore.alias
- bootstrap.yml
springcloud应用启动引导文件,所有可变的属性都按以下描述定义了环境变量,应用启动时,可通过设置环境变量,让同一份jar(镜像)在不同的环境发布运行:
-
申明了当前的应名:${spring.application.name},默认情况下,注册到eureka的服务名也使用此属性;
-
申明了连接配置中心地址/用户/密码的环境变量:CONFIG_SERVER_URI/CONFIG_SERVER_USERNAME/CONFIG_SERVER_PASSWORD
-
申明了连接vault的IP/PORT/Token的环境变量:VAULT_TOKEN/VAULT_PORT/VAULT_HOST
-
申明了使用配置解密的server.jks相关的文件地址/密码/密钥/别名
- KEYSTORE_FILE/ KEYSTORE_PWD/ KEYSTORE_SECRET/ KEYSTORE_ALIAS
-
特别的,spring.profiles.active取值非local时,会请求bootstrap.yml定义的配置中心服务地址,通过http接口由配置中心服务从git仓库中读取对应profile的配置文件,配置中心存放配置的git仓库目录结构为:
| ├── application.yml└── application-{spring.application.name}/├── {spring.application.name}-${profile}.properties |
|---|
- application.yml
公共的全局SpringCloud配置文件,基础配置,不能随意修改
- application-${profile}.properties
公共的全局SpringCloud配置文件,可以根据不同的profile配置不同的属性,一般不建议修改
- ${spring.application.name}
定义在项目boostrap.yml中的spring.application.name值,应用启动时根据此值定位属性配置
- ${spring.application.name}.yml
项目对应的公共配置,一般存储不可变的配置,里面可以根据{spring.application.name}-${profile}.properties中的属性配置
- {profile}.properties
项目对应的个性化配置,每个环境可配置一份,用于区分不同环境的属性配置