該規範基于阿裡開發規範和1024創新實驗室團隊的開發規範整合自己公司的規範整理而成
文章目錄
- 一、程式設計規範
-
- 1、項目命名規範
- 2、`TODO/FIXME` 規範
- 3、方法參數規範
- 4、注釋
-
- 4.1、方法内邏輯注釋
- 4.2、方法注釋
- 5、狀态型取值應使用枚舉規範
- 6、固定常量使用常量類
- 7、所有mapper接口和service接口需要加上注釋
- 8、ThreadLocal在set()後需要在正确的時間remove()
- 二、項目規範
-
- 1、目錄結構
- 2、domain中的javabean規範
-
- 1)javabean整體要求:
- 2)model包下javabean要求:
- 3)傳輸對象;`XxxxDTO`,要求:
- 4)視圖對象;`XxxxVO`,要求:
- 5)業務對象; `BO`,要求:
- 三、MVC 規範
-
- 1、整體分層
- 2、 `controller` 層規範
- 3、 `service` 層規範
- 4、 `manager` 層規範
- 5、 `mapper` 層規範
- 6、`boolean`類型的屬性命名規範
- 四、資料庫 規範
-
- 1、建表規範
- 2、枚舉類表字段注釋需要将所有枚舉含義進行注
- 3、資料庫編碼
- 4、合理結合業務給表字段添加索引和唯一索引具體索引規範請參照《阿裡巴巴 Java 開發手冊》索引規約
- 五、其他
-
- 1、代碼送出規範
- 3、保持項目整潔
一、程式設計規範
1、項目命名規範
全部采用小寫方式, 以中劃線分隔。
正例:
mall-management-system / order-service-client / user-api
反例:
mall_management-system / mallManagementSystem / orderServiceClient
2、 TODO/FIXME
規範
TODO/FIXME
TODO/TBD(to be determined)
注釋一般用來描述已知待改進、待補充的修改點,并且加上作者名稱。
FIXME
注釋一般用來描述已知缺陷,它們都應該有統一風格,友善文本搜尋統一處理。如:
// TODO <author-name>: 補充XX處理
// FIXME <author-name>: XX缺陷
3、方法參數規範
無論是
controller,service,manager,dao
亦或是其他的代碼,每個方法最多
3
個參數,如果超出
3
個參數的話,要封裝成
javabean
對象。
- 友善他人調用,降低出錯幾率。尤其是當參數是同一種類型,僅僅依靠順序區分,稍有不慎便是災難性後果,而且排查起來也極其惡心。
- 保持代碼整潔、清晰度。當一個個方法裡充斥着一堆堆參數的時候,再堅強的人,也會身心疲憊。
反例:
/**
* 使用證書加密資料工具方法
*
* @param param
* @param password 加密密碼
* @param priCert 私鑰
* @param pubCert 公鑰
* @return 傳回加密後的字元串
*/
public String signEnvelop(JdRequestParam param, String password, String priCert, String pubCert){}
4、注釋
4.1、方法内邏輯注釋
注釋除了說明作用、邏輯之外。還有一個很重要的原因:當業務邏輯過于複雜,代碼過于龐大的時候,注釋就變成了一道道美化環境、分離與整理邏輯思路的路标。這是很重要的一點,它能有效得幫助我們免于陷入代碼與業務邏輯的泥沼之中。
/**
* 開始抽獎方法
* 儲存中獎資訊、獎勵使用者積分等
* @param luckDrawDTO
* @return ResponseDTO 傳回中獎資訊
*/
public ResponseDTO<String> startLuckDraw(LuckDrawDTO luckDrawDTO) {
// -------------- 1、校驗抽獎活動基本資訊 ------------------------
xxx僞代碼一頓操作
// -------------- 2、新增抽獎記錄 -------------------------------
xxx僞代碼一頓操作
// -------------- 3、如果需要消耗積分,則扣除鋼镚積分 -------------
xxx僞代碼一頓操作
// -------------- 4、擷取獎品資訊,開始翻滾吧 --------------------
xxx僞代碼一頓操作
return ResponseDTO.succ(luckDrawPrizeVO);
}
4.2、方法注釋
方法要盡量通過方法名自解釋,不要寫無用、資訊備援的方法頭,不要寫空有格式的方法頭注釋。
方法頭注釋内容可選,但不限于:功能說明、傳回值,用法、算法實作等等。尤其是對外的方法接口聲明,其注釋,應當将重要、有用的資訊表達清楚。
正例:
/**
* 解析轉換時間字元串為 LocalDate 時間類
* 調用前必須校驗字元串格式 否則可能造成解析失敗的錯誤異常
*
* @param dateStr 必須是 yyyy-MM-dd 格式的字元串
* @return LocalDate
*/
public static LocalDate parseYMD(String dateStr){}
反例:
/**
* 校驗對象
*
* @param t
* @return String
*/
public static <T> String checkObj(T t);
反例中出現的問題:
- 方法注釋沒有說明具體的作用、使用事項。
- 參數、傳回值,空有格式沒内容。這是非常重要一點,任何人調用任何方法之前都需要知道方法對參數的要求,以及傳回值是什麼。
5、狀态型取值應使用枚舉規範
舉例:
@Getter
public enum BasketStatusEnum {
FREE(1, "空閑"),
BOUND(2, "已綁定"),
DELIVERING(3, "配送中(已上車)"),
IN_SCHOOL(4, "在學校(已下車)"),
BREAK(7, "損壞");
private int code;
private String name;
BasketStatusEnum(int code, String name) {
this.code = code;
this.name = name;
}
}
6、固定常量使用常量類
例如無加工的加工方式code固定為
WJG
,應該使用常量類收納該常量
常量類應該私有化構造方法,且使用
final
修飾類,防止常量類被繼承後,常量類中的常量影響子類中的字段
/**
* @author zhangxing
* @date 2020/04/20
*/
public final class ApplicationConstant {
private ApplicationConstant() {
}
public static final String PROFILE_DEVELOPMENT = "dev";
public static final String PROFILE_PRODUCTION = "prod";
/**
* 無加工加工方式
*/
public static final String NO_MANUFACTURE_HANDLE_METHOD = "WJG";
}
7、所有mapper接口和service接口需要加上注釋
8、ThreadLocal在set()後需要在正确的時間remove()
二、項目規範
1、目錄結構
top.sclf.jyhpv.子產品名 源碼目錄
|-- domain javabean(下方有domain的專門說明)
|-- |--- bo 業務對象(Business Object)
|-- |--- dto 傳輸層對象(Data Transfer Object)
|-- |--- vo 視圖對象(View Object)
|-- |--- model 資料對象(和資料庫一一對應的對象)
|-- converter 用于但不限于domain下的對象轉換
|-- controller 控制器
|-- service 業務層
|-- |--- impl 業務實作層
|-- manager manager層,下方會詳細說明
|-- mapper mapper接口和xml
|-- enums 枚舉
|-- constant 常量(使用class,且class上添加final,請勿使用interface和enum)
|-- annotations 注解
|-- validator 自定義校驗類
|-- commons 通用對象
|-- config 配置類,如swagger配置等..
|-- listener 監聽器(包含接口與實作)
|-- factory 工廠類
|-- server 服務相關,如WebSocket伺服器
|-- aspect 切面
|-- database 資料庫/資料源相關
|-- exception 異常相關
|-- util 工具類
|-- task 定時任務
|-- export 資料導出業務
|-- interceptor 攔截器
|-- Application.java 啟動類
2、domain中的javabean規範
1)javabean整體要求:
- 不得有任何的業務邏輯或者計算
- 基本資料類型必須使用包裝類型
(Integer, Double、Boolean 等)
- 不允許有任何的預設值
- 每個屬性必須添加注釋,并且必須使用多行注釋。
- 必須使用
簡化lombok
方法getter/setter
- 建議對象使用
的lombok
,同時使用這兩個注解,簡化對象構造方法以及set方法。@Builder ,@NoArgsConstructor
2)model包下javabean要求:
- Xxxx 與資料庫表名保持一緻,駝峰規則
- 類中字段要與資料庫字段保持一緻,不能缺失或者多餘
- 類中的每個字段添加注釋,并與資料庫注釋保持一緻
- 不允許有組合
- 項目内的日期類型必須統一,使用
。java.time.LocalDateTime
3)傳輸對象; XxxxDTO
,要求:
XxxxDTO
- 不可繼承自
Entity
-
可以繼承、組合其他VO
等對象DTO,VO,BO
-
隻能用于傳回前端、rpc 的業務資料封裝對象VO
4)視圖對象; XxxxVO
,要求:
XxxxVO
- 不可繼承自
Entity
-
可以繼承、組合其他VO
等對象DTO,VO,BO
-
隻能用于傳回前端、rpc 的業務資料封裝對象VO
5)業務對象; BO
,要求:
BO
- 不可以繼承自
Entity
-
對象隻能用于BO
層,不得用于service,manager,dao
層controller
三、MVC 規範
1、整體分層
- controller 層
- service 層
- manager 層
- dao 層
2、 controller
層規範
controller
1)RESTful的URL設計參考https://cizixs.com/2016/12/12/restful-api-design-guide/
2)每個方法必須添加
swagger
文檔注解
@ApiOperation
,并填寫接口描述資訊,描述最後必須加上作者資訊
@author zhangxing
。
正例:
@ApiOperation("更新部門資訊 @author zhangxing")
@PostMapping("/department/update")
public ResponseDTO<String> updateDepartment(@Valid @RequestBody DeptUpdateDTO deptUpdateDTO) {
return departmentService.updateDepartment(deptUpdateDTO);
}
3)controller 負責協同和委派業務,充當路由的角色,每個方法要保持簡潔:
- 不做任何的業務邏輯操作
- 不做任何的參數、業務校驗,參數校驗隻允許使用@Valid 注解做簡單的校驗
- 不做任何的資料組合、拼裝、指派等操作
- 允許設定預設排序,并且需要判斷排序規則為空時可以設定預設排序
正例:
@ApiOperation("分頁查詢訂單明細變更記錄 @author zhangxing")
@GetMapping("/pages")
public PageInfo<DemandHisOrderDetailChange> getOrderDetailChangePage(Pagination pagination, DemandHisOrderDetailChangeQueryDTO queryDTO) {
if (StringUtils.isBlank(pagination.getSort())) {
pagination.setSort("delivery_date, use_date");
pagination.setOrder("ASC");
}
return demandHisOrderDetailChangeService.getPage(pagination, queryDTO);
}
3、 service
層規範
service
1)BaseService的使用,BaseService中注入了相應的mapper,不強制要求使用
XxxService接口繼承
BaseService<T, Q>
接口
T,Q分别為對應的資料庫model和查詢條件對象。舉例:
/**
* <p>
* 系統日志表 服務類
* </p>
*/
public interface SysLogService extends BaseService<SysLog, SysLogQueryDTO> {
}
XxxServiceImpl實作XxxService接口,繼承
BaseServiceImpl<M, T, Q>
M,T,Q分别為Mapper層和model層和查詢條件,需要實作
getList
方法。舉例:
/**
* <p>
* 系統日志表 服務實作類
* </p>
*/
@Service
public class SysLogServiceImpl extends BaseServiceImpl<SysLogMapper, SysLog, SysLogQueryDTO> implements SysLogService {
@Override
public List<SysLog> getList(SysLogQueryDTO queryDTO) {
return mapper.selectList(queryDTO);
}
}
2)合理拆分 service 檔案,如果業務較大,請拆分為多個 service。
如訂單業務,所有業務都寫到 OrderService 中會導緻檔案過大,故需要進行拆分如下:
-
訂單查詢業務OrderQueryService
-
訂單建立業務OrderCreateService
-
訂單發貨業務OrderDeliverService
-
訂單驗證業務OrderValidatorService
3)謹慎處理
@Transactional
事務注解的使用,不要簡單對
service
的方法添加個
@Transactional
注解就覺得萬事大吉了。應當合并對資料庫的操作,盡量減少添加了
@Transactional
方法内的業務邏輯。
@Transactional
注解内的
rollbackFor
值必須使用異常的基類
Throwable.class
對于@Transactional 注解,當 spring 遇到該注解時,會自動從資料庫連接配接池中擷取 connection,并開啟事務然後綁定到 ThreadLocal 上,如果業務并沒有進入到最終的 操作資料庫環節,那麼就沒有必要擷取連接配接并開啟事務,應該直接将 connection 傳回給資料庫連接配接池,供其他使用(比較難以講解清楚,如果不懂的話就主動去問)。
反例:
@Transactional(rollbackFor = Throwable.class)
public ResponseDTO<String> upOrDown(Long departmentId, Long swapId) {
// 驗證 1
DepartmentEntity departmentEntity = departmentDao.selectById(departmentId);
if (departmentEntity == null) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS);
}
// 驗證 2
DepartmentEntity swapEntity = departmentDao.selectById(swapId);
if (swapEntity == null) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS);
}
// 驗證 3
Long count = employeeDao.countByDepartmentId(departmentId)
if (count != null && count > 0) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.EXIST_EMPLOYEE);
}
// 操作資料庫 4
Long departmentSort = departmentEntity.getSort();
departmentEntity.setSort(swapEntity.getSort());
departmentDao.updateById(departmentEntity);
swapEntity.setSort(departmentSort);
departmentDao.updateById(swapEntity);
return ResponseDTO.succ();
}
以上代碼前三步都是使用 connection 進行驗證操作,由于方法上有@Transactional 注解,是以這三個驗證都是使用的同一個 connection。
若對于複雜業務、複雜的驗證邏輯,會導緻整個驗證過程始終占用該 connection 連接配接,占用時間可能會很長,直至方法結束,connection 才會交還給資料庫連接配接池。
對于複雜業務的不可預計的情況,長時間占用同一個 connection 連接配接不是好的事情,應該盡量縮短占用時間。
正例:
DepartmentService.java
public ResponseDTO<String> upOrDown(Long departmentId, Long swapId) {
DepartmentEntity departmentEntity = departmentDao.selectById(departmentId);
if (departmentEntity == null) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS);
}
DepartmentEntity swapEntity = departmentDao.selectById(swapId);
if (swapEntity == null) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS);
}
Long count = employeeDao.countByDepartmentId(departmentId)
if (count != null && count > 0) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.EXIST_EMPLOYEE);
}
departmentManager.upOrDown(departmentSort,swapEntity);
return ResponseDTO.succ();
}
DepartmentManager.java
@Transactional(rollbackFor = Throwable.class)
public void upOrDown(DepartmentEntity departmentEntity ,DepartmentEntity swapEntity){
Long departmentSort = departmentEntity.getSort();
departmentEntity.setSort(swapEntity.getSort());
departmentDao.updateById(departmentEntity);
swapEntity.setSort(departmentSort);
departmentDao.updateById(swapEntity);
}
将資料在 service 層準備好,然後傳遞給 manager 層,由 manager 層添加@Transactional 進行資料庫操作。
4)需要注意的是:注解
@Transactional
事務在類的内部方法調用是不會生效的
反例:如果發生異常,saveData方法上的事務注解并不會起作用
@Service
public class OrderService{
public void createOrder(OrderCreateDTO createDTO){
this.saveData(createDTO);
}
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO){
orderDao.insert(createDTO);
}
}
Spring采用動态代理(AOP)實作對bean的管理和切片,它為我們的每個class生成一個代理對象。隻有在代理對象之間進行調用時,可以觸發切面邏輯。而在同一個class中,方法A調用方法B,調用的是原對象的方法,而不通過代理對象。是以Spring無法攔截到這次調用,也就無法通過注解保證事務了。簡單來說,在同一個類中的方法調用,不會被方法攔截器攔截到,是以事務不會起作用。
解決方案:
- 可以将方法放入另一個類,如新增
,通過spring注入,這樣符合了在對象之間調用的條件。manager層
- 啟動類添加
,方法内使用@EnableAspectJAutoProxy(exposeProxy = true)
獲得代理類,使用事務。AopContext.currentProxy()
SpringBootApplication.java
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
OrderService.java
public void createOrder(OrderCreateDTO createDTO){
OrderService orderService = (OrderService)AopContext.currentProxy();
orderService.saveData(createDTO);
}
5)service是具體的業務處理邏輯服務層,盡量避免将web層某些參數傳遞到service中。
反例:
public ResponseDTO<String> handlePinganRequest(HttpServletRequest request){
InputStreamReader inputStreamReader = new InputStreamReader(request.getInputStream(), "GBK");
BufferedReader reader = new BufferedReader(inputStreamReader);
StringBuilder sb = new StringBuilder();
String str;
while ((str = reader.readLine()) != null) {
sb.append(str);
}
if(!JSON.isValid(msg)){
return ResponseDTO.wrap(ResponseCodeConst.ERROR_PARAM);
}
PinganMsgDTO PinganMsgDTO = JSON.parseObject(msg,PinganMsgDTO.class);
// 示例結束
}
反例中出現的問題:
- 反例中把
傳遞到service中,是為了擷取Request流中的字元資訊,然後才是真正的業務處理。按照分層的初衷:将代碼、業務邏輯解耦,正确的做法應該是HttpServletRequest
方法将handlePinganRequest
字元作為參數直接處理業務,将從String
中擷取字元的操作放入Request
中。controller
- 另一個壞處是不友善做單元測試,還得一個
一個new
并制造一個HttpServletRequest
,然而這樣做并不能模拟到真實的業務情景及資料。InputStream
4、 manager
層規範
manager
manager 層的作用(引自《阿裡 java 手冊》):
- 對第三方平台封裝的層,預處理傳回結果及轉化異常資訊;
- 對 Service 層通用能力的下沉,如緩存方案、中間件通用處理;
緩存示例
XxxManager.java
public class XxxManager { @Autowired private XxxMapper mapper; @AutoWired private XxxCacheService cacheService; public Xxx getById(id) { Xxx x = cacheService.getById(id) if(x == null){ x = mapper.getById(id); cacheService.setById(x); } return x; } }
- 與 mapper 層互動,對多個 mapper 的組合複用。
5、 mapper
層規範
mapper
優先使用 tk的通用mapper架構。
1)所有 mapper接口 繼承自
BaseMapper<T>
2)禁止使用 tk通用mapper的
Example
條件建構器
3)盡量不直接在 mybatis xml 中寫死常量,應從 mapper 中傳入到 xml 中
3)建議不要使用星号
*
代替所有字段
正例:
NoticeDao.java
Integer noticeCount(@Param("sendStatus") Integer sendStatus);
---------------------------------------------
NoticeMapper.xml
<select id="noticeCount" resultType="integer">
select
count(1)
from t_notice
where
send_status = #{sendStatus}
</select>
反例:
NoticeDao.java
Integer noticeCount();
---------------------------------------------
NoticeMapper.xml
<select id="noticeCount" resultType="integer">
select
count(1)
from t_notice
where
send_status = 0
</select>
3)mapper層方法命名規範
- 擷取單個對象的方法用
做字首。find
- 擷取多個對象的方法用
做字首。list
- 擷取統計值的方法用
做字首。count
- 插入的方法用
做字首。insert
- 删除的方法用
做字首。delete
- 修改的方法用
做字首。update
建議:mapper層方法命名盡量以sql語義命名,避免與業務關聯。
正例:
反例:
反例中出現的不規範操作:
- find代表單個查詢,批量查詢的應該 list 開頭。
- 命名與業務關聯,局限了mapper方法的使用場景和範圍,降低了方法的複用性,造成他人困惑以及重複造輪子。
6、 boolean
類型的屬性命名規範
boolean
類中布爾類型的變量,都不要加is,否則部分架構解析會引起序列化錯誤。反例:定義為基本資料類型 Boolean isDeleted;的屬性,它的方法也是 isDeleted(),RPC在反向解析的時候,“以為”對應的屬性名稱是 deleted,導緻屬性擷取不到,進而抛出異常。
這是阿裡巴巴開發手冊中的原文,我們團隊的規定是:
boolean
類型的類屬性和資料表字段都統一使用
flag
結尾。雖然使用
isDeleted,is_deleted
從字面語義上更直覺,但是比起可能出現的潛在錯誤,這點犧牲還是值得的。
正例:
deletedFlag,deleted_flag,onlineFlag,online_flag
四、資料庫 規範
1、建表規範
非中間關系表必備三字段:id, creator_id, creator_name, modifier_id, modifier_name, create_time, modify_time
- id 字段 Long 類型,單表自增,自增長度為 1
- create_time 字段 datetime 類型,預設值 CURRENT_TIMESTAMP
- update_time 字段 datetime 類型,預設值 CURRENT_TIMESTAMP
2、枚舉類表字段注釋需要将所有枚舉含義進行注
修改或增加字段的狀态描述,必須要及時同步更新注釋。
如下表的
sync_status
字段
同步狀态 0 未開始 1同步中 2同步成功 3失敗
。
正例:
CREATE TABLE `t_change_data` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`sync_status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '同步狀态 0 未開始 1同步中 2同步成功 3失敗',
`sync_time` DATETIME NULL DEFAULT NULL COMMENT '同步時間',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
)
反例:
CREATE TABLE `t_change_data` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`sync_status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '同步狀态 ',
`sync_time` DATETIME NULL DEFAULT NULL COMMENT '同步時間',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
)
3、資料庫編碼
字元集:
utf8mb4
排序規則:
utf8mb4_general_ci
4、合理結合業務給表字段添加索引和唯一索引具體索引規範請參照《阿裡巴巴 Java 開發手冊》索引規約
五、其他
1、代碼送出規範
- 如果改動影響代碼非常多,必須新增一個分支送出
- 送出前應該冷靜、仔細檢查一下,確定沒有忘記加入版本控制或不應該送出的檔案。
- 送出前應該先編譯一次(idea裡ctrl+F9),防止出現編譯都報錯的情況。
- 送出前先更新pull一次代碼,送出前發生沖突要比送出後發生沖突容易解決的多。
- 送出前檢查代碼是否格式化,是否符合代碼規範,無用的包引入、變量是否清除等等。
- 送出時檢查注釋是否準确簡潔的表達出了本次送出的内容。
- 送出代碼時必須填寫詳細備注,如完成功能,注釋為“新增XX功能”;
- 若此次送出代碼對應禅道中的任務或者bug,格式如下:
task#[任務id] [任務标題] [具體事項]
bug#[bug id] [bug标題] [具體事項]
- 例子如下:
task#1101 開發smartreload功能 完成線程池的編碼
bug#1102 smartreload時間不正确 線程池的大小問題
3、保持項目整潔
使用git,必須添加 .gitignore 忽略配置檔案。
不要送出與項目無關的内容檔案:idea配置、target包等。