步步深入看代理

1.意义

代理模式的定义:为其它对象提供一种代理以控制对这个对象的访问。

这句话非常简洁,一下子可能感受不到代理模式的强大,直到接触大量业务后才能体会它的应用之丰富:

监控:在原始方法前后进行记录,可以度量方法的处理时间、qps、参数分布等;

限流:依据监控的数据对该方法的不同请求进行限流,特定一段时间内仅允许一定的访问次数;

重定向:依据参数判断本地是否应该处理这个请求,是则处理,否则返回重定向信息;

……

所以代理模式的精髓就在 “控制”
二字上面,而控制是有着非常丰富的含义的,下次需要控制的时候就可以考虑一下代理了。

2.步步深入实现

假设我们有一个服务

public interface BusinessService {
    /**
     * 业务逻辑
     * @param uid 用户ID
     * @return 业务处理结果
     */
    String doJob(int uid);
}

最开始实现如下,依据用户ID和当前进程ID返回一段字符串

public class BusinessImpl implements BusinessService {
    public static int pid;
    
    static {
        String name = ManagementFactory.getRuntimeMXBean().getName();
        String pids = name.split("@")[0];
        pid = Integer.valueOf(pids);
    }

    @Override
    public String doJob(int uid){
        return "businessLogic of user: "+uid+" processed by "+pid;
    }
}

现在我们想对doJob方法进行用户分流:只有用户ID和进程ID模5的结果相同,本进程才处理请求,否则给用户返回失败信息让其寻找其它服务节点。怎么实现呢?

2.1.静态代理

所谓静态代理是指:定义接口或者父类,使得被代理对象与代理对象实现相同的接口或者是继承相同父类,这种代理方式下,class文件是进程启动前静态生成(由java文件编译而来)的,代理类如下:

public class BusinessImplNew implements BusinessService {
    // 被代理的原始类
    private BusinessService business;
    
    public BusinessImplNew(BusinessService businessimpl){
        this.business = businessimpl;
    }

    @Override
    public String doJob(int uid){
        int pid = BusinessImpl.pid;
        if (pid % 5 == uid % 5){
            return business.doJob(uid);
        }else {
            return "user "+uid+" not in this section "+pid;
        }
    }
}

客户代码调用如下:

public class Main {
    public static void main(String[] args) throws InterruptedException {
            Random random = new Random();
            // 原始
            // BusinessService businessService = new BusinessImpl();
            // 静态代理
            BusinessService businessService = new BusinessImplNew(new BusinessImpl());
            while (true){
                int uid = Math.abs(random.nextInt());
                System.out.println(businessService.doJob(uid));
                Thread.sleep(1000);
            }
        }
    }
}

只需声明一个service,代理类和原始类只是不同的实现而已,在Spring里面可以轻易的通过@Qualifier来实现不修改客户代码的代理改造。

这种代理的原理非常简单,就是简单的接口实现与委托,如果非要挖一挖,那就是多态了,方法实现是在运行时而非编译时确定的,具体可以了解下 itable和vtable。

2.2.动态代理

静态代理有2个问题:一是必须提前写好代理类,会导致大量的冗余java代码,如果不想写那么多代理类咋办?二是代理类必须在被代理类和接口之后产生,如果代理逻辑对大量的业务类都适用,那么我是不是可以在所有业务类实现之前完成代理逻辑呢?

是动态代理上场的时候了。

import java.lang.reflect.Proxy;

public class ProxyFactory {

    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxy, method, args) -> {
                // 只拦截这一个方法
                if (method.getName().equals("doJob")){
                    int pid = BusinessImpl.pid;
                    int uid = (int) args[0];
                    if (pid % 5 == uid % 5) {
                        return method.invoke(target, args);
                    } else {
                        return "user " + uid + " not in this section " + pid;
                    }
                }
                return method.invoke(target,args);
            }
        );
    }
}

客户代码:

// 原始
// BusinessService businessService = new BusinessImpl();    
// 动态代理
BusinessService businessService = (BusinessService) new ProxyFactory(new BusinessImpl()).getProxyInstance();
while (true){
    int uid = Math.abs(random.nextInt());
    System.out.println(businessService.doJob(uid));
    Thread.sleep(1000);
}

这种代理的原理就是动态生成与原有类继承相同接口的类的字节码,如果我们在客户代码加入属性

System._getProperties_().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

那么生成的字节码就会保存下来,我们可以直接用idea打开该字节码,idea会帮我们反编译成Java代码

package com.sun.proxy;

public final class $Proxy0 extends Proxy implements BusinessService {
    private static Method m0;
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
      public final String doJob(int var1) throws  {
        try {
            return (String)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
    static {
        try {
            m3 = Class.forName("cn.mageek.BusinessService").getMethod("doJob", Integer.TYPE);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可见生成的类的确是实现了BusinessService,还继承了来自Object的方法(这里篇幅原因省略了)。这里doJob方法的实现就是我们在newProxyInstance中传入的InvocationHandler的invoke方法(这里用Lambda简化了),我们观察一下ProxyGenerator(代理类字节码产生的逻辑)源码即可和上面的结果验证。

private byte[] generateClassFile() {
    visit(V14, accessFlags, dotToSlash(className), null,
          JLR_PROXY, typeNames(interfaces));
    /*
         * Add proxy methods for the hashCode, equals,
         * and toString methods of java.lang.Object.  This is done before
         * the methods from the proxy interfaces so that the methods from
         * java.lang.Object take precedence over duplicate methods in the
         * proxy interfaces.
         的确是都会生成这3个方法
         */
    addProxyMethod(hashCodeMethod);
    addProxyMethod(equalsMethod);
    addProxyMethod(toStringMethod);

    /*
         * Accumulate all of the methods from the proxy interfaces.
         */
    for (Class<?> intf : interfaces) {
        for (Method m : intf.getMethods()) {
            if (!Modifier.isStatic(m.getModifiers())) {
                addProxyMethod(m, intf);
            }
        }
    }

    /*
         * For each set of proxy methods with the same signature,
         * verify that the methods' return types are compatible.
         */
    for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
        checkReturnTypes(sigmethods);
    }

    generateConstructor();

    for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
        for (ProxyMethod pm : sigmethods) {
            // add static field for the Method object
            visitField(Modifier.PRIVATE | Modifier.STATIC, pm.methodFieldName,
                       LJLR_METHOD, null, null);

            // Generate code for proxy method
            pm.generateMethod(this, className);
        }
    }
    generateStaticInitializer();
    return toByteArray();
}

最后提一下,我们可以在 InvocationHandler
中依据方法/参数等的不同来制定各种个样的代理策略(工厂方法+策略模式),而不需要写大量的代理类。比如下面的例子就是对“doJob”方法采取uid模5的分流策略,对“doHardJob”方法采取uid后3位尾数模8的分流策略。

public class ProxyFactory {

    private Object target;

    /**
     *  需要拦截的方法及其具体拦截策略
     */
    private Map<String, InterceptionStrategy> methodInterceptionMap = new HashMap<>();

    public ProxyFactory(Object target) {
        this.target = target;
        methodInterceptionMap.put("doJob",new Interception5());
        methodInterceptionMap.put("doHardJob",new Interception8());
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxy, method, args) -> {
                InterceptionStrategy interception = methodInterceptionMap.get(method.getName());
                // 不拦截
                if (interception == null){
                    return method.invoke(target,args);
                }
                return interception.work(target,proxy,method,args);
            }
        );
    }
}

interface InterceptionStrategy {
    Object work(Object target, Object proxy, Method method, Object[] args) throws Exception;
}

class Interception5 implements InterceptionStrategy {
    @Override
    public Object work(Object target, Object proxy, Method method, Object[] args) throws Exception {
        int pid = BusinessImpl.pid;
        int uid = (int) args[0];
        if (pid % 5 == uid % 5) {
            return method.invoke(target, args);
        } else {
            return "user " + uid + " not in this section " + pid;
        }
    }
}

class Interception8 implements InterceptionStrategy {
    @Override
    public Object work(Object target, Object proxy, Method method, Object[] args) throws Exception {
        int pid = BusinessImpl.pid;
        int uid = (int) args[0] % 1000;
        if (pid % 8 == uid % 8) {
            return method.invoke(target, args);
        } else {
            return "user " + uid + " not in this section " + pid;
        }
    }
}

2.3.Cglib代理

静态代理和动态代理的问题是必须依赖接口的存在,如果业务逻辑是这样声明的,它们就行不通了

public class BusinessImpl {
    ....

这时候就该Cglib上场了

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class BusinessProxy implements MethodInterceptor {
    private Object target;
    public BusinessProxy(Object target) {
        this.target = target;
    }

    public Object getProxyInstance(){
        Enhancer en = new Enhancer();
        en.setSuperclass(target.getClass());
        en.setCallback(this);
        return en.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 只拦截这一个方法
        if (method.getName().equals("doJob")){
            int pid = BusinessImpl.pid;
            Integer uid = (Integer) args[0];
            if (pid % 5 == uid % 5){
                return method.invoke(target, args);
            }else {
                return "user "+uid+" not in this section "+pid;
            }
        }else {
            return method.invoke(target, args);
        }
    }
}

客户代码

Random random = new Random();
// 原始无接口
BusinessImpl businessService = new BusinessImpl();
// Cglib代理
businessService = (BusinessImpl) new BusinessProxy(businessService).getProxyInstance();
while (true){
    int uid = Math.abs(random.nextInt());
    System.out.println(businessService.doJob(uid));
    Thread.sleep(1000);
}

这种代理的本质是通过继承原有类/接口,对外呈现一致的类似于接口的行为,从而实现代理。

同样可以通过参数设置,保存生成的class文件

System._setProperty_(DebuggingClassWriter._DEBUG_LOCATION_PROPERTY_, "./");

这个文件关键部分如下

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class BusinessImpl$$EnhancerByCGLIB$$869bd7f5 extends BusinessImpl implements Factory {
    
    private MethodInterceptor CGLIB$CALLBACK_0;

    public final String doJob(int var1) {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (this.CGLIB$CALLBACK_0 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }
        return var10000 != null ? (String)var10000.intercept(this, CGLIB$doJob$0$Method, new Object[]{new Integer(var1)}, CGLIB$doJob$0$Proxy) : super.doJob(var1);
    }
}

2.4.字节码修改

如果BusinessImpl声明成final

public final class BusinessImpl  {
......

上面几种方法都无效了,因为既不能通过接口代理也不能通过继承代理,这时候就是 instrumentation
上场的时候了,我们可以直接改变类的字节码,从而改变类的行为,自然就可以实现代理的方法,严格意义上已经不能叫代理了,可以做的事情更多。

修改字节码不像修改java代码那么容易,是需要 时机
工具
的。

关于时机,JDK5开始,提供了premain,支持在jvm启动前修改类的字节码。

2.4.1.premain

首先新建一个module或者项目

<groupId>**.**</groupId>
    <artifactId>agent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <!-- 包含资源 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <!--包含文件夹以及子文件夹下所有资源-->
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <!--使用manifestFile属性配置自定义的参数文件所在的-->
                        <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <!--JDK6及以上才能用-->
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
            <plugin>
                <!-- 依赖打包 -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.10</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <!-- 后面修改字节码要用到的依赖 -->
    <dependencies>
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

声明premain:

public class PreMain {
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new Transformer(), true);
        System.out.println("Agent pre Done");
    }
}

在/src/main/resources/MEAT-INF/MANIFEST.MF中指明premain的相关参数:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: **.**.PreMain

重写一个BusinessImpl类:

public class BusinessImpl  {
    public static int pid;
    static {
        // 耗时的操作
        String name = ManagementFactory.getRuntimeMXBean().getName();
        String pids = name.split("@")[0];
        pid = Integer.valueOf(pids);
    }
    public String doJob(int uid){
        if (pid % 5 == uid % 5){
            return "agent businessLogic of user: "+uid+" processed by "+pid;
        }else {
            return "user "+uid+" not in this section "+pid;
        }
    }

}

定义转换类:

class Transformer implements ClassFileTransformer {
    // 防止递归替换
    final AtomicBoolean first = new AtomicBoolean(true);
  
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        
        // 待修改的类
        String toBeChanged = "BusinessImpl";
        if (className.contains(toBeChanged) && first.get()){
            System.out.println(className+",change behaviors");
            first.set(false);
            // 用重新定义的类编译的字节码替换原有类的字节码
            return getBytesFromFile(toBeChanged);
        }else {
            // null 表示不更改字节码
            return null;
        }
    }

    byte[] getBytesFromFile(String className) {
        try {
            File file = new File("/Users/**/workspace/*/target/classes/*/"+className+".class");
            InputStream is = new FileInputStream(file);
            long length = file.length();
            byte[] bytes = new byte[(int) length];

            int offset = 0;
            int numRead;
            while (offset <bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("Could not completely read file " + file.getName());
            }

            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!" + e.getClass().getName());
            return null;
        }
    }
}

将上述模块打包(顺便也就把新写的BusinessImpl编译成了class文件)。

然后再看客户类:没什么变化,依然像以前一样调用业务代码:

Random random = new Random();     
BusinessImpl businessService = new BusinessImpl();
while (true){
    int uid = Math.abs(random.nextInt());
    System.out.println(businessService.doJob(uid));
    Thread.sleep(1000);
}

只是启动的时候要配置参数,指明agent所在的jar:

-javaagent:/Users/*/workspace/*/target/agent-1.0-SNAPSHOT.jar=version=1

这种代理的本质就是修改字节码,自然能实现代理(甚至直接修改类的行为)。

原理上,当目标jvm启动并指明了 -javaagent参数,在加载类之前,agent中的premain方法就会首先被调用(先于目标jvm的main方法,从字面也能看出来含义),然后就会调用transform方法修改类的字节码。

2.4.2.agentmain

刨根问底,有人肯定想问了,类是final的,并且jvm已经启动了,怎么进行代理?

JDK6开始提供了agentmain,支持在jvm启动后修改类的字节码。

同样的在MANIFEST.MF中指明agentmain的相关参数:

Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: **.**.AgentMain

然后修改BusinessImpl类,与premain不同的是,agentmain修改类有较多限制:

  • 父类是同一个,实现的接口数也要相同,否则报错:attempted to change superclass or interfaces;
  • 类访问符必须一致,否则报错:attempted to change the class modifiers;
  • 字段数和字段名必须一致,否则:attempted to change the schema (add/remove fields);
  • 新增/删除的方法必须是 private static/final 的,否则报错:attempted to add/delete a method。对premain如果把将被调用的方法删掉,程序可以运行,但调用该方法的时候就会报错:java.lang.NoSuchMethodError

之所以有这么多限制,按我理解,Java是一种强类型静态语言。举个例子,本来服务定义如下:

class BusinessImpl implements BusinessService;
class BusinessImplNew implements BusinessService;

然后客户也是多态地用:

BusinessService business = new BusinessImpl();
business.doJob;
......
BusinessService businessNew = new BusinessImplNew();
businessNew.doJob;

用户程序跑的好好地,结果你写个agent,把类声明改了

class BusinessImpl

这时候客户代码第1行的存在就是一个矛盾,首先Java是强类型的,所以不允许不同类型进行隐式转换,其次Java是静态类型,所以客户代码的错误应该在编译时就报错了,但这时候程序已经运行,所以就矛盾了,为了杜绝这种矛盾,自然不允许你做这种类的修改。

总结起来,运行时做类的修改,可以修改实现,但是类本身对其它类暴露的 "接口"
应该在修改前后保持一致,不然就会把本属于编译时的错误引入到运行时,与Java的设计理念相违背,自然会被杜绝。这里 "接口"
不只是Java语言里面的接口(implements 或者 extends),而是指对外部的行为。

  • 为啥不能加public方法?因为外部可以调用public方法,所以也是一种对外接口
  • 为啥不能加private 方法?但是可以加private static/final方法呢? 其实这里其实和不同虚拟机实现有关:Java规范中对 isRetransformClassesSupported
    )有说明如下,禁止添加/删除方法,但是也说将来可能放宽限制。

The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions.

HotSpot的实现就没有完全遵守规范,其 jvmtiRedefineClasses
有如下判断,可以添加private static/final 方法。

static bool can_add_or_delete(Method* m) {
      // Compatibility mode
  return (AllowRedefinitionToAddDeleteMethods &&
          (m->is_private() && (m->is_static() || m->is_final())));
}

为什么支持这个添加我也没整明白,但是我理解这个是不应该的,因为即使private,对内部类也是可见的。估计官方也考虑到这点,所以在JDK13后加上了 AllowRedefinitionToAddDeleteMethods
参数,并且默认是false

  • 好,你说那我能不能加private final 的 field呢,这个总对外不可见吧?对, field本身的确对外不可见,但是schema对外确是可见的,比如你现在序列化business实例,agent加载之前序列化,agent之后反序列化,结果字段不一样(数量或者名字)不就可能导致失败吗?

回到具体操作上来,定义agentmain方法

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException,
            InterruptedException {
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new Transformer(), true);
        inst.retransformClasses(BusinessImpl.class);
        System.out.println("Agent Main Done");
    }
}

将该模块打包成jar。

再看客户代码(就是是main方法所在类),依然正常使用之前的业务逻辑方式。

然后我们新起一个jvm来修改目标jvm中想被代理的类的代码:

public class Attach {
    public static void main(String[] args){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // Lists the Java virtual machines known to this provider.
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            // 找到目标jvm
            if (vmd.displayName().endsWith("Main")) {
                try {
                    VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                    // 加载agent来修改字节码
                    virtualMachine.loadAgent("/Users/*/workspace/*/target/agent-1.0-SNAPSHOT.jar", "version=2");
                    virtualMachine.detach();
                } catch (AgentLoadException | AgentInitializationException | IOException | AttachNotSupportedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


}

此时能看到目标jvm的业务发生了变化:

businessLogic of user: 133033937 processed by 78344
businessLogic of user: 2033106653 processed by 78344
agentArgs : version=2
//BusinessImpl,change behaviors
Agent Main Done
user 1594461603 not in this section 78344
agent businessLogic of user: 1837400384 processed by 78344
user 563122492 not in this section 78344
user 2005311866 not in this section 78344
agent businessLogic of user: 631373574 processed by 78344
agent businessLogic of user: 1439396359 processed by 78344

那么问题又来了

1.上面我们看得出字节码修改的逻辑是在目标jvm发生的,如果这段逻辑抛了异常对目标jvm有影响吗?修改transform方法:

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
    System.out.println("load Class     :" + className);
    // 待修改的类
    String toBeChanged = "BusinessImpl";
    if (className.contains(toBeChanged) && first.get()){
        System.out.println(className+",change behaviors,type:"+type);
        // 直接抛异常
        System.out.println("throw rt exception");
        throw new RuntimeException("can wen crash?");
    }
    return null;
}

结果如下,可见抛出异常导致修改失败了,但是对原有代码逻辑没有影响,等于没修改(premain也一样)。

businessLogic of user: 1277419295 processed by 26152
businessLogic of user: 1554140834 processed by 26152
agentArgs : version=2
load Class     :/BusinessImpl type:2
/BusinessImpl,change behaviors,type:2
throw rt exception
Agent Main Done
businessLogic of user: 348179760 processed by 26152
businessLogic of user: 337107270 processed by 26152

2.如果在修改的代码中抛出异常会咋样?修改方法

private String createJavaString() {
    StringBuilder sb = new StringBuilder();
    sb.append("{if (pid % 5 != uid % 5){");
    sb.append("throw new RuntimeException(/"can wen crash?/");}}");
    return sb.toString();
}

结果如下,不出所料,既然你改了目标jvm的代码,那么抛出的异常肯定是能感知到的(premain自然也一样)。

businessLogic of user: 2049987640 processed by 26182
businessLogic of user: 1456301228 processed by 26182
agentArgs : version=2
load Class     :cn/mageek/BusinessImpl 
cn/mageek/BusinessImpl,change behaviors
insert string: {if (pid % 5 != uid % 5){throw new RuntimeException("can wen crash?");}}
Agent Main Done
......
Exception in thread "main" java.lang.RuntimeException: can wen crash?

关于异常premain和agentmain不一样的地方在于,如果premian这个agent不规范(比如字节码错误)或者premain方法本身抛出了异常,目标jvm直接就不能启动;而agentmain的目标jvm则会直接忽略之,特别地,当agentmain方法在inst.retransformClasses(BusinessImpl.class);之后抛出异常,修改还是成功的。

2.4.3.字节码修改方式

上面讲了修改字节码的时机,下面来讲讲修改字节码的工具。

代码编译

也就是上面用的方式,通过重写这个类,加上拦截逻辑,然后覆盖掉原有类的字节码,这种方式需要重写原有代码业务逻辑,显然只能当作demo看看,不具备实际可行性。

Javassist

javasist支持Java代码和字节码两种级别的修改方式,修改Java代码的方式更加简单易懂也更流行,如下:

byte[] javaAsistCode(String className,byte[] classfileBuffer){

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ByteArrayClassPath(className,classfileBuffer));
        CtClass cclass;
        try {
            cclass = pool.get(className.replaceAll("/", "."));
        } catch (NotFoundException e) {
            System.out.println("no class:"+className);
            e.printStackTrace();
            return null;
        }
        String insert = "empty";
        if (!cclass.isFrozen()) {
            for (CtMethod currentMethod : cclass.getDeclaredMethods()) {
                if (currentMethod.getName().equals("doJob")){
                    try {
                        insert = createJavaString();
                        currentMethod.insertBefore(insert);
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }
                }
            }
        }else {
            System.out.println("class isFrozen:"+className);
        }
        System.out.println("insert string: "+insert);
        try {
            return cclass.toBytecode();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String createJavaString() {
        StringBuilder sb = new StringBuilder();
        sb.append("{if (pid % 5 != uid % 5){");
        sb.append("return /"user /"+uid+/" not in this section /"+pid;}}");
        return sb.toString();
    }

修改transform方法

@Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        
        // 待修改的类
        String toBeChanged = "BusinessImpl";
        if (className.contains(toBeChanged) && first.get()){
            System.out.println(className+",change behaviors");
            first.set(false);
            // 用增强的字节码替换原有类的字节码
            return javaAsistCode(className,classfileBuffer);
        }
        return null;
    }

Cglib

还可以用Cglib直接进行字节码修改,这种方式的问题是需要对字节码非常了解,我这里就没有继续追究下去了。

3.总结

假设我们有一个服务,想对他进行代理:

  • 如果应用还处于开发阶段

    • 服务是以接口的形式暴露,那么我们可以用静态代理的方式进行;如果想代理与被代理分离,并且不想写那么多代理类,那么可用通过代理工厂和策略模式进行动态代理
    • 服务如果以类的形式暴露,那么我们可以用Cglib的形式进行代理,同样可以上面的设计模式
    • 如果服务以final类的形式暴露,那么只能用instrumentation的形式修改字节码实现代理
  • 如果应用已经开发完毕并上线

    • 服务可以中断,那么可以使用premain的方式,写好代理逻辑并使用instrumentation修改字节码实现代理,重新启动服务
    • 服务不能中断,那么可以用agentmain的方式,写好代理逻辑并使用instrumentation修改字节码实现代理,直接新起一个进程修改目标jvm的被代理对象字节码

参考: Instrumentation 新功能
, 字节码操纵技术
, 《深入理解Java虚拟机》

原文 

https://segmentfault.com/a/1190000022359295

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 步步深入看代理

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址