后端开发规范

本文说明后端规范相关的开发、调试和维护要求,适用于项目开发与交付过程。

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)