背景:项目中有这种写法:
@Slf4j
@Component
public class MoaServiceProxy {
...
public void appendRecommond(List<LiveFrameAnchorRecommondDTO> dtos){
recommondService.appendRecommond(dtos);
BizLogUtils.bizLog("rank-moa-appendRecommond", JsonUtils.toJSON(dtos));
}
@Cacheable(value = CaffeineConfig.MoaProxy, key = "'getAnchorGradeMedalLabelByUserIds'.concat(#userIds)")
public Map<String, Label> getAnchorGradeMedalLabelByUserIds(List<String> userIds){
return gradeService.getAnchorGradeMedalLabelByUserIds(userIds);
}
作用是通过把rpc方法调用套写一遍,添加日志或缓存的功能。
但这样写,加深了编码层级,调用时也不清楚调用的哪个服务了。亲手套写方法,也浪费了spring的能力。
下面展示一下,使用spring自定义注解的方式,在保持直接注入rpc接口对象的前提下,添加日志或缓存的功能。
最终使用效果:
/**
* 成就服务 - 带业务日志和缓存
* 替代原来的: moaServiceProxy.completeAchievement(userId, achievementId, expireSeconds)
*/
@Resource
@BizLogUtils("'rank-moa-'.concat(#methodName)")
@CacheableAll(value = CaffeineConfig.MoaProxy, key = "#methodName.concat(#arg0).concat(#arg1)")
private AchievementMoaService achievementMoaService;
1、核心代码BeanPostProcessor,作用是在注入接口对象时,对需要扩展功能的接口加一层代理:
/**
* MOA服务字段注解处理器
* 自动处理带有@BizLogUtils和@CacheableAll注解的字段
*/
@Slf4j
@Component
public class MoaServiceFieldProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> clazz = bean.getClass();
// 遍历所有字段
for (Field field : clazz.getDeclaredFields()) {
BizLogUtils bizLogAnnotation = field.getAnnotation(BizLogUtils.class);
CacheableAll cacheableAnnotation = field.getAnnotation(CacheableAll.class);
// 如果字段有这两个注解,则创建代理
if (bizLogAnnotation != null || cacheableAnnotation != null) {
try {
field.setAccessible(true);
Object originalService = field.get(bean);
if (originalService != null) {
// 创建代理对象
Object proxy = MoaServiceProxyFactory.createProxy(
originalService,
bizLogAnnotation,
cacheableAnnotation
);
// 替换原始对象
field.set(bean, proxy);
log.info("Created MOA service proxy for field: {}.{}",
clazz.getSimpleName(), field.getName());
}
} catch (Exception e) {
log.error("Failed to create MOA service proxy for field: {}.{}",
clazz.getSimpleName(), field.getName(), e);
}
}
}
return bean;
}
}
2、代理工厂:
/**
* MOA服务代理工厂
* 用于创建带有缓存和日志功能的MOA服务代理
*/
@Slf4j
public class MoaServiceProxyFactory {
private static final ExpressionParser parser = new SpelExpressionParser();
@Resource
private CacheManager cacheManager;
/**
* 创建MOA服务代理
* @param originalService 原始服务
* @param bizLogAnnotation 业务日志注解
* @param cacheableAnnotation 缓存注解
* @return 代理对象
*/
public static Object createProxy(Object originalService,
BizLogUtils bizLogAnnotation,
CacheableAll cacheableAnnotation) {
return Proxy.newProxyInstance(
originalService.getClass().getClassLoader(),
originalService.getClass().getInterfaces(),
new MoaServiceInvocationHandler(originalService, bizLogAnnotation, cacheableAnnotation)
);
}
/**
* MOA服务调用处理器
*/
private static class MoaServiceInvocationHandler implements InvocationHandler {
private final Object originalService;
private final BizLogUtils bizLogAnnotation;
private final CacheableAll cacheableAnnotation;
private final CacheManager cacheManager;
public MoaServiceInvocationHandler(Object originalService,
BizLogUtils bizLogAnnotation,
CacheableAll cacheableAnnotation) {
this.originalService = originalService;
this.bizLogAnnotation = bizLogAnnotation;
this.cacheableAnnotation = cacheableAnnotation;
this.cacheManager = SpringContextUtil.getBean(CacheManager.class);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// 构建缓存key
String cacheKey = null;
if (cacheableAnnotation != null) {
cacheKey = buildCacheKey(cacheableAnnotation.key(), methodName, args);
}
// 尝试从缓存获取
if (cacheableAnnotation != null) {
Cache cache = cacheManager.getCache(cacheableAnnotation.value());
if (cache != null) {
Cache.ValueWrapper cachedValue = cache.get(cacheKey);
if (cachedValue != null) {
// 记录业务日志
if (bizLogAnnotation != null) {
// 构建业务日志key
String bizLogKey = buildBizLogKey(bizLogAnnotation.value(), methodName, args);
logBizLog(bizLogKey, args, cachedValue.get(), false);
}
return cachedValue.get();
}
}
}
// 执行原始方法
Object result = method.invoke(originalService, args);
// 记录业务日志
if (bizLogAnnotation != null) {
// 构建业务日志key
String bizLogKey = buildBizLogKey(bizLogAnnotation.value(), methodName, args);
logBizLog(bizLogKey, args, result, false);
}
// 缓存结果
if (cacheableAnnotation != null && result != null) {
Cache cache = cacheManager.getCache(cacheableAnnotation.value());
if (cache != null) {
cache.put(cacheKey, result);
}
}
return result;
}
private String buildCacheKey(String cacheKeyExpression, String methodName, Object[] args) {
try {
Expression expression = parser.parseExpression(cacheKeyExpression);
EvaluationContext context = new StandardEvaluationContext();
// 设置方法名
context.setVariable("methodName", methodName);
// 设置参数
for (int i = 0; i < args.length; i++) {
context.setVariable("arg" + i, args[i]);
}
return expression.getValue(context, String.class);
} catch (Exception e) {
log.warn("Failed to build cache key for expression: {}", cacheKeyExpression, e);
return methodName;
}
}
private String buildBizLogKey(String bizLogExpression, String methodName, Object[] args) {
try {
Expression expression = parser.parseExpression(bizLogExpression);
EvaluationContext context = new StandardEvaluationContext();
// 设置方法名
context.setVariable("methodName", methodName);
// 设置参数
for (int i = 0; i < args.length; i++) {
context.setVariable("arg" + i, args[i]);
}
return expression.getValue(context, String.class);
} catch (Exception e) {
log.warn("Failed to build biz log key for expression: {}", bizLogExpression, e);
return "rank-moa-" + methodName;
}
}
private void logBizLog(String bizLogKey, Object[] args, Object result, boolean fromCache) {
try {
String logContent = String.join(",", Arrays.toString(args));
if (result != null) {
logContent += "," + result.toString();
}
if (fromCache) {
logContent += ",fromCache=true";
}
com.immomo.jplive.base.util.BizLogUtils.bizLog(bizLogKey, logContent);
} catch (Exception e) {
log.warn("Failed to log biz log: {}", bizLogKey, e);
}
}
}
}
3、定义扩展功能注解:
/**
* 字段级别缓存注解
* 用于字段级别的缓存配置
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheableAll {
/**
* 缓存配置名称
*/
String value();
/**
* 缓存key表达式,支持SpEL表达式
* 例如: "#methodName.concat(#userIds)"
*/
String key();
}
/**
* 业务日志注解
* 用于字段级别的业务日志记录
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BizLogUtils {
/**
* 业务日志key表达式,支持SpEL表达式
* 例如: "'rank-moa-'.concat(#methodName)"
*/
String value();
}