需求
通常情况下,如果我们没有系统内部的调用情况,比如我们这里重点聚焦的Service层的接口性能指标 ,比如 调用次数、Avg执行时间、Min执行时间、Max执行时间、成功次数、失败次数、慢执行次数等等,以及根据监控结果触发某些告警等等 ,上述指标都是没有办法很灵活的采集到的
采集方案
我们先来讨论下实现上述需求的方案
- 硬编码
- AOP
- JavaAgent 字节码插桩
那如何做到更灵活的实现代码零侵入的实现Service层的接口监控呢?
OK ,直奔主题 。方案必然是第三种,使用字节码插桩实现Service的零侵入监控
采集目标
我们需要对哪些对象插桩呢?
@Service注解 标注的类吗? 这里犯了一个致命的错误,如果想要做这种底层的基础组件,不要对用户的使用场景做设定 ,方案要更具有通用性
我们更倾向于让用户自主配置监控的 include 与 exclude .
我们不知道统计哪个类,也不知道统计哪个方法 ,一切都是基于用户自主的配置
模型设计
核心: 使用JavaAgent获取到用户配置的数据, 匹配(排除)后 使用javassist来修改字节码,进行插桩 ,插入我们的监控逻辑。
那我们都需要监控哪些指标呢?
开始时间、用时、异常消息、异常类型、服务类名、方法名 ,当然了都是可以扩展的比如我们可以增加主机IP、应用名称、标识追踪ID等等
简单起见,我们先不引入过多的字段。
Code
我们先聚焦到Javassit修改字节码,不要和JavaAgent掺和在一起,先实现第一步
public class ArtisanServiceCollect extends AbstractCollect { private String targetPackage; public ArtisanServiceCollect(String target_package) { this.targetPackage = target_package; } public void transform(Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className == null) { return null; } if (!className.startsWith(targetPackage.replaceAll("\\.", "/"))) { return null; } try { return buildCtClass(loader, className. replaceAll("/", ".")).toBytecode(); } catch (Exception e) { e.printStackTrace(); } return null; } }); } public CtClass buildCtClass(ClassLoader loader, String className) throws NotFoundException { ClassPool pool = new ClassPool(); pool.insertClassPath(new LoaderClassPath(loader)); CtClass ctClass = pool.get(className); CtMethod[] methods = ctClass.getDeclaredMethods(); for (CtMethod m : methods) { if (!Modifier.isPublic(m.getModifiers())) { continue; } if (Modifier.isStatic(m.getModifiers())) { continue; } if (Modifier.isNative(m.getModifiers())) { continue; } try { buildMethod(ctClass, m); } catch (Exception e) { e.printStackTrace(); } } return ctClass; } public void buildMethod(CtClass ctClass, CtMethod oldMethod) throws Exception { // copy 一个方法 // 修改源方法名称 $agent // 原方法中 插入模板代码 CtMethod newMethod = CtNewMethod.copy(oldMethod, ctClass, null); oldMethod.setName(oldMethod.getName() + "$agent"); String beginSrc = String. format("Object stat=com.artisan.agent.collect.ServiceCollect.begin(\"%s\",\"%s\");", ctClass.getName(), oldMethod.getName()); String errorSrc = "com.artisan.agent.collect.ServiceCollect.error(e,stat);"; String endSrc = "com.artisan.agent.collect.ServiceCollect.end(stat);"; String template = oldMethod.getReturnType().getName().equals("void") ? voidSource : source; newMethod.setBody(String.format(template, beginSrc, newMethod.getName(), errorSrc, endSrc)); ctClass.addMethod(newMethod); } public static ServiceStatistics begin(String className, String methodName) { ServiceStatistics bean = new ServiceStatistics(); bean.setBeginTime(System.currentTimeMillis()); bean.setServiceName(className); bean.setMethodName(methodName); bean.setModelType("service"); System.out.println(JSON.toJSONString(bean)); return bean; } public static void error(Throwable e, Object obj) { ServiceStatistics bean = (ServiceStatistics) obj; bean.setErrorType(e.getClass().getSimpleName()); bean.setErrorMsg(e.getMessage()); } public static void end(Object obj) { ServiceStatistics bean = (ServiceStatistics) obj; bean.setUseTime(System.currentTimeMillis() - bean.getBeginTime()); System.out.println(JSON.toJSONString(obj)); } // Object obj= begin (className,methodName) // error(err,obj) // end(obj) final static String source = "{\n" + "%s" + " Object result=null;\n" + " try {\n" + " result=($w)%s$agent($$);\n" + " } catch (Throwable e) {\n" + "%s" + " throw e;\n" + " }finally{\n" + "%s" + " }\n" + " return ($r) result;\n" + "}\n"; final static String voidSource = "{\n" + "%s" + " try {\n" + " %s$agent($$);\n" + " } catch (Throwable e) {\n" + "%s" + " throw e;\n" + " }finally{\n" + "%s" + " }\n" + "}\n"; }
写个单元测试测试一下javassit是否生效。
当然了仅仅有这个javassit是无法直接运行的,我们还要依靠javaagent来实现对类的拦截