Skip to main content

开发流程

开发流程

环境实施者作业类型作业内容
-设计人员设计分析业务,划分领域模型,设计数据实体,定义资源,设计 REST API
Git 服务器持续集成管理员配置通过 Gitolite 创建业务模块的代码库,并将访问权限授予开发者
持续集成服务器持续集成管理员配置在 Jenkins 中创建自动部署的 Pipeline 任务
Git 服务器持续集成管理员配置为代码库添加 hooks/post-receive 脚本以触发 Jenkins 的自动部署任务
数据库服务器数据库管理员配置创建业务领域的数据库(如果直接对数据库进行操作)
开发环境开发人员开发克隆代码库到本地工作空间
开发环境开发人员开发根据【模块结构说明】创建各 Maven 的 POM 模块
开发环境开发人员开发  创建业务领域通用模块
开发环境开发人员开发    创建业务领域通用的数据实体、数据传输对象、注解等组件
开发环境开发人员开发  创建命令领域模块(如果业务上需要)和查询领域模块(如果业务上需要)
开发环境开发人员开发    创建接口定义模块(domain-command-api 及 domain-query-api)
开发环境开发人员开发      定义数据实体(Domain Entity)
开发环境开发人员开发      定义接口参数及返回值的数据传输对象(DTO)
开发环境开发人员开发      定义 REST API 接口(FeignClient)
开发环境开发人员开发    创建领域模型实现模块(domain-command 及 domain-query)
开发环境开发人员开发      定义数据实体的数据仓库接口
开发环境开发人员开发      定义领域模型服务接口并实现
开发环境开发人员开发      创建 REST Controller(REST API 接口的实现)
开发环境开发人员开发    创建领域服务的启动器(domain-command-starter 及 domain-query-starter)
开发环境开发人员开发      配置应用
开发环境开发人员开发      创建应用入口类(DomainCommandStarter.java 及 DomainQueryStarter.java)
开发环境开发人员开发  在根路径创建 Jenkinsfile 文件,填写自动部署 Pipeline 指令
开发环境开发人员测试在开发环境进行测试
开发环境开发人员集成将代码推送到代码库
Git 服务器dxsuite部署执行 hooks/post-receive,向 Jenkins 发送 Pipeline 编译请求
持续集成服务器dxsuite部署接收到 Pipeline 编译请求,从 Git 拉取代码,执行编译、测试、部署
持续集成服务器dxsuite部署成功部署时,重新启动微服务

数据实体(Domain Entity)定义

所有数据实体都应为 net.dxsuite.cloud.entity.BaseEntity 的派生类。

若要对一个数据实体进行版本管理,则这个数据实体应继承 net.dxsuite.cloud.entity.BaseVersionedEntity

下面以用户业务为例:

configure pipeline
// net.dxsuite.cloud.user.entity.UserBaseEntity
@MappedSuperclass
public abstract class UserBaseEntity extends BaseVersionedEntity {

@Column(nullable = false, length = 45)
private String name;

@Column(length = 255)
private String logo;

@Column(nullable = false, length = 32)
private String username;

/* Getters & Setters */
}
// net.dxsuite.cloud.user.command.entity.UserCommandEntity
@Entity
@Table(name = "user")
public class UserCommandEntity extends UserBaseEntity {

@Column(nullable = false, length = 60)
private String password;

/* Getter & Setter */
}
// net.dxsuite.cloud.user.query.entity.UserQueryEntity
@Entity
@Table(name = "user")
public class UserQueryEntity extends UserBaseEntity {
}

注解说明:

注解说明
@MappedSuperclass标明当前类为实体数据实体的超类
@Entity标明当前类为数据实体,注意,该注解不可与 @MappedSuperclass 一起使用
@Table标明数据与数据实体映射的数据库表名
@Column配置数据库表的列,如列名、是否可以为 NULL、最大长度等

数据传输对象(DTO)定义

数据传输对象是一个简单的 POJO 对象,用于描述客户端提交给服务器的数据结构,如 HTTP 请求 URL 中的查询参数(Query String Parameters)、HTTP 请求体数据(HTTP Request Body)等。

所有数据传输对象都应为 net.dxsuite.cloud.dto.BaseDTO 的派生类。

所有分页查询参数对象都应为 net.clodelet.cloud.dto.PaginationDTO 的派生类。

下面以用户查询为例:

configure pipeline
// net.dxsuite.cloud.user.query.dto.UserQueryDTO
public class UserQueryDTO extends PaginationDTO {

@ApiModelPropery("用户姓名")
private String name;

@ApiModelPropery("登录用户名")
private String username;

/* Getters & Setters */
}

注解说明:

注解说明
@ApiModelPropertySwagger 注解,用于在 API 文档中对属性进行说明

数据仓库(Repository)接口定义

Spring 框架会自动实现 org.springframework.data.repository.Repository 从而实现对数据库持久化数据的操作。

Spring Data JPA 还提供了两个 Repository 接口的扩展接口:

  • org.springframework.data.repository.CrudRepository:定义了基本的 CRUD 操作
  • org.springframework.data.repository.PagingAndSortingRepository:定义了基本分页查询操作

所有查询领域的数据仓库都应扩展自 net.dxsuite.cloud.repository.BaseRepository(以实现关联数据的取得)。

下面以用户查询为例:

configure pipeline
// net.dxsuite.cloud.user.query.repository.UserQueryRepository
public interface UserQueryRepository extends BaseRepository {

Page<UserQueryEntity> findByName(String name, Pageable pageable);

Page<UserQueryEntity> findByUsername(String username, Pageable pageable)

@Query("SELECT u FROM UserQueryEntity u WHERE u.name LIKE %:query.name% OR u.username LIKE %:query.username%")
Page<UserQueryEntity> search(@Param("query") UserQueryDTO query, Pageable pageable);

/* ... */
}

注解说明:

注解说明
@Query定义 JPA 查询语句
@Param定义 JPA 查询语句中的参数

领域业务服务实现

先定义领域业务服务的接口,再对接口进行实现。

数据实体的领域业务服务应实现 net.dxsuite.cloud.service.DomainObjectService

领域业务服务之间不可互相调用,若存在共通业务逻辑,应提取到 Business 中。

如果需要操作 Redis 中的数据则需要继承 net.dxsuite.cloud.service.StringRedisService

下面以用户查询为例:

// net.dxsuite.cloud.user.query.service.UserQueryService
public interface UserQueryService {
Page<UserQueryEntity> search(UserQueryDTO query, Pageable pageable);
UserEntity get(String userId);
}
// net.dxsuite.cloud.user.query.service.impl.UserQueryServiceImpl
@Component
public class UserQueryServiceImpl exends StringRedisService implements UserQueryService, DomainObjectService {

private final UserQueryRepository userQueryRepository;

@Autowired
public UserQueryServiceImpl(StringRedisTemplate stringRedisTemplate, UserQueryRepository userQueryRepository) {
super(stringRedisTemplate);
this.userQueryRepository = userQueryRepository;
}

@Override
public Page<UserQueryEntity> search(UserQueryDTO query, Pageable pageable) {
return userQueryRepository.search(query, pageable);
}

@Override
public get(String userId) {
return userQueryRepository.findById(userId);
}

/* ... */
}

注解说明:

注解说明
@ComponentSpring 将调用构造方法,注入所需组件,并将实例化结果作为 Bean 注册到应用上下文中
@Autowired作用与构造方法时,Spring 将根据方法的参数名从上下文中获取相应的 Bean 并注入到构造方法中

REST API 接口定义及实现

下面以用户查询为例:

// net.dxsuite.cloud.user.query.api.UserQueryApi
@FeignClient(
contextId = "user-query",
name = "${services.user.query.name:user-query}"
)
public interface UserQueryApi {

@GetMapping("/users")
Page<UserQueryEntity> search(@SpringQueryMap UserQueryDTO query);

@GetMapping("/users/{userId}")
UserQueryEntity get(@PathVariable("userId") String userId);
}
// net.dxsuite.cloud.user.query.controller.UserQueryController
@Api(tags = {"用户账号"})
@RestController
public class UserQueryController extends BaseController implements UserQueryApi {

private final UserQueryService userQueryService;

@Autowired
public UserQueryController(@Valid UserQueryService userQueryService) {
this.userQueryService = userQueryService;
}

@Override
@ApiOperation("查询用户")
public Page<UserQueryEntity> search(UserQueryDTO query) {
return userQueryService.search(query, query.toPageable());
}

@Override
@ApiOperation("取得用户信息")
public UserQueryEntity get(@ApiParam("用户 ID") String userId) {
return userQueryService.get(userId);
}
}

注解说明:

注解说明
@FeignClientSpring 框架将根据 REST API 接口实现 HTTP 客户端
@GetMapping定义一个请求方法为 GET 的 REST 路由,另有 @PostMapping@PutMapping@PatchMapping@DeleteMapping
@SpringQueryMap将一个对象或 Map 以 Query 字符串形式传递(而不是作为请求数据 JSON 的形式)
@PathVariable定义 REST API 路径参数
@ApiSwagger API 文档标签
@RestController标明当前类为一个 REST 控制器
@ApiOperation对 REST 路由进行说明
@ApiParam对参数进行说明
@Valid根据 POJO 类中属性的校验注解对属性进行校验,以保证数据的有效性,校验注解说明参照下表

表单数据校验注解说明:

注解示例说明
@NotNull@NotNull不可为 null
@NotEmpty@NotEmpty不可为 null、空字符串、空集合
@NotBlank@NotBlank不可为 null 或空白字符
@Size@Size(min = 3, max = 16)字符串或集合长度必须在 3(含)到 16(含)之间
@Min@Min(0)不得小于 0
@Max@Max(16)不得大于 16
@Pattern@Pattern(regexp = "^[a-zA-Z][0-9a-zA-Z]{2,23}$")由英文字母及数字组成,且第一个字符必须为字母,且总长度必须在 3(含)到 24(含)位之间
@Email@Email必须符合电子邮箱地址格式

HTTP 响应数据的数据结构说明

控制器方法的返回结果将会被包装成 JSON API 的格式(参照 net.dxsuite.cloud.aspect.JsonApiResponseAspect)返回给客户端。

通过 FeignClient 调用其他服务时通过 net.dxsuite.cloud.feign.ResponseDecoder 对转换后的数据进行解码。

典型的响应数据的结构如下:

{
"success": true,
"meta": {
"count": 47,
"pages": 5,
"pageNo": 3,
"pageSize": 10,
"isFirstPage": false,
"hasPreviousPage": true,
"hasNextPage": true,
"isLastPage": false
},
"data": [
{
"id": "BMEDYJ5S1DY5PPQH",
"name": "郭强",
"createdBy": {"$ref": "BMEDYH6EP4Q97Z0M"}
},
{
"id": "BMEDYHJB21ULW3AY",
"name": "姜旭光",
"createdBy": {"$ref": "BMEDYH6EP4Q97Z0M"}
}
],
"included": {
"BMEDYH6EP4Q97Z0M": {
"name": "system"
}
}
}
configure pipeline

国际化(i18n)

国际化消息文件位于模块的 src/main/resources 下,文件名格式为 messages_语言代码.properties

如:

  • messages.properties:默认语言(英文)
  • messages_zh_CN.properties:简体中文
  • messages_jp.properties:日文

下面以用的登录为例:

# messages.properties
error.validation.authentication.username-is-required=username is required.
error.validation.authentication.password-is-required=password is required.
# messages_zh_CN.properties
error.validation.authentication.username-is-required=必须指定登录用户名。
error.validation.authentication.password-is-required=必须指定登录密码。
// net.dxsuite.cloud.auth.command.dto.CredentialDTO
public class CredentialDTO extends BaseDTO {

@ApiModelProperty("登录用户名")
@NotNull(message = "{error.validation.authentication.username-is-required}")
private String username;

@ApiModelProperty("登录密码")
@NotNull(message = "{error.validation.authentication.password-is-required}")
private String password;

/* Getters & Setters */
}

客户端语言通过 Accept-Language 请求头设置。

启动类配置

下面以组织查询为例:

// net.dxsuite.cloud.organization.query.OrganizationQueryStarter
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
@EnableFeignClients({"net.dxsuite.cloud.user.query.api"})
@EntityScan({"net.dxsuite.cloud"})
@EnableJpaRepositories({"net.dxsuite.cloud"})
@ComponentScan({"net.dxsuite.cloud"})
@EnableDiscoveryClient
public class OrganizationQueryStarter {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(OrganizationQueryStarter.class);
application.addListeners(new ApplicationPidFileWriter("./organization-query.pid"));
}
}

注解说明:

注解说明
@SpringBootApplication表示当前模块为一个 Spring Boot 应用
@EnableFeignClientsSpring 将根据指定包内带有 @FeignClient 注解的接口生成 FeignClient Bean
@EntityScan扫描指定包下的数据实体
@EnableJpaRepositoriesSpring 将根据指定的包内的 Repository 接口的定义生成数据仓库的 Bean
@ComponentScan扫描置顶包下的组件、服务、配置等 Bean
@EnableDiscoveryClient当前模块的服务启动后将注册到服务注册中心

应用配置

Spring Boot 工程通过 src/resources/ 下的 application.yml 文件对应用进行配置。

工程的配置可以存放在多个配置文件中,每一个文件对应一个 profile,并命名为 application-profile.yml,

例如,可以将通用配置保存在 application.yml 文件中,将开发环境和产品环境的配置分别保存在 application-development.yml 和 application-production.yml 文件中。

工程打包后,通过 Java 的 -D 参数指定 spring.profiles.active 全局变量即可选择要使用的配置(也可以在 application.yml 文件中设置)。

$ java -Dspring.profiles.active=production -jar ./user-query-0.0.1-SNAPSHOT.jar

application.yml 示例:

server:
port: 8041

spring:
application:
name: user-query
cloud:
consul:
host: 192.168.1.247
port: 8940
discovery:
enabled: true
tags: domain=user,type=query
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: false
hibernate:
ddl-auto: none
datasource:
url: jdbc:mysql://192.168.1.247:3306/codelet_cloud_user?useUnicode=true&characterEncoding=utf-8&useSSL=false

username: dxsuite
password: 1qazxsw2
hikari:
connection-timeout: 60000
minimum-idle: 5
maximum-pool-size: 50
idle-timeout: 120000
max-lifetime: 240000
auto-commit: true
redis:
host: 192.168.1.247
port: 6301
password: 1qazxsw2
database: 0
mail:
default-encoding: UTF-8
host: smtp.mail.com
port: 25
protocol: smtp
username: dxsuite@mail.com
password: Pa5sw0rd
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
connectionTimeout: 60000
tiimeout: 60000
writetimeout: 60000
debug: false

连接到 Redis 集群时使用如下配置:

spring:
redis:
cluster:
nodes: 192.168.1.247:6301,192.168.1.248:6301,192.168.1.249:6301
max-redirects: 8
password: 1qazxsw2
database: 0

可以自定义配置,例如:

application:
security:
access-token-key: MXviFblBleCahWxkR8pXEXq3smAmpLWDqbvl
access-token-ttl: 1296000
access-token-renew-frequency: 86400
cache:
ttl:
user-agent-id: 60
access-token-renewed-at: 15

将应用配置注入到 Bean 中:

@Configuration
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "application.security")
public class SecurityConfiguration {

private String accessTokenKey;

private long accessTokenTtl;

private long accessTokenRenewFrequency;

/* Getters/Setters ... */
}

还可以使用 @Value 注解:

@Component
public class SecurityConfiguration {

@Value("${application.security.access-token-key:Default}")
private String accessTokenKey;

@Value("${application.security.access-token-ttl}")
private long accessTokenTtl;

@Value("${application.security.access-token-renew-frequency}")
private long accessTokenRenewFrequency;

/* Getters/Setters ... */
}