APM之实现篇

来源:互联网 发布:淘宝买的office2016 编辑:程序博客网 时间:2024/04/19 15:37

在前文中已经详细介绍了APM的android端的原理,接下来会通过代码实现记录某类异常日志这个小功能来深入理解APM的实现原理。场景如下,记录所有捕获的IndexOutOfBoundsException。前文中提到,APM一般分为3个部分,plugin、agent和具体的业务代码。本文也将会按这三个分类来介绍。
注:由于篇幅有限,本文所展示的只有部分关键代码,有兴趣的可自行阅读github上的源码。

  • 业务代码
    我们的业务场景很简单,只需提供一个处理异常的方法就足够了。
public static void pushException(Throwable th){    //在这里处理异常,如打印或上传日志}
  • agent
    agent是最复杂的一个部分。它最终要达到的目的就是改写dexer.Main,在它执行processClass方法内的代码之前通过ASM工具修改第二个参数,即源class文件的byte数组。如果这个class的某个方法中包含了捕获IndexOutOfBoundsException的try-catch代码块,我们将在catch内调用上面的pushException方法。然后将这个修改过后的类对应的byte数组再替换回去。
    首先说明,字节码是通过异常表来处理异常的,有兴趣的可以通过字节码查看工具来查看异常表长什么样子。大概就是表里面每行记录都定义了如果代码在start行到end行之间抛出了异常,那么将转到handle行进行处理。这里的start到end就相当于try到catch之间的代码,而handle就是catch内开始的代码。查看ASM文档,AdviceAdapter中的visitTryCatchBlock和visitLabel这两个方法正好能满足我们的需求。只需要在visitTryCatchBlock方法中记录目标exception处理的handle,然后,如果在visitLabel中传入的正是我们刚才记录的handle,则加上调用pushException方法的代码。
public class ExceptionLogMethodAdapter extends AdviceAdapter {    private TransformContext context;    //记录所有目标exception的handle    //key为handle,value是此handle对应的exception。    //注:一个catch可能包含了多个exception,    //如catch(IndexOutOfBoundsException | Exception e)    private HashMap<Label, ArrayList<String>> matchedHandle = new HashMap<>();    protected ExceptionLogMethodAdapter(TransformContext context            , MethodVisitor methodVisitor, int access, String name, String desc) {        super(Opcodes.ASM5, methodVisitor, access, name, desc);        this.context = context;    }    @Override    public void visitTryCatchBlock(Label start, Label end, Label handle,                                   String exception) {        //目标exception,在本文中为java/lang/IndexOutOfBoundsException        HashSet<String> targetException = context.getExceptions();        if (exception != null && targetException.contains(exception)) {            context.getLog().d("find exception " + exception);            ArrayList<String> handles = matchedHandle.get(handle);            if(handles == null) handles = new ArrayList<>();            handles.add(exception);            matchedHandle.put(handle, handles);        }        super.visitTryCatchBlock(start, end, handle, exception);    }    @Override    public void visitLabel(Label label) {        super.visitLabel(label);        ArrayList<String> exceptions;        if(label != null && (exceptions = matchedHandle.get(label)) != null){            context.getLog().d("instrument exception");            Label matched = new Label();            Label end = new Label();            //捕获的是目标exception的实例才进行处理            final int N = exceptions.size() - 1;            if (N >= 1) {                for (int i = 0; i < N; i++) {                    compareInstance(IFNE, exceptions.get(i), matched);                }            }            compareInstance(IFEQ, exceptions.get(N), end);            visitLabel(matched);            dup();            //调用pushException方法            invokeStatic(Type.getObjectType("com/github/sgwhp/openapm/monitor/Monitor")                    , new Method("pushException", "(Ljava/lang/Throwable;)V"));            visitLabel(end);            //将此类标记为已修改            context.markModified();        }    }    private void compareInstance(int mode, String type, Label to){        dup();        instanceOf(Type.getObjectType(type));        visitJumpInsn(mode, to);    }}

前文提到dexer.Main与plugin不在同一个进程,所以要达到改写dexer.Main的目的还必须先改写ProcessBuilder的command成员变量,往其中插入-javaagent参数。同样还是通过ASM工具,当访问到ProcessBuilder的start方法时,如果start的目标是java或者dx,则加入-javaagent或-Jjavaagent参数。

//由于ClassLoader的关系,此类实现InvocationHandler接口//具体原因请见前文解释public class ProcessBuilderInvocationHandler implements InvocationHandler {    private InvocationDispatcher dispatcher;    private Log log;    public ProcessBuilderInvocationHandler(InvocationDispatcher dispatcher, Log log) {        this.dispatcher = dispatcher;        this.log = log;    }    //当ASM访问到start时会调用此方法,传入的args参数就是ProcessBuilder的command成员    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        List<String> list = (List<String>) args[0];        String str1 = list.get(0);        File file = new File(str1);        String param = null;        if (TransformAgent.dx.contains(file.getName().toLowerCase()))            //getAgentPath获取agent路径,具体实现见下文            param = "-Jjavaagent:" + TransformAgent.getAgentPath();        else if (TransformAgent.java.contains(file.getName().toLowerCase()))            param = "-javaagent:" + TransformAgent.getAgentPath();        if (param != null) {            if (TransformAgent.attachParams != null)                param = param + "=" + TransformAgent.attachParams;            list.add(1, toParam(param));        }        log.d("Execute: " + list.toString());        return null;    }    private String toParam(String param) {        if (System.getProperty("os.name").toLowerCase().contains("win"))            return "\"" + param + "\"";        return param;    }}

最后要实现的就是agent的入口了。我们要提供一个public static void agentmain(String args, Instrumentation inst)方法,给inst参数设置一个ClassFileTransformer,在这个transformer内分别调用我们上面给出的代码来实现对dexer.Main和ProcessBuilder进行改造。

public class TransformAgent {    public static final Class LOGGER = Logger.class;    public static final Set<String> dx = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String[] { "dx", "dx.bat" })));    public static final Set<String> java = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String[] { "java", "java.exe" })));    //入口    public static void agentmain(String args, Instrumentation inst){        premain(args, inst);    }    public static void premain(String args, Instrumentation inst) {        try {            //设置ClassFileTransformer,            //内部将对ProcessBuilder和dexer.Main进行改造            IClassTransformer modifier = new ClassTransformer(log);            createInvocationDispatcher(log);            inst.addTransformer(modifier, true);            Class[] classes = inst.getAllLoadedClasses();            ArrayList<Class> classesToBeTransform = new ArrayList<>();            for (Class cls : classes) {                if(modifier.transforms(cls)){                    classesToBeTransform.add(cls);                }            }            if(!classesToBeTransform.isEmpty()){                if(inst.isRetransformClassesSupported()){                    inst.retransformClasses(classesToBeTransform.toArray(new Class[classesToBeTransform.size()]));                }             }            redefineClass(inst, modifier, ProcessBuilder.class);        } catch (Exception e) {            throw new RuntimeException("agent startup error");        }    }    //改造ProcessBuilder的类是ProcessBuilderInvocationHandler,    //改造dexer.Main的类本文没列出来,可以将这两个类的派发都放到一个类去做,    //然后将这个类的实例设置到Logger里去,这样ProcessBuilder和dexer.Main就能获取到了    private static void createInvocationDispatcher(Log log) throws Exception {        Field treeLock = LOGGER.getDeclaredField("treeLock");        treeLock.setAccessible(true);        Field modifiers = Field.class.getDeclaredField("modifiers");        modifiers.setAccessible(true);        modifiers.setInt(treeLock, treeLock.getModifiers() & 0xFFFFFFEF);//去掉final        if (!(treeLock.get(null) instanceof InvocationDispatcher)) {            treeLock.set(null, new InvocationDispatcher(log));        }    }    public static String getAgentPath() throws URISyntaxException {        return new File(TransformAgent.class.getProtectionDomain()                .getCodeSource().getLocation().toURI().getPath()).getAbsolutePath();    }}

别忘了把MANIFEST文件加上,在src/META-INF目录下新建MANIFEST.MF文件,里面加一行Agent-Class: agentmain所属类全限定名。

  • plugin
    我们以android studio的gradle插件为例。这个插件要实现什么功能?没错,就是要把agent加载进来。用IntelliJ新建一个gradle工程,然后把tools.jar(在jdk的lib目录下)和前面创建的agent.jar加入到Libraries中。创建一个实现Plugin< Project >的类,通过它来启动agent。
public class OpenAPMPlugin implements Plugin<Project> {    @Override    public void apply(Project project) {        String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();        int p = nameOfRunningVM.indexOf('@');        String pid = nameOfRunningVM.substring(0, p);        try {            String jarFilePath = TransformAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();            jarFilePath = new File(jarFilePath).getCanonicalPath();            VirtualMachine vm = VirtualMachine.attach(pid);            vm.loadAgent(jarFilePath, System.getProperty("openapm.agentArgs"));            vm.detach();        } catch (URISyntaxException | IOException | AgentInitializationException | AttachNotSupportedException | AgentLoadException e) {            throw new RuntimeException(e);        }    }}

plugin同样也需要配置文件,目录为resources/META-INF/gradle-plugins,具体名称可自行定义,加入implementation-class=插件类的全限定名。
打包插件的时候需要注意,不要把tools.jar和agent.jar给打包进去了。

  • 使用
    将plugin.jar和agent.jar放到android项目根目录的plugin文件夹中,并在build.gradle中添加dependencies
classpath fileTree(dir: 'plugin', include: '*.jar')

最后在app目录的build.gradle中添加apply plugin: ‘plugin配置文件的名称’,done。

2 0
原创粉丝点击