美文网首页Java服务端面试
JavaAgent 与 动态代理

JavaAgent 与 动态代理

作者: 04040d1599e6 | 来源:发表于2019-02-12 22:31 被阅读95次

    ASM, CGlib, Java Proxy, Javassist都是可以操作字节码,但是这些操作字节码都需要等到类加载到JVM中之后再对字节码进行重写。
    JavaAgent则是一个可以在加载前就进行重写,然后再加载的方式。

    JavaAgent

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

    public class TestPreMain {
        public static void premain(String agentOps, Instrumentation inst) {
            System.out.println("=========premain方法执行========");
            System.out.println(agentOps);
            inst.addTransformer(new FirstAgent());
        }
    }
    

    说明一下premain方法。该方法的原理与 main 应用程序入口点类似。在 Java 虚拟机 (JVM) 初始化后,每个 premain 方法将按照指定代理的顺序调用,然后将调用实际的应用程序 main 方法。

    JVMTI(Java Virtual Machine Tool Interface)是一套本地编程接口集合,它提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。关于 JVMTI 的详细信息,请参考 Java SE 6 文档(请参见 参考资源)当中的介绍。

    java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。

    Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。

    
    public class FirstAgent implements ClassFileTransformer {
        public final String injectedClassName = "test.agent.wxl";
    
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            className = className.replace("/", ".");
    //        System.out.println(className);
            if (className.startsWith(injectedClassName)) {
                CtClass ctclass = null;
                try {
                    ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
                    CtMethod[] ctmethods = ctclass.getMethods();
                    for (CtMethod ctMethod : ctmethods) {
                        CodeAttribute ca = ctMethod.getMethodInfo2().getCodeAttribute();
                        if (ca == null) {
                            continue;
                        }
                        if (!ctMethod.isEmpty()) {
    //                        System.out.println(ctMethod.getName());
                              ctMethod.insertBefore("System.out.println(\"hello Im agent : " + ctMethod.getName() + "\");");
                        }
                    }
                    return ctclass.toBytecode();
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                }
            }
            return null;
        }
    }
    
    

    实现 ClassFileTransformer 这个接口的目的就是在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。其中代码的修改使用到了javassist。
    上段代码实现了把特定包下面的class的所有类的所有有body的非native的方法前面添加一句
    System.out.println("hello im agent :" + ctMethod.getName());

    在pom.xml文件中添加插件:

              <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <version>3.0.0</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                            <configuration>
                                <transformers>
                                    <transformer
                                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                        <manifestEntries>
                                            <Premain-Class>agent.TestPreMain</Premain-Class>
                                        </manifestEntries>
                                    </transformer>
                                </transformers>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    

    构建后会发现jar包里面的MANIFEST.MF文件如下

    Manifest-Version: 1.0
    Premain-Class: agent.TestPreMain
    Archiver-Version: Plexus Archiver
    Built-By: xiaolong.wei
    Created-By: Apache Maven 3.3.9
    Build-Jdk: 1.8.0_121
    
    

    最后创建一个被代理的工程类

    public class TestAgentDemo {
    
        public static void main(String... args) {
            testAgent();
    
            testAgent1("1", "2");
        }
    
        public static void testAgent() {
            System.out.println("test agent say hello");
        }
    
        public static void testAgent1(String one, String two) {
            System.out.println("test agent say hello" + one + two);
        }
    }
    
    

    在启动的时候使用-javaagent:来指定jar包

    javaagent

    运行:

    运行结果

    可以看到方法main, testAgent testAgent1 的前面都被加入了代码.

    JavaAgent 实际应用中

    1. 可以在加载java文件之前做拦截把字节码做修改
    2. 获取所有已经被加载过的类
    3. 获取某个对象的大小
    4. 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
    5. 将某个jar加入到classpath里供AppClassload去加载
    6. 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
    

    动态代理

    动态代理的实现:

    1. cglib (Code Generation Library) 实现
    2. InvocationHandler

    代理模式

    代理模式就是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

    如下图

    代理模式

    实际使用中。客户端获取到的是代理对象,而代理对象ProxyObject把请求转发给RealObject,ProxyObject是不做业务的,实际工作的其实只有RealObject 。

    如果编写一个类的同时就要一个代理类,那这样系统就会变得臃肿且难以维护,所以出现了动态代理。在运行状态中,需要代理的地方,根据AbstractObject和RealObject,动态地创建一个Proxy,用完之后,就会销毁,这样就可以避免了Proxy 角色的class在系统中冗杂的问题了。

    InvocationHandler

    在考虑代理类的时候,我们无非是想在代理类调用实际对象的前后进行一些业务处理,如下图。


    代理

    这就造成了代理类的通用性,只需要在invoke的前后加入特定的代码就可以了。所以就出现了将所有通用的调用真实方法的管理器,让这个触发管理器统一去管理就叫做Invocation Handler。

    JDK通过 java.lang.reflect包下面的ProxyInvocationHandler来支持动态代理。

    RealObject这个类创建一个动态代理对象,JDK主要会做以下工作:

    1.   获取 RealObject上的所有接口列表;
    2.   确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX ;
    3.   根据需要实现的接口信息,在代码中动态创建 该Proxy类的字节码;
    4 .  将对应的字节码转换为对应的class 对象;
    5.   创建InvocationHandler实例handler,用来处理Proxy所有方法调用;
    6.   Proxy 的class对象 以创建的handler对象为参数,实例化一个proxy对象
    

    InvocationHandler,我们需要实现下列的invoke方法:
    在调用代理对象中的每一个方法时,在代码内部,都是直接调用了InvocationHandler 的invoke方法,而invoke方法根据代理类传递给自己的method参数来区分是什么方法。

    public interface Subject {
        int getInt(Integer i);
    }
    
    public interface Subject2 {
        String getString(String source);
    }
    
    public class RealSubject implements Subject, Subject2 {
        @Override
        public int getInt(Integer i) {
            System.out.println("this is method of getInt " + i++);
            return i;
        }
    
        @Override
        public String getString(String source) {
            System.out.println("source string is :" + source);
            source = source + " suffix";
            return source;
        }
    }
    

    代理类

    public class SubjectProxy implements InvocationHandler {
    
        private RealSubject subject;
    
        public SubjectProxy(RealSubject subject) {
            this.subject = subject;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("method " + method.getName() + "invoke by proxy, start");
            Object o = method.invoke(subject, args);
            System.out.println("method " + method.getName() + "invoke by proxy, end");
            return o;
        }
    }
    

    测试代码

    public class ProxyMain {
        public static void main(String... args) {
            RealSubject subject = new RealSubject();
            //创建一个Invoke Handler 
            SubjectProxy proxy = new SubjectProxy(subject);
            // 通过Proxy newProxyInstance方法创建代理类
            Object o = Proxy.newProxyInstance(ProxyMain.class.getClassLoader(), subject.getClass().getInterfaces(), proxy);
            Subject realSubject = (Subject) o;
            int result = realSubject.getInt(12);
            System.out.println("result:" + result);
            Subject2 s2 = (Subject2) o;
            s2.getString("hello");
        }
    }
    

    看一下代码运行结果


    image.png

    当然JDK的InvokeHandler也是生成了一个代理类。

    JDK提供了sun.misc.ProxyGenerator.generateProxyClass(String proxyName,class[] interfaces) 底层方法来产生动态代理类的字节码:

    cglib —— 通过类继承

    JDK提供的生成动态代理的方式有个明显的特点就是被代理的类它必须要有实现的接口才行。既RealObject必须实现了某个接口或者某几个接口,才能代理接口这些接口的方法。
    还好有CGLib。CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。而CGLIB底层使用的是ASM库。

    cglib 创建某个类A的动态代理类的模式是:

    1.   查找A上的所有非final 的public类型的方法定义;
    2.   将这些方法的定义转换成字节码;
    3.   将组成的字节码转换成相应的代理的class对象;
    4.   实现 MethodInterceptor接口,用来处理 对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)
    

    被代理类

    public class ElectricCar {
        public boolean charging(Double fee) {
            System.out.println("charging ~~ Fee:" + fee);
            return true;
        }
    
        public boolean driver(String van, Integer color) {
            System.out.println(van + " is runing ");
            return true;
        }
    }
    

    代理方法

    public class CarInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("intercept in car before");
            Object result = proxy.invokeSuper(obj, args);
            System.out.println("intercept in car after");
            return result;
        }
    }
    

    测试方法

    public class CgLibMain {
    
        public static void main(String... args) {
            //生成class文件 保存
            System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\work\\agentDemo\\target");
          
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(ElectricCar.class);
            CarInterceptor carInterceptor = new CarInterceptor();
            //设置代理前后的Interceptor
            enhancer.setCallback(carInterceptor);
            ElectricCar electricCar = (ElectricCar) enhancer.create();
            electricCar.driver("川A12131", 1);
            electricCar.charging(12d);
        }
    }
    

    代码运行结果:


    image.png

    参考:
    https://blog.csdn.net/luanlouis/article/details/24589193
    https://www.ibm.com/developerworks/cn/java/j-lo-jse61/

    相关文章

      网友评论

        本文标题:JavaAgent 与 动态代理

        本文链接:https://www.haomeiwen.com/subject/awdceqtx.html