摘要: 原创出处 http://www.iocoder.cn/Apollo/portal-create-app/ 「芋道源码」欢迎转载,保留摘要,谢谢!
阅读源码最好的方式,是使用 IDEA 进行调试 Apollo 源码,不然会一脸懵逼。 胖友可以点击「芋道源码」扫码关注,回复 git018 关键字 获得艿艿添加了中文注释的 Apollo 源码地址。 阅读源码很孤单,加入源码交流群,一起坚持!
老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》 。
本文分享 Portal 创建 App 的流程,整个过程涉及 Portal、Admin Service ,如下图所示:
流程
下面,我们先来看看 App 的实体结构
老艿艿:因为 Portal 是管理后台,所以从代码实现上,和业务系统非常相像。也因此,本文会略显啰嗦。
在 apollo-common
项目中, com.ctrip.framework.apollo.common.entity.App
,继承 BaseEntity 抽象类,应用信息实体。代码如下:
@Entity
@Table(name = "App")
@SQLDelete(sql = "Update App set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class App extends BaseEntity {
/**
* App 名
*/
@Column(name = "Name", nullable = false)
private String name;
/**
* App 编号
*/
@Column(name = "AppId", nullable = false)
private String appId;
/**
* 部门编号
*/
@Column(name = "OrgId", nullable = false)
private String orgId;
/**
* 部门名
*
* 冗余字段
*/
@Column(name = "OrgName", nullable = false)
private String orgName;
/**
* 拥有人名
*
* 例如在 Portal 系统中,使用系统的管理员账号,即 UserPO.username 字段
*/
@Column(name = "OwnerName", nullable = false)
private String ownerName;
/**
* 拥有人邮箱
*
* 冗余字段
*/
@Column(name = "OwnerEmail", nullable = false)
private String ownerEmail;
}
@SQLDelete(...)
+ @Where(...)
注解,配合 BaseEntity.extends
字段,实现 App 的逻辑删除。com.ctrip.framework.apollo.common.entity.BaseEntity
,基础实体抽象类。代码如下:
@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BaseEntity {
/**
* 编号
*/
@Id
@GeneratedValue
@Column(name = "Id")
private long id;
/**
* 是否删除
*/
@Column(name = "IsDeleted", columnDefinition = "Bit default '0'")
protected boolean isDeleted = false;
/**
* 数据创建人
*
* 例如在 Portal 系统中,使用系统的管理员账号,即 UserPO.username 字段
*/
@Column(name = "DataChange_CreatedBy", nullable = false)
private String dataChangeCreatedBy;
/**
* 数据创建时间
*/
@Column(name = "DataChange_CreatedTime", nullable = false)
private Date dataChangeCreatedTime;
/**
* 数据最后更新人
*
* 例如在 Portal 系统中,使用系统的管理员账号,即 UserPO.username 字段
*/
@Column(name = "DataChange_LastModifiedBy")
private String dataChangeLastModifiedBy;
/**
* 数据最后更新时间
*/
@Column(name = "DataChange_LastTime")
private Date dataChangeLastModifiedTime;
/**
* 保存前置方法
*/
@PrePersist
protected void prePersist() {
if (this.dataChangeCreatedTime == null) dataChangeCreatedTime = new Date();
if (this.dataChangeLastModifiedTime == null) dataChangeLastModifiedTime = new Date();
}
/**
* 更新前置方法
*/
@PreUpdate
protected void preUpdate() {
this.dataChangeLastModifiedTime = new Date();
}
/**
* 删除前置方法
*/
@PreRemove
protected void preRemove() {
this.dataChangeLastModifiedTime = new Date();
}
// ... 省略 setting / getting 方法
}
@MappedSuperclass
注解,见 《Hibernate 中 @MappedSuperclass 注解的使用说明》 文章。@Inheritance(...)
注解,见 《Hibernate(11)映射继承关系二之每个类对应一张表(@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)》 文章。id
字段,编号,Long 型,全局自增。isDeleted
字段,是否删除,用于逻辑删除的功能。dataChangeCreatedBy
和 dataChangeCreatedTime
字段,实现数据的创建人和时间的记录,方便追踪。dataChangeLastModifiedBy
和 dataChangeLastModifiedTime
字段,实现数据的更新人和时间的记录,方便追踪。@PrePersist
、@PreUpdate
、@PreRemove
注解,CRD 操作前,设置对应的时间字段。在文初的流程图中,我们看到 App 创建时,在 Portal Service 存储完成后,会异步同步到 Admin Service 中,这是为什么呢?
在 Apollo 的架构中,一个环境( Env ) 对应一套 Admin Service 和 Config Service 。 而 Portal Service 会管理所有环境( Env ) 。因此,每次创建 App 后,需要进行同步。
或者说,App 在 Portal Service 中,表示需要管理的 App 。而在 Admin Service 和 Config Service 中,表示存在的 App 。
在 apollo-portal
项目中,com.ctrip.framework.apollo.portal.controller.AppController
,提供 App 的 API 。
在创建项目的界面中,点击【提交】按钮,调用创建 App 的 API 。
创建项目
代码如下:
1: @RestController
2: @RequestMapping("/apps")
3: public class AppController {
4:
5: @Autowired
6: private UserInfoHolder userInfoHolder;
7: @Autowired
8: private AppService appService;
9: /**
10: * Spring 事件发布者
11: */
12: @Autowired
13: private ApplicationEventPublisher publisher;
14: @Autowired
15: private RolePermissionService rolePermissionService;
16:
17: /**
18: * 创建 App
19: *
20: * @param appModel AppModel 对象
21: * @return App 对象
22: */
23: @RequestMapping(value = "", method = RequestMethod.POST)
24: public App create(@RequestBody AppModel appModel) {
25: // 将 AppModel 转换成 App 对象
26: App app = transformToApp(appModel);
27: // 保存 App 对象到数据库
28: App createdApp = appService.createAppInLocal(app);
29: // 发布 AppCreationEvent 创建事件
30: publisher.publishEvent(new AppCreationEvent(createdApp));
31: // 授予 App 管理员的角色
32: Set<String> admins = appModel.getAdmins();
33: if (!CollectionUtils.isEmpty(admins)) {
34: rolePermissionService.assignRoleToUsers(RoleUtils.buildAppMasterRoleName(createdApp.getAppId()),
35: admins, userInfoHolder.getUser().getUserId());
36: }
37: // 返回 App 对象
38: return createdApp;
39: }
40:
41: // ... 省略其他接口和属性
42: }
apps
接口,Request Body 传递 JSON 对象。com.ctrip.framework.apollo.portal.entity.model.AppModel
,App Model 。在 com.ctrip.framework.apollo.portal.entity.model
包下,负责接收来自 Portal 界面的复杂请求对象。例如,AppModel 一方面带有创建 App 对象需要的属性,另外也带有需要授权管理员的编号集合 admins
,即存在跨模块的情况。#transformToApp(AppModel)
方法,将 AppModel 转换成 App 对象。? 转换方法很简单,点击方法,直接查看。AppService#createAppInLocal(App)
方法,保存 App 对象到 Portal DB 数据库。在 「3.2 AppService」 中,详细解析。ApplicationEventPublisher#publishEvent(AppCreationEvent)
方法,发布 com.ctrip.framework.apollo.portal.listener.AppCreationEvent
事件。在 apollo-portal
项目中,com.ctrip.framework.apollo.portal.service.AppService
,提供 App 的 Service 逻辑。
#createAppInLocal(App)
方法,保存 App 对象到 Portal DB 数据库。代码如下:
1: @Autowired
2: private UserInfoHolder userInfoHolder;
3: @Autowired
4: private AppRepository appRepository;
5: @Autowired
6: private AppNamespaceService appNamespaceService;
7: @Autowired
8: private RoleInitializationService roleInitializationService;
9: @Autowired
10: private UserService userService;
11:
12: @Transactional
13: public App createAppInLocal(App app) {
14: String appId = app.getAppId();
15: // 判断 `appId` 是否已经存在对应的 App 对象。若已经存在,抛出 BadRequestException 异常。
16: App managedApp = appRepository.findByAppId(appId);
17: if (managedApp != null) {
18: throw new BadRequestException(String.format("App already exists. AppId = %s", appId));
19: }
20: // 获得 UserInfo 对象。若不存在,抛出 BadRequestException 异常
21: UserInfo owner = userService.findByUserId(app.getOwnerName());
22: if (owner == null) {
23: throw new BadRequestException("Application's owner not exist.");
24: }
25: app.setOwnerEmail(owner.getEmail()); // Email
26: // 设置 App 的创建和修改人
27: String operator = userInfoHolder.getUser().getUserId();
28: app.setDataChangeCreatedBy(operator);
29: app.setDataChangeLastModifiedBy(operator);
30: // 保存 App 对象到数据库
31: App createdApp = appRepository.save(app);
32: // 创建 App 的默认命名空间 "application"
33: appNamespaceService.createDefaultAppNamespace(appId);
34: // 初始化 App 角色
35: roleInitializationService.initAppRoles(createdApp);
36: // 【TODO 6001】Tracer 日志
37: Tracer.logEvent(TracerEventType.CREATE_APP, appId);
38: return createdApp;
39: }
AppRepository#findByAppId(appId)
方法,判断 appId
是否已经存在对应的 App 对象。若已经存在,抛出 BadRequestException 异常。UserService#findByUserId(userId)
方法,获得 com.ctrip.framework.apollo.portal.entity.bo.UserInfo
对象。com.ctrip.framework.apollo.portal.entity.bo
包下,负责返回 Service 的业务对象。例如,UserInfo 只包含 com.ctrip.framework.apollo.portal.entity.po.UserPO
的部分属性:userId
、username
、email
。UserInfoHolder#getUser()#getUserId()
方法,获得当前登录用户,并设置为 App 的创建和修改人。关于 UserInfoHolder ,后续文章,详细分享。AppRepository#save(App)
方法,保存 App 对象到数据库中。AppNameSpaceService#createDefaultAppNamespace(appId)
方法,创建 App 的默认 Namespace (命名空间) "application"
。对于每个 App ,都会有一个默认 Namespace 。具体的代码实现,我们在 《Apollo 源码解析 —— Portal 创建 Namespace》在 apollo-portal
项目中,com.ctrip.framework.apollo.common.entity.App.AppRepository
,继承 org.springframework.data.repository.PagingAndSortingRepository
接口,提供 App 的数据访问,即 DAO 。
代码如下:
public interface AppRepository extends PagingAndSortingRepository<App, Long> {
App findByAppId(String appId);
List<App> findByOwnerName(String ownerName, Pageable page);
List<App> findByAppIdIn(Set<String> appIds);
}
基于 Spring Data JPA 框架,使用 Hibernate 实现。详细参见 《Spring Data JPA、Hibernate、JPA 三者之间的关系》 文章。
? 不熟悉 Spring Data JPA 的胖友,可以看下 《Spring Data JPA 介绍和使用》 文章。
com.ctrip.framework.apollo.portal.listener.AppCreationEvent
,实现 org.springframework.context.ApplicationEvent
抽象类,App 创建事件。
代码如下:
public class AppCreationEvent extends ApplicationEvent {
public AppCreationEvent(Object source) {
super(source);
}
public App getApp() {
Preconditions.checkState(source != null);
return (App) this.source;
}
}
#getApp()
方法,获得事件对应的 App 对象。com.ctrip.framework.apollo.portal.listener.CreationListener
,对象创建监听器,目前监听 AppCreationEvent 和 AppNamespaceCreationEvent 事件。
我们以 AppCreationEvent 举例子,代码如下:
1: @Autowired
2: private PortalSettings portalSettings;
3: @Autowired
4: private AdminServiceAPI.AppAPI appAPI;
5:
6: @EventListener
7: public void onAppCreationEvent(AppCreationEvent event) {
8: // 将 App 转成 AppDTO 对象
9: AppDTO appDTO = BeanUtils.transfrom(AppDTO.class, event.getApp());
10: // 获得有效的 Env 数组
11: List<Env> envs = portalSettings.getActiveEnvs();
12: // 循环 Env 数组,调用对应的 Admin Service 的 API ,创建 App 对象。
13: for (Env env : envs) {
14: try {
15: appAPI.createApp(env, appDTO);
16: } catch (Throwable e) {
17: logger.error("Create app failed. appId = {}, env = {})", appDTO.getAppId(), env, e);
18: Tracer.logError(String.format("Create app failed. appId = %s, env = %s", appDTO.getAppId(), env), e);
19: }
20: }
21: }
@EventListener
注解 + 方法参数,表示 #onAppCreationEvent(...)
方法,监听 AppCreationEvent 事件。不了解的胖友,可以看下 《Spring 4.2框架中注释驱动的事件监听器详解》 文章。BeanUtils#transfrom(Class<T> clazz, Object src)
方法,将 App 转换成 com.ctrip.framework.apollo.common.dto.AppDTO
对象。com.ctrip.framework.apollo.common.dto
包下,提供 Controller 和 Service 层的数据传输。? 笔者思考了下,Apollo 中,Model 和 DTO 对象很类似,差异点在 Model 更侧重 UI 界面提交“复杂”业务请求。另外 Apollo 中,还有 VO 对象,侧重 UI 界面返回复杂业务响应。整理如下图:PortalSettings#getActiveEnvs()
方法,获得有效的 Env 数组,例如 PROD
UAT
等。后续文章,详细分享该方法。AppAPI#createApp(Env, AppDTO)
方法,调用对应的 Admin Service 的 API ,创建 App 对象,从而同步 App 到 Config DB。com.ctrip.framework.apollo.portal.api.AdminServiceAPI
,Admin Service API 集合,包含 Admin Service 所有模块 API 的调用封装。简化代码如下:
代码
com.ctrip.framework.apollo.portal.api.API
,API 抽象类。代码如下:
public abstract class API {
@Autowired
protected RetryableRestTemplate restTemplate;
}
restTemplate
的属性注入。对于 RetryableRestTemplate 的源码实现,我们放到后续文章分享。com.ctrip.framework.apollo.portal.api.AdminServiceAPI.AppAPI
,实现 API 抽象类,封装对 Admin Service 的 App 模块的 API 调用。代码如下:
@Service
public static class AppAPI extends API {
public AppDTO loadApp(Env env, String appId) {
return restTemplate.get(env, "apps/{appId}", AppDTO.class, appId);
}
public AppDTO createApp(Env env, AppDTO app) {
return restTemplate.post(env, "apps", app, AppDTO.class);
}
public void updateApp(Env env, AppDTO app) {
restTemplate.put(env, "apps/{appId}", app, app.getAppId());
}
}
restTemplate
,调用对应的 API 接口。在 apollo-adminservice
项目中, com.ctrip.framework.apollo.adminservice.controller.AppController
,提供 App 的 API 。
#create(AppDTO)
方法,创建 App 。代码如下:
1: @RestController
2: public class AppController {
3:
4: @Autowired
5: private AppService appService;
6: @Autowired
7: private AdminService adminService;
8:
9: /**
10: * 创建 App
11: *
12: * @param dto AppDTO 对象
13: * @return App 对象
14: */
15: @RequestMapping(path = "/apps", method = RequestMethod.POST)
16: public AppDTO create(@RequestBody AppDTO dto) {
17: // 校验 appId 格式。若不合法,抛出 BadRequestException 异常
18: if (!InputValidator.isValidClusterNamespace(dto.getAppId())) {
19: throw new BadRequestException(String.format("AppId格式错误: %s", InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE));
20: }
21: // 将 AppDTO 转换成 App 对象
22: App entity = BeanUtils.transfrom(App.class, dto);
23: // 判断 `appId` 是否已经存在对应的 App 对象。若已经存在,抛出 BadRequestException 异常。
24: App managedEntity = appService.findOne(entity.getAppId());
25: if (managedEntity != null) {
26: throw new BadRequestException("app already exist.");
27: }
28: // 保存 App 对象到数据库
29: entity = adminService.createNewApp(entity);
30: // 将保存的 App 对象,转换成 AppDTO 返回
31: dto = BeanUtils.transfrom(AppDTO.class, entity);
32: return dto;
33: }
34:
35: // ... 省略其他接口和属性
36: }
apps
接口,Request Body 传递 JSON 对象。InputValidator#isValidClusterNamespace(appId)
方法,校验 appId
是否满足 "[0-9a-zA-Z_.-]+"
格式。若不合法,抛出 BadRequestException 异常。BeanUtils#transfrom(Class<T> clazz, Object src)
方法,将 AppDTO 转换成 App对象。AppService#findOne(appId)
方法,判断 appId
是否已经存在对应的 App 对象。若已经存在,抛出 BadRequestException 异常。AdminService#createNewApp(App)
方法,保存 App 对象到数据库。BeanUtils#transfrom(Class<T> clazz, Object src)
方法,将保存的 App 对象,转换成 AppDTO 返回。com.ctrip.framework.apollo.biz.service.AdminService
,? 无法定义是什么模块的 Service ,目前仅有 #createNewApp(App)
方法,代码如下:
1: @Service
2: public class AdminService {
3:
4: @Autowired
5: private AppService appService;
6: @Autowired
7: private AppNamespaceService appNamespaceService;
8: @Autowired
9: private ClusterService clusterService;
10: @Autowired
11: private NamespaceService namespaceService;
12:
13: @Transactional
14: public App createNewApp(App app) {
15: // 保存 App 对象到数据库
16: String createBy = app.getDataChangeCreatedBy();
17: App createdApp = appService.save(app);
18: String appId = createdApp.getAppId();
19: // 创建 App 的默认命名空间 "application"
20: appNamespaceService.createDefaultAppNamespace(appId, createBy);
21: // 创建 App 的默认集群 "default"
22: clusterService.createDefaultCluster(appId, createBy);
23: // 创建 Cluster 的默认命名空间
24: namespaceService.instanceOfAppNamespaces(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, createBy);
25: return app;
26: }
27:
28: }
AppService#save(App)
方法,保存 App 对象到数据库中。AppNamespaceService#createDefaultAppNamespace(appId, createBy)
方法,创建 App 的默认 Namespace (命名空间) "application"
。具体的代码实现,我们在 《Apollo 源码解析 —— Portal 创建 Namespace》 详细解析。ClusterService#createDefaultCluster(appId, createBy)
方法,创建 App 的默认 Cluster "default"
。后续文章,详细分享。NamespaceService#instanceOfAppNamespaces(appId, createBy)
方法,创建 Cluster 的默认命名空间。在 apollo-biz
项目中,com.ctrip.framework.apollo.biz.service.AppService
,提供 App 的 Service 逻辑给 Admin Service 和 Config Service 。
#save(App)
方法,保存 App 对象到数据库中。代码如下:
1: @Autowired
2: private AppRepository appRepository;
3: @Autowired
4: private AuditService auditService;
5:
6: @Transactional
7: public App save(App entity) {
8: // 判断是否已经存在。若是,抛出 ServiceException 异常。
9: if (!isAppIdUnique(entity.getAppId())) {
10: throw new ServiceException("appId not unique");
11: }
12: // 保护代码,避免 App 对象中,已经有 id 属性。
13: entity.setId(0); // protection
14: App app = appRepository.save(entity);
15: // 记录 Audit 到数据库中
16: auditService.audit(App.class.getSimpleName(), app.getId(), Audit.OP.INSERT, app.getDataChangeCreatedBy());
17: return app;
18: }
#isAppIdUnique(appId)
方法,判断是否已经存在。若是,抛出 ServiceException 异常。代码如下:
public
boolean
isAppIdUnique(String appId)
{
Objects.requireNonNull(appId, "AppId must not be null");
return Objects.isNull(appRepository.findByAppId(appId));
}
id
属性。AppRepository#save(App)
方法,保存 App 对象到数据库中。com.ctrip.framework.apollo.biz.repository.AppRepository
,继承 org.springframework.data.repository.PagingAndSortingRepository
接口,提供 App 的数据访问 给 Admin Service 和 Config Service 。代码如下:
public interface AppRepository extends PagingAndSortingRepository<App, Long> {
@Query("SELECT a from App a WHERE a.name LIKE %:name%")
List<App> findByName(@Param("name") String name);
App findByAppId(String appId);
}
我们知道,但凡涉及跨系统的同步,无可避免会有事务的问题,对于 App 创建也会碰到这样的问题,例如:
那么 Apollo 是怎么解决这个问题的呢?? 感兴趣的胖友,可以先自己翻翻源码。嘿嘿。