关于编程模式的总结与思考
淘宝创新业务的优化迭代是非常高频且迅速的,在这过程中要求技术也必须是快且稳的,而为了适应这种快速变化的节奏,我们在项目开发过程中采用了一些面向拓展以及敏捷开发的设计,本文旨在总结并思考其中一些通用的编程模式。
静心守护业务是淘宝今年4月份启动的创新项目,项目的核心逻辑是通过敲木鱼、冥想、盘手串等疗愈玩法为用户带来内心宁静的同时推动文物的保护与修复,进一步弘扬我们的传统文化。
作为创新项目,业务形态与产品方案的优化迭代是非常高频且迅速的:项目从4月底投入开发到7月份最终外灰,整体方案经历过大的推倒重建,也经历过多轮小型重构优化,项目上线后也在做持续的迭代优化甚至改版升级。
▐ 基于Spring容器与反射的策略模式
策略模式是一种经典的行为设计模式,它的本质是定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换,后续也能根据需要灵活拓展出新的算法。这里推荐的是一种基于Spring容器和反射结合的策略模式,这种模式的核心思路是:每个策略模式的实现都是一个bean,在Spring容器启动时基于反射获取每个策略场景的接口类型,并基于该接口类型再获取此类型的所有策略实现bean并记录到一个map(key为该策略bean的唯一标识符,value为bean对象)中,后续可以自定义路由策略来从该map中获取bean对象并使用相应的策略。
模式解构
模式具体实现方式大致如下面的UML类图所描述的:
其中涉及的各个组件及作用分别为:
Handler(interface):策略的顶层接口,定义的type方法表示策略唯一标识的获取方式。
HandlerFactory(abstract class):策略工厂的抽象实现,封装了反射获取Spring bean并维护策略与其标识映射的逻辑,但不感知策略的真实类型。
AbstractHandler(interface or abstracr class):各个具体场景下的策略接口定义,该接口定义了具体场景下策略所需要完成的行为。如果各个具体策略实现有可复用的逻辑,可以结合模版方法模式在该接口内定义模版方法,如果模板方法依赖外部bean注入,则该接口的类型需要为abstract class,否则为interface即可。
HandlerImpl(class):各个场景下策略接口的具体实现,承载主要的业务逻辑,也可以根据需要横向拓展。
HandlerFactoryImpl(class):策略工厂的具体实现,感知具体场景策略接口的类型,如果有定制的策略路由逻辑也可以在此实现。
这种模式的主要优点有:
策略标识维护自动化:策略实现与标识之间的映射关系完全委托给Spring容器进行维护(在HandlerFactory中封装,每个场景的策略工厂直接继承该类即可,无需重复实现),后续新增策略不用再手动修改关系映射。
场景维度维护标识映射:HandlerFactory中在扫描策略bean时是按照AbstractHandler的类型来分类维护的,从而避免了不同场景的同名策略发生冲突。
策略接口按场景灵活定义:具体场景的策略行为定义在AbstractHandler中,在这里可以根据真实的业务需求灵活定义行为,甚至也可以结合其他设计模式做进一步抽象处理,在提供灵活拓展的同时减少重复代码。
实践案例分析
我们先简单了解下该模块的业务背景:静心守护的成就体系中有一类是称号,如下图。用户可以通过多种行为去解锁不同类型的称号,比如说通过参与主玩法(敲木鱼、冥想、盘手串),主玩法参与达到一定次数后即可解锁特定类型的称号。当然后续也可能会有其他种类的称号:比如签到类(按照用户签到天数解锁)、捐赠类(按照用户捐赠项目的行为解锁),所以对于称号的解锁操作应该是面向未来可持续拓展的。
基于这样的思考,我选择使用上面的策略模式去实现称号解锁模块。该模块的核心类图组织如下:
下面是其中部分核心代码的分析解读:
public interface Handler<T> {
/**
* handler类型
*
* @return
*/
T type();
}
@Slf4j
public abstract class HandlerFactory<T, H extends Handler<T>> implements InitializingBean, ApplicationContextAware {
private Map<T, H> handlerMap;
private ApplicationContext appContext;
/**
* 根据 type 获得对应的handler
*
* @param type
* @return
*/
public H getHandler(T type) {
return handlerMap.get(type);
}
/**
* 根据 type 获得对应的handler,支持返回默认
*
* @param type
* @param defaultHandler
* @return
*/
public H getHandlerOrDefault(T type, H defaultHandler) {
return handlerMap.getOrDefault(type, defaultHandler);
}
/**
* 反射获取泛型参数handler类型
*
* @return handler类型
*/
@SuppressWarnings("unchecked")
protected Class<H> getHandlerType() {
Type type = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[1];
//策略接口使用了范型参数
if (type instanceof ParameterizedTypeImpl) {
return (Class<H>) ((ParameterizedTypeImpl)type).getRawType();
} else {
return (Class<H>) type;
}
}
@Override
public void afterPropertiesSet() {
// 获取所有 H 类型的 handlers
Collection<H> handlers = appContext.getBeansOfType(getHandlerType()).values();
handlerMap = Maps.newHashMapWithExpectedSize(handlers.size());
for (final H handler : handlers) {
log.info("HandlerFactory {}, {}", this.getClass().getCanonicalName(), handler.type());
handlerMap.put(handler.type(), handler);
}
log.info("handlerMap:{}", JSON.toJSONString(handlerMap));
}
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
this.appContext = applicationContext;
}
}
HandlerFactory在前面也提到过,是策略工厂的抽象实现,封装了反射获取具体场景策略接口类型,并查找策略bean在内存中维护策略与其标识的映射关系,后续可以直接通过标识或者对应的策略实现。这里有二个细节:
为什么HandlerFactory是abstract class?其实可以看到该类并没有任何抽象方法,直接将其定义为class也不会有什么问题。这里将其定义为abstract class主要是起到实例创建的约束作用,因为我们对该类的定义是工厂的抽象实现,只希望针对具体场景来创建实例,针对该工厂本身创建实例其实是没有任何实际意义的。
getHandlerType方法使用了@SuppressWarnings注解并标记了unchecked。这里也确实是存在潜在风险的,因为Type类型转Class类型属于向下类型转换,是存在风险的,可能其实际类型并非Class而是其他类型,那么此处强转就会出错。这里处理了两种最通用的情况:AbstractHandler是带范型的class和最普通的class。
@Component
public class TitleUnlockHandlerFactory
extends HandlerFactory<String, BaseTitleUnlockHandler<BaseTitleUnlockParams>> {}
public abstract class BaseTitleUnlockHandler<T extends BaseTitleUnlockParams> implements Handler<String> {
@Resource
private UserTitleTairManager userTitleTairManager;
@Resource
private AchievementCountManager achievementCountManager;
@Resource
private UserUnreadAchievementTairManager userUnreadAchievementTairManager;
......
/**
* 解锁称号
*
* @param params
* @return
*/
public @CheckForNull TitleUnlockResult unlockTitles(T params) {
TitleUnlockResult titleUnlockResult = this.doUnlock(params);
if (null == titleUnlockResult) {
return null;
}
List<TitleAchievementVO> titleAchievements = titleUnlockResult.getUnlockedTitles();
if (CollectionUtils.isEmpty(titleAchievements)) {
titleUnlockResult.setUnlockedTitles(new ArrayList<>());
return titleUnlockResult;
}
//基于注入的bean和计算出的称号列表进行后置操作,如:更新成就计数、更新用户称号缓存、更新用户未读成就等
......
return titleUnlockResult;
}
/**
* 计算出要解锁的称号
*
* @param param
* @return
*/
protected abstract TitleUnlockResult doUnlock(T param);
@Override
public abstract String type();
}
BaseTitleUnlockHandler定义了称号解锁行为,并且在此确定了策略标识的类型为String。此外,该类是一个abstract class,是因为该类定义了一个模版方法unlockTitles,在该方法里封装了称号解锁所要进行的一些公共操作,比如更新用户的称号计数、用户的称号缓存数据等,这些都依赖于注入的一些外部bean,而interface不支持非静态成员变量,所以该类通过abstract class来定义。具体的称号解锁行为通过doUnlock定义,这也是该策略的具体实现类需要实现的方法。
@Component
public class GameplayTitleUnlockHandler extends BaseTitleUnlockHandler<GameplayTitleUnlockParams> {
@Resource
private BlessTitleAchievementDiamondConfig blessTitleAchievementDiamondConfig;
@Resource
private UserTitleTairManager userTitleTairManager;
@Override
protected TitleUnlockResult doUnlock(GameplayTitleUnlockParams params) {
//获取称号元数据
List<TitleMetadata> titleMetadata = blessTitleAchievementDiamondConfig.getTitleMetadata();
if (CollectionUtils.isEmpty(titleMetadata)) {
return null;
}
List<TitleAchievementVO> titleAchievements = new ArrayList<>();
Result<DataEntry> result = userTitleTairManager.queryRawCache(params.getUserId());
//用户称号数据查询异常
if (null == result || !result.isSuccess()) {
return null;
}
if (Objects.equals(result.getRc(), ResultCode.SUCCESS)) {
//解锁新称号
titleAchievements = unlockNewTitles(params, titleMetadata);
} else if (Objects.equals(result.getRc(), ResultCode.DATANOTEXSITS)) {
//初始化历史称号
titleAchievements = initHistoricalTitles(params, titleMetadata);
}
TitleUnlockResult titleUnlockResult = new TitleUnlockResult();
titleUnlockResult.setUserTitleCache(result);
titleUnlockResult.setUnlockedTitles(titleAchievements);
return titleUnlockResult;
}
@Override
public String type() {
return TitleType.GAMEPLAY;
}
......
}
上面是一个策略的具体实现类的大致示例,可以看到该实现类核心明确了以下信息:
策略标识:给出了type方法的具体实现,返回了一个策略标识的常量
策略处理逻辑:此处是玩法类称号解锁的业务逻辑,读者无需关注其细节
称号解锁行参:给出了玩法类称号解锁所需的真实参数类型
▐ 抽象疲劳度管控体系
在我们的业务需求中经常会遇到涉及疲劳度管控相关的逻辑,比如每日签到允许用户每天完成1次、首页项目进展弹窗要求对所有用户只弹1次、首页限时回访任务入口则要对用户每天都展示一次,但用户累计完成3次后便不再展示......因此我们设计了一套疲劳度管控的模式,以降低后续诸如上述涉及疲劳度管控相关需求的开发成本。
自顶向下的视角
FatigueLimiter(interface):FatigueLimiter是最顶层抽象的疲劳度管控接口,它定义了疲劳度管控相关的行为,比如:疲劳度的查询、疲劳度清空、疲劳度增加、是否达到疲劳度限制的判断等。 BaseFatigueLdbLimiter(abstract class):疲劳度数据的存储方案可以是多种多样的,在我们项目中主要利用ldb进行疲劳度存储,而BaseFatigueLdbLimiter正是基于ldb【注:阿里内部自研的一款持久化k-v数据库,读者可将其理解为类似level db的项目】对疲劳度数据进行管控的抽象实现,它封装了ldb相关的操作,并基于ldb的数据操作实现了FatigueLimiter的疲劳度管控方法。但它并不感知具体业务的身份和逻辑,因此定义了几个业务相关的方法交给下层去实现,分别是: scene:标识具体业务的场景,会利用该方法返回值去构造Ldb存储的key buildCustomKey:对Ldb存储key的定制逻辑 getExpireSeconds:对应着Ldb存储kv失效时间,对应着疲劳度的管控周期 Ldb周期性疲劳度管控的解决方案层(abstract class):在这一层提供了多种周期的开箱即用的疲劳度管控实现类,如BaseFatigueDailyLimiter提供的是天级别的疲劳度管控能力,BaseFatigueNoCycleLimiter则表示疲劳度永不过期,而BaseFatigueCycleLimiter则支持用户实现cycle方法定制疲劳度周期。 业务场景层:这一层则是各个业务场景对疲劳度管控的具体实现,实现类只需要实现scene方法来声明业务场景的身份标识,随后继承对应的解决方案,即可实现快速的疲劳度管控。比如上面的DailyWishSignLimiter就对应着本篇开头我们所说的“每日签到允许用户每天完成1次”,这就要求为用户的签到行为以天维度构建key同时失效时间也为1天,因此直接继承解决方案层的BaseFatigueDailyLimiter即可。其代码实现非常简单,如下:
@Component
public class DailyWishSignLimiter extends BaseFatigueLdbDailyLimiter {
@Override
protected String scene() {
return LimiterScene.dailyWish;
}
}
有一个“异类”
/**
* 首页入口引导限时任务-天级疲劳度管控
*
*/
@Component
public class HomeEnterGuideDailyLimiter extends BaseFatigueLdbDailyLimiter {
@Override
protected String scene() {
return LimiterScene.homeEnterGuide;
}
}
/**
* 首页入口引导限时任务-总次数疲劳度管控
*
*/
@Component
public class HomeEnterGuideNoCycleLimiter extends BaseFatigueLdbNoCycleLimiter {
@Override
protected String scene() {
return LimiterScene.homeEnterGuide;
}
@Override
protected int maxSize() {
return 3;
}
}
/**
* 首页入口引导限时任务-疲劳度服务
*
*/
@Component
public class HomeEnterGuideLimiter implements FatigueLimiter {
@Resource
private FatigueLimiter homeEnterGuideDailyLimiter;
@Resource
private FatigueLimiter homeEnterGuideNoCycleLimiter;
@Override
public boolean isLimit(String customKey) {
return homeEnterGuideNoCycleLimiter.isLimit(customKey) || homeEnterGuideDailyLimiter.isLimit(customKey);
}
@Override
public Integer incrLimit(String customKey) {
homeEnterGuideDailyLimiter.incrLimit(customKey);
return homeEnterGuideNoCycleLimiter.incrLimit(customKey);
}
@Override
public boolean isLimit(Integer fatigue) {
throw new UnsupportedOperationException();
}
@Override
public Map<String, Integer> batchQueryLimit(List<String> keys) {
throw new UnsupportedOperationException();
}
@Override
public void removeLimit(String customKey) {
homeEnterGuideDailyLimiter.removeLimit(customKey);
homeEnterGuideNoCycleLimiter.removeLimit(customKey);
}
@Override
public Integer queryLimit(String customKey) {
throw new UnsupportedOperationException();
}
/**
* 查询首页限时任务的每日疲劳度
*
* @param customKey 用户自定义key
* @return 疲劳度计数
*/
public Integer queryDailyLimit(String customKey) {
return homeEnterGuideDailyLimiter.queryLimit(customKey);
}
/**
* 查询首页限时任务的全周期疲劳度
*
* @param customKey 用户自定义key
* @return 疲劳度计数
*/
public Integer queryNoCycleLimit(String customKey) {
return homeEnterGuideNoCycleLimiter.queryLimit(customKey);
}
}
▐ 函数式行为参数化
再谈行为参数化
从实践中来,到代码中去
/**
* 清除未读成就
*
* @param uid 用户ID
* @param achievementType 需要清除未读成就列表的成就类型
* @return
*/
public boolean clearUnreadAchievements(long uid, Set<String> achievementTypes) {
if (CollectionUtils.isEmpty(achievementTypes)) {
return true;
}
Result<DataEntry> ldbRes = super.rawGet(buildKey(uid), false);
//用户称号数据查询失败
if (Objects.isNull(ldbRes)) {
recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
return false;
}
boolean success = false;
ResultCode resultCode = ldbRes.getRc();
//不存在用户称号数据则进行初始化
if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type));
success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);
} else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {
DataEntry ldbEntry = ldbRes.getValue();
//存在新数据则对其进行更新
if (Objects.nonNull(ldbEntry)) {
Object data = ldbEntry.getValue();
if (data instanceof String) {
UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type))
success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
}
}
}
//缓存解锁的称号失败
if (!success) {
recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
}
return success;
}
/**
* 写入新的未读成就
*
* @param uid 用户ID
* @param achievementTypeIdMap 需要新增的成就类型和成就ID列表的映射
* @return
*/
public boolean writeUnreadAchievements(long uid, Map<String, List<String>> achievementTypeIdMap) {
if (MapUtils.isEmpty(achievementTypeIdMap)) {
return true;
}
Result<DataEntry> ldbRes = super.rawGet(buildKey(uid), false);
//用户称号数据查询失败
if (Objects.isNull(ldbRes)) {
recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
return false;
}
boolean success = false;
ResultCode resultCode = ldbRes.getRc();
//不存在用户称号数据则进行初始化
if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(userUnreadAchievementsCache, key, value));
success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);
} else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {
DataEntry ldbEntry = ldbRes.getValue();
//存在新数据则对其进行更新
if (Objects.nonNull(ldbEntry)) {
Object data = ldbEntry.getValue();
if (data instanceof String) {
UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(oldCache, key, value));
success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
}
}
}
//缓存解锁的称号失败
if (!success) {
recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
}
return success;
}
从结构上看,上面两段代码其实是非常类似的:整个结构都是先判空,然后查询历史的未读成就数据,如果数据未初始化,则进行初始化,如果已经初始化,则对数据进行更新。只不过写入/清除对数据的初始化和更新逻辑并不相同。因此可以将数据初始化和更新抽象为行为参数,将剩余部分提取为公共方法,基于这样的思路重构后的代码如下:
/**
* 创建or更新缓存
*
* @param uid 用户ID
* @param initCacheSupplier 缓存初始化策略
* @param updater 缓存更新策略
* @return
*/
private boolean upsertCache(long uid, Supplier<UserUnreadAchievementsCache> initCacheSupplier,
Function<UserUnreadAchievementsCache, UserUnreadAchievementsCache> updater) {
Result<DataEntry> ldbRes = super.rawGet(buildKey(uid), false);
//用户称号数据查询失败
if (Objects.isNull(ldbRes)) {
recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
return false;
}
boolean success = false;
ResultCode resultCode = ldbRes.getRc();
//不存在用户称号数据则进行初始化
if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
UserUnreadAchievementsCache userUnreadAchievementsCache = initCacheSupplier.get();
success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);
} else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {
DataEntry ldbEntry = ldbRes.getValue();
//存在新数据则对其进行更新
if (Objects.nonNull(ldbEntry)) {
Object data = ldbEntry.getValue();
if (data instanceof String) {
UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
userUnreadAchievementsCache = updater.apply(userUnreadAchievementsCache);
success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
}
}
}
//缓存解锁的称号失败
if (!success) {
recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
}
return success;
}
/**
* 写入新的未读成就
*
* @param uid 用户ID
* @param achievementTypeIdMap 需要新增的成就类型和成就ID列表的映射
* @return
*/
public boolean writeUnreadAchievements(long uid, Map<String, List<String>> achievementTypeIdMap) {
if (MapUtils.isEmpty(achievementTypeIdMap)) {
return true;
}
return upsertCache(uid,
() -> {
UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(userUnreadAchievementsCache, key, value));
return userUnreadAchievementsCache;
},
oldCache -> {
achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(oldCache, key, value));
return oldCache;
}
);
}
/**
* 清除未读成就
*
* @param uid 用户ID
* @param achievementType 需要清除未读成就列表的成就类型
* @return
*/
public boolean clearUnreadAchievements(long uid, Set<String> achievementTypes) {
if (CollectionUtils.isEmpty(achievementTypes)) {
return true;
}
return upsertCache(uid,
() -> {
UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type));
return userUnreadAchievementsCache;
},
oldCache -> {
achievementTypes.forEach(type -> clearCertainTypeIds(oldCache, type));
return oldCache;
}
);
}
重构的核心是提取了upsert方法,该方法将缓存数据的初始化和更新策略以函数式接口进行定义,从而支持从调用侧进行透传,避免了模板方法的重复编写。这是一个抛砖引玉的例子,在日常开发中,我们可以更多地尝试用函数式编程的思维去思考和重构代码,也许会发现另一个神奇的编程世界。
▐ 切面编程的一些实践
AOP想必大家都已经十分熟悉了,在此便不再赘述其基本概念,而是开门见山直接分享一些AOP在静心守护项目中的实际应用。
服务层异常统一收口
@Slf4j
@HSFProvider(serviceInterface = MtopBlessHomeService.class)
public class MtopBlessHomeServiceImpl implements MtopBlessHomeService {
//依赖的bean注入
......
@Override
public MtopResult<EntranceAVO> entranceA(EntranceARequest request) {
try {
startDiagnose(request.getUserId());
//该入口下的业务逻辑
......
} catch (InteractBizException e) {
log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
"MtopBlessHomeServiceImpl.entranceA", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId());
recordErrorCode(e);
return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
} catch (Exception e) {
log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
"MtopBlessHomeServiceImpl.entranceA", buildMethodParamsStr(request), EagleEye.getTraceId(), e);
recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
return MtopUtils.sysErrMtopResult();
} finally {
DiagnoseClient.end();
}
}
@Override
public MtopResult<EntranceBVO> entranceB(EntranceBRequest request) {
try {
startDiagnose(request.getUserId());
//该入口下的业务逻辑
......
} catch (InteractBizException e) {
log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
"MtopBlessHomeServiceImpl.entranceB", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId());
recordErrorCode(e);
return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
} catch (Exception e) {
log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
"MtopBlessHomeServiceImpl.entranceB", buildMethodParamsStr(request), EagleEye.getTraceId(), e);
recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
return MtopUtils.sysErrMtopResult();
} finally {
DiagnoseClient.end();
}
}
@Override
public MtopResult<EntranceCVO> entranceC(EntranceCRequest request) {
try {
startDiagnose(query.getUserId());
//该入口下的业务逻辑
......
} catch (InteractBizException e) {
log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
"MtopBlessHomeServiceImpl.entranceC", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId());
recordErrorCode(e);
return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
} catch (Exception e) {
log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
"MtopBlessHomeServiceImpl.entranceC", buildMethodParamsStr(request), EagleEye.getTraceId(), e);
recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
return MtopUtils.sysErrMtopResult();
} finally {
DiagnoseClient.end();
}
}
}
每个服务的方法还是需要显式调用工具类方法
为了保证监控信息的齐全,还需要在参数里手动透传一些监控相关的信息
而AOP则不存在这些问题:AOP基于动态代理实现,在实现上述逻辑时对服务层的代码编写完全透明。此外,AOP还封装了调用端方法的各种元信息,可以轻松实现各种监控信息的自动化打印。下面是我们提供的AOP切面。其中值得注意的点是切点的选择要尽量准确,避免增强了不必要的方法。下面我们选择的切点是mtop包下所有Impl结尾类的public方法。
@Aspect
@Component
@Slf4j
public class MtopServiceAspect {
/**
* MtopService层服务
*/
@Pointcut("execution(public com.taobao.mtop.common.MtopResult com.taobao.gaia.veyron.bless.service.mtop.*Impl.*(..))")
public void mtopService(){}
/**
* 对mtop服务进行增强
*
* @param pjp 接入点
* @return
* @throws Throwable
*/
@Around("com.taobao.gaia.veyron.bless.aspect.MtopServiceAspect.mtopService()")
public Object enhanceService(ProceedingJoinPoint pjp) throws Throwable {
try {
startDiagnose(pjp);
return pjp.proceed();
} catch (InteractBizException e) {
log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}",
AspectUtils.extractMethodName(pjp), buildMethodParamsStr(pjp), e.getErrCode(), EagleEye.getTraceId());
recordErrorCode(e);
return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg());
} catch (Exception e) {
log.error("Service invoke fail. Method name:{}, params:{}, trace:{}",
AspectUtils.extractMethodName(pjp), buildMethodParamsStr(pjp), EagleEye.getTraceId(), e);
recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build());
return MtopUtils.sysErrMtopResult();
} finally {
DiagnoseClient.end();
}
}
}
@Slf4j
@HSFProvider(serviceInterface = MtopBlessHomeService.class)
public class MtopBlessHomeServiceImpl implements MtopBlessHomeService {
//依赖的bean注入
......
@Override
public MtopResult<EntranceAVO> entranceA(EntranceARequest request) {
//业务逻辑
......
}
@Override
public MtopResult<EntranceBVO> entranceB(EntranceBRequest request) {
//业务逻辑
......
}
@Override
public MtopResult<EntranceCVO> entranceC(EntranceCRequest request) {
//业务逻辑
......
}
}
切点选择的策略
目标对象规律分布
@Pointcut("execution(public * com.taobao.gaia.serverless.veyron.bless.dao.*DAO.*(..))")
public void charityProjectDataAccess() {
}
这样实现的监控粒度是具体到每个DAO对象-方法级别的粒度,监控效果如下:
一个失效案例
由于各个业务场景的tair管理实现类分散在各个业务包下,想要对它们进行统一切入比较困难。因此我们选择对抽象类进行切入。但这样就会遇到一个同类调用导致AOP失效的问题:抽象类本身不会有实例对象,因此基于CGLIB创建代理对象后,代理对象本质上调用的还是各个业务场景tair管理类的对象,而在使用这些对象时,我们不会直接调用tair抽象类封装的数据访问方法,而是调用这些业务tair管理对象进一步封装的带业务语义的方法,基于这些方法再去调用tair抽象类的数据访问方法。这种同类方法间接调用最终就导致了抽象类的方法没有如期被增强。文字描述兴许有些绕,可以参考下面的图:
我们选择的解决方法则是从上面的MultiClusterTairManager入手,这个类是tair为我们提供的TairManger的一种默认实现,我们之前的做法是为该类实例化一个bean,然后提供给所有业务Tair管理类使用,也就是说所有业务Tair管理类使用的TairManager都是同一个bean实例(因为业务流量没那么大,一个tair实例暂时绰绰有余)。那么我们可以自己提供一个TairManager的实现,基于继承+组合MultiClusterTairManager的方式,只对我们项目内用到数据访问操作进行重写,并委托给原先的MultiClusterTairManager bean进行处理。这样我们可以在设置AOP切点时选择对自己实现的TairManager的所有方法做增强,进而避开上面的问题。经过这样改写后,上面的两张图会演变成下面这样:
基于注解切入
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface VeyronJoinPoint {}
并将该注解标识在需要增强的方法上,随后通过下面的方式描述切点,即可获取到所有需要增强的方法。
@Pointcut("@annotation(com.taobao.gaia.serverless.veyron.aspect.VeyronJoinPoint)")
public void lindormDataAccess() {}
上面的方法也有进一步改良的空间:在注解内增加属性来描述具体的业务场景,不同的切面根据业务场景来对捕获的方法进行过滤,只留下当前业务场景所需要的方法。不然按照现有的做法,如果新的切面也要基于注解来寻找切点,那只能定义新的注解,否则会与原先注解产生冲突。
业务需求千变万化,对应的解法也见仁见智。在研发过程中对各种变化中不变的部分进行总结,从中提取出自己的模式与方法论进行整理沉淀,会让我们以后跑的更快。也正应了学生时期,老师常说的那句话:“我们要把厚厚的书本读薄才能装进脑子里。”
最后,如果大家有好的实践模式推荐或者建议,欢迎在评论区分享交流~
END
大型纪录片《PowerShell 小孩哥传奇》
这里有最新开源资讯、软件更新、技术干货等内容
微信扫码关注该文公众号作者