最近接了一个埋点的需求,与我们平常接触的埋点不同, 平常接触的埋点或是对用户的点击率或登录情况等用户侧进行埋点 , 而这一次接触的埋点是: 对某一个功能的业务流程进行埋点统计, 主要是提供 分析在业务流程操作中, 一般出错率最高是在哪一个流程之中。
基于这一需求结合现有的成熟产品, 我们应该怎么设计才能达到: 高效、高质量、易扩展三结合为一体的功能?
实现的方式有很多,但需与当前项目的结构相匹配才是最好的实现方式。
分析问题的方向:什么人在什么地方操作了什么?
针对这一问题分析如下:
1.业务功能的操作,操作的是接口,那么功能的实现是针对接口进行埋点(因项目已实现了登录权限,则可获取当前操作人,一般的项目都是必须得有)
2.同一个操作可能针对不同的业务端,比如,上传用户数据,而在操作端这一块, 可能有分B端,管理端或C端等, 那么需对操作端进行汇总记录。(因项目的接口有对业务端进行区分模块,则我是知道哪一端)
3.不同的操作可能操作不同的业务功能,那么需对这些操作进行业务功能汇总: 比如,在不同的业务功能中, 都有导入execl数据, 那么可以把这些业务功能归类为:上传数据(自定义)。
好,分析完, 上设计图:
主要使用的技术: 自定义注解,切面,多线程异步处理
设计模式:策略模式,模板模式
代码如下:
/**
* 事件埋点注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventTrackingAnnotation {
/**
* 事件埋点策略枚举
* @return
*/
EventTrackingEnum eventTrackingEnum();
/**
* 模块标识
* @return
*/
BuriedPointModule buriedPointModule();
/**
* 平台标识
* @return
*/
BuriedPointPlatform buriedPointPlatform();
/**
* 事件埋点节点
*/
BuriedPointNode buriedPointNode();
/**
* 事件埋点切面
*/
@Slf4j
@Component
@Aspect
@Order(1)
public class EventTrackingAspect {
@Resource
private ApplicationEventPublisher publisher;
@Pointcut("@annotation(com.test.test.strategy.event.tracking.annotation.EventTrackingAnnotation)")
public void eventTracking() {
}
/**
* 环绕
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("eventTracking()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//获取注解的入参参数
EventTrackingAnnotation annotation = method.getAnnotation(EventTrackingAnnotation.class);
//事件埋点策略枚举
EventTrackingEnum eventTrackingEnum = annotation.eventTrackingEnum();
//模块标识
BuriedPointModule buriedPointModule = annotation.buriedPointModule();
//平台标识
BuriedPointPlatform buriedPointPlatform = annotation.buriedPointPlatform();
//事件埋点节点
BuriedPointNode buriedPointNode = annotation.buriedPointNode();
//获取业务参数
Object[] args = joinPoint.getArgs();
Map<String, Object> fieldsName = getFieldsName(method,args);
boolean flag = true;
if (fieldsName != null) {
for (Map.Entry<String, Object> obj : fieldsName.entrySet()) {
Object value = obj.getValue();
//这里需要过滤导入导出等参数接口 如果不过滤则打印json时会报错 踩坑后补上的逻辑
//测试只需要将接口中的参数加入 一下三个参数中的一个即可(注释到下面的判断)测试出结果
//当然这个目前项目中只有这三种不能打印,其他的没有一一测试,以实际为准
if (value instanceof MultipartFile || value instanceof HttpServletRequest || value instanceof HttpServletResponse) {
flag = false;
}
}
}
if (flag) {
log.info("请求参数为:{}", JSON.toJSONString(fieldsName));
}
boolean isSuccess = true ;
String message = null;
try {
// result的值就是被拦截方法的返回值
Object result = joinPoint.proceed();
log.info("请求结束,controller的返回值是:{}", JSON.toJSONString(result));
return result;
} catch (Throwable throwable){
isSuccess = false;
message = throwable.getMessage();
throw throwable;
} finally {
// 异步处理事件埋点记录
EventTrackingEvent event = new EventTrackingEvent();
event.setEventTrackingEnum(eventTrackingEnum);
event.setBuriedPointModule(buriedPointModule);
event.setBuriedPointPlatform(buriedPointPlatform);
event.setBuriedPointNode(buriedPointNode);
event.setState(isSuccess);
event.setFailMsg(message);
event.setFieldsName(fieldsName);
publisher.publishEvent(event);
}
}
/**
* 根据切面获取请求参数列表
*/
private static Map<String, Object> getFieldsName(Method method,Object[] args) {
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
String[] parameterNames = pnd.getParameterNames(method);
Map<String, Object> paramMap = new HashMap<>(32);
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
}
return paramMap;
}
}
/**
* 事件埋点异步处理器
*/
@Slf4j
@Component
public class EventTrackingListener {
@Autowired
private EventTrackingFactory eventTrackingFactory;
@Async
@TransactionalEventListener(fallbackExecution = true, phase = TransactionPhase.AFTER_COMMIT)
public void touchTrackingFactory(EventTrackingEvent event) {
log.info("事件埋点异步处理器,event={}",event);
EventTrackingEnum eventTrackingEnum = event.getEventTrackingEnum();
BuriedPointModule buriedPointModule = event.getBuriedPointModule();
BuriedPointPlatform buriedPointPlatform = event.getBuriedPointPlatform();
BuriedPointNode buriedPointNode = event.getBuriedPointNode();
boolean state = event.isState();
String failMsg = event.getFailMsg();
Map<String, Object> fieldsName = event.getFieldsName();
//初始化具体的策略
EventTrackingStrategy strategy = eventTrackingFactory.getStrategy(eventTrackingEnum);
//各自的实现策略根据接口入参, 各自封装自己需要的业务参数
Map<String, Object> stringObjectMap = strategy.initParam(fieldsName);
//触发埋点
TrackingDTO trackingDTO = new TrackingDTO();
trackingDTO.setBuriedPointModule(buriedPointModule);
trackingDTO.setBuriedPointPlatform(buriedPointPlatform);
trackingDTO.setBuriedPointNode(buriedPointNode);
trackingDTO.setState(state);
trackingDTO.setFailMsg(failMsg);
trackingDTO.setData(stringObjectMap);
strategy.tracking(trackingDTO);
}
}
/**
* 事件埋点工厂类
*/
@Slf4j
@Component
public class EventTrackingFactory {
Map<EventTrackingEnum,EventTrackingStrategy> handlerMap = new HashMap<>(4);
@PostConstruct
private void init(){
Map<String, EventTrackingStrategy> beansMap = SpringHelper.getApplicationContext().getBeansOfType(EventTrackingStrategy.class);
beansMap.forEach((k,v)->{
this.handlerMap.put(v.getStrategy(),v);
});
}
public EventTrackingStrategy getStrategy(EventTrackingEnum strategyEnum){
return handlerMap.get(strategyEnum);
}
}
/**
* 事件埋点策略抽象类
*/
public interface EventTrackingStrategy {
/**
* 获取指定策略
* @return
*/
EventTrackingEnum getStrategy();
/**
* 封装参数
* @return 用Map的原因是针对不同的接口,有对应的业务参数, 可对业务参数处理完后返回自身需要的参数
*/
Map<String,Object> initParam(Map<String,Object> paramMap);
/**
* 埋点记录
* @return
*/
void tracking(TrackingDTO trackingDTO);
}
/**
* 事件埋点基类
*/
public class EventTrackingBase {
/**
* 埋点记录
* @param signBuriedPointLog
*/
public void sendSls(SignBuriedPointLog signBuriedPointLog) {
//根据需求对埋点数据进行入库操作
}
/**
* Object转成指定的类型
* @param obj
* @param type
* @param <T>
* @return
*/
public static<T> T convert(Object obj, Class<T> type) {
if (obj != null && StringUtils.isNotBlank(obj.toString())) {
if (type.equals(Integer.class)||type.equals(int.class)) {
return (T)Integer.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Long.class)||type.equals(long.class)) {
return (T)Long.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Boolean.class)||type.equals(boolean.class)) {
return (T)Boolean.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Short.class)||type.equals(short.class)) {
return (T)Short.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Float.class)||type.equals(float.class)) {
return (T)Float.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Double.class)||type.equals(double.class)) {
return (T)Double.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Byte.class)||type.equals(byte.class)) {
return (T)Byte.valueOf(StringUtils.trim(obj.toString()));
} else if (type.equals(Character.class)||type.equals(char.class)) {
return (T)Character.valueOf(obj.toString().charAt(0));
} else if (type.equals(String.class)) {
return (T) obj;
} else if (type.equals(BigDecimal.class)) {
return (T) new BigDecimal(StringUtils.trim(obj.toString()));
} else if (type.equals(LocalDateTime.class)) {
return (T) LocalDateTime.parse(obj.toString());
} else if (type.equals(Date.class)) {
try {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return (T) formatter.parse(obj.toString());
} catch (ParseException e) {
throw new RuntimeException(e.getMessage());
}
}else{
return null;
}
} else {
return null;
}
}
}
以下为策略具体的实现类,我就放一个了, 其他可自行实现
/**
* 数据上传埋点策略实现类
*/
@Slf4j
@Component
public class DataUploadEventTrackingStrategy extends EventTrackingBase implements EventTrackingStrategy {
@Override
public EventTrackingEnum getStrategy() {
return EventTrackingEnum.DATA_UPLOAD;
}
@Override
public Map<String,Object> initParam(Map<String,Object> paramMap) {
return null;
}
@Override
public void tracking(TrackingDTO trackingDTO) {
BuriedPointModule buriedPointModule = trackingDTO.getBuriedPointModule();
BuriedPointPlatform buriedPointPlatform = trackingDTO.getBuriedPointPlatform();
BuriedPointNode buriedPointNode = trackingDTO.getBuriedPointNode();
boolean state = trackingDTO.isState();
String failMsg = trackingDTO.getFailMsg();
Map<String, Object> data = trackingDTO.getData();
Date date = new Date();
String traceId = TraceContext.traceId();
//触发埋点记录
SignBuriedPointLog signBuriedPointLog = SignBuriedPointLog.builder()
.bizTime(date)
.traceId(traceId)
.module(buriedPointModule.name())
.platform(buriedPointPlatform.name())
.node(buriedPointNode.name())
.resultFlag(state)
.failReason(failMsg)
.build();
sendSls(signBuriedPointLog);
}
}
好,总体思路是这样, 部分实体类对象就不提供了, 分享的是一种解决该类似需求的方案。
个人能力有限,各位大佬可一起探讨更多更好的实现
不喜勿喷哈
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。