淘宝创新业务的优化迭代是非常高频且迅速的,在这过程中要求技术也必须是快且稳的,而为了适应这种快速变化的节奏,我们在项目开发过程中采用了一些面向拓展以及敏捷开发的设计,本文旨在总结并思考其中一些通用的编程模式。
静心守护业务是淘宝今年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 {
/**
* handler类型
*
* @return
*/
T type();
}
@Slf4j
public abstract class HandlerFactory> implements InitializingBean, ApplicationContextAware {
private Map 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 getHandlerType() {
Type type = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[1];
//策略接口使用了范型参数
if (type instanceof ParameterizedTypeImpl) {
return (Class) ((ParameterizedTypeImpl)type).getRawType();
} else {
return (Class) type;
}
}
@Override
public void afterPropertiesSet() {
// 获取所有 H 类型的 handlers
Collection 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> {}
TitleUnlockHandlerFactory
是策略工厂的具体实现,由于不需要在此定制策略的路由逻辑,所以只声明了相关的参数类型,而没有对父类的方法做什么覆盖。
public abstract class BaseTitleUnlockHandler implements Handler {
@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 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 {
@Resource
private BlessTitleAchievementDiamondConfig blessTitleAchievementDiamondConfig;
@Resource
private UserTitleTairManager userTitleTairManager;
@Override
protected TitleUnlockResult doUnlock(GameplayTitleUnlockParams params) {
//获取称号元数据
List titleMetadata = blessTitleAchievementDiamondConfig.getTitleMetadata();
if (CollectionUtils.isEmpty(titleMetadata)) {
return null;
}
List titleAchievements = new ArrayList();
Result 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失效时间,对应着疲劳度的管控周期
@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 batchQueryLimit(List 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 achievementTypes) {
if (CollectionUtils.isEmpty(achievementTypes)) {
return true;
}
Result 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> achievementTypeIdMap) {
if (MapUtils.isEmpty(achievementTypeIdMap)) {
return true;
}
Result 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 initCacheSupplier,
Function updater) {
Result 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> 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 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 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 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 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 entranceA(EntranceARequest request) {
//业务逻辑
......
}
@Override
public MtopResult entranceB(EntranceBRequest request) {
//业务逻辑
......
}
@Override
public MtopResult 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() {}
上面的方法也有进一步改良的空间:在注解内增加属性来描述具体的业务场景,不同的切面根据业务场景来对捕获的方法进行过滤,只留下当前业务场景所需要的方法。不然按照现有的做法,如果新的切面也要基于注解来寻找切点,那只能定义新的注解,否则会与原先注解产生冲突。
业务需求千变万化,对应的解法也见仁见智。在研发过程中对各种变化中不变的部分进行总结,从中提取出自己的模式与方法论进行整理沉淀,会让我们以后跑的更快。也正应了学生时期,老师常说的那句话:“我们要把厚厚的书本读薄才能装进脑子里。”
最后,如果大家有好的实践模式推荐或者建议,欢迎在评论区分享交流~
团队介绍
我们是淘天业务技术用户消息与社交团队,负责淘宝消息、客服、Push、分享、我淘、关系、社交互动等业务,涵盖淘宝APP中两个一级Tab,第三个消息tab和第五个我的淘宝tab,这里有一流的产品技术,为消费者提供更好的消息与社交服务;丰富的业务场景,为淘系业务增加助力;几十万QPS的高并发流量,可以与淘系各位技术大牛合作,思想激荡碰撞,共同提升,包含以下方向:
-
在淘宝IM基础上构建以用户实时意图感知、统一投放引擎为核心的全域触达体系,通过跨场景的触达方案,赋能淘系搜索、互动、用增等业务增长,每日触达亿级用户。
-
社交域基础平台服务,我的淘宝、淘友、互动等业务,服务上亿淘宝用户。
-
淘宝消息tab、千牛商家消息,通过建立平台,消费者,商家之间的链接,提升手淘DAU,助力商家更好的服务消费者,拥有亿级电商IM消息即时通讯产品,可以深入掌握分布式高可靠设计理念和架构方法论。
招聘持续火热进行中,如果有兴趣可将简历发至lingye.jly@taobao.com,期待您的加入!
拓展阅读
终端技术|
音视频技术
|
技术质量|
数据算法
本文分享自微信公众号 – 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
*欢迎来到上班休息区,请交出你的程序员专属表情包! *这个假期,我们经历了太多的欢乐和放松,现在,是时候重新整理心情,迎接新的工作挑战了。不论你是笑着接受还是哭着面对,这都不重要,因为这就是生活,这就是我们作为职业人士的责任。 以下是一些可以帮助你在假期结束后…