《设计模式》10.Javassist动态代理(结构型)

本文深入解析Javassist动态代理的工作原理和技术细节,包括如何通过动态生成类实现代理功能,以及如何利用Javassist的高级API进行字节码级别的定制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • Javassist 动态代理与 Cglib 一样是通过动态生成继承自目标类的代理类实现

  • 与其他类似的字节码编辑器不同,Javassist提供两个级别的API:源级别和字节码级别。如果用户使用源级API,他们可以在不了解Java字节码规范的情况下编辑类文件。 整个API仅使用Java语言的词汇表进行设计。 您甚至可以以源文本的形式指定插入的字节码; Javassist即时编译它。

    1. 源级别:高级API,人类可读的计算机语言指令,如:CtClass

    2. 字节码级别: 低级API,二进制操作指令,如:ClassFileWriter、ClassFile

      节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是一种中间码,它比机器码更抽象。

      如果你想要产生一个简单的类文件,javassist.bytecode.ClassFileWriter可能提供了最好的API。它提供了比javassist.bytecode.ClassFile更快的速度,尽管这个API更小一些。

  • Javassist 使用了 MethodHandle 性能更好;

  • Javassist 动态代理是基于反射实现,最终调用:method.invoke

  • Javassist 可以通过 MethodFilter 可以过滤不必要的方法使字节码文件更精简;

目标类:ProxyTarget

public class ProxyTarget {
    public void doSomething() {
        System.out.println("do something...");
    }
}

Javassist 代理工厂:JavassistProxyFactory,继承 ProxyFactory

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;
import java.lang.reflect.Method;

public class JavassistProxyFactory<T> extends ProxyFactory {
    private T target;
    public JavassistProxyFactory(T target) {
        this.target = target;
    }

	//获取代理类
    public T getProxy() throws IllegalAccessException, InstantiationException {
        setSuperclass(target.getClass());
        setDefaultFilter();
        T proxy = (T) createClass().newInstance();
        ((ProxyObject) proxy).setHandler(getDefaultHandler());
        return proxy;
    }

	//设置默认的方法过滤器,如果此处不做过滤,代理类中将会把 Object 的基础方法都重写并生成对应的代理方法
    private void setDefaultFilter() {
        this.setFilter((Method m) -> {
            if (m.getName().equals("doSomething")) {
                return true;
            }
            return false;
        });
    }

	//为代理类设置处理句柄
    private MethodHandler getDefaultHandler() {
        return (self, thisMethod, proceed, args) -> {
            System.out.println("before " + thisMethod.getName() + " execution...");
            Object result = thisMethod.invoke(target, args);
            System.out.println("after " + thisMethod.getName() + " execution...");
            return result;
        };
    }
}

ProxyFactory::constructor

	//是否使用缓存:默认true
    public static volatile boolean useCache = true;
    //是否在代理类中生成 writeReplace 方法,用于将代理实例转换为可序列化的对象
    public static volatile boolean useWriteReplace = true;
    //是否只代理 public 修饰的方法
    public static boolean onlyPublicMethods = false;

    public ProxyFactory() {
        superClass = null;
        interfaces = null;
        methodFilter = null;
        handler = null;
        signature = null;
        signatureMethods = null;
        //方法签名是否以:getHandler:() 开始 (getMethods中使用)
        //决定代理类是继承自 Proxy(true) | ProxyObject(false)
        hasGetHandler = false;
        thisClass = null;
        //如果非 null,将代理类 Class 文件写入到指定目录
        writeDirectory = null;
        factoryUseCache = useCache;
        factoryWriteReplace = useWriteReplace;
    }

ProxyFactory::createClass

开始创建代理类 Class 对象

    public Class<?> createClass() {
    	//signature默认为null
        if (signature == null) {
            computeSignature(methodFilter);
        }
        return createClass1(null);
    }

ProxyFactory::computeSignature

  1. 获取所有的声明方法(包含所有父类、接口的 public / protected / private 方法)并排序
  2. 过滤方法:不能被 final / static / private 修饰,方法声明类和目标类需要同包,再根据自定义过滤器过滤
  3. 将过滤后的方法,通过标记在长度为方法列表长度 / 8 的 byte 数组中,1 byte = 8 bit,每个 bit 对应一个方法
    private void computeSignature(MethodFilter filter) // throws CannotCompileException
    {
        makeSortedMethodList();

        int l = signatureMethods.size();
        int maxBytes = ((l + 7) >> 3);
        signature = new byte[maxBytes];
        for (int idx = 0; idx < l; idx++)
        {
            Method m = signatureMethods.get(idx).getValue();
            int mod = m.getModifiers();
            if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod)
                    && isVisible(mod, basename, m) && (filter == null || filter.isHandled(m))) {
                setBit(signature, idx);
            }
        }
    }

ProxyFactory::isVisible

判断方法是否可见:

  1. private 修饰为不可见
  2. public / protected 修饰为可见
  3. 方法声明类和目标类包名相同为可见
    private static boolean isVisible(int mod, String from, Member meth) {
        if ((mod & Modifier.PRIVATE) != 0)
            return false;
        else if ((mod & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0)
            return true;
        else {
            String p = getPackageName(from);
            String q = getPackageName(meth.getDeclaringClass().getName());
            if (p == null)
                return q == null;
            return p.equals(q);
        }
    }

ProxyFactory::setBit

将 byte 素组对应 bit 设置为 1,表明方法需要在代理类中生成

    private void setBit(byte[] signature, int idx) {
        int byteIdx = idx >> 3;
        if (byteIdx < signature.length) {
            int bitIdx = idx & 0x7;
            int mask = 0x1 << bitIdx;
            int sigByte = signature[byteIdx];
            signature[byteIdx] = (byte)(sigByte | mask);
        }
    }

ProxyFactory::makeSortedMethodList

  1. 检查类是否可以继承,和确定代理类名前缀
  2. 获取所有的声明方法(包含所有父类、接口的 public / protected / private 方法)列表,并排序,每个元素为一个 Map.Entry<String key, Method value>,其中 key为:
    String key = m.getName() + ':' + RuntimeSupport.makeDescriptor(m); // see keyToDesc().
    即:方法名 + : + 方法签名(包含返回值(java 1.5后加入),参数类型)
    private void makeSortedMethodList() {
        checkClassAndSuperName();

        hasGetHandler = false;      // getMethods() may set this to true.
        Map<String,Method> allMethods = getMethods(superClass, interfaces);
        signatureMethods = new ArrayList<Map.Entry<String,Method>>(allMethods.entrySet());
        Collections.sort(signatureMethods, sorter);
    }

ProxyFactory::checkClassAndSuperName

  1. 确定代理类名前缀,如果类名以 java. / jdk. 开始或者只允许生成 public 方法,代理类名前缀将被设置为:"javassist.util.proxy." + basename.replace('.', '_');
  2. 检查类是否可以继承(是否被 final 修饰),如果不可继承直接抛出异常
    private void checkClassAndSuperName() {
        if (interfaces == null)
            interfaces = new Class[0];

        if (superClass == null) {
            superClass = OBJECT_TYPE;
            superName = superClass.getName();
            basename = interfaces.length == 0 ? superName
                                              : interfaces[0].getName();
        } else {
            superName = superClass.getName();
            basename = superName;
        }

        if (Modifier.isFinal(superClass.getModifiers()))
            throw new RuntimeException(superName + " is final");

        // Since java.base module is not opened, its proxy class should be
        // in a different (open) module.  Otherwise, it could not be created
        // by reflection.
        if (basename.startsWith("java.") || basename.startsWith("jdk.") || onlyPublicMethods)
            basename = packageForJavaBase + basename.replace('.', '_');
    }

    private static final String packageForJavaBase = "javassist.util.proxy.";

RuntimeSupport::makeDescriptor

拼接方法签名:(参数签名1,参数签名2...)返回类型签名

    public static String makeDescriptor(Class<?>[] params, Class<?> retType) {
        StringBuffer sbuf = new StringBuffer();
        sbuf.append('(');
        for (int i = 0; i < params.length; i++)
            makeDesc(sbuf, params[i]);

        sbuf.append(')');
        if (retType != null)
            makeDesc(sbuf, retType);

        return sbuf.toString();
    }

RuntimeSupport::makeDesc

根据各个类型拼接类型签名

    private static void makeDesc(StringBuffer sbuf, Class<?> type) {
        if (type.isArray()) {
            sbuf.append('[');
            makeDesc(sbuf, type.getComponentType());
        }
        else if (type.isPrimitive()) {
            if (type == Void.TYPE)
                sbuf.append('V');
            else if (type == Integer.TYPE)
                sbuf.append('I');
            else if (type == Byte.TYPE)
                sbuf.append('B');
            else if (type == Long.TYPE)
                sbuf.append('J');
            else if (type == Double.TYPE)
                sbuf.append('D');
            else if (type == Float.TYPE)
                sbuf.append('F');
            else if (type == Character.TYPE)
                sbuf.append('C');
            else if (type == Short.TYPE)
                sbuf.append('S');
            else if (type == Boolean.TYPE)
                sbuf.append('Z');
            else
                throw new RuntimeException("bad type: " + type.getName());
        }
        else
            sbuf.append('L').append(type.getName().replace('.', '/'))
                .append(';');
    }

ProxyFactory::createClass1

开始生成代理类 Class 对象,默认 lookup = null; factoryUseCache = true 执行 createClass2(cl, lookup):

    private Class<?> createClass1(Lookup lookup) {
        Class<?> result = thisClass;
        if (result == null) {
            ClassLoader cl = getClassLoader();
            synchronized (proxyCache) {
                if (factoryUseCache)
                    createClass2(cl, lookup);
                else
                    createClass3(cl, lookup);

                result = thisClass;
                // don't retain any unwanted references
                thisClass = null;
            }
        }

        return result;
    }

ProxyFactory::createClass2

  1. 拼接缓存 key:目标类名:接口名1:接口名2:signature字节数组转换的字符串:w
  2. 通过 key 去缓存中搜索,存在直接从缓存的 ProxyDetail 中获取 Class 对象返回
  3. 否则,通过 createClass3 动态生成 Class 对象,存入缓存并返回
    private void createClass2(ClassLoader cl, Lookup lookup) {
        String key = getKey(superClass, interfaces, signature, factoryWriteReplace);
        /*
         * Excessive concurrency causes a large memory footprint and slows the
         * execution speed down (with JDK 1.5).  Thus, we use a jumbo lock for
         * reducing concrrency.
         */
        // synchronized (proxyCache) {
            Map<String,ProxyDetails> cacheForTheLoader = proxyCache.get(cl);
            ProxyDetails details;
            if (cacheForTheLoader == null) {
                cacheForTheLoader = new HashMap<String,ProxyDetails>();
                proxyCache.put(cl, cacheForTheLoader);
            }
            details = cacheForTheLoader.get(key);
            if (details != null) {
                Reference<Class<?>> reference = details.proxyClass;
                thisClass = reference.get();
                if (thisClass != null) {
                    return;
                }
            }
            createClass3(cl, lookup);
            details = new  ProxyDetails(signature, thisClass, factoryWriteReplace);
            cacheForTheLoader.put(key, details);
        // }
    }

ProxyFactory::getKey

拼接缓存 key:目标类名:接口名1:接口名2:signature字节数组转换的字符串:w

    private static char[] hexDigits =
            { '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    public String getKey(Class<?> superClass, Class<?>[] interfaces, byte[] signature, boolean useWriteReplace)
    {
        StringBuffer sbuf = new StringBuffer();
        if (superClass != null){
            sbuf.append(superClass.getName());
        }
        sbuf.append(":");
        for (int i = 0; i < interfaces.length; i++) {
            sbuf.append(interfaces[i].getName());
            sbuf.append(":");
        }
        for (int i = 0; i < signature.length; i++) {
            byte b = signature[i];
            int lo = b & 0xf;
            int hi = (b >> 4) & 0xf;
            sbuf.append(hexDigits[lo]);
            sbuf.append(hexDigits[hi]);
        }
        if (useWriteReplace) {
            sbuf.append(":w");
        }

        return sbuf.toString();
    }

ProxyFactory::createClass3

  1. 生成代理类类名
  2. 创建 ClassFile
  3. 根据 writeDirectory 取值决定是否将 Class 写入到指定存储目录
  4. 用 ClassLoader 加载 ClassFile 字节码
  5. 为 Class 对象的字段 _filter_signature 和 default_interceptor 设置值(不会在指定目录的 Class 文件中写入)
    private void createClass3(ClassLoader cl, Lookup lookup) {
        // we need a new class so we need a new class name
        allocateClassName();

        try {
            ClassFile cf = make();
            if (writeDirectory != null)
                FactoryHelper.writeFile(cf, writeDirectory);

            if (lookup == null)
                thisClass = FactoryHelper.toClass(cf, getClassInTheSamePackage(), cl, getDomain());
            else
                thisClass = FactoryHelper.toClass(cf, lookup);

            setField(FILTER_SIGNATURE_FIELD, signature);
            // legacy behaviour : we only set the default interceptor static field if we are not using the cache
            if (!factoryUseCache) {
                setField(DEFAULT_INTERCEPTOR, handler);
            }
        }
        catch (CannotCompileException e) {
            throw new RuntimeException(e.getMessage(), e);
        }

    }

ProxyFactory::make

创建 ClassFile 对象:

  1. 写入类定义:类名,继承父类,实现接口(Proxy | ProxyObject)
  2. 创建常量池 ConstPool
  3. 如果关闭使用缓存,添加:public static MethodHandler default_interceptor;
  4. 添加:private MethodHandler handler;
  5. 添加:public static byte[] _filter_signature;
  6. 添加:public static final long serialVersionUID;
  7. 根据目标类为代理类添加相应构造器
  8. 添加方法签名过滤后的方法
  9. 添加:private static 方法块加载时初始化类
  10. 添加:public void setHandler(final MethodHandler handler)方法
  11. 如果 hasGetHandler = false(默认为 false),添加public MethodHandler getHandler()方法
  12. 如果 factoryWriteReplace = true(默认为 true),添加Object writeReplace()方法
    private ClassFile make() throws CannotCompileException {
        ClassFile cf = new ClassFile(false, classname, superName);
        cf.setAccessFlags(AccessFlag.PUBLIC);
        setInterfaces(cf, interfaces, hasGetHandler ? Proxy.class : ProxyObject.class);
        ConstPool pool = cf.getConstPool();

        // legacy: we only add the static field for the default interceptor if caching is disabled
        if  (!factoryUseCache) {
            FieldInfo finfo = new FieldInfo(pool, DEFAULT_INTERCEPTOR, HANDLER_TYPE);
            finfo.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
            cf.addField(finfo);
        }

        // handler is per instance
        FieldInfo finfo2 = new FieldInfo(pool, HANDLER, HANDLER_TYPE);
        finfo2.setAccessFlags(AccessFlag.PRIVATE);
        cf.addField(finfo2);

        // filter signature is per class
        FieldInfo finfo3 = new FieldInfo(pool, FILTER_SIGNATURE_FIELD, FILTER_SIGNATURE_TYPE);
        finfo3.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        cf.addField(finfo3);

        // the proxy class serial uid must always be a fixed value
        FieldInfo finfo4 = new FieldInfo(pool, SERIAL_VERSION_UID_FIELD, SERIAL_VERSION_UID_TYPE);
        finfo4.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC| AccessFlag.FINAL);
        cf.addField(finfo4);

        // HashMap allMethods = getMethods(superClass, interfaces);
        // int size = allMethods.size();
        makeConstructors(classname, cf, pool, classname);

        List<Find2MethodsArgs> forwarders = new ArrayList<Find2MethodsArgs>();
        int s = overrideMethods(cf, pool, classname, forwarders);
        addClassInitializer(cf, pool, classname, s, forwarders);
        addSetter(classname, cf, pool);
        if (!hasGetHandler)
            addGetter(classname, cf, pool);

        if (factoryWriteReplace) {
            try {
                cf.addMethod(makeWriteReplace(pool));
            }
            catch (DuplicateMemberException e) {
                // writeReplace() is already declared in the super class/interfaces.
            }
        }

        thisClass = null;
        return cf;
    }

ProxyFactory::overrideMethods

添加方法签名后的方法,byte[] signature 对应 bit 值为1的被添加到 ClassFile 中

    private int overrideMethods(ClassFile cf, ConstPool cp, String className, List<Find2MethodsArgs> forwarders)
        throws CannotCompileException
    {
        String prefix = makeUniqueName("_d", signatureMethods);
        Iterator<Map.Entry<String,Method>> it = signatureMethods.iterator();
        int index = 0;
        while (it.hasNext()) {
            Map.Entry<String,Method> e = it.next();
            if (ClassFile.MAJOR_VERSION < ClassFile.JAVA_5 || !isBridge(e.getValue()))
            	if (testBit(signature, index)) {
            		override(className, e.getValue(), prefix, index,
            				 keyToDesc(e.getKey(), e.getValue()), cf, cp, forwarders);
            	}

            index++;
        }

        return index;
    }

ProxyFactory::nameGenerator

生成代理类名,规则为:basename + "_$$_jvst" + 三位(this.hashCode() & 0xfff) + "_" + counter

    public static UniqueName nameGenerator = new UniqueName() {
        private final String sep = "_$$_jvst" + Integer.toHexString(this.hashCode() & 0xfff) + "_";
        private int counter = 0;

        @Override
        public String get(String classname) {
            return classname + sep + Integer.toHexString(counter++);
        }
    };

FactoryHelper::toClass

  1. 将 ClassFile 转换为字节流
  2. 根据类名和字节流等生成 Class 对象
    DefineClassHelper.toClassDefineClassHelper.toPublicClass通过 MethodHandle(Jdk7新特性,引入 java.lang.invoke包)更加灵活和轻量级。
    public static Class<?> toClass(ClassFile cf, Class<?> neighbor,
                                   ClassLoader loader, ProtectionDomain domain)
        throws CannotCompileException
    {
        try {
            byte[] b = toBytecode(cf);
            if (ProxyFactory.onlyPublicMethods)
                return DefineClassHelper.toPublicClass(cf.getName(), b);
            else
                return DefineClassHelper.toClass(cf.getName(), neighbor,
                                                 loader, domain, b);
        }
        catch (IOException e) {
            throw new CannotCompileException(e);
        }
     }
	
	public static Class<?> toClass(ClassFile cf, java.lang.invoke.MethodHandles.Lookup lookup)
        throws CannotCompileException
    {
        try {
            byte[] b = toBytecode(cf);
            return DefineClassHelper.toClass(lookup, b);
        }
        catch (IOException e) {
            throw new CannotCompileException(e);
        }
     }

FactoryHelper::writeFile

将 ClassFile 写入到指定存储目录

    public static void writeFile(ClassFile cf, String directoryName)
            throws CannotCompileException {
        try {
            writeFile0(cf, directoryName);
        }
        catch (IOException e) {
            throw new CannotCompileException(e);
        }
    }

    private static void writeFile0(ClassFile cf, String directoryName)
            throws CannotCompileException, IOException {
        String classname = cf.getName();
        String filename = directoryName + File.separatorChar
                + classname.replace('.', File.separatorChar) + ".class";
        int pos = filename.lastIndexOf(File.separatorChar);
        if (pos > 0) {
            String dir = filename.substring(0, pos);
            if (!dir.equals("."))
                new File(dir).mkdirs();
        }

        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
                new FileOutputStream(filename)));
        try {
            cf.write(out);
        }
        catch (IOException e) {
            throw e;
        }
        finally {
            out.close();
        }
    }
}

测试类

public class JavassistDynamicProxyTester {
    public static void main(String args[]) throws Exception {
        ProxyTarget target = new ProxyTarget();
        JavassistProxyFactory<ProxyTarget> factory = new JavassistProxyFactory<>(target);
        //设置动态生成代理类文件的输出目录
        factory.writeDirectory = "D:/javassist-dynamic-proxy";
        ProxyTarget proxy = factory.getProxy();
        proxy.doSomething();
    }
}

目录结构

D:\javassist-dynamic-proxy>
D:.
|   javassist-3.24.1-GA.jar
|   JavassistDynamicProxyTester.java
|   JavassistProxyFactory.java
|   procyon-decompiler-0.5.34.jar
|   ProxyTarget.java
|   ProxyTarget_$$_jvst45a_0.class

执行命令

D:\javassist-dynamic-proxy>SET CLASSPATH=%CLASSPATH%;./javassist-3.24.1-GA.jar
// -encoding UTF-8 防止中文GBK编码导致 error
D:\javassist-dynamic-proxy>javac -encoding UTF-8 -d ./ ./*.java
D:\javassist-dynamic-proxy>java JavassistDynamicProxyTester
D:\javassist-dynamic-proxy>java -jar procyon-decompiler-0.5.34.jar ProxyTarget_$$_jvst45a_0.class -o .

procyon反编译代理类 Class 文件:ProxyTarget_$$_javassist_0.java

import java.io.ObjectStreamException;
import javassist.util.proxy.RuntimeSupport;
import java.lang.reflect.Method;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyObject;

// 
// Decompiled by Procyon v0.5.34
// 

public class ProxyTarget_$$_jvst45a_0 extends ProxyTarget implements ProxyObject
{
    private MethodHandler handler;
    public static byte[] _filter_signature;
    public static final long serialVersionUID;
    private static Method[] _methods_;
    
    public ProxyTarget_$$_jvst45a_0() {
        this.handler = RuntimeSupport.default_interceptor;
    }
    
    public final void _d1doSomething() {
        super.doSomething();
    }
    
    @Override
    public final void doSomething() {
        final Method[] methods_ = ProxyTarget_$$_jvst45a_0._methods_;
        this.handler.invoke((Object)this, methods_[2], methods_[3], new Object[0]);
    }
    
    static throws ClassNotFoundException {
        final Method[] methods_ = new Method[26];
        RuntimeSupport.find2Methods((Class)Class.forName("ProxyTarget_$$_jvst45a_0"), "doSomething", "_d1doSomething", 2, "()V", methods_);
        ProxyTarget_$$_jvst45a_0._methods_ = methods_;
        serialVersionUID = -1L;
    }
    
    public void setHandler(final MethodHandler handler) {
        this.handler = handler;
    }
    
    public MethodHandler getHandler() {
        return this.handler;
    }
    
    Object writeReplace() throws ObjectStreamException {
        return RuntimeSupport.makeSerializedProxy((Object)this);
    }
}

RuntimeSupport::default_interceptor

代理类构造器中指定默认的拦截器:直接调用:proceed.invoke(self, args);

    public static MethodHandler default_interceptor = new DefaultMethodHandler();

    static class DefaultMethodHandler implements MethodHandler, Serializable {
        /** default serialVersionUID */
        private static final long serialVersionUID = 1L;

        @Override
        public Object invoke(Object self, Method m,
                             Method proceed, Object[] args)
            throws Exception
        {
            return proceed.invoke(self, args);
        }
    };

RuntimeSupport::find2Methods

检索指定的方法名的两个方法,将它们存入 methods 对应索引 index (目标类方法) 和 index + 1 (代理类方法) 的位置

    public static void find2Methods(Class<?> clazz, String superMethod,
                                    String thisMethod, int index,
                                    String desc, java.lang.reflect.Method[] methods)
    {
        methods[index + 1] = thisMethod == null ? null
                                                : findMethod(clazz, thisMethod, desc);
        methods[index] = findSuperClassMethod(clazz, superMethod, desc);
    }

RuntimeSupport::makeSerializedProxy

将代理类实例转换为可用于流传输的序列化对象:

    public static SerializedProxy makeSerializedProxy(Object proxy)
        throws java.io.InvalidClassException
    {
        Class<?> clazz = proxy.getClass();

        MethodHandler methodHandler = null;
        if (proxy instanceof ProxyObject)
            methodHandler = ((ProxyObject)proxy).getHandler();
        else if (proxy instanceof Proxy)
            methodHandler = ProxyFactory.getHandler((Proxy)proxy);

        return new SerializedProxy(clazz, ProxyFactory.getFilterSignature(clazz), methodHandler);
    }

Javassist
官方首页:http://www.javassist.org
教程:http://www.javassist.org/tutorial/tutorial.html
Github:https://github.com/jboss-javassist/javassist
参考:https://www.jianshu.com/p/43424242846b

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值