后端开发规范
本文说明后端规范相关的开发、调试和维护要求,适用于项目开发与交付过程。
1. 创建ModuleCode
ModuleCode:模块编码 一个模块编码对应一个controller,且在项目开发过程中属于统一话术标准
编辑文件文件:server/base/src/main/java/com/isxcode/zhiqingyun/base/constants/ModuleCode.java
- 要求1:变量名大写
- 要求2:参数小写
- 要求3:下划线分割
- 要求4:含义明确
/**
* Demo案例模块.
*/
String EXAMPLE_DEMO = "example_demo";
2. 创建SQL脚本
基于Flyway管理Sql,目前仅支持h2和MySQL和PostgreSQL数据库
编辑文件:spark-yun/backend/main/src/main/resouces/db/migration/h2/V30__GH-88.sql
编辑文件:spark-yun/backend/main/src/main/resouces/db/migration/MySQL/V30__GH-88.sql
编辑文件:spark-yun/backend/main/src/main/resouces/db/migration/postgres/V30__GH-88.sql
- 要求1:文件名称
V「顺序写编号」__「Github Issue编号」,V一定要大写,下划线一定要两个,例如GH-88,则写成V30__GH-88.sql - 要求2:建表中要包含系统字段,如create_by、create_date_time、last_modified_by、last_modified_date_time、version_number、deleted、tenant_id
- 要求3:
SY_「xxx」(SY为spark_yun的缩写) XXX结尾不需要加S、表名全部大写、用下划线分割
create table if not exists SY_EXAMPLE_DEMO
(
id varchar(200) not null unique primary key comment '唯一id',
demo_name varchar(200) not null comment '名称',
demo_content varchar(200) null comment '内容',
remark varchar(500) null comment '备注',
create_by varchar(200) not null comment '创建人',
create_date_time datetime not null comment '创建时间',
last_modified_by varchar(200) not null comment '更新人',
last_modified_date_time datetime not null comment '更新时间',
version_number int not null comment '版本号',
deleted int default 0 not null comment '逻辑删除',
tenant_id varchar(200) not null comment '租户id'
);
3. 创建Entity
基于Jpa实现对象持久化,支持Jpa乐观锁、假删除、多租户、基础系统字段 实体需要与数据库中的表一一对映
编辑文件:spark-yun/backend/modules/src/main/java/com/isxcode/spark/modules/example/demo/entity/ExampleDemoEntity.java
- 要求1:一定在entity目录下创建文件,modules/example/demo/entity
- 要求2:文件名「XXX」Entity.java
- 要求3:目录需要根据下划线分为多个目录,比如
EXAMPLE_DEMO,则在example/demo/entity目录下
@Data
@Entity
@SQLDelete(sql = "UPDATE SY_EXAMPLE_DEMO SET deleted = 1 WHERE id = ? and version_number = ?")
@Where(clause = "deleted = 0 ${TENANT_FILTER} ")
@Table(name = "SY_EXAMPLE_DEMO")
@JsonIgnoreProperties({"hibernateLazyInitializer"})
@EntityListeners(AuditingEntityListener.class)
public class ExampleDemoEntity {
@Id
@GeneratedValue(generator = "sy-id-generator")
@GenericGenerator(name = "sy-id-generator", strategy = "com.isxcode.zhiqingyun.config.GeneratedValueConfig")
private String id;
private String demoName;
private String demoContent;
private String remark;
@CreatedDate
private LocalDateTime createDateTime;
@LastModifiedDate
private LocalDateTime lastModifiedDateTime;
@CreatedBy
private String createBy;
@LastModifiedBy
private String lastModifiedBy;
@Version
private Long versionNumber;
@Transient
private Integer deleted;
private String tenantId;
@PrePersist
public void prePersist() {
this.tenantId = ContextHolder.getTenantId();
}
}
4. 创建Repository
基于Jpa对数据库进行互动,对数据库进行增删改查
编辑文件 spark-yun/backend/modules/src/main/java/com/isxcode/spark/modules/example/demo/repository/ExampleDemoRepository.java
- 要求1:一定在repository目录下创建文件,modules/example/demo/repository
- 要求2:文件名「XXX」Repository.java
- 要求3:不要超过三张表Join
函数命名规范:
- findByXXX : 查询单个对象
- findXXXByXXX:查询自定义单个对象
- findAllByXXX :查询对象数组
- findAllXXXByXXX :查询自定义对象数组
- pageByXXX : 分页对象查询
- pageSearchByXXX : 分页搜索对象查询
- pageSearchXXXByXXX:分页搜索自定义对象查询
- deleteByXXX :删除对象
- deleteAllByXXX :批量删除对象
- save : 保存单个对象
- saveAll : 批量保存对象
- updateXXXByXXX :更新对象 ,不推荐使用
- getXXXByXXX:获取某个具体的返回单值
- countXXXByXXX:获取个数总和
常见参数命名规范:
- searchKeyWord:搜索关键字段
- pageable:分页请求对象
@Repository
@CacheConfig(cacheNames = {ModuleCode.EXAMPLE_DEMO})
public interface ExampleDemoRepository extends JpaRepository<ExampleDemoEntity, String> {
Optional<DemoIspongEntity> findByAgeAndUsername(Integer age, String username);
void deleteByAgeAndUsername(Integer age, String username);
List<DemoIspongEntity> findAllByAgeAndUsername(Integer age, String username);
// 自定义更新,非特殊情况禁止使用
@Modifying
@Query(value = "update ExampleDemoEntity set content = :content where name = :name")
void updateContentByName(@Param("content") String content, @Param("name") String name);
// 对象分页搜索查询
@Query("SELECT D FROM ExampleDemoEntity D WHERE D.name LIKE %:searchKeyWord% order by D.createDateTime desc")
Page<DemoIspongEntity> searchAll(@Param("searchKeyWord") String searchKeyWord, Pageable pageable);
}
5. 创建AO对象
多表关联查询返回对象「XXX」AO.java, Association Object联合对象
- 要求1:注意,一定要写构造器,不能使用lombok
- 要求2:jpa写法,数据库字段与构造器中的参数顺序保持一致
- 要求3:最好不要超过三张表Join关系
- 要求4:一定在ao目录下创建文件,
spark-yun/backend/api/src/main/java/com/isxcode/spark/api/example/demo/ao - 要求5:文件名「XXX」Ao.java
@Data
public class ExampleInfoAo {
private String username;
private String content;
public ExampleInfoAo(String username,String content) {
this.username = username;
this.content = content;
}
}
@Query(value =
"select "
+ " new com.isxcode.zhiqingyun.api.instance.ao.WorkflowInstanceAo("
+ " U.account,"
+ " E.content) "
+ "from ExampleDemoEntity E left join UserEntity U on D.id = U.id "
+ "where U.username LIKE %:keyword% AND E.tenantId=:tenantId order by D.createDateTime desc")
Page<ExampleInfoAo> pageExampleInfoAo(
@Param("tenantId") String tenantId,
@Param("searchKeyWord") String searchKeyWord,
Pageable pageable);
6.创建Mapper
基于MapStruct对象转换层,A对象转换B对象
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface DemoIspongMappers {
@Mapping(target = "node",expression = "java( engineEntity.getActiveNodeNum()+ \"/\" +engineEntity.getAllNodeNum())")
PageClusterRes clusterEntityToPageClusterRes(ClusterEntity clusterEntity);
}
7.创建Service
通用业务层,禁止引用其他的Service服务
8.创建BizService
业务层,与controller一一对应
9.创建BizImplService
业务实现层,扩展业务层使用,可以创建多个
10.创建Controller
对外接口管理
11.创建Req
请求体
12.创建Res
响应体
13.创建Constant
静态变量
14.创建Dto
过程存储对象 Data To Object
15.修改Properties
backend/api
所有的java基础对象:
Pojos :业务对象,req、res、bo、dto、vo Constants :静态值 Exceptions :异常类 Properties :Spring的配置类
backend/modules
所有模块逻辑代码:
Controller:接口层 Mapper:对象转换层 Entity:数据库对象层 Repository:数据持久化层 Service:业务服务层
vip/backend
类似modules结构,该代码为闭源代码
service调用 注意:需要查询所有租户数据时,使用allData包裹查询
public Page<PageMetaTableRes> pageMetaTable(PageMetaTableReq pageMetaTableReq) {
Page<MetaTableAo> metaTablePage = allData(() -> metaTableRepository.searchAll(
ContextHolder.getTenantId(),
pageMetaTableReq.getSearchKeyWord(),
pageMetaTableReq.getDatasourceId(), PageRequest.of(pageMetaTableReq.getPage(), pageMetaTableReq.getPageSize())));
return metaTablePage.map(metaMapper::metaTableEntityToPageMetaTableRes);
}
Controller
基于OpenApi(Swagger)管理接口信息
com.isxcode.zhiqingyun.modules.<module_code>.controller.<module_code>Controller
所有接口一律使用Post
接口命名规范:
- 添加对象接口 : addXXX
- 更新对象接口 : updateXXX
- 条件更新接口:updateXXXByXXX
- 分页搜索查询接口 :pageSearchXXX
- 分页查询接口 :pageXXX
- 删除对象接口:deleteXXX
- 删除批量对象接口:deleteAllXXX
- 查询单个对象接口:findXXX
- 条件查询单个对象接口:findXXXByXXX
- 返回数组对象接口:findAllXXX
- 条件查询返回数组对象接口:findAllXXXByXXX
- 其他行为接口 : action/check/remove/setXXX
- 免密接口:openXXX
案例:
@Tag(name = "计算集群模块")
@RestController
@RequestMapping(ModuleCode.CLUSTER)
@RequiredArgsConstructor
public class ClusterController {
// 只可以调用BizService服务层
private final ClusterBizService clusterBizService;
// 各前缀需保持一致
@Operation(summary = "添加计算引擎接口")
@PostMapping("/addCluster")
@SuccessResponse("添加成功")
public AddClusterRes addCluster(@Valid @RequestBody AddClusterReq addClusterReq) {
return clusterBizService.addCluster(addClusterReq);
}
// 注解控制权限
@Secured({RoleType.TENANT_MEMBER, RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "添加数据源接口")
@PostMapping("/addDatasource")
@SuccessResponse("添加成功")
public void addDatasource(@Valid @RequestBody AddDatasourceReq addDatasourceReq) {
datasourceBizService.addDatasource(addDatasourceReq);
}
// 免密接口
@Operation(summary = "用户登录接口")
@PostMapping("/open/openLogin")
@SuccessResponse("登录成功")
public OpenLoginRes openLogin(@Valid @RequestBody OpenLoginReq openLoginReq) {
return userBizService.openLogin(openLogingReq);
}
// 分页查询接口
@Operation(summary = "查询所有启用用户接口")
@PostMapping("/pageEnableUser")
@SuccessResponse("查询成功")
public Page<PageEnableUserRes> pageEnableUser(@Valid @RequestBody PageEnableUserReq pageEnableUserReq) {
return userBizService.pageEnableUser(pageEnableUserReq);
}
// 文件上传接口
@Operation(summary = "作业流导入接口(Swagger有Bug不能使用)")
@PostMapping("/importWorkflow")
@SuccessResponse("导入成功")
public void importWorkflow(
@RequestParam("workflowConfigFile") MultipartFile workflowConfigFile,
@Schema(description = "作业流唯一id", example = "sy_ba1f12b5c8154f999a02a5be2373a438") @RequestParam(required = false)
String workflowId) {
workflowBizService.importWorkflow(workflowConfigFile, workflowId);
}
// 文件下载接口,不需要使用@SuccessResponse
@Operation(summary = "作业流导出接口")
@PostMapping("/exportWorkflow")
public void exportWorkflow(@Valid @RequestBody ExportWorkflowReq exportWorkflowReq, HttpServletResponse response) {
workflowBizService.exportWorkflow(exportWorkflowReq, response);
}
}
Service
业务服务代码分为两部分:
- XXXService:通用服务层,可以被任何的Biz服务层调用,但禁止调用任何Biz服务层。
- XXXBizService:业务逻辑服务层,与Controller中的接口保持一一对应。
com.isxcode.zhiqingyun.modules.<module_code>.service.<module_code>Servicecom.isxcode.zhiqingyun.modules.<module_code>.service.biz.<module_code>BizService
例如:通用服务层Service 严格使用Javadoc写注释,后续会被多次复用
@Service
@RequiredArgsConstructor
public class ClusterNodeService {
/**
* <p>
* 获取代理默认端口号
* </p>
*
* @param agentPort 代理端口号
* @return 代理端口号
*/
public String getDefaultAgentPort(String agentPort) {
if (Strings.isEmpty(agentPort)) {
return sparkYunProperties.getDefaultAgentPort();
} else {
return agentPort;
}
}
}
例如:业务逻辑服务层
<module_code>BizService
@Service
@RequiredArgsConstructor
public class ClusterBizService {
// 添加对象
public void addCluster(AddClusterReq addClusterReq) {
ClusterEntity cluster = clusterMapper.addEngineReqToClusterEntity(addClusterReq);
clusterRepository.save(cluster);
}
// 更新对象
public void updateCluster(UpdateClusterReq updateClusterReq) {
ClusterEntity cluster = clusterService.getCluster(updateClusterReq.getClusterId());
cluster = clusterMapper.updateEngineReqToClusterEntity(updateClusterReq, cluster);
clusterRepository.save(cluster);
}
// 分页搜索查询对象
public Page<PageClusterRes> pageCluster(PageClusterReq pageClusterReq) {
Page<ClusterEntity> clusterPage = clusterRepository.pageCluster(
pageClusterReq.getSearchKeyWord(),
PageRequest.of(pageClusterReq.getPage(), pageClusterReq.getPageSize()));
// 使用mapper转换对象
return clusterPage.map(clusterMapper::clusterEntityToPageClusterRes);
}
}
Pojos
禁止使用Object类型 支持Spring的Validation 基础对象:
- XXXReq:请求对象(Request),与接口一一对应,不可复用 (包含子对象Dto)
- XXXRes:响应对象(Response),与接口一一对应,不可复用(包含子对象Vo)
- XXXVo:响应对象中的前端显示数据(View Object),可复用
- XXXBo:通用服务层Service中接口的对象(Business Object),可复用
- XXXDto:数据传输对象(Data Transfer Object),可复用
- XXXReq,分页请求对象
@EqualsAndHashCode(callSuper = true)
@Data
public class PageUserReq extends BasePageRequest {
}
XXXReq,必传字段校验和注释
@Data
public class AddUserReq {
@Schema(title = "名字", example = "仅供内部使用")
@NotEmpty(message = "用户名称不能为空")
@Size(min = 1, max = 100, message = "名称长度5~100")
private String username;
@Schema(title = "邮箱", example = "ispong@123.com")
@Email(message = "邮箱不合法")
private String email;
}
XXXRes,Null不返回给前端
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PageClusterRes {
private String id;
}
Properties
Spring 配置类
com.isxcode.zhiqingyun.api.main.properties.SparkYunProperties
@Data
@Component
@ConfigurationProperties(prefix = "spark-yun")
@EnableConfigurationProperties(SparkYunProperties.class)
public class SparkYunProperties {
/** 可以让脚本临时复制的目录. */
private String tmpDir = "/tmp";
/** 代理默认端口号. */
private String defaultAgentPort = "30177";
}
Exceptions
异常类,默认使用IsxAppException
if (!tenantUserEntityOptional.isPresent()) {
throw new IsxAppException("用户不存在");
}
获取用户变量 如何获取当前用户的userId 和 tenantId
import com.isxcode.zhiqingyun.common.security.ContextHolder;
System.out.println(ContextHolder.getTenantId());
System.out.println(ContextHolder.getUserId());
Swagger规范
后面不用写接口文档,但是要严格写swagger
Req
package com.isxcode.zhiqingyun.api.cluster.pojos.req;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import lombok.Data;
@Data
public class AddClusterReq {
@Schema(title = "计算引擎名称", example = "至轻云内部计算引擎")
@NotEmpty(message = "引擎名称不能为空")
private String name;
@Schema(title = "计算引擎类型", example = "
写清楚,类型的枚举值
Yarn
K8s
Spark
")
@NotEmpty(message = "计算引擎类型不能为空")
private String clusterType;
@Schema(title = "备注", example = "仅供内部使用")
private String remark;
}
Res
package com.isxcode.zhiqingyun.api.cluster.pojos.req;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import lombok.Data;
@Data
public class AddClusterReq {
// 标记 是否为后端逻辑字段,前端不用填充,说明清楚
@Schema(title = "计算引擎名称", example = "至轻云内部计算引擎")
@NotEmpty(message = "引擎名称不能为空")
private String name;
@Schema(title = "计算引擎类型", example = "
写清楚,类型的枚举值
Yarn
K8s
Spark
")
@NotEmpty(message = "计算引擎类型不能为空")
private String clusterType;
@Schema(title = "备注", example = "仅供内部使用")
private String remark;
}
Controller
package com.isxcode.zhiqingyun.modules.datasource.controller;
import com.isxcode.zhiqingyun.api.datasource.pojos.res.GetConnectLogRes;
import com.isxcode.zhiqingyun.api.datasource.pojos.res.PageDatasourceRes;
import com.isxcode.zhiqingyun.api.datasource.pojos.res.TestConnectRes;
import com.isxcode.zhiqingyun.base.constants.ModuleCode;
import com.isxcode.zhiqingyun.base.constants.RoleType;
import com.isxcode.zhiqingyun.common.annotations.successResponse.SuccessResponse;
import com.isxcode.zhiqingyun.modules.datasource.service.biz.DatasourceBizService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "数据源模块")
@RestController
@RequestMapping(ModuleCode.DATASOURCE)
@RequiredArgsConstructor
public class DatasourceController {
private final DatasourceBizService datasourceBizService;
@Secured({RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "添加数据源接口")
@PostMapping("/addDatasource")
@SuccessResponse("添加成功")
public void addDatasource(@Valid @RequestBody AddDatasourceReq addDatasourceReq) {
datasourceBizService.addDatasource(addDatasourceReq);
}
@Secured({RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "更新数据源接口")
@PostMapping("/updateDatasource")
@SuccessResponse("更新成功")
public void updateDatasource(@Valid @RequestBody UpdateDatasourceReq updateDatasourceReq) {
datasourceBizService.updateDatasource(updateDatasourceReq);
}
@Secured({RoleType.TENANT_MEMBER, RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "查询数据源列表接口")
@PostMapping("/pageDatasource")
@SuccessResponse("查询数据源成功")
public Page<PageDatasourceRes> pageDatasource(@Valid @RequestBody PageDatasourceReq pageDatasourceReq) {
return datasourceBizService.pageDatasource(pageDatasourceReq);
}
@Secured({RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "删除数据源接口")
@PostMapping("/deleteDatasource")
@SuccessResponse("删除成功")
public void deleteDatasource(@Valid @RequestBody DeleteDatasourceReq deleteDatasourceReq) {
datasourceBizService.deleteDatasource(deleteDatasourceReq);
}
@Secured({RoleType.TENANT_MEMBER, RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "测试数据源连接接口")
@PostMapping("/testConnect")
@SuccessResponse("检测完成")
public TestConnectRes testConnect(@Valid @RequestBody GetConnectLogReq testConnectReq) {
return datasourceBizService.testConnect(testConnectReq);
}
@Secured({RoleType.TENANT_MEMBER, RoleType.TENANT_SUPER_ADMIN})
@Operation(summary = "查询连接日志")
@PostMapping("/getConnectLog")
@SuccessResponse("获取成功")
public GetConnectLogRes getConnectLog(@Valid @RequestBody GetConnectLogReq getConnectLogReq) {
return datasourceBizService.getConnectLog(getConnectLogReq);
}
}
数据库DO对象校验
// 用上面的方法 getXXXEntity
WorkEntity work = workService.getWorkEntity(saveSyncWorkConfigReq.getWorkId());
// 不要重复写
Optional<WorkEntity> workEntityOptional = workRepository.findById(saveSyncWorkConfigReq.getWorkId());
if (!workEntityOptional.isPresent()) {
throw new IsxAppException("作业不存在");
}
多表关联分页查询
构建多表返回对象
MetaColumnAo
@Data
public class MetaColumnAo {
private String datasourceId;
private String tableName;
private String columnName;
private String columnComment;
private String columnType;
private String customComment;
private LocalDateTime lastModifiedDateTime;
public MetaColumnAo(String datasourceId, String tableName, String columnName, String columnComment,
String columnType, String customComment, LocalDateTime lastModifiedDateTime) {
this.columnType = columnType;
this.datasourceId = datasourceId;
this.tableName = tableName;
this.columnName = columnName;
this.columnComment = columnComment;
this.customComment = customComment;
this.lastModifiedDateTime = lastModifiedDateTime;
}
}
编写多表查询sql 注意:一定要指定
tenantId字段
@Repository
@CacheConfig(cacheNames = {ModuleCode.VIP_META})
public interface MetaColumnRepository extends JpaRepository<MetaColumnEntity, MetaColumnId> {
@Query("SELECT " +
"new com.isxcode.zhiqingyun.api.meta.ao.MetaColumnAo(" +
" M.datasourceId," +
" M.tableName," +
" M.columnName," +
" M.columnComment," +
" M.columnType," +
" MI.customComment," +
" M.lastModifiedDateTime) FROM MetaColumnEntity M " +
"left join MetaColumnInfoEntity MI " +
"on M.datasourceId=MI.datasourceId and M.tableName = MI.tableName and M.columnName=MI.columnName " +
"WHERE M.tenantId=:tenantId " +
" AND " +
"(M.columnName LIKE %:keyword% OR M.columnComment LIKE %:keyword% OR M.tableName LIKE %:keyword% OR MI.customComment LIKE %:keyword%) " +
"order by M.createDateTime desc")
Page<MetaColumnAo> searchAll(@Param("tenantId") String tenantId, @Param("keyword") String searchKeyWord,
Pageable pageable);
}
代码规范
- 禁止在项目中注释代码,直接删除代码即可
- 禁止使用关键词 命名 举例:connect
- 不要注释代码 直接删除 !!! 注释代码看的很烦
- 使用 - 作为分割符,易于阅读
- java中静态,使用_ 作为分割符
- 禁止使用,不常见的缩写,或者自己定义的缩写
- 后端捕获到的异常,一定要 log.error(e.getMessage(),e)
