private String ownerEmail;
}
-
ORM 选用 Hibernate 框架。
-
@SQLDelete(…) + @Where(…) 注解,配合 BaseEntity.extends 字段,实现 App 的逻辑删除。
-
字段比较简单,胖友看下注释。
2.1 BaseEntity
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 操作前,设置对应的时间字段。
-
在 Apollo 中,所有实体都会继承 BaseEntity ,实现公用字段的统一定义。这种设计值得借鉴,特别是创建时间和更新时间这两个字段,特别适合线上追踪问题和数据同步。
2.2 为什么需要同步
在文初的流程图中,我们看到 App 创建时,在 Portal Service 存储完成后,会异步同步到 Admin Service 中,这是为什么呢?
在 Apollo 的架构中,一个环境( Env ) 对应一套 Admin Service 和 Config Service 。
而 Portal Service 会管理所有环境( Env ) 。因此,每次创建 App 后,需要进行同步。
或者说,App 在 Portal Service 中,表示需要管理的 App 。而在 Admin Service 和 Config Service 中,表示存在的 App 。
3. Portal 侧
================
3.1 AppController
在 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 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: }
-
POST `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,即存在跨模块的情况。
-
第 26 行:调用 `#transformToApp(AppModel)` 方法,将 AppModel 转换成 App 对象。 转换方法很简单,点击方法,直接查看。
-
第 28 行:调用 AppService#createAppInLocal(App) 方法,保存 App 对象到 Portal DB 数据库。在 「3.2 AppService」 中,详细解析。
-
第 30 行:调用 ApplicationEventPublisher#publishEvent(AppCreationEvent) 方法,发布 com.ctrip.framework.apollo.portal.listener.AppCreationEvent 事件。
-
第 31 至 36 行:授予 App 管理员的角色。详细解析,见 《Apollo 源码解析 —— Portal 认证与授权(二)之授权》 。
-
第 38 行:返回创建的 App 对象。
3.2 AppService
在 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: }
-
第 15 至 19 行:调用 AppRepository#findByAppId(appId) 方法,判断 appId 是否已经存在对应的 App 对象。若已经存在,抛出 BadRequestException 异常。
-
第 20 至 25 行:调用 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 。
-
第 27 至 29 行:调用 UserInfoHolder#getUser()#getUserId() 方法,获得当前登录用户,并设置为 App 的创建和修改人。关于 UserInfoHolder ,后续文章,详细分享。
-
第 31 行:调用 AppRepository#save(App) 方法,保存 App 对象到数据库中。
-
第 33 行:调用 AppNameSpaceService#createDefaultAppNamespace(appId) 方法,创建 App 的默认 Namespace (命名空间) “application” 。对于每个 App ,都会有一个默认 Namespace 。具体的代码实现,我们在 《Apollo 源码解析 —— Portal 创建 Namespace》
-
第 35 行:初始化 App 角色。详解解析,见 《Apollo 源码解析 —— Portal 认证与授权(二)之授权》 。
-
第 37 行:【TODO 6001】Tracer 日志
3.3 AppRepository
在 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 findByOwnerName(String ownerName, Pageable page);
List findByAppIdIn(Set appIds);
}
基于 Spring Data JPA 框架,使用 Hibernate 实现。详细参见 《Spring Data JPA、Hibernate、JPA 三者之间的关系》 文章。
不熟悉 Spring Data JPA 的胖友,可以看下 《Spring Data JPA 介绍和使用》 文章。
3.4 AppCreationEvent
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;
}
}
-
构造方法,将 App 对象作为方法参数传入。
-
#getApp() 方法,获得事件对应的 App 对象。
3.4.1 CreationListener
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 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框架中注释驱动的事件监听器详解》 文章。
-
第 9 行:调用 BeanUtils#transfrom(Class 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 界面返回复杂业务响应。整理如下图:
-
各种 Entity 整理
-
老艿艿认为,PO 对象,可以考虑不暴露给 Controller 层,只在 Service 和 Repository 之间传递和返回。
-
和彩笔老徐交流了下,实际项目可以简化,使用 VO + DTO + PO 。
-
第 11 行:调用 PortalSettings#getActiveEnvs() 方法,获得有效的 Env 数组,例如 PROD UAT 等。后续文章,详细分享该方法。
-
第 12 至 20 行:循环 Env 数组,调用 AppAPI#createApp(Env, AppDTO) 方法,调用对应的 Admin Service 的 API ,创建 App 对象,从而同步 App 到 Config DB。
3.5 AdminServiceAPI
com.ctrip.framework.apollo.portal.api.AdminServiceAPI ,Admin Service API 集合,包含 Admin Service 所有模块 API 的调用封装。简化代码如下:
代码
3.5.1 API
com.ctrip.framework.apollo.portal.api.API ,API 抽象类。代码如下:
public abstract class API {
@Autowired
protected RetryableRestTemplate restTemplate;
}
- 提供统一的 restTemplate 的属性注入。对于 RetryableRestTemplate 的源码实现,我们放到后续文章分享。
3.5.2 AppAPI
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());
}