Java探针

使用java代理来实现java字节码注入
使用JavaSsist可以对字节码进行修改
使用ASM可以修改字节码

使用Java代理和ASM字节码技术开发java探针工具可以修改字节码

备注:javassist是一个库,实现ClassFileTransformer接口中的transform()方法。ClassFileTransformer 这个接口的目的就是在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。

备注:ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。


详细总结

1 使用Java代理和Java字节码注入技术

开发java探针工具来分析复杂的接口方法,无需修改代码,简单部署就可以实时抓取代码的运行轨迹和方法耗时。

2 基于Java代理和Java字节码注入技术的java探针工具技术原理

动态代理功能实现说明

我们利用Java代理和ASM字节码技术开发java探针工具,实现原理如下:

jdk1.5以后引入了Java代理技术,Java代理是运行方法之前的拦截器。我们利用Java代理和ASM字节码技术,在JVM加载class二进制文件的时候,利用ASM动态的修改加载的class文件,在监控的方法前后添加计时器功能,用于计算监控方法耗时,同时将方法耗时及内部调用情况放入处理器,处理器利用栈先进后出的特点对方法调用先后顺序做处理,当一个请求处理结束后,将耗时方法轨迹和入参map输出到文件中,然后根据map中相应参数或耗时方法轨迹中的关键代码区分出我们要抓取的耗时业务。最后将相应耗时轨迹文件取下来,转化为xml格式并进行解析,通过浏览器将代码分层结构展示出来,方便耗时分析,如图所示:

java探针工具原理图

1:在JVM加载class二进制文件的时候,利用ASM动态的修改加载的class文件,在监控的方法前后添加计时器功能,用于计算监控方法耗时;
2:将监控的相关方法 和 耗时及内部调用情况,按照顺序放入处理器;
3:处理器利用栈先进后出的特点对方法调用先后顺序做处理,当一个请求处理结束后,将耗时方法轨迹入参map输出到文件中;
4:然后区分出耗时的业务,转化为xml格式进行解析和分析。

Java探针工具功能点:

1、支持方法执行耗时范围抓取设置,根据耗时范围抓取系统运行时出现在设置耗时范围的代码运行轨迹。

2、支持抓取特定的代码配置,方便对配置的特定方法进行抓取,过滤出关系的代码执行耗时情况。

3、支持APP层入口方法过滤,配置入口运行前的方法进行监控,相当于监控特有的方法耗时,进行方法专题分析。

4、支持入口方法参数输出功能,方便跟踪耗时高的时候对应的入参数。

5、提供WEB页面展示接口耗时展示、代码调用关系图展示、方法耗时百分比展示、可疑方法凸显功能。


2.1 Java代理小例子

参考:基于 JavaAgent 的应用实例

JavaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法。
那么如何实现一个 Java代理呢?很简单,只需要增加 premain 方法即可。

源码:JavaAgent

目录结构

Test1.pre_MyProgram.java

package agent;

import java.lang.instrument.Instrumentation;

public class pre_MyProgram
{
    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     */
    public static void premain(String agentOps, Instrumentation inst)
    {

        System.out.println("====premain 方法执行");
        System.out.println(agentOps);

    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst) 则会执行 premain(String
     * agentOps)
     */
    public static void premain(String agentOps)
    {

        System.out.println("====premain方法执行2====");
        System.out.println(agentOps);
    }

    public static void main(String[] args)
    {
    }

}

在 src 目录下添加 META-INF/MANIFEST.MF 文件

Manifest-Version: 1.0
Premain-Class: agent.pre_MyProgram
Can-Redefine-Classes: true

打包代码为 pre_MyProgram.jar;注意打包的时候选择我们自己定义的 MANIFEST.MF

Test2.MyProgram.java

package alibaba;

public class MyProgram
{
    public static void main(String[] args)
    {
        System.out.println("====MyProgram====");
    }
}
Manifest-Version: 1.0
Main-Class: alibaba.MyProgram

导出main的jar包命名为:MyProgram.jar

结果

命令中的Hello1为我们传递给 premain 方法的字符串参数,这就是一个简单的Java代理

2.2 基于 JavaAgent 的应用实例

JDK5中只能通过命令行参数在启动JVM时指定javaagent参数来设置代理类,而JDK6中已经不仅限于在启动JVM时通过配置参数来设置代理类,JDK6中通过 Java Tool API 中的 attach 方式,我们也可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

Instrumentation 的最大作用,就是类定义动态改变和操作。

最简单的一个例子,计算某个方法执行需要的时间,不修改源代码的方式,使用Instrumentation 代理来实现这个功能,给力的说,这种方式相当于在JVM级别做了AOP支持,这样我们可以在不修改应用程序的基础上就做到了AOP,是不是显得略吊。

创建一个 ClassFileTransformer 接口的实现类 MyTransformer实现 ClassFileTransformer 这个接口的目的就是在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。那么首先要了解MonitorTransformer 这个类的目的,就是对想要修改的类做一次转换,这个用到了javassist对字节码进行修改,可以暂时不用关心jaavssist的原理,用ASM同样可以修改字节码,只不过比较麻烦些。

项目结构

源码:JavaAgent

MyAgent.MyAgent.java

package agent;

import java.lang.instrument.Instrumentation;

public class MyAgent

{
    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     */
    public static void premain(String agentOps, Instrumentation inst)
    {
        System.out.println("=========premain方法执行========");
        System.out.println(agentOps);
        // 添加Transformer
        inst.addTransformer(new MyTransformer());

    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst) 则会执行 premain(String
     * agentOps)
     */
    public static void premain(String agentOps)
    {

        System.out.println("====premain方法执行2====");
        System.out.println(agentOps);
    }

    public static void main(String[] args)
    {

    }

}

MyAgent.MyTransformer

package agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
/**
 * 检测方法的执行时间
 */
public class MyTransformer implements ClassFileTransformer
{

    final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
    final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";

    // 被处理的方法列表
    final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();

    public MyTransformer()
    {
        add("com.shanhy.demo.TimeTest.sayHello");
        add("com.shanhy.demo.TimeTest.sayHello2");
    }

    private void add(String methodString)
    {
        String className = methodString.substring(0, methodString.lastIndexOf("."));
        String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
        List<String> list = methodMap.get(className);
        if (list == null)
        {
            list = new ArrayList<String>();
            methodMap.put(className, list);
        }
        list.add(methodName);
    }

    // 重写此方法
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
    {
        className = className.replace("/", ".");
        if (methodMap.containsKey(className))
        {
            // 判断加载的class的包路径是不是需要监控的类
            CtClass ctclass = null;
            try
            {
                ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
                for (String methodName : methodMap.get(className))
                {
                    String outputStr = "\nSystem.out.println(\"this method " + methodName
                            + " cost:\" +(endTime - startTime) +\"ms.\");";

                    CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 得到这方法实例
                    String newMethodName = methodName + "$old";// 新定义一个方法叫做比如sayHello$old
                    ctmethod.setName(newMethodName);// 将原来的方法名字修改

                    // 创建新的方法,复制原来的方法,名字为原来的名字
                    CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);

                    // 构建新的方法体
                    StringBuilder bodyStr = new StringBuilder();
                    bodyStr.append("{");
                    bodyStr.append(prefix);
                    bodyStr.append(newMethodName + "($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数
                    bodyStr.append(postfix);
                    bodyStr.append(outputStr);
                    bodyStr.append("}");

                    newMethod.setBody(bodyStr.toString());// 替换新方法
                    ctclass.addMethod(newMethod);// 增加新方法
                    System.err.println(outputStr);
                }
                return ctclass.toBytecode();
            }
            catch (Exception e)
            {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
        }
        return null;
    }
}

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist-3.12.1-GA.jar

package alibaba;

public class TimeTest
{

    public static void main(String[] args)
    {
        System.err.println("======TimeTest执行========");
        sayHello();
        sayHello2("hello world222222222");
    }

    public static void sayHello()
    {
        try
        {
            Thread.sleep(2000);
            System.out.println("hello world!!");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void sayHello2(String hello)
    {
        try
        {
            Thread.sleep(1000);
            System.out.println(hello);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Main-Class: alibaba.TimeTest

结果:

结果

3 使用 spring-loaded 实现 jar 包热部署

在项目开发中我们可以把一些重要但又可能会变更的逻辑封装到某个 logic.jar 中,当我们需要随时更新实现逻辑的时候,可以在不重启服务的情况下让修改后的 logic.jar 被重新加载生效。

spring-loaded是一个开源项目,项目地址:https://github.com/spring-projects/spring-loaded

使用方法:

在启动主程序之前指定参数
-javaagent:C:/springloaded-1.2.5.RELEASE.jar -noverify
123

如果你想让 Tomat 下面的应用自动热部署,只需要在 catalina.sh 中添加:

set JAVA_OPTS=-javaagent:springloaded-1.2.5.RELEASE.jar -noverify1

这样就完成了 spring-loaded 的安装,它能够自动检测Tomcat 下部署的webapps ,在不重启Tomcat的情况下,实现应用的热部署。

通过使用 -noverify 参数,关闭 Java 字节码的校验功能。
使用参数 -Dspringloaded=verbose;explain;watchJars=tools.jar 指定监视的jar (verbose;explain; 非必须),多个jar用“冒号”分隔,如 watchJars=tools.jar:utils.jar:commons.jar

当然,它也有一些小缺限:

    1. 目前官方提供的1.2.4 版本在linux上可以很好的运行,但在windows还存在bug,官网已经有人提出:https://github.com/spring-projects/spring-loaded/issues/145
    2. 对于一些第三方框架的注解的修改,不能自动加载,比如:spring mvc的@RequestMapping
    3. log4j的配置文件的修改不能即时生效。

原文:https://www.cnblogs.com/sea520/p/10251838.html

原文地址:https://www.cnblogs.com/yx88/p/11291636.html

时间: 2024-08-03 06:08:12

Java探针的相关文章

Java探针-Java Agent技术-阿里面试题

最近面试阿里,面试官先是问我类加载的流程,然后问了个问题,能否在加载类的时候,对字节码进行修改 我懵逼了,答曰不知道,面试官说可以的,使用Java探针技术,能够实现 我查了一下关于探针技术的知识: 2. 基于javaAgent和Java字节码注入技术的java探针工具技术原理 图0-0:动态代理功能实现说明 我们利用javaAgent和ASM字节码技术开发java探针工具,实现原理如下: jdk1.5以后引入了javaAgent技术,javaAgent是运行方法之前的拦截器.我们利用javaAg

干货 | 云智慧透视宝Java代码性能监控实现原理

这篇图文并茂,高端大气上档次,思维缜密的文章,一看就和我平时的风格不同.对了.这不是我写的,是我家高大英俊,写一手好代码,炒一手好菜的男神架构师老公的大作,曾发表于技术公号,经本人授权转载,如有技术问题,我代为请他本人解答~~ 一.Java平台体系及应用场景 从1995年Sun Microsystems公司正式推出Java,到2006年时Sun公司将其开源,迄今为止已经有了20年的历史.Java本身已不仅仅只是一门面向对象的编程语言,而是由一系列计算机软件和规范形成的技术体系,这个技术体系提供了

如何使用阿里云ARMS诊断Java服务端报错问题

摘要: 这是ARMS团队推出的"网站常见问题1分钟定位系列篇"第二文. 我的网站为什么一错再错网页报错,尤其是5XX错误是互联网应用最常见的问题之一.5XX错误通常发生于服务端.服务端是业务逻辑最复杂,也是整条网络请求链路中最容易出错.出了错最难排查的地方. 运维工程师与研发工程师排查此类问题,通常要通过登录机器查看日志来定位问题.对于一般的Java应用错误日志,通常是这幅模样: 一般来说,对于逻辑不算太复杂.历史不算"太悠久"的应用来说,登录机器看日志的方式能够很

Java 调式、热部署、JVM 背后的支持者 Java Agent

我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着.但其实我们一直在用它,而且接触的机会非常多.下面这些技术都使用了 Java Agent 技术,看一下你就知道为什么了. -各个 Java IDE 的调试功能,例如 eclipse.IntelliJ : -热部署功能,例如 JRebel.XRebel. spring-loaded: -各种线上诊断工具,例如 Btrace.Greys,还有阿里的 Arthas: -各种性能分析工具,例如 Visual VM.JConsole 等:

IDEA + maven 零基础构建 java agent 项目

200316-IDEA + maven 零基础构建 java agent 项目 Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的:虽说在实际的业务开发中,很少会涉及到 agent 开发,但是每个 java 开发都用过,比如使用 idea 写了个 HelloWorld.java,并运行一下, 仔细看控制台输出 本篇将作为 Java Agent 的入门篇,手把手教你开发一个统计方法耗时的 Java A

kubernetes听云实战发布版

?更多技术干货请戳:听云博客 听云线上使用k8s已经有一段时间了,下面对一些听云使用过程中的问题进行一些梳理,包括架构设计,安装部署和后期维护. 目录结构如下: 下面进入正题: 我们以听云系统的一个报表系统为例来详细进行说明: 1.系统架构 该应用属于Java Web报表类应用,部署在Ucloud云上.系统架构如下: 这是典型web应用的系统架构,在听云内部大部分应用都是此形式,对于并发量不高的业务均可参考此文档. 基本规范: 1.configmap用来管理配置文件 2.deployment用来

听云第六期应用性能管理大讲堂——北京、上海、成都三站同时开启!

9月19日 北京 上海 成都 三地同时上演 应用性能管理大讲堂 好戏精彩不断! 北京站: 应用性能管理大讲堂——互联网时代新标配 演讲嘉宾: e袋洗 CTO 岑永洪 听云研发副总裁 廖雄杰 听云研发副总裁架构师,现任听云技术副总裁,致力于应用性能优化及流式数据处理,对构建高性能Java应用有深入研究. 神秘嘉宾 即将揭晓 沙龙时间:9月19日 13:00——17:00 沙龙地点:#北京市中关村创业大街3W咖啡# 上海站: 听云&Ucloud应用性能管理大讲堂——O2O电商 如何借助“云+APM”

基于Linux的WebSphere性能调优与故障诊断

一.关于was数据源等问题的配置 (1)关于was数据源连接池的最大.最小配置多大合适?怎样去计算? (2)关于JVM的配置,64位系统,64位WAS,最值小和最大配置多大最优?怎样去计算? (3)应用服务器线程池,怎么样配置最优? 怎样去计算? (4)linux上安装was完成后,linux必须配置哪些参数,was性能最优?如果不配置的话,性能影响大吗 数据库连接池建议初始化调优10-100.was连接池参数说明及调优V1.0:http://www.webspherechina.net/Doc

基于Kafka+ELK搭建海量日志平台

早在传统的单体应用时代,查看日志大都通过SSH客户端登服务器去看,使用较多的命令就是 less 或者 tail.如果服务部署了好几台,就要分别登录到这几台机器上看,等到了分布式和微服务架构流行时代,一个从APP或H5发起的请求除了需要登陆服务器去排查日志,往往还会经过MQ和RPC调用远程到了别的主机继续处理,开发人员定位问题可能还需要根据TraceID或者业务唯一主键去跟踪服务的链路日志,基于传统SSH方式登陆主机查看日志的方式就像图中排查线路的工人一样困难,线上服务器几十上百之多,出了问题难以