代码改变世界

动态修改字节码以替换用反射调用get set方法的形式

2018-04-20 15:48  chen.simon  阅读(1552)  评论(0编辑  收藏  举报

1. 起因

在前两天,为了解决websphere和JDK8上部署的应用发起webservice调用(框架用的cxf)时报错的问题,跟了一些代码,最终发现可以通过加上参数-Dcom.sun.xml.bind.v2.bytecode.ClassTailor.noOptimize=true来解决。

2. ClassTailor.noOptimize优化了什么

分析jaxb的代码分析,由于webservice调用要用到xml与bean对象的转换,于是就是用到对bean字段的get set。通常的想法此处用反射便可以完成。但是jaxb在这里用了动态生成字节码的方式直接调用bean的get set方法,以达到节提升性能的目的,本质上就是换了class文件中常量池的UFT8字符串

具体可以参见com.sun.xml.bind.v2.bytecode.ClassTailor类,com.sun.xml.bind.v2.runtime.reflect.opt.AccessorInjector 类。

3. 按他的方式做一个demo

demo代码打包在这里下载,可以跑起来的

假设需要操作的bean是User 如下:

/*
 * 文 件 名:  User.java
 * 版    权:   . Copyright 2008-2017,  All rights reserved   Co.,Ltd.
 * 描    述:  <描述>
 * 修 改 人:  simon
 * 修改时间:  2018年4月20日
 */
package com.cnblogs.simoncook.jaxb.bean;

/**
 * <一句话功能简述>
 * <功能详细描述>
 * 
 * @author  simon
 * @version  [版本号, 2018年4月20日]
 * @see  [相关类/方法]
 * @since  [产品/模块版本]
 */
public class User
{
    
    private String name;

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }
    
}

转换的模板类与接口

/*
 * 文 件 名:  IMethodAccess.java
 * 版    权:   . Copyright 2008-2017,  All rights reserved   Co.,Ltd.
 * 描    述:  <描述>
 * 修 改 人:  simon
 * 修改时间:  2018年4月20日
 */
package com.cnblogs.simoncook.jaxb.helper;

/**
 * <一句话功能简述>
 * <功能详细描述>
 * 
 * @author  simon
 * @version  [版本号, 2018年4月20日]
 * @see  [相关类/方法]
 * @since  [产品/模块版本]
 */
public interface IMethodAccess
{
    public Object get(Object bean);
    
    public void set(Object instance, Object value);
}
/*
 * 文 件 名:  MethodAcessRef.java
 * 版    权:   . Copyright 2008-2017,  All rights reserved   Co.,Ltd.
 * 描    述:  <描述>
 * 修 改 人:  simon
 * 修改时间:  2018年4月20日
 */
package com.cnblogs.simoncook.jaxb.helper;

/**
 * <一句话功能简述> <功能详细描述>
 * 
 * @author simon
 * @version [版本号, 2018年4月20日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public class MethodAcessTemplate implements IMethodAccess
{
    
    public MethodAcessTemplate()
    {
    }
    
    public Object get(Object bean)
    {
        return ((Bean)bean).getRef();
    }
    
    public void set(Object instance, Object value)
    {
        ((Bean)instance).setRef((Ref)value);
    }
}

动态字节码生成与加载调用类

/*
 * 文 件 名:  ClassTailor.java
 * 版    权:   . Copyright 2008-2017,  All rights reserved   Co.,Ltd.
 * 描    述:  <描述>
 * 修 改 人:  simon
 * 修改时间:  2018年4月20日
 */
package com.cnblogs.simoncook.jaxb.helper;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;

import com.cnblogs.simoncook.jaxb.bean.User;

/**
 * <一句话功能简述> <功能详细描述>
 * 
 * @author simon
 * @version [版本号, 2018年4月20日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public class ClassTailor
{
    
    /**
     * <一句话功能简述> <功能详细描述>
     * 
     * @param args
     * @see [类、类#方法、类#成员]
     */
    public static void main(String[] args)
    {
        User u = new User();
        ClassLoader classLoader = ClassTailor.class.getClassLoader();
        String templateClassName = "com/cnblogs/simoncook/jaxb/helper/MethodAcessTemplate";
        String methodAcessRefTemplate = templateClassName + ".class";
        InputStream templateClassResource = classLoader.getResourceAsStream(methodAcessRefTemplate);
        String[] replacements = {"com/cnblogs/simoncook/jaxb/helper/Bean", "com/cnblogs/simoncook/jaxb/bean/User",
            "com/cnblogs/simoncook/jaxb/helper/Ref", "java/lang/String", "()Lcom/cnblogs/simoncook/jaxb/helper/Ref;",
            "()Ljava/lang/String;", "(Lcom/cnblogs/simoncook/jaxb/helper/Ref;)V", "(Ljava/lang/String;)V", "getRef",
            "getName", "setRef", "setName"};
        
        String newClassName = "com/cnblogs/simoncook/jaxb/bean/User_getset_name";
        byte[] accessClassByte = tailor(templateClassResource, templateClassName, newClassName, replacements);
        dumpClass(newClassName, accessClassByte);
        
        try
        {
            @SuppressWarnings("unchecked")
            Class<IMethodAccess> c = buildGetSetClass(classLoader, newClassName, accessClassByte);
            IMethodAccess accessor = (IMethodAccess)c.newInstance();
            accessor.set(u, "simon");
            System.out.println(u.getName());
        }
        catch (NoSuchMethodException e)
        {
            e.printStackTrace();
        }
        catch (SecurityException e)
        {
            e.printStackTrace();
        }
        catch (IllegalAccessException e)
        {
            e.printStackTrace();
        }
        catch (IllegalArgumentException e)
        {
            e.printStackTrace();
        }
        catch (InvocationTargetException e)
        {
            e.printStackTrace();
        }
        catch (InstantiationException e)
        {
            e.printStackTrace();
        }
        
    }
    
    /**
     * <一句话功能简述> <功能详细描述>
     * 
     * @param classLoader
     * @param newClassName
     * @param accessClassByte
     * @return
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @see [类、类#方法、类#成员]
     */
    private static Class<IMethodAccess> buildGetSetClass(ClassLoader classLoader, String newClassName,
        byte[] accessClassByte)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException
    {
        final Method defineClass =
            ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, Integer.TYPE, Integer.TYPE);
        final Method resolveClass = ClassLoader.class.getDeclaredMethod("resolveClass", Class.class);
        AccessController.doPrivileged(new PrivilegedAction<Void>()
        {
            
            public Void run()
            {
                // TODO: check security implication
                // do these setAccessible allow anyone to call these methods freely?s
                defineClass.setAccessible(true);
                resolveClass.setAccessible(true);
                return null;
            }
        });
        ClassLoader filedTypeClassLoader = classLoader;
        
        @SuppressWarnings("unchecked")
        Class<IMethodAccess> c = (Class<IMethodAccess>)defineClass.invoke(filedTypeClassLoader,
            newClassName.replace('/', '.'),
            accessClassByte,
            0,
            accessClassByte.length);
        resolveClass.invoke(filedTypeClassLoader, c);
        return c;
    }
    
    /**
     * <一句话功能简述> <功能详细描述>
     * 
     * @param newClassName
     * @param accessClassByte
     * @see [类、类#方法、类#成员]
     */
    private static void dumpClass(String newClassName, byte[] accessClassByte)
    {
        FileOutputStream out = null;
        BufferedOutputStream bufW = null;
        try
        {
            File file = new File("/Users/simon/700.temp_/jaxbclass/" + newClassName);
            if (!file.getParentFile().exists())
            {
                file.getParentFile().mkdirs();
            }
            out = new FileOutputStream(file);
            bufW = new BufferedOutputStream(out);
            bufW.write(accessClassByte);
            bufW.flush();
            bufW.close();
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (out != null)
            {
                try
                {
                    out.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
            if (bufW != null)
            {
                try
                {
                    bufW.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static byte[] tailor(InputStream image, String templateClassName, String newClassName,
        String... replacements)
    {
        DataInputStream in = new DataInputStream(image);
        
        try
        {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
            DataOutputStream out = new DataOutputStream(baos);
            
            // skip until the constant pool count
            long l = in.readLong();
            out.writeLong(l);
            
            // read the constant pool size
            short count = in.readShort();
            out.writeShort(count);
            
            // replace constant pools
            for (int i = 0; i < count; i++)
            {
                byte tag = in.readByte();
                out.writeByte(tag);
                switch (tag)
                {
                    case 0:
                        // this isn't described in the spec,
                        // but class files often seem to have this '0' tag.
                        // we can apparently just ignore it, but not sure
                        // what this really means.
                        break;
                    
                    case 1: // CONSTANT_UTF8
                    {
                        String value = in.readUTF();
                        if (value.equals(templateClassName))
                            value = newClassName;
                        else
                        {
                            for (int j = 0; j < replacements.length; j += 2)
                                if (value.equals(replacements[j]))
                                {
                                    value = replacements[j + 1];
                                    break;
                                }
                        }
                        out.writeUTF(value);
                    }
                        break;
                    
                    case 3: // CONSTANT_Integer
                    case 4: // CONSTANT_Float
                        out.writeInt(in.readInt());
                        break;
                    
                    case 5: // CONSTANT_Long
                    case 6: // CONSTANT_Double
                        i++; // doubles and longs take two entries
                        out.writeLong(in.readLong());
                        break;
                    
                    case 7: // CONSTANT_Class
                    case 8: // CONSTANT_String
                        out.writeShort(in.readShort());
                        break;
                    
                    case 9: // CONSTANT_Fieldref
                    case 10: // CONSTANT_Methodref
                    case 11: // CONSTANT_InterfaceMethodref
                    case 12: // CONSTANT_NameAndType
                        out.writeInt(in.readInt());
                        break;
                    
                    default:
                        throw new IllegalArgumentException("Unknown constant type " + tag);
                }
            }
            
            // then copy the rest
            byte[] buf = new byte[512];
            int len;
            while ((len = in.read(buf)) > 0)
                out.write(buf, 0, len);
            
            in.close();
            out.close();
            
            // by now we got the properly tailored class file image
            return baos.toByteArray();
        }
        catch (Exception e)
        {
        }
        return null;
    }
    
}

4. 分析

用JD 对模板类class与之前的class(需要dump)进行反编译,结果如下,一目了然。

Snip20180420_9

Snip20180420_8

用classpy查看两个class的结构,也能看出来,本质上就是换了常量池的UFT8字符串

 

Snip20180420_12

 

Snip20180420_11

 

 

5. 与cglib比较

简单的看了下cglib的BeanMap,原理相同,手法不同。差异在于,他是用asm去动态生成class。

asm对class的生成做了一些封装,比如你要生成什么名字的class 构造函数是什么,版本是是什么,你只要调用他封装的接口,把这些要素当做参数传进去即可。不需要自己一个byte一个byte的去按class的文件格式组织。