本文介绍一下,当下比较基础但是使用场景却很多的一种技术,稍微偏底层点,就是字节码插庄技术了...,如果之前大家熟悉了asm,cglib以及javassit等技术,那么下面说的就很简单了...,因为下面要说的功能就是基于javassit实现的,接下来先从javaagent的原理说起,最后会结合一个完整的实例演示实际中如何使用..
javaagent的主要功能有哪些?
& 可以在加载java文件之前做拦截把字节码做修改
& 获取所有已经被加载过的类
& 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
& 获取某个对象的大小
& 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
& 将某个jar加入到classpath里供AppClassloard去加载
& 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
JVM Tool Interface,是jvm暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
比如说我们最常见的想在某个类的字节码文件读取之后类定义之前能修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那我们就可以实现一个回调函数赋给JvmtiEnv(JVMTI的运行时,通常一个JVMTIAgent对应一个jvmtiEnv,但是也可以对应多个)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数里来了,大致实现如下:
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
JVMTIAgent其实就是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
说到javaagent必须要讲的是一个叫做instrument的JVMTIAgent(linux下对应的动态库是libinstrument.so),因为就是它来实现javaagent的功能的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),从这名字里也完全体现了其最本质的功能:就是专门为java语言编写的插桩服务提供支持的。
instrument agent实现了Agent_OnLoad
和Agent_OnAttach
两方法,也就是说我们在用它的时候既支持启动的时候来加载agent,也支持在运行期来动态来加载这个agent,其中启动时加载agent还可以通过类似-javaagent:myagent.jar
的方式来间接加载instrument agent,运行期动态加载agent依赖的是jvm的attach机制JVM Attach机制实现,通过发送load命令来加载agent。
这里解释下几个重要项:
premain
以及agentmain
方法的时候注意到了有个Instrumentation的参数,这个参数其实就是这里的对象。sun.instrument.InstrumentationImpl.loadClassAndCallPremain
方法,如果agent是在启动的时候加载的,那该方法会被调用。sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain
方法,该方法在通过attach的方式动态加载agent的时候调用。sun.instrument.InstrumentationImpl.transform
方法。Agent-Class
。Can-Redefine-Classes:true
。Can-Set-Native-Method-Prefix:true
。Can-Retransform-Classes:true
,那将会设置mRetransformEnvironment的mIsRetransformer为true。红色标注的是我们最常用的,下面的列子也是会用到的...,接下来看一个具体的例子,如果熟悉分布式调用链系统的人肯定知道,调用链中最基础的一个功能就是统计一个服务里面的某个方法执行了多长时间...,其实这个就目前来说大多数系统底层都是基于字节码插桩技术实现的,接下来就演示一个完整的例子....,定义一个业务类,类里面定义几个方法,然后在执行这个方法的时候,会动态实现方法的耗时统计..
看业务类定义:
package com.suning.dynamic_proxy.javaagent.service; import java.util.LinkedList; import java.util.List; /** * @Author 18011618 * @Description 模拟数据插入服务 * @Date 10:45 2018/6/21 * @Modify By */ public class InsertService { /** * 模拟数据插入 * @param num */ public void insert2(int num){ Listlist = new LinkedList<>(); for (int i =0;i ;i++){ list.add(i); } } public void insert1(int num){ List list = new LinkedList<>(); for (int i =0;i ;i++){ list.add(i); } } public void insert3(int num){ List list = new LinkedList<>(); for (int i =0;i ;i++){ list.add(i); } } }
删除服务:
package com.suning.dynamic_proxy.javaagent.service; import java.util.List; /** * @Author 18011618 * @Description 模拟数据删除的方法 * @Date 10:45 2018/6/21 * @Modify By */ public class DeleteService { public void delete(Listlist){ for (int i=0;i ;i++){ list.remove(i); } } }
ok,接下来就是要编写javaagent的相关实现:
定义agent的入口
package com.suning.dynamic_proxy.javaagent; import java.lang.instrument.Instrumentation; /** * agent的入口类 */ public class TimeMonitorAgent {
//peremain 这个方法名称是固定写法 不能写错或修改 public static void premain(String agentArgs, Instrumentation inst) { System.out.println("execute insert method interceptor...."); System.out.println(agentArgs); //添加自定义类转换器 inst.addTransformer(new TimeMonitorTransformer(agentArgs)); } }
接下来看最重要的Transformer的实现:
package com.suning.dynamic_proxy.javaagent; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.reflect.Modifier; import java.security.ProtectionDomain; import java.util.Objects; /** * 类方法的字节码替换 * @author Allen Lee * @date 2017-03-11 */ public class TimeMonitorTransformer implements ClassFileTransformer { private static final String START_TIME = "\nlong startTime = System.currentTimeMillis();\n"; private static final String END_TIME = "\nlong endTime = System.currentTimeMillis();\n"; private static final String METHOD_RUTURN_VALUE_VAR = "__time_monitor_result"; private static final String EMPTY = ""; private String classNameKeyword; public TimeMonitorTransformer(String classNameKeyword){ this.classNameKeyword = classNameKeyword; } /** * * @param classLoader 默认类加载器 * @param className 类名的关键字 因为还会进行模糊匹配 * @param classBeingRedefined * @param protectionDomain * @param classfileBuffer * @return * @throws IllegalClassFormatException */ public byte[] transform(ClassLoader classLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/", "."); CtClass ctClass = null; try { //使用全称,用于取得字节码类 ctClass = ClassPool.getDefault().get(className); //匹配类的机制是基于类的关键字 这个是客户端传过来的参数 满足就会获取所有的方法 不满足跳过 if(Objects.equals(classNameKeyword, EMPTY)||(!Objects.equals(classNameKeyword, EMPTY)&&className.indexOf(classNameKeyword)!=-1)){ //所有方法 CtMethod[] ctMethods = ctClass.getDeclaredMethods(); //遍历每一个方法 for(CtMethod ctMethod:ctMethods){ //修改方法的字节码 transformMethod(ctMethod, ctClass); } } //重新返回修改后的类 return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 为每一个拦截到的方法 执行一个方法的耗时操作 * @param ctMethod * @param ctClass * @throws Exception */ private void transformMethod(CtMethod ctMethod,CtClass ctClass) throws Exception{ //抽象的方法是不能修改的 或者方法前面加了final关键字 if((ctMethod.getModifiers()&Modifier.ABSTRACT)>0){ return; } //获取原始方法名称 String methodName = ctMethod.getName(); String monitorStr = "\nSystem.out.println(\"method " + ctMethod.getLongName() + " cost:\" +(endTime - startTime) +\"ms.\");"; //实例化新的方法名称 String newMethodName = methodName + "$impl"; //设置新的方法名称 ctMethod.setName(newMethodName); //创建新的方法,复制原来的方法 ,名字为原来的名字 CtMethod newMethod = CtNewMethod.copy(ctMethod,methodName, ctClass, null); StringBuilder bodyStr = new StringBuilder(); //拼接新的方法内容 bodyStr.append("{"); //返回类型 CtClass returnType = ctMethod.getReturnType(); //是否需要返回 boolean hasReturnValue = (CtClass.voidType != returnType); if (hasReturnValue) { String returnClass = returnType.getName(); bodyStr.append("\n").append(returnClass + " " + METHOD_RUTURN_VALUE_VAR + ";"); } bodyStr.append(START_TIME); if (hasReturnValue) { bodyStr.append("\n").append(METHOD_RUTURN_VALUE_VAR + " = ($r)" + newMethodName + "($$);"); } else { bodyStr.append("\n").append(newMethodName + "($$);"); } bodyStr.append(END_TIME); bodyStr.append(monitorStr); if (hasReturnValue) { bodyStr.append("\n").append("return " + METHOD_RUTURN_VALUE_VAR+" ;"); } bodyStr.append("}"); //替换新方法 newMethod.setBody(bodyStr.toString()); //增加新方法 ctClass.addMethod(newMethod); } }
其实也很简单就两个类就实现了要实现的功能,那么如何使用呢?需要把上面的代码打成jar包才能执行,建议大家使用maven打包,下面是pom.xml的配置文件
xml version="1.0" encoding="UTF-8"?>xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.sn.dynamic dynamic 1.0-SNAPSHOT javassist javassist 3.12.1.GA cglib cglib 3.2.5 oro oro 2.0.8 org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 utf-8 org.apache.maven.plugins maven-shade-plugin 3.0.0 package shade com.suning.dynamic_proxy.javaagent.TimeMonitorAgent
强调一下,红色标准的非常关键,因为如果要想jar能够运行,必须要把运行清单打包到jar中,且一定要让jar的主类是Permain-Class,否则无法运行,运行清单的目录是这样的,
如果打包正确的话,里面的内容应该如下所示:
Manifest-Version: 1.0
Premain-Class: com.suning.dynamic_proxy.javaagent.TimeMonitorAgent
Archiver-Version: Plexus Archiver
Built-By: jack
Created-By: Apache Maven
Build-Jdk: 1.8.0_144
OK至此整体代码和打包就完成了,那么接下来再讲解如何使用
部署方式:
1 基于IDE开发环境运行
首先,编写一个service的测试类如下:
/** * @Author 18011618 * @Description * @Date 10:51 2018/6/21 * @Modify By */ public class ServiceTest { public static void main(String[] args) {
//插入服务 InsertService insertService = new InsertService(); //删除服务
DeleteService deleteService = new DeleteService(); System.out.println("....begnin insert...."); insertService.insert1(1003440); insertService.insert2(2000000); insertService.insert3(30003203); System.out.println(".....end insert....."); Listlist = new LinkedList<>(); for (int i =0;i<29988440;i++){ list.add(i); } System.out.println(".....begin delete......"); deleteService.delete(list); System.out.println("......end delete........"); } }
选择编辑配置:如下截图所示
进行如下修改:
javassit-5.0.jar这个就是刚刚编写那个javaagent类的代码打成的jar包,ok 让我们看一下最终的效果如何:
实际应用场景中,可以把这些结果写入到log然后发送到es中,就可以做可视化数据分析了...还是蛮强大的,接下来对上面的业务进行扩展,因为上面默认是拦截类里面的所有方法,如果业务需求是拦截类的特定的方法该怎么实现呢?其实很简单就是通过正则匹配,下面给出核心代码:
定义入口agent:
package com.suning.dynamic_proxy.javaagent.patter; import com.suning.dynamic_proxy.javaagent.TimeMonitorTransformer; import java.lang.instrument.Instrumentation; /** * Created by jack on 2018/6/19. */ public class TimeMonitorPatterAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new PatternTransformer()); } }
定义transformer:
package com.suning.dynamic_proxy.javaagent.patter; import javassist.CtClass; import org.apache.oro.text.regex.PatternCompiler; import org.apache.oro.text.regex.PatternMatcher; import org.apache.oro.text.regex.Perl5Compiler; import org.apache.oro.text.regex.Perl5Matcher; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; /** * Created by jack on 2018/6/19. * */ public class PatternTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { PatternMatcher matcher = new Perl5Matcher(); PatternCompiler compiler = new Perl5Compiler(); //指定的业务类 String interceptorClass ="com.service.InsertService"; //指定的方法 String interceptorMethod = "insert1"; try { if (matcher.matches(className,compiler.compile(interceptorClass))){ ByteCode byteCode = new ByteCode(); CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod); return ctClass.toBytecode(); } } catch (Exception e){ e.printStackTrace(); } return null; } }
修改字节码的实现:
package com.suning.dynamic_proxy.javaagent.patter; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; /** * Created by jack on 2018/6/19. * 修改拦截类的方法的字节码 */ public class ByteCode { public CtClass modifyByteCode(String className,String method)throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get(className); CtMethod oldMethod = ctClass.getDeclaredMethod(method); String oldMethodName = oldMethod.getName(); String newName = oldMethodName + "$impl"; oldMethod.setName(newName); CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null); StringBuffer sb = new StringBuffer(); sb.append("{"); sb.append("\nSystem.out.println(\"start to modify bytecode\");\n"); sb.append("long start = System.currentTimeMillis();\n"); sb.append(newName+"($$);\n"); sb.append("System.out.println(\"call method"+oldMethodName+"took\"+(System.currentTimeMillis()-start))"); sb.append("}"); newMethod.setBody(sb.toString()); ctClass.addMethod(newMethod); return ctClass; } }
OK,这个时候再重新打包,然后修改上面的运行配置之后再看效果,只能拦截到insert1方法
最后 再说一下如何使用jar运行,其实很简单如下:把各个项目都打成jar,比如把上面的service打成service.jar,然后使用java命令运行:
java -javaagent:d://javassit-5.0.jar=Service -jar service.jar,效果是一样的!