最近公司項目用到了Apollo,花了幾天功夫把Apollo的官方文檔過了一遍,不得不說寫的非常詳細。基本的使用,已經簡單的原理都介紹的明明白白的。
在文檔上有這麼句話:據說Apollo非常適合作爲初學者第一個通讀源碼學習的分佈式中間件產品
那麼就開始吧。
這裏我主要爲了記錄一些在閱讀Apollo源碼時所學習到的一些開發技巧,方便自己回顧。
我是一邊參考:Apollo源碼解析,一邊自己閱讀源碼,有很多東西也是從那邊粘過來的。
APP
首先我們看一下apollo-common
模塊中的com.ctrip.framework.apollo.common.entity.App
:
@Entity
@Table(name = "App")
@SQLDelete(sql = "Update App set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class App extends BaseEntity {
@NotBlank(message = "Name cannot be blank")
@Column(name = "Name", nullable = false)
private String name;
......
public static class Builder {
public Builder() {
}
private App app = new App();
public Builder name(String name) {
app.setName(name);
return this;
}
......
public App build() {
return app;
}
}
public static Builder builder() {
return new Builder();
}
}
- 使用@SQLDelete來實現邏輯刪除,只對Hibernate起作用,它會在我們調用delete刪除該對象的時候執行@SQLDelete註解中的sql。
- 使用@Where是爲了配合邏輯刪除做查詢條件的,也是在Hibernate中起作用,它會在我們調用查詢方法時,在where條件中追加@Where中的clause。
- 繼承BaseEntity是爲了抽取出Entity中的公共屬性。減少重複代碼。
- 使用建造者模式,將對象複雜的構建邏輯與展示代碼分離。
順着這條線,看一下com.ctrip.framework.apollo.common.entity.BaseEntity
:
@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Id")
private long id;
@Column(name = "IsDeleted", columnDefinition = "Bit default '0'")
protected boolean isDeleted = false;
@Column(name = "DataChange_CreatedBy", nullable = false)
private String dataChangeCreatedBy;
@Column(name = "DataChange_CreatedTime", nullable = false)
private Date dataChangeCreatedTime;
@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();
}
- @Inheritance註解是用在有繼承關係的對象,strategy是對於繼承關係,使用哪種策略映射數據表。SINGLE_TABLE:所有父子類的字段在一張表中;TABLE_PER_CLASS:每個父子類獨立一張表,子類的表會包含父類和自己的全部字段;JOINED:每個父子類獨立一張表,表與其對應的類字段完全一樣,父子關係用外鍵來關聯。
- @MappedSuperclass註解只能在類上,標註爲@MappedSuperclass的類將不是一個完整的實體類,他將不會映射到數據庫表,但是他的屬性都將映射到其子類的數據庫字段中。
- @PrePersist:該對象持久化之前;@PreUpdate:該對象更新之前;@PreRemove:該對象刪除之前,而且這裏既然有@Prexxx,那必然會有@Postxxx,用於對象做完某件事之後的操作。
接下來我們追一下創建App的流程,apollo-portal
模塊下的com.ctrip.framework.apollo.portal.controller.AppController
:
@RestController
@RequestMapping("/apps")
public class AppController {
private final AppService appService;
private final ApplicationEventPublisher publisher;
private final RolePermissionService rolePermissionService;
@PostMapping
public App create(@Valid @RequestBody AppModel appModel) {
App app = transformToApp(appModel);
App createdApp = appService.createAppInLocal(app);
publisher.publishEvent(new AppCreationEvent(createdApp));
Set<String> admins = appModel.getAdmins();
if (!CollectionUtils.isEmpty(admins)) {
rolePermissionService
.assignRoleToUsers(RoleUtils.buildAppMasterRoleName(createdApp.getAppId()),
admins, userInfoHolder.getUser().getUserId());
}
return createdApp;
}
}
- 通過事件驅動異步的將創建App的信息同步到Admin模塊,減少了主流程的執行時間,降低代碼間的耦合度。
時間的監聽者在這com.ctrip.framework.apollo.portal.listener.CreationListener
:
@EventListener
public void onAppCreationEvent(AppCreationEvent event) {
AppDTO appDTO = BeanUtils.transform(AppDTO.class, event.getApp());
List<Env> envs = portalSettings.getActiveEnvs();
for (Env env : envs) {
try {
appAPI.createApp(env, appDTO);
} catch (Throwable e) {
logger.error("Create app failed. appId = {}, env = {})", appDTO.getAppId(), env, e);
Tracer.logError(String.format("Create app failed. appId = %s, env = %s", appDTO.getAppId(), env), e);
}
}
}
- 這個方法就會監聽AppCreationEvent事件,事件觸發後會獲取到App對象發送給Admin模塊
項目中大量的使用了各種各樣的實體對象,這裏總結一點我自己的理解:
- Model對象只用於接受Controller層的入參
- VO對象只用於Controller層的出參
- DTO對象用於中間層的數據傳輸,比如service層之間、服務調用之間等
- PO只用於與數據庫的交互
Cluster
同樣也是追創建cluster的流程,先來看一下apollo-biz
模塊下的com.ctrip.framework.apollo.biz.entity.Cluster
:
@Entity
@Table(name = "Cluster")
@SQLDelete(sql = "Update Cluster set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class Cluster extends BaseEntity implements Comparable<Cluster> {
@Column(name = "Name", nullable = false)
private String name;
@Column(name = "AppId", nullable = false)
private String appId;
@Column(name = "ParentClusterId", nullable = false)
private long parentClusterId;
......
}
- parentClusterId這個字段用於灰度發佈時記錄回滾版本號
再看一下apollo-portal
模塊下的com.ctrip.framework.apollo.portal.controller.ClusterController
:
@RestController
public class ClusterController {
private final ClusterService clusterService;
private final UserInfoHolder userInfoHolder;
@PreAuthorize(value = "@permissionValidator.hasCreateClusterPermission(#appId)")
@PostMapping(value = "apps/{appId}/envs/{env}/clusters")
public ClusterDTO createCluster(@PathVariable String appId, @PathVariable String env,
@Valid @RequestBody ClusterDTO cluster) {
String operator = userInfoHolder.getUser().getUserId();
cluster.setDataChangeLastModifiedBy(operator);
cluster.setDataChangeCreatedBy(operator);
return clusterService.createCluster(Env.valueOf(env), cluster);
}
......
}
- 這裏使用@PreAuthorize註解對方法做前置的權限校驗
然後這裏是apollo-adminservice
模塊中保存cluster的方法:
@Transactional
public Cluster saveWithoutInstanceOfAppNamespaces(Cluster entity) {
if (!isClusterNameUnique(entity.getAppId(), entity.getName())) {
throw new BadRequestException("cluster not unique");
}
entity.setId(0);//protection
Cluster cluster = clusterRepository.save(entity);
auditService.audit(Cluster.class.getSimpleName(), cluster.getId(), Audit.OP.INSERT,
cluster.getDataChangeCreatedBy());
return cluster;
}
entity.setId(0);
這裏給對象id設置爲0是防止被注入id導致更新對象