Русский English Тэги View Sergey Zolotaryov's profile on LinkedIn Вход
Борьба с thread contention в XmlBeans
Постоянная ссылка 28-03-2011 anydoby java

Недавно пришлось распараллеливать одну штуку и наткнулся на очень неприятную особенность XmlBeans. Дело в том, что мы используем XmlBeans как... гм... парсер xml :) и объекты, которые он сгенерил, лежат у нас в конфигурации и используются многими потоками. В режиме только для чтения.

А XmlBeans в каждый сгенерённый метод всовывает вот такое:


    /**
     * Gets the "value" attribute
     */
    public java.lang.String getValue()
    {
        synchronized (monitor())
        {
            check_orphaned();
            org.apache.xmlbeans.SimpleValue target = null;
            target = (org.apache.xmlbeans.SimpleValue)get_store().find_attribute_user(VALUE$0);
            if (target == null)
            {
                return null;
            }
            return target.getStringValue();
        }
    }

Мягко говоря, такого от обычного getter метода мало кто ожидает. Особенно убил synchronized. Понятно, они защищают свою попу, к DOM дереву обращается всегда только один поток, но ведь можно дать юзеру выбор - хочу объект, из которого будет только браться, а писаться не будет - не надо синхронизировать.

В результате получается, что если объект конфигурации просто читать из множества потоков, то потоки иногда дожидаются секунды своей очереди, что я увидел в профайлере.

Как с этим бороться? Я выбрал просто способ (хотел сказать, элегантный, но язык не повернулся :). Таки вот, XmlBeans, молодец, он не просто генерит POJOs, как, например, JAXB. Он генерит интерфейсы, за что ему большое спасибо. И вот эта вся колбаса выше это реализация интерфейса. Так что ничто нам не мешает использовать другую реализацию. Так как классов у нас до чёртиков, то руками это делать не решился, а написал вот такое:


package com.anydoby.utilities;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;

import org.apache.xmlbeans.XmlObject;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
 * This helper class allows copying of stuff from XmlObject instances to immutable POJOs which
 * implement the same interface. Immutable here means that only get/is and sizeOf methods will be
 * available.
 */
public class ImmutableUtils
{

    private static final Pattern GETTER_PATTERN = Pattern.compile("(is|get)([A-Z].+)");

    /**
     * Makes an immutable copy of object which does not block when concurrently invoking getters.
     * 
     * @param <E>
     * @param object
     * @return deep copy of the object
     */
    @SuppressWarnings("unchecked")
    public static <E extends XmlObject> E asImmutable(E object)
    {
        if (object == null)
        {
            return null;
        }
        try
        {
            Class< ? >[] interfaces = object.getClass().getInterfaces();
            Assert.isTrue(interfaces.length == 1, "Only one interface is expected.");

            String interfaze = interfaces[0].getName();
            String immutableClassName = interfaze + "Immutable";
            Class< ? > intfc = interfaces[0];

            Class< ? > clazz = compileProxyClass(immutableClassName, intfc);
            E copy = (E) clazz.getConstructors()[0].newInstance(object);
            return copy;
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }
    }

    private static Class< ? > compileProxyClass(String immutableClassName, Class< ? > interfaze)
            throws ClassNotFoundException, CannotCompileException, NotFoundException, InstantiationException,
            IllegalAccessException
    {
        Class< ? > clazz;
        try
        {
            clazz = Class.forName(immutableClassName);
        }
        catch (ClassNotFoundException e)
        {
            CtClass ctClass = createCopyClass(immutableClassName, interfaze);
            clazz = ctClass.toClass();
        }
        return clazz;
    }

    public static List< ? > asImmutable(List< ? > list)
    {
        if (list == null)
        {
            return null;
        }
        List<Object> newList = new ArrayList<Object>();
        for (Object e : list)
        {
            if (e instanceof XmlObject)
            {
                XmlObject object = asImmutable((XmlObject) e);
                newList.add(object);
            }
            else
            {
                newList.add(e);
            }
        }
        return newList;
    }

    public static Object asImmutable(Object o)
    {
        Object result;
        if (o instanceof XmlObject)
        {
            result = asImmutable((XmlObject) o);
        }
        else if (o instanceof List)
        {
            result = asImmutable((List< ? >) o);
        }
        else
        {
            result = o;
        }
        return result;
    }

    private static CtClass createCopyClass(String className, Class< ? > intfc) throws CannotCompileException,
            NotFoundException, InstantiationException, IllegalAccessException, ClassNotFoundException
    {
        ClassPool pool = ClassPool.getDefault();
        pool.importPackage(ImmutableUtils.class.getName());
        pool.importPackage(XmlObject.class.getName());
        CtClass c = pool.makeClass(className);
        CtClass xmlBeansInterface = pool.get(intfc.getName());
        c.setInterfaces(new CtClass[]
        { xmlBeansInterface });

        CtMethod[] methods = c.getMethods();
        for (CtMethod ctMethod : methods)
        {
            String name = ctMethod.getName();
            Matcher matcher = GETTER_PATTERN.matcher(name);
            if ((matcher.matches() && !name.endsWith("Array")) || name.startsWith("sizeOf"))
            {
                CtClass declaringClass = ctMethod.getDeclaringClass();
                if (declaringClass.getName().startsWith("org.apache.xmlbeans")
                        || declaringClass.getName().startsWith("java.lang"))
                {
                    continue;
                }
                CtClass returnType = ctMethod.getReturnType();
                CtField field = new CtField(returnType, name, c);
                c.addField(field);
                CtMethod method = CtNewMethod.getter(name, field);
                c.addMethod(method);
            }
        }

        CtConstructor constructor = new CtConstructor(new CtClass[]
        { xmlBeansInterface }, c);
        CtField[] fields = c.getDeclaredFields();
        CtField field = new CtField(xmlBeansInterface, "delegate", c);
        c.addField(field);

        StringBuilder ctor = new StringBuilder();
        ctor.append("{\n");
        ctor.append("this.delegate = $1;\n");
        for (CtField ctField : fields)
        {
            CtClass type = ctField.getType();
            if (isSafeToAssignAsIs(type))
            {
                ctor.append("this." + ctField.getName() + " = $1." + ctField.getName() + "();");
            }
            else
            {
                ctor.append("this." + ctField.getName() + " = (" + ctField.getType().getName()
                        + ")ImmutableUtils.asImmutable($1." + ctField.getName() + "());");
            }
            ctor.append('\n');
        }
        ctor.append("}");
        constructor.setBody(ctor.toString());
        c.addConstructor(constructor);

        StringBuilder toString = new StringBuilder();
        toString.append("public String toString() {return \"").append(intfc.getSimpleName()).append("[\"");
        boolean first = true;
        for (CtField ctField : fields)
        {
            String name = ctField.getName();
            Matcher matcher = GETTER_PATTERN.matcher(name);
            if (matcher.matches()) {
                if (!first) {
                    toString.append("+\",\"");
                }
                toString.append("+\"").append(name).append("=\"+");
                toString.append("java.lang.String.valueOf(this.").append(name).append(")");
                first^=first;
            }
        }
        toString.append("+\"]\";}");
        CtMethod method = CtNewMethod.make(toString.toString(), c);
        c.addMethod(method);
        
        {            
            // we want to be able to use xpath
            CtMethod selectPath = CtNewMethod.make("public XmlObject[] selectPath ( String path ){return delegate.selectPath(path);}", c);
            c.addMethod(selectPath);
        }
        {
            // we want to be able to use xmlText() - for comparisons
            CtMethod xmlText = CtNewMethod.make("public String xmlText(){return delegate.xmlText();}", c);
            c.addMethod(xmlText);            
        }
        
        return c;
    }
    
    private static boolean isSafeToAssignAsIs(CtClass type) throws NotFoundException, ClassNotFoundException
    {
        if (type.isPrimitive() || type.isEnum() || isPrimitiveWrapperOrJavaLang(type))
        {
            return true;
        }
        if (type.isArray())
        {
            CtClass componentType = type.getComponentType();
            return isSafeToAssignAsIs(componentType);
        }
        return false;
    }

    private static boolean isPrimitiveWrapperOrJavaLang(CtClass type) throws ClassNotFoundException
    {
        String name = type.getName();
        if (name.startsWith("java.lang.")) {
            return true;
        }
        Class< ? > clazz = Class.forName(name);
        return ClassUtils.isPrimitiveOrWrapper(clazz);
    }

}

Чего оно делает? Создает реализацию нужного интерфейса на ходу и копирует все свойства объекта куда надо. Получается immutable версия XmlObject. Для того, чтобы работал Xpath, есть еще selectPath, он работает на оригинале.

Вот и вся нелёгкая :)

Добавить комментарий

Предыдущая статья OOM в Oracle Следующая статья Сравнение производительности RandomAccessFile и FileChannel