(一)创建项目

介绍

项目是基于 Spring bootmaven 项目,本章节介绍怎样创建基于HZERO平台的项目。

  1. 新建maven项目
  2. 添加项目依赖
  3. 添加默认配置文件

创建maven项目

$ mkdir -p hzero-todo-service
$ cd hzero-todo-service

添加项目依赖

$ touch pom.xml

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--hzero-parent dependency-->
    <parent>
        <groupId>org.hzero</groupId>
        <artifactId>hzero-parent</artifactId>
        <version>1.2.0.RELEASE</version>
    </parent>
    <artifactId>hzero-todo-service</artifactId>
    <dependencies>
        <!--hzero-->
        <dependency>
            <groupId>org.hzero.starter</groupId>
            <artifactId>hzero-starter-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hzero.starter</groupId>
            <artifactId>hzero-starter-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hzero.boot</groupId>
            <artifactId>hzero-boot-admin</artifactId>
        </dependency>
        <!-- undertow -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- config client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-client</artifactId>
        </dependency>
        <!-- register client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <repositories>
            <repository>
            <id>HzeroRelease</id>
            <name>Hzero-Release Repository</name>
            <url>http://nexus.saas.hand-china.com/content/repositories/Hzero-Release/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>HzeroSnapshot</id>
            <name>Hzero-Snapshot Repository</name>
            <url>http://nexus.saas.hand-china.com/content/repositories/Hzero-Snapshot/</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>

根据子级模块所需jar包添加需要的依赖。

  • (必须)hzero-starter-core:核心工具包,提供了基础支持类、消息工具、Redis工具、及其他一些通用工具、常量封装等。
  • hzero-starter-mybatis-mapper,通用 mapper 和分页插件集成,扩展多语言、审计字段等功能。

添加默认配置文件

在根目录下创建源码文件夹和资源文件夹。

$ mkdir -p src/main/java
$ mkdir -p src/main/resources

项目采用spring boot 进行管理。需要在子项目中配置默认的配置项。

resource文件夹中创建 application.yml, bootstrap.yml

$ cd src/main/resources
$ touch application.yml
$ touch bootstrap.yml
  • bootstrap.yml: 配置不会通过环境变量替换和必须在bootstrap中指定的变量。包括项目端口,应用名,hzero-config地址等。
  • application.yml: 配置项目的应用程序配置,包含默认的线上数据库连接配置,注册中心地址等,这些变量可以通过profile或者环境变量修改。
  • 二者区别:bootstrap.yml 在程序引导时执行,应用于更加早期配置信息读取,如可以配置application.yml中使用到参数。application.yml 是应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。bootstrap.yml 先于 application.yml 加载。

bootstrap.yml

server:
  # 服务端口
  port: 8088
management:
  server:
    # 监控管理端口
    port: 8089
  endpoints:
    web:
      exposure:
        # 需要开放的 Actuator 监控端点,默认开放所有
        include: '*'
spring:
  application:
    name: hzero-todo-service
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:default}
  cloud:
    config:
      fail-fast: false
      # 是否启用配置中心
      enabled: ${SPRING_CLOUD_CONFIG_ENABLED:false}
      # 配置中心地址
      uri: ${SPRING_CLOUD_CONFIG_URI:http://dev.hzero.com.cn:8010}
      retry:
        # 最大重试次数
        maxAttempts: 6
        multiplier: 1.1
        # 重试间隔时间
        maxInterval: 2000
      # 标签
      label: ${SPRING_CLOUD_CONFIG_LABEL:}
    inetutils:
      # 本地多网卡时,忽略回环网卡
      ignored-interfaces[0]: lo
      # 本地多网卡时,选择注册的网段
      preferred-networks[0]: 192.168

application.yml

# 日志配置
logging:
  level:
    org.hzero: ${LOG_LEVEL:debug}
    org.apache.ibatis: ${LOG_LEVEL:debug}
    io.choerodon: ${LOG_LEVEL:debug}

编写TodoApplication类

src/main/java中创建TodoApplication,在项目根目录下执行如下命名:

$ mkdir -p src/main/java/org/hzero/todo
$ touch src/main/java/org/hzero/todo/TodoApplication.java

TodoApplication.java

package org.hzero.todo;
import io.swagger.annotations.ApiOperation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import io.choerodon.core.iam.ResourceLevel;
import io.choerodon.resource.annoation.EnableChoerodonResourceServer;
import io.choerodon.swagger.annotation.Permission;
@SpringBootApplication
@RestController
// @EnableChoerodonResourceServer 用于开启资源认证、关闭 Security 安全认证
@EnableChoerodonResourceServer
public class TodoApplication {
    public static void main(String[] args) {
        SpringApplication.run(TodoApplication.class, args);
    }
    @GetMapping
    @Permission(level = ResourceLevel.SITE, permissionPublic = true)
    @ApiOperation(value = "demo")
    public ResponseEntity<String> hello() {
        return new ResponseEntity<>("hello hzero!", HttpStatus.OK);
    }
}

编写TodoExtraDataManager类

src/main/java中创建TodoExtraDataManager,在项目根目录下执行如下命名:

$ mkdir -p src/main/java/org/hzero/todo
$ touch src/main/java/org/hzero/todo/config/TodoExtraDataManager.java

TodoExtraDataManager.java

指定当前服务的路由信息

package org.hzero.todo.config;
import io.choerodon.core.swagger.ChoerodonRouteData;
import io.choerodon.swagger.annotation.ChoerodonExtraData;
import io.choerodon.swagger.swagger.extra.ExtraData;
import io.choerodon.swagger.swagger.extra.ExtraDataManager;
@ChoerodonExtraData
public class TodoExtraDataManager implements ExtraDataManager {
    @Override
    public ExtraData getData() {
        ChoerodonRouteData choerodonRouteData = new ChoerodonRouteData();
        choerodonRouteData.setName("htdo");
        choerodonRouteData.setPath("/htdo/**");
        choerodonRouteData.setServiceId("hzero-todo-service");
        choerodonRouteData.setPackages("org.hzero.todo");
        extraData.put(ExtraData.ZUUL_ROUTE_DATA, choerodonRouteData);
        return extraData;
    }
}

启动应用

启动Eureka

cd D:\Workspace\Raycus\hzero\hzero-service\hzero-register
mvn spring-boot:run

http://localhost:8000

启动redis

docker run -d --name redis -p 6379:6379 redis:latest

启动应用

$ mvn clean spring-boot:run

控制台打印出如下信息,则表示启动成功。
Started TodoApplication in 14.189 seconds (JVM running for 14.942)

此时可以打开浏览器,在浏览器输入:http://localhost:8089/actuator/health

返回如下信息:

{
  status: "UP"
}

在浏览器输入:http://localhost:8088/hello,页面打印 hello world

这样,一个简单的Spring boot 应用就已经搭建成功。

(二)初始化数据库

介绍

项目创建成功之后,需要初始化本地数据库。在开发之前,请确保本地项目已经创建成功,详见 创建项目

创建用户

CREATE USER 'hzero'@'%' IDENTIFIED BY "hzero";

创建数据库

CREATE DATABASE todo_service DEFAULT CHARACTER SET utf8;
GRANT ALL PRIVILEGES ON todo_service.* TO hzero@'%';
FLUSH PRIVILEGES;

设计表结构

  • 先使用Excel设计 todo_usertodo_task 表结构。Excel 设计参考:TODO表设计

HZERO后端应用开发完整流程:hzero-todo-service springboot完整项目代码_spring boot

  • 设计完成后,将相应数据库的脚本拷贝到 todo_service 库下执行,创建表。

在todo_service数据库中创建如下两张表:

todo_user表

Drop Table IF EXISTS TODO_USER;

Create Table TODO_USER
(
 ID bigint Not Null auto_increment primary key,
 EMPLOYEE_NAME Varchar(30) Not Null,
 EMPLOYEE_NUMBER Varchar(30) Not Null,
 EMAIL Varchar(60),
 object_version_number BigInt(20) Not Null Default 1,
 creation_date  datetime Not Null Default CURRENT_TIMESTAMP,
 created_by bigint(20) Not Null Default -1,
 last_updated_by bigint(20) Not Null Default -1,
 last_update_date datetime Not Null Default CURRENT_TIMESTAMP
);
ALTER TABLE TODO_USER COMMENT '用户表';
ALTER TABLE TODO_USER MODIFY `ID` bigint Not Null auto_increment Comment '表ID,主键,供其他表做外键';
ALTER TABLE TODO_USER MODIFY `EMPLOYEE_NAME` Varchar(30) Not Null Comment '员工名';
ALTER TABLE TODO_USER MODIFY `EMPLOYEE_NUMBER` Varchar(30) Not Null Comment '员工编号';
ALTER TABLE TODO_USER MODIFY `EMAIL` Varchar(60) Comment '邮箱';
ALTER TABLE TODO_USER MODIFY `object_version_number` BigInt(20) Not Null Default 1 Comment '行版本号,用来处理锁';

ALTER TABLE `TODO_USER` ADD UNIQUE TODO_USER_u1(`EMPLOYEE_NUMBER`);

todo_task表

Drop Table IF EXISTS TODO_TASK;


Create Table TODO_TASK
(
 ID bigint Not Null auto_increment primary key,
 EMPLOYEE_ID BigInt(20) Not Null,
 STATE Varchar(30) Not Null,
 TASK_NUMBER Varchar(60) Not Null,
 TASK_DESCRIPTION Varchar(240),
 TENANT_ID BigInt(20) Not Null,
 object_version_number BigInt(20) Not Null Default 1,
 creation_date  datetime Not Null Default CURRENT_TIMESTAMP,
 created_by bigint(20) Not Null Default -1,
 last_updated_by bigint(20) Not Null Default -1,
 last_update_date datetime Not Null Default CURRENT_TIMESTAMP
);
ALTER TABLE TODO_TASK COMMENT '任务表';
ALTER TABLE TODO_TASK MODIFY `ID` bigint Not Null auto_increment Comment '表ID,主键,供其他表做外键';
ALTER TABLE TODO_TASK MODIFY `EMPLOYEE_ID` BigInt(20) Not Null Comment '员工ID,TODO_USER.ID';
ALTER TABLE TODO_TASK MODIFY `STATE` Varchar(30) Not Null Comment '状态,值集:TODO.STATE';
ALTER TABLE TODO_TASK MODIFY `TASK_NUMBER` Varchar(60) Not Null Comment '任务编号';
ALTER TABLE TODO_TASK MODIFY `TASK_DESCRIPTION` Varchar(240) Comment '任务描述';
ALTER TABLE TODO_TASK MODIFY `TENANT_ID` BigInt(20) Not Null Comment '租户ID';
ALTER TABLE TODO_TASK MODIFY `object_version_number` BigInt(20) Not Null Default 1 Comment '行版本号,用来处理锁';

ALTER TABLE `TODO_TASK` ADD UNIQUE TODO_TASK_u1(`TASK_NUMBER`,`TENANT_ID`);

导出Groovy脚本

HZERO 采用Liquibase + groovy 的方式对数据库管理,便于后续同步各个环境数据库结构以及升级。

更多有关Liguibase的资料见 Liquibase 官网

  • 使用代码生成器工具生成 Groovy 脚本。

todo_user.groovy

package script.db
databaseChangeLog(logicalFilePath: 'script/db/todo_user.groovy') {
    changeSet(author: "your.email@email.com", id: "2020-02-03-todo_user") {
        def weight = 1
        if(helper.isSqlServer()){
            weight = 2
        } else if(helper.isOracle()){
            weight = 3
        }
        if(helper.dbType().isSupportSequence()){
            createSequence(sequenceName: 'todo_user_s', startValue:"1")
        }
        createTable(tableName: "todo_user", remarks: "用户表") {
            column(name: "ID", type: "bigint(20)", autoIncrement: true ,   remarks: "表ID,主键,供其他表做外键")  {constraints(primaryKey: true)} 
            column(name: "EMPLOYEE_NAME", type: "varchar(" + 30 * weight + ")",  remarks: "员工名")  {constraints(nullable:"false")}  
            column(name: "EMPLOYEE_NUMBER", type: "varchar(" + 30 * weight + ")",  remarks: "员工编号")  {constraints(nullable:"false")}  
            column(name: "EMAIL", type: "varchar(" + 60 * weight + ")",  remarks: "邮箱")   
            column(name: "object_version_number", type: "bigint(20)",   defaultValue:"1",   remarks: "行版本号,用来处理锁")  {constraints(nullable:"false")}  
            column(name: "creation_date", type: "datetime",   defaultValueComputed:"CURRENT_TIMESTAMP",   remarks: "")  {constraints(nullable:"false")}  
            column(name: "created_by", type: "bigint(20)",   defaultValue:"-1",   remarks: "")  {constraints(nullable:"false")}  
            column(name: "last_updated_by", type: "bigint(20)",   defaultValue:"-1",   remarks: "")  {constraints(nullable:"false")}  
            column(name: "last_update_date", type: "datetime",   defaultValueComputed:"CURRENT_TIMESTAMP",   remarks: "")  {constraints(nullable:"false")}  
        }
        addUniqueConstraint(columnNames:"EMPLOYEE_NUMBER",tableName:"todo_user",constraintName: "TODO_USER_u1")
    }
}

todo_task.groovy

package script.db
databaseChangeLog(logicalFilePath: 'script/db/todo_task.groovy') {
    changeSet(author: "your.email@email.com", id: "2020-02-03-todo_task") {
        def weight = 1
        if(helper.isSqlServer()){
            weight = 2
        } else if(helper.isOracle()){
            weight = 3
        }
        if(helper.dbType().isSupportSequence()){
            createSequence(sequenceName: 'todo_task_s', startValue:"1")
        }
        createTable(tableName: "todo_task", remarks: "任务表") {
            column(name: "ID", type: "bigint(20)", autoIncrement: true ,   remarks: "表ID,主键,供其他表做外键")  {constraints(primaryKey: true)} 
            column(name: "EMPLOYEE_ID", type: "bigint(20)",  remarks: "员工ID,TODO_USER.ID")  {constraints(nullable:"false")}  
            column(name: "STATE", type: "varchar(" + 30 * weight + ")",  remarks: "状态,值集:TODO.STATE")  {constraints(nullable:"false")}  
            column(name: "TASK_NUMBER", type: "varchar(" + 60 * weight + ")",  remarks: "任务编号")  {constraints(nullable:"false")}  
            column(name: "TASK_DESCRIPTION", type: "varchar(" + 240 * weight + ")",  remarks: "任务描述")   
            column(name: "TENANT_ID", type: "bigint(20)",  remarks: "租户ID")  {constraints(nullable:"false")}  
            column(name: "object_version_number", type: "bigint(20)",   defaultValue:"1",   remarks: "行版本号,用来处理锁")  {constraints(nullable:"false")}  
            column(name: "creation_date", type: "datetime",   defaultValueComputed:"CURRENT_TIMESTAMP",   remarks: "")  {constraints(nullable:"false")}  
            column(name: "created_by", type: "bigint(20)",   defaultValue:"-1",   remarks: "")  {constraints(nullable:"false")}  
            column(name: "last_updated_by", type: "bigint(20)",   defaultValue:"-1",   remarks: "")  {constraints(nullable:"false")}  
            column(name: "last_update_date", type: "datetime",   defaultValueComputed:"CURRENT_TIMESTAMP",   remarks: "")  {constraints(nullable:"false")}  
        }
        addUniqueConstraint(columnNames:"TASK_NUMBER,TENANT_ID",tableName:"todo_task",constraintName: "TODO_TASK_u1")
    }
}

同步表结构

database-init.sh的使用

将表结构同步到其它环境时,可使用数据安装工具来安装数据库,将 groovy 脚本导出后,放到本地 hzero-resource 项目的 groovy 目录下,然后启动工具安装数据库即可。

  • 将导出的 groovy 脚本放到 ~/hzero-resource/groovy/todo_service 目录下
  • 修改 ~/hzero-resource/docs/mapping/service-mapping.xml,加入 todo_service 的数据库信息
<service name="hzero-todo-service" filename="todo_service" schema="todo_service" description="TODO示例服务"/>
  • 通过 database-init.sh 启动安装工具

HZERO后端应用开发完整流程:hzero-todo-service springboot完整项目代码_后端_02

  • 访问安装工具页面,选择 todo_service 安装

HZERO后端应用开发完整流程:hzero-todo-service springboot完整项目代码_spring_03

  • 生成 groovy 之后,如果表结构有变更,首先更新 Excel 表设计,再向 groovy 脚本中添加 changeSet 来添加变更记录,再使用安装工具来同步其它环境。

验证表结构

登录数据库,查询现有的表结构。

mysql> show tables;
+---------------------------------------+
| Tables_in_todo_service |
+---------------------------------------+
| DATABASECHANGELOG                     |
| DATABASECHANGELOGLOCK                 |
| TODO_TASK                             |
| TODO_USER                             |
+---------------------------------------+
5 rows in set (0.00 sec)

项目数据库配置

添加pom依赖

pom.xml 文件中添加数据库操作相关依赖。

<dependency>
    <groupId>org.hzero.starter</groupId>
    <artifactId>hzero-starter-mybatis-mapper</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

配置application.yml

在项目的 application.yml 文件中添加数据库连接信息:

spring:
  datasource:
    url: ${SPRING_DATASOURCE_URL:jdbc:mysql://db.hzero.com.cn:3306/todo_service?useUnicode=true&characterEncoding=utf-8&useSSL=false}
    username: ${SPRING_DATASOURCE_USERNAME:hzero}
    password: ${SPRING_DATASOURCE_PASSWORD:hzero}
# MyBatis Mapper 扫描
mybatis:
  mapperLocations: classpath*:/mapper/*.xml
  configuration:
    mapUnderscoreToCamelCase: true

启动项目

项目根目录下执行命令。项目正常启动,则数据库连接配置正常。

$ mvn clean spring-boot:run

(三)编写domain-领域模型层

前置条件

在开发之前,请确保

  • 本地项目已经创建成功,详见 创建项目
  • 数据库创建成功,详见 初始化数据库

介绍

demo 需涉及到 domain 层的 entity、多 entityservicerepository 接口类以及 infra 层的 repository 实现类。

编写entity

实体规范

  • 实体继承 AuditDomain,AuditDomain 包含标准的审计字段
  • 使用 @Table (javax.persistence.Table) 映射表名
  • 使用 @ModifyAudit 注解标明在更新数据时需要更新 lastUpdateDate、lastUpdatedBy 两个审计字段
  • 使用 @VersionAudit 注解标明在更新数据时需要更新版本号 objectVersionNumber
  • 使用 @ApiModel 注解说明实体含义,在 Swagger 文档上就可以看到实体说明。
  • 实体主键使用 @Id (javax.persistence.Id) 注解标注
  • 对于自增张、序列(SEQUENCE)类型的主键,需要添加注解 @GeneratedValue。 序列命名规范:表名_S。例如:表SYS_USER对应的序列为 SYS_USER_S
  • 实体字段使用 @ApiModelProperty 说明字段含义,在 Swagger 文档上可以看到字段说明。
  • 非数据库字段使用 @Transient 注解标注,如果页面用到的非数据库字段比较多,建议使用 DTO 封装数据。
  • 所有属性均为private属性,每一个属性需要生成对应的 gettersetter 方法。
  • 字段名称应根据驼峰命名规则从数据库列名转换过来。例如:数据库列名为 USER_NAME ,则字段名为 userName,特殊字段名称,可以在字段在添加 @Column(name = "xxx")注解,指定数据库列名。
  • 不使用基本类型,全部使用基本类型的包装类,如 Long 对应数据库中的 INTEGER,而不是使用 long
  • 数字类型主键统一采用 Long。金额、数量 等精度严格浮点类型采用 BigDecimal (注意:BigDecimal 在计算、比较方面的特殊性)
  • 实体中可以包含一些实体自治的方法,这些方法通常用于对本身的属性做一些计算、操作等。

User.java 代码

package org.hzero.todo.domain.entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.hibernate.validator.constraints.Length;
import io.choerodon.mybatis.annotation.ModifyAudit;
import io.choerodon.mybatis.annotation.VersionAudit;
import io.choerodon.mybatis.domain.AuditDomain;
import org.hzero.core.util.Regexs;
@ApiModel("用户信息") // Swagger 实体描述
@ModifyAudit //在类上使用,启用审计字段支持,实体类加上该注解后,插入和更新会启动对 lastUpdateDate、lastUpdatedBy 自维护字段支持
@VersionAudit //在类上使用,启用objectVersionNumber自维护支持,插入一条数据objectVersionNumber默认为1,每次update后objectVersionNumber自增1
@Table(name = "todo_user") // 表映射
@JsonInclude(JsonInclude.Include.NON_NULL) // 数据返回前端时,排除为空的字段
public class User extends AuditDomain { //AuditDomain包含5个自维护字段,使用@ModifyAudit和@VersionAudit的实体类要继承该类
    @Id // 主键主键,注意是 javax.persistence.Id
    @GeneratedValue //对于自增张、序列(SEQUENCE)类型的主键,需要添加该注解
    private Long id;
    @Length(max = 30) // 长度控制
    @NotBlank // 非空控制
    @ApiModelProperty("员工姓名") // Swagger 字段描述
    private String employeeName;
    @Length(max = 30)
    @NotBlank
    @Pattern(regexp = Regexs.CODE, message = "htdo.warn.user.numberFormatIncorrect") // 格式控制
    @ApiModelProperty("员工编号")
    private String employeeNumber;
    @Length(max = 60)
    @Pattern(regexp = Regexs.EMAIL, message = "htdo.warn.user.emailFormatIncorrect")
    @ApiModelProperty("员工邮箱")
    private String email;
    // 省略 getter/setter 方法
}

Task.java 代码

package org.hzero.todo.domain.entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.hibernate.validator.constraints.Length;
import io.choerodon.mybatis.annotation.ModifyAudit;
import io.choerodon.mybatis.annotation.VersionAudit;
import io.choerodon.mybatis.domain.AuditDomain;
@ApiModel("任务信息")
@ModifyAudit
@VersionAudit
@Table(name = "todo_task")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Task extends AuditDomain {
    public static final String FIELD_ID = "id";
    public static final String FIELD_EMPLOYEE_ID = "employeeId";
    public static final String FIELD_STATE = "state";
    public static final String FIELD_TASK_DESCRIPTION = "taskDescription";
    @Id
    @GeneratedValue
    private Long id;
    @NotNull(message = "用户ID不能为空")
    @ApiModelProperty("用户ID")
    private Long employeeId;
    @ApiModelProperty("任务状态")
    private String state;
    @ApiModelProperty("任务编号")
    private String taskNumber;
    @Length(max = 240)
    @ApiModelProperty("任务描述")
    private String taskDescription;
    @NotNull
    @ApiModelProperty("租户ID")
    private Long tenantId;
    @Transient
    @ApiModelProperty("员工编号")
    private String employeeNumber;
    @Transient
    @ApiModelProperty("员工姓名")
    private String employeeName;
    /**
     * 生成任务编号
     */
    public void generateTaskNumber() {
        this.taskNumber = UUID.randomUUID().toString().replace("-", "");
    }
    // 省略 getter/setter
}

编写Repository

Repository 接口类

  • Repository 接口类定义了数据操作的一系列接口,并不提供实现,具体实现需要通过 Repository实现层提供。创建在项目模块的 xxx.domain.repository 包下。
  • 每一个 Repository 对应一个 entity ,命名为 entity 类名尾缀替换为 Repository。如:TaskRepository 对应 Task
  • Repository 继承 BaseRepository<T>BaseRepository 封装了基本的资源库增删改查、批量增删改等。单表查询基本就不需要我们再写方法了。

UserRepository.java 代码

package org.hzero.todo.domain.repository;
import org.hzero.mybatis.base.BaseRepository;
import org.hzero.todo.domain.entity.User;
/**
 * 用户资源库
 */
public interface UserRepository extends BaseRepository<User> {
}

TaskRepository.java 代码

package org.hzero.todo.domain.repository;
import java.util.List;
import io.choerodon.core.domain.Page;
import io.choerodon.mybatis.pagehelper.domain.PageRequest;
import org.hzero.mybatis.base.BaseRepository;
import org.hzero.todo.domain.entity.Task;
/**
 * 任务资源库
 */
public interface TaskRepository extends BaseRepository<Task> {
    /**
     * 分页查询任务
     * 
     * @param task Task
     * @param pageRequest 分页参数
     * @return Page<Task>
     */
    Page<Task> pageTask(Task task, PageRequest pageRequest);
    /**
     * 根据员工ID查询任务
     * 
     * @param employeeId 员工ID
     * @return List<Task>
     */
    List<Task> selectByEmployeeId(Long employeeId);
    /**
     * 根据任务编号查询任务详细(包含员工姓名)
     * 
     * @param taskNumber 任务编号
     * @return Task
     */
    Task selectDetailByTaskNumber(String taskNumber);
}

编写Service

Domain Service 接口类

  • 领域层的Service 不是程序必要组成,如没有特殊或复杂的业务逻辑处理,则可以不需要领域服务类。
  • 领域层的Service 是业务软件的核心,是反应多个领域模型的业务情况的具体实现,是领域模型对外提供的实际服务。
  • Service 接口类定义了业务操作的一系列接口,并不提供实现,具体实现需要通过服务实现层提供,所以属于供应方的服务接口层。创建在项目模块 的 xxx.domain.service 包下。
  • 每一个 Service 对应多个 entity 类,因需要与appservice区分,所以规定命名为 I + 涉及主要entity类名 + Service。如:ITaskService

Service 实现类

  • Service 接口的具体实现通过服务实现层提供,所以属于供应方的服务实现层。创建在项目模块的 xxx.domian.service.impl 包下。
  • 实现类,如无特殊情况,需要用 @Service 标注,以自动扫描注册

(四)编写infra-基础设置层

前置条件

在开发之前,请确保

  • 本地项目已经创建成功,详见 创建项目
  • 数据库创建成功,详见 初始化数据库

介绍

demo 需涉及到 infra 层的 mapper 类及 repository 实现类。

Mapper

mapper 接口类

  • mapper 接口类即为传统意义上的 DAO,但与 interface 不同,mapper 本身就是对数据访问的具体实现,所以属于供应方的服务实现层。创建在 项目模块 的 xxx.infra.mapper 包下。
  • 每一个 mapper 接口类封装了对数据库表的操作,每一个 mapper 对应一个 实体 类,命名为 实体 类名尾缀替换为 Mapper 。如:TaskMapper 对应实体Task 类。
  • 基础的 CRUD 操作不需要再次实现,通过继承 BaseMapper<T> 类实现。其中 T 为 对应 实体 的泛型。
  • 复杂的数据库操作需要定义具体的接口方法。

mapper.xml

  • Mapperxml文件 是数据库的的具体映射,与 Mapper 接口同级,创建在 项目模块 resources 目录下的 mapper 目录下。
  • Mapperxml文件,与 Mapper 接口对应。所以命名与 Mapper 接口类相同。
  • Mapperxml文件非必须,由于继承BaseMapper类后基本的 CRUD 不需要进行配置,所以只有CRUD操作时不需要创建对应的 xml 文件。
  • 对于自定义的数据库方法,需要创建对应的 mapper.xml 文件。
  • Mapperxml 中的操作 id 对应 Mapper 接口类的方法名。

UserMapper.java 代码

package org.hzero.todo.infra.mapper;
import io.choerodon.mybatis.common.BaseMapper;
import org.hzero.todo.domain.entity.User;
/**
 * UserMapper
 */
public interface UserMapper extends BaseMapper<User> {
}

TaskMapper.java 代码

package org.hzero.todo.infra.mapper;
import java.util.List;
import io.choerodon.mybatis.common.BaseMapper;
import org.hzero.todo.domain.entity.Task;
/**
 * TaskMapper
 */
public interface TaskMapper extends BaseMapper<Task> {
    /**
     * 查询任务
     * 
     * @param params 任务查询参数
     * @return Task
     */
    List<Task> selectTask(Task params);
}

UserMapper.xml 文件

<?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.hzero.todo.infra.mapper.UserMapper">
</mapper>

TaskMapper.xml 文件

<?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.hzero.todo.infra.mapper.TaskMapper">
    <select id="selectTask" resultType="org.hzero.todo.domain.entity.Task">
        select
            tt.id,
            tt.employee_id,
            tt.state,
            tt.task_number,
            tt.task_description,
            tt.tenant_id,
            tt.object_version_number,
            tu.employee_name,
            tu.employee_number
        from todo_task tt join todo_user tu on tt.employee_id = tu.id
        <where>
            <if test="taskNumber != null and taskNumber != ''">
                and tt.task_number = #{taskNumber}
            </if>
            <if test="taskDescription != null and taskDescription != ''">
                <bind name="taskDescriptionLike" value="'%' + taskDescription + '%'" />
                and tt.task_description like #{taskDescriptionLike}
            </if>
        </where>
    </select>
</mapper>

Repository

  • Repository 接口的具体实现。创建在项目模块的 xxx.infra.repository.impl 包下。
  • 每一个 Repository 实现类对应一个 Repository 接口类,命名为 Repository 接口类名 + Impl。如:TaskRepositoryImpl 对应 TaskRepository
  • Repository 继承 BaseRepositoryImpl<T> 类,该类是 BaseRepository<T> 的实现。
  • 需要通过@Component纳入spring管理。

UserRepositoryImpl.java 代码

package org.hzero.todo.infra.repository.impl;
import org.springframework.stereotype.Component;
import org.hzero.mybatis.base.impl.BaseRepositoryImpl;
import org.hzero.todo.domain.entity.User;
import org.hzero.todo.domain.repository.UserRepository;
/**
 * 用户资源库实现
 */
@Component
public class UserRepositoryImpl extends BaseRepositoryImpl<User> implements UserRepository {
}

TaskRepositoryImpl.java 代码

package org.hzero.todo.infra.repository.impl;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import io.choerodon.core.domain.Page;
import io.choerodon.mybatis.pagehelper.PageHelper;
import io.choerodon.mybatis.pagehelper.domain.PageRequest;
import org.hzero.mybatis.base.impl.BaseRepositoryImpl;
import org.hzero.mybatis.common.Criteria;
import org.hzero.todo.domain.entity.Task;
import org.hzero.todo.domain.repository.TaskRepository;
import org.hzero.todo.infra.mapper.TaskMapper;
/**
 * 任务资源库实现
 */
@Component
public class TaskRepositoryImpl extends BaseRepositoryImpl<Task> implements TaskRepository {
    private final TaskMapper taskMapper;
    public TaskRepositoryImpl(TaskMapper taskMapper) {
        this.taskMapper = taskMapper;
    }
    @Override
    public Page<Task> pageTask(Task task, PageRequest pageRequest) {
        return PageHelper.doPage(pageRequest, () -> taskMapper.selectTask(task));
    }
    @Override
    public List<Task> selectByEmployeeId(Long employeeId) {
        Task task = new Task();
        task.setEmployeeId(employeeId);
        return this.selectOptional(task, new Criteria()
                .select(Task.FIELD_ID, Task.FIELD_EMPLOYEE_ID, Task.FIELD_STATE, Task.FIELD_TASK_DESCRIPTION)
                .where(Task.FIELD_EMPLOYEE_ID)
        );
    }
    @Override
    public Task selectDetailByTaskNumber(String taskNumber) {
        Task params = new Task();
        params.setTaskNumber(taskNumber);
        List<Task> tasks = taskMapper.selectTask(params);
        return CollectionUtils.isNotEmpty(tasks) ? tasks.get(0) : null;
    }
}

(五)编写app-应用层

前置条件

在开发之前,请确保

  • 本地项目已经创建成功,详见 创建项目
  • 数据库创建成功,详见 初始化数据库

介绍

demo 需涉及到 app 层的 service 接口类与其实现类。

特别说明 为了Demo的完整性,这里使用了app层的服务类,如业务十分简单,api层亦可直接调用通用的repository相关方法,不需要 app 服务

service 调用领域对象或服务来解决问题,应用层Service主要有以下特性:

  1. 负责事务处理,所以事务的注解可以在这一层的service中使用。
  2. 只处理非业务逻辑,重点是调度业务处理流程。业务逻辑处理一定要放在领域层处理。
  3. 不做单元测试,只做验收测试。
  4. 可能会有比较多的依赖组件(领域服务),使用field注入依赖的组件。

Service 接口类

  • Service 接口类定义了业务操作的一系列接口,并不提供实现,具体实现需要通过服务实现层提供,所以属于供应方的服务接口层。创建在项目模块的 xxx.app.service 包下。

UserService.java 代码

package org.hzero.todo.app.service;
import org.hzero.todo.domain.entity.User;
/**
 * 用户应用服务
 */
public interface UserService {
    /**
     * 创建用户
     * 
     * @param user User
     * @return User
     */
    User create(User user);
    /**
     * 删除用户(同时删除任务)
     *
     * @param userId 用户ID
     */
    void delete(Long userId);
}

TaskService.java 代码

package org.hzero.todo.app.service;
import org.hzero.todo.domain.entity.Task;
/**
 * 任务应用服务
 */
public interface TaskService {
    /**
     * 创建任务
     *
     * @param task 任务
     * @return Task
     */
    Task create(Task task);
    /**
     * 更新任务
     *
     * @param task 任务
     * @return Task
     */
    Task update(Task task);
    /**
     * 根据任务编号删除
     *
     * @param taskNumber 任务编号
     */
    void deleteByTaskNumber(String taskNumber);
}

Service 实现类

  • Service 接口的具体实现通过服务实现层提供,所以属于供应方的服务实现层。创建在项目模块的 xxx.app.service.impl 包下。
  • 实现类,需要用 @Service 标注

UserServiceImpl.java 代码

package org.hzero.todo.app.service.impl;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import io.choerodon.core.exception.CommonException;
import org.hzero.todo.app.service.UserService;
import org.hzero.todo.domain.entity.Task;
import org.hzero.todo.domain.entity.User;
import org.hzero.todo.domain.repository.TaskRepository;
import org.hzero.todo.domain.repository.UserRepository;
/**
 * 用户应用服务实现
 */
@Service
public class UserServiceImpl implements UserService {
    private final TaskRepository taskRepository;
    private final UserRepository userRepository;
    public UserServiceImpl(TaskRepository taskRepository, UserRepository userRepository) {
        this.taskRepository = taskRepository;
        this.userRepository = userRepository;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public User create(User user) {
        userRepository.insert(user);
        return user;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(Long userId) {
        User exist = userRepository.selectByPrimaryKey(userId);
        if (exist == null) {
            throw new CommonException("htdo.warn.user.notFound");
        }
        // 删除用户
        userRepository.deleteByPrimaryKey(userId);
        // 删除与用户关联的任务
        List<Task> tasks = taskRepository.selectByEmployeeId(userId);
        if (CollectionUtils.isNotEmpty(tasks)) {
            taskRepository.batchDelete(tasks);
        }
    }
}

TaskServiceImpl.java 代码

package org.hzero.todo.app.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import io.choerodon.core.exception.CommonException;
import org.hzero.todo.app.service.TaskService;
import org.hzero.todo.domain.entity.Task;
import org.hzero.todo.domain.repository.TaskRepository;
/**
 * 任务应用服务实现
 */
@Service
public class TaskServiceImpl implements TaskService {
    @Autowired
    private TaskRepository taskRepository;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Task create(Task task) {
        // 生成任务编号
        task.generateTaskNumber();
        // 插入数据
        taskRepository.insert(task);
        return task;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Task update(Task task) {
        Task exist = taskRepository.selectByPrimaryKey(task);
        if (exist == null) {
            throw new CommonException("htdo.warn.task.notFound");
        }
        // 更新指定字段
        taskRepository.updateOptional(task,
                Task.FIELD_STATE,
                Task.FIELD_TASK_DESCRIPTION
        );
        return task;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteByTaskNumber(String taskNumber) {
        Task task = new Task();
        task.setTaskNumber(taskNumber);
        taskRepository.delete(task);
    }
}

(六)编写api-展现层

前置条件

在开发之前,请确保

  • 本地项目已经创建成功,详见 创建项目
  • 数据库创建成功,详见 初始化数据库

介绍

demo 需涉及 api 层的 controller

编写 Controller

  • Controller 负责对 ModelView 的处理,创建在项目模块的 xxx.api.controller.v1 包下。如 xxx.api.controller.v1
  • 需要通过 @Controller 指定该类为一个 Controller 类。

UserController.java代码

package org.hzero.todo.api.controller.v1;
import ....;
/**
 * 用户接口
 */
@Api(tags = SwaggerApiConfig.USER)
@RestController("userController.v1")
@RequestMapping("/v1/users")
public class UserController extends BaseController {
    private final UserService userService;
    private final UserRepository userRepository;
    public UserController(UserService userService, UserRepository userRepository) {
        this.userService = userService;
        this.userRepository = userRepository;
    }
    @Permission(level = ResourceLevel.SITE)
    @ApiOperation(value = "分页查询用户")
    @GetMapping
    public ResponseEntity<Page<User>> list(User user, PageRequest pageRequest) {
        return Results.success(userRepository.pageAndSort(pageRequest, user));
    }
    @Permission(level = ResourceLevel.SITE)
    @ApiOperation(value = "创建 todo 用户")
    @PostMapping
    public ResponseEntity<User> create(@RequestBody User user) {
        // 简单数据校验
        this.validObject(user);
        // 创建用户
        return Results.success(userService.create(user));
    }
    @Permission(level = ResourceLevel.SITE)
    @ApiOperation(value = "删除 todo 用户")
    @DeleteMapping
    public ResponseEntity<User> delete(@RequestBody User user) {
        // 数据防篡改校验
        SecurityTokenHelper.validToken(user);
        // 删除用户
        userService.delete(user.getId());
        return Results.success();
    }
}

TaskController.java代码

package org.hzero.todo.api.controller.v1;
import ....;
/**
 * 任务接口(全是租户级接口)
 */
@Api(tags = SwaggerApiConfig.TASK)
@RestController("taskController.v1")
@RequestMapping("/v1/{organizationId}/tasks")
public class TaskController extends BaseController {
    private final TaskService taskService;
    private final TaskRepository taskRepository;
    public TaskController(TaskService taskService, TaskRepository taskRepository) {
        this.taskService = taskService;
        this.taskRepository = taskRepository;
    }
    /**
     * 注意分页参数是 io.choerodon.mybatis.pagehelper.domain.PageRequest;
     */
    @Permission(level = ResourceLevel.ORGANIZATION)
    @ApiOperation(value = "根据taskNumber分页查询task")
    @GetMapping
    public ResponseEntity<Page<Task>> list(@PathVariable("organizationId") Long tenantId, Task task, PageRequest pageRequest) {
        task.setTenantId(tenantId);
        return Results.success(taskRepository.pageTask(task, pageRequest));
    }
    @Permission(level = ResourceLevel.ORGANIZATION)
    @ApiOperation(value = "创建task")
    @PostMapping
    public ResponseEntity<Task> create(@PathVariable("organizationId") Long tenantId, @RequestBody Task task) {
        task.setTenantId(tenantId);
        // 简单数据校验
        this.validObject(task);
        return Results.success(taskService.create(task));
    }
    @Permission(level = ResourceLevel.ORGANIZATION)
    @ApiOperation(value = "更新task")
    @PutMapping
    public ResponseEntity<Task> update(@PathVariable("organizationId") Long tenantId, @RequestBody Task task) {
        // 简单数据校验
        this.validObject(task);
        // 数据防篡改校验
        SecurityTokenHelper.validToken(task);
        return Results.success(taskService.update(task));
    }
    @Permission(level = ResourceLevel.ORGANIZATION)
    @ApiOperation(value = "根据taskNumber查询task")
    @ApiImplicitParams({
            @ApiImplicitParam(value = "任务编号", paramType = "string")
    })
    @GetMapping("/{taskNumber}")
    public ResponseEntity<Task> query(@PathVariable Long organizationId, @PathVariable String taskNumber) {
        return Results.success(taskRepository.selectDetailByTaskNumber(taskNumber));
    }
    @Permission(level = ResourceLevel.ORGANIZATION)
    @ApiOperation(value = "根据taskNumber删除task")
    @DeleteMapping("/{taskNumber}")
    public void delete(@PathVariable Long organizationId, @PathVariable @ApiParam(value = "任务编号") String taskNumber) {
        taskService.deleteByTaskNumber(taskNumber);
    }
}

编写 SwaggerApiConfig

  • Controller 在 Swagger 上的描述需要定义在配置文件中
  • 一般会把配置相关的建在 config 包下,如 xxx.config
  • SwaggerApiConfig 需要使用 @Configuration 注解标注

SwaggerApiConfig.java 代码

package org.hzero.todo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.Tag;
import springfox.documentation.spring.web.plugins.Docket;
/**
 * Swagger Api 描述配置
 */
@Configuration
public class SwaggerApiConfig {
    public static final String USER = "User";
    public static final String TASK = "Task";
    @Autowired
    public SwaggerApiConfig(Docket docket) {
        docket.tags(
                new Tag(USER, "用户信息"),
                new Tag(TASK, "任务信息")
        );
    }
}

Controller 类相关标签

  • @RestController,是一个组合注解,是 @ResponseBody@Controller 的组合。
  • @Permission,设置API访问权限,常用有三种属性
  • level :设置访问资源层级,包括 siteorganization 两种层级
  • permissionLogin :设置是否需要登录访问
  • permissionPublic :设置任意访问。
  • @ApiOperation ,显示在swagger ui上的接口注释,同时与该接口对应的权限表中的描述字段对应(iam_permission.description)
  • @GetMapping ,是@RequestMapping(mathod = RequestMethod.GET) 的缩写,@PostMapping等同理。
  • @Api(tags = SwaggerApiConfig.TASK),在类上对类进行说明,显示在 Swagger 文档上
  • @ApiImplicitParams,在方法上对方法参数进行说明,显示在 Swagger 文档上
  • @ApiParam,在方法参数上对参数进行说明,显示在 Swagger 文档上

编写DTO

  • DTO 类用来封装用户请求的数据信息,可以用来屏蔽一些程序交互细节。
  • 创建在 项目模块 的 xxx.api.dto 包下,DTO不是必要选项,需要根据需求自行决定。

文档地址

后端开发手册