Commits

musachy  committed fab897f

add caching to map accessors...add byte code accessors

git-svn-id: http://svn.opensymphony.com/svn/xwork/branches/parameter-binder@2010e221344d-f017-0410-9bd5-d282ab1896d7

  • Participants
  • Parent commits 313fa06
  • Branches parameter-binder

Comments (0)

Files changed (9)

File core/src/main/java/com/opensymphony/xwork2/config/providers/XWorkConfigurationProvider.java

 import com.opensymphony.xwork2.TextProvider;
 import com.opensymphony.xwork2.UnknownHandlerManager;
 import com.opensymphony.xwork2.parameters.XWorkParametersBinder;
+import com.opensymphony.xwork2.parameters.bytecode.AccessorBytecodeUtil;
 import com.opensymphony.xwork2.parameters.accessor.*;
 import com.opensymphony.xwork2.config.Configuration;
 import com.opensymphony.xwork2.config.ConfigurationException;
                .factory(ParametersPropertyAccessor.class, Map.class.getName(), ParametersMapPropertyAccessor.class, Scope.SINGLETON)
                .factory(ParametersPropertyAccessor.class, CompoundRoot.class.getName(), ParametersCompoundRootAccessor.class, Scope.SINGLETON)
                .factory(ParametersPropertyAccessor.class, Object.class.getName(), ParametersObjectPropertyAccessor.class, Scope.SINGLETON)
+               .factory(AccessorBytecodeUtil.class)
                
                // silly workarounds for ognl since there is no way to flush its caches
                .factory(PropertyAccessor.class, List.class.getName(), XWorkListPropertyAccessor.class, Scope.SINGLETON)

File core/src/main/java/com/opensymphony/xwork2/parameters/accessor/ParametersMapPropertyAccessor.java

 import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
 import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer;
 import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.parameters.bytecode.AccessorBytecodeUtil;
 import com.opensymphony.xwork2.inject.Inject;
 
 import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * Same code as XWorkMapPropertyAccessor, but all exceptions are catched from
     private XWorkConverter xworkConverter;
     private ObjectFactory objectFactory;
     private ObjectTypeDeterminer objectTypeDeterminer;
+    private AccessorBytecodeUtil accessorBytecodeUtil;
+
+    private final Map<Key, Class> keyClassCache = new WeakHashMap<Key, Class>();
+
+    @Inject
+    public void setAccessorBytecodeUtil(AccessorBytecodeUtil accessorBytecodeUtil) {
+        this.accessorBytecodeUtil = accessorBytecodeUtil;
+    }
 
     @Inject
     public void setXWorkConverter(XWorkConverter conv) {
             LOG.debug("Entering getProperty ("+context+","+target+","+name+")");
         }
 
-        ReflectionContextState.updateCurrentPropertyPath(context, name);
-       
         Object result = null;
 
 
             // commented out the above -- it makes absolutely no sense for when setting basic maps!
             return name;
         }
-        Class keyClass = objectTypeDeterminer.getKeyClass(lastClass, lastProperty);
+
+        Key key = new Key(lastClass, lastProperty);
+        //lookup in the cache first
+        Class keyClass = keyClassCache.get(key);
+
+        if (keyClass == null) {
+            keyClass = objectTypeDeterminer.getKeyClass(lastClass, lastProperty);
+
+            if (keyClass != null)
+                keyClassCache.put(key, keyClass);    
+        }
+
         if (keyClass == null) {
             keyClass = String.class;
+            keyClassCache.put(key, String.class);    
         }
 
         return xworkConverter.convertValue(context, name, keyClass);
 
     }
 }
+
+class Key {
+    Class clazz;
+    String property;
+
+    Key(Class clazz, String property) {
+        this.clazz = clazz;
+        this.property = property;
+    }
+
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Key key = (Key) o;
+
+        if (clazz != null ? !clazz.equals(key.clazz) : key.clazz != null) return false;
+        if (property != null ? !property.equals(key.property) : key.property != null) return false;
+
+        return true;
+    }
+
+    public int hashCode() {
+        int result = clazz != null ? clazz.hashCode() : 0;
+        result = 31 * result + (property != null ? property.hashCode() : 0);
+        return result;
+    }
+}

File core/src/main/java/com/opensymphony/xwork2/parameters/accessor/ParametersObjectPropertyAccessor.java

 import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
 import com.opensymphony.xwork2.util.reflection.ReflectionProvider;
+import com.opensymphony.xwork2.parameters.bytecode.AccessorBytecodeUtil;
+import com.opensymphony.xwork2.parameters.bytecode.Getter;
+import com.opensymphony.xwork2.parameters.bytecode.Setter;
 
 import java.util.Map;
 
 
 public class ParametersObjectPropertyAccessor implements ParametersPropertyAccessor {
-    protected ReflectionProvider reflectionProvider;
+    protected AccessorBytecodeUtil accessorBytecodeUtil;
 
     @Override
     public Object getProperty(Map context, Object target, Object property) throws Exception {
-        Object obj = reflectionProvider.getValue(property.toString(), context, target);
+        Getter getter = accessorBytecodeUtil.getGetter(target.getClass(), property.toString());
+        Object obj = getter.invoke(target);
 
         context.put(XWorkConverter.LAST_BEAN_CLASS_ACCESSED, target.getClass());
         context.put(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED, property.toString());
-        ReflectionContextState.updateCurrentPropertyPath(context, property);
         return obj;
     }
 
     @Override
     public void setProperty(Map context, Object target, Object property, Object value) throws Exception {
-        reflectionProvider.setProperty(property.toString(), value, target, context);
+        Setter setter = accessorBytecodeUtil.getSetter(target.getClass(), value.getClass(), property.toString());
+        setter.invoke(target, value);
     }
 
     @Inject
-    public void setReflectionProvider(ReflectionProvider reflectionProvider) {
-        this.reflectionProvider = reflectionProvider;
+    public void setAccessorBytecodeUtil(AccessorBytecodeUtil accessorBytecodeUtil) {
+        this.accessorBytecodeUtil = accessorBytecodeUtil;
     }
 }

File core/src/main/java/com/opensymphony/xwork2/parameters/bytecode/AccessorBytecodeUtil.java

+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.opensymphony.xwork2.parameters.bytecode;
+
+import java.util.*;
+import java.lang.reflect.Constructor;
+
+import org.objectweb.asm.*;
+import org.apache.commons.lang.StringUtils;
+import com.opensymphony.xwork2.util.reflection.ReflectionProvider;
+import com.opensymphony.xwork2.inject.Inject;
+
+public class AccessorBytecodeUtil implements Opcodes {
+    private static int counter;
+
+    protected final Map<Key, Setter> settersCache = new HashMap<Key, Setter>();
+    protected final Map<Key, Getter> gettersCache = new HashMap<Key, Getter>();
+
+    protected final String SETTER_INTERFACE = toPathName(Setter.class.getName());
+    protected final String GETTER_INTERFACE = toPathName(Getter.class.getName());
+
+    protected final AccessorsClassLoader classLoader = new AccessorsClassLoader();
+
+    protected ReflectionProvider reflectionProvider;
+
+    @Inject
+    public void setReflectionProvider(ReflectionProvider reflectionProvider) {
+        this.reflectionProvider = reflectionProvider;
+    }
+
+    public Getter getGetter(Class targetType, String propertyName) throws Exception {
+        ClassWriter cw = new ClassWriter(0);
+        FieldVisitor fv;
+        MethodVisitor mv;
+        AnnotationVisitor av0;
+
+        String targetClassName = toPathName(targetType.getName());
+        String methodName = "get" + StringUtils.capitalize(propertyName);
+
+        //cache key
+        Key key = new Key();
+        key.targetClassName = targetClassName;
+        key.methodName = methodName;
+
+        //get from cache
+        if (gettersCache.containsKey(key))
+            return gettersCache.get(key);
+
+        //use reflection to determine the return type of the getter, this will happen only once by accessor
+        Class returnType = reflectionProvider.getGetMethod(targetType, propertyName).getReturnType();
+
+        int postfix = counter++;
+        String className = targetClassName + postfix;
+
+        cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", new String[]{GETTER_INTERFACE});
+
+        {
+            fv = cw.visitField(ACC_PRIVATE, "propertyName", "Ljava/lang/String;", null, null);
+            fv.visitEnd();
+        }
+        {
+            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;)V", null, null);
+            mv.visitCode();
+            mv.visitVarInsn(ALOAD, 0);
+            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
+            mv.visitVarInsn(ALOAD, 0);
+            mv.visitVarInsn(ALOAD, 1);
+            mv.visitFieldInsn(PUTFIELD, className, "propertyName", "Ljava/lang/String;");
+            mv.visitInsn(RETURN);
+            mv.visitMaxs(2, 2);
+            mv.visitEnd();
+        }
+        {
+            mv = cw.visitMethod(ACC_PUBLIC, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;", null, null);
+            mv.visitCode();
+            mv.visitVarInsn(ALOAD, 1);
+            mv.visitTypeInsn(CHECKCAST, targetClassName);
+            mv.visitMethodInsn(INVOKEVIRTUAL, targetClassName, methodName, "()" +  Type.getDescriptor(returnType));
+            mv.visitInsn(ARETURN);
+            mv.visitMaxs(1, 2);
+            mv.visitEnd();
+        }
+        {
+            mv = cw.visitMethod(ACC_PUBLIC, "getPropertyName", "()Ljava/lang/String;", null, null);
+            mv.visitCode();
+            mv.visitVarInsn(ALOAD, 0);
+            mv.visitFieldInsn(GETFIELD, className, "propertyName", "Ljava/lang/String;");
+            mv.visitInsn(ARETURN);
+            mv.visitMaxs(1, 1);
+            mv.visitEnd();
+        }
+        cw.visitEnd();
+
+        //this one needs "." instead of "/" in the name
+        String finalClassName = targetType.getName() + postfix;
+
+        Class clazz = classLoader.defineClass(finalClassName, cw.toByteArray());
+        Constructor constructor = clazz.getConstructor(new Class[]{String.class});
+        Getter getter = (Getter) constructor.newInstance(new Object[]{propertyName});
+        gettersCache.put(key, getter);
+        return getter;
+    }
+
+    public Setter getSetter(Class targetType, Class valueType, String propertyName) throws Exception {
+        ClassWriter cw = new ClassWriter(0);
+        FieldVisitor fv;
+        MethodVisitor mv;
+        AnnotationVisitor av0;
+
+        String targetClassName = toPathName(targetType.getName());
+        String valueClassName = toPathName(valueType.getName());
+        String methodName = "set" + StringUtils.capitalize(propertyName);
+
+        //cache key
+        Key key = new Key();
+        key.targetClassName = targetClassName;
+        key.valueClassName = valueClassName;
+        key.methodName = methodName;
+
+        //get from cache
+        if (settersCache.containsKey(key))
+            return settersCache.get(key);
+
+        int postfix = counter++;
+        String className = targetClassName + postfix;
+
+        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", new String[]{SETTER_INTERFACE});
+
+        {
+            fv = cw.visitField(ACC_PRIVATE, "propertyName", "Ljava/lang/String;", null, null);
+            fv.visitEnd();
+        }
+        {
+            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;)V", null, null);
+            mv.visitCode();
+            mv.visitVarInsn(ALOAD, 0);
+            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
+            mv.visitVarInsn(ALOAD, 0);
+            mv.visitVarInsn(ALOAD, 1);
+            mv.visitFieldInsn(PUTFIELD, className, "propertyName", "Ljava/lang/String;");
+            mv.visitInsn(RETURN);
+            mv.visitMaxs(2, 2);
+            mv.visitEnd();
+        }
+        {
+            mv = cw.visitMethod(ACC_PUBLIC, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)V", null, null);
+            mv.visitCode();
+            mv.visitVarInsn(ALOAD, 1);
+            mv.visitTypeInsn(CHECKCAST, targetClassName);
+            mv.visitVarInsn(ALOAD, 2);
+            mv.visitTypeInsn(CHECKCAST, valueClassName);
+            mv.visitMethodInsn(INVOKEVIRTUAL, targetClassName, methodName, "(" + Type.getDescriptor(valueType) + ")V");
+            mv.visitInsn(RETURN);
+            mv.visitMaxs(2, 3);
+            mv.visitEnd();
+        }
+        {
+            mv = cw.visitMethod(ACC_PUBLIC, "getPropertyName", "()Ljava/lang/String;", null, null);
+            mv.visitCode();
+            mv.visitVarInsn(ALOAD, 0);
+            mv.visitFieldInsn(GETFIELD, className, "propertyName", "Ljava/lang/String;");
+            mv.visitInsn(ARETURN);
+            mv.visitMaxs(1, 1);
+            mv.visitEnd();
+        }
+        cw.visitEnd();
+
+        //this one needs "." instead of "/" in the name
+        String finalClassName = targetType.getName() + postfix;
+
+        Class clazz = classLoader.defineClass(finalClassName, cw.toByteArray());
+        Constructor constructor = clazz.getConstructor(new Class[]{String.class});
+        Setter setter = (Setter) constructor.newInstance(new Object[]{propertyName});
+        settersCache.put(key, setter);
+        return setter;
+    }
+
+    private String toPathName(String name) {
+        return name.replace('.', '/');
+    }
+}
+
+class AccessorsClassLoader extends ClassLoader {
+    public Class defineClass(String name, byte[] bytes) {
+        return defineClass(name, bytes, 0, bytes.length);
+    }
+}
+
+class Key {
+    String targetClassName;
+    String valueClassName;
+    String methodName;
+
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Key setterKey = (Key) o;
+
+        if (methodName != null ? !methodName.equals(setterKey.methodName) : setterKey.methodName != null) return false;
+        if (targetClassName != null ? !targetClassName.equals(setterKey.targetClassName) : setterKey.targetClassName != null)
+            return false;
+        if (valueClassName != null ? !valueClassName.equals(setterKey.valueClassName) : setterKey.valueClassName != null)
+            return false;
+
+        return true;
+    }
+
+    public int hashCode() {
+        int result = targetClassName != null ? targetClassName.hashCode() : 0;
+        result = 31 * result + (valueClassName != null ? valueClassName.hashCode() : 0);
+        result = 31 * result + (methodName != null ? methodName.hashCode() : 0);
+        return result;
+    }
+}

File core/src/main/java/com/opensymphony/xwork2/parameters/bytecode/Getter.java

+package com.opensymphony.xwork2.parameters.bytecode;
+
+/**
+ * Classes implementing this interface will call
+ * target.get${propertyName} when invoke(...) is called.
+ */
+public interface Getter {
+    Object invoke(Object target);
+    String getPropertyName();    
+}

File core/src/main/java/com/opensymphony/xwork2/parameters/bytecode/Setter.java

+package com.opensymphony.xwork2.parameters.bytecode;
+
+/**
+ * Classes implementing this interface will call
+ * target.set${propertyName}(param) when invoke(...) is called.
+ */
+public interface Setter {
+    void invoke(Object target, Object param);
+    String getPropertyName();
+}

File core/src/main/java/com/opensymphony/xwork2/util/reflection/ReflectionContextState.java

 		String currentPath=getCurrentPropertyPath(context);
 		if (name!=null) {
 			if (currentPath!=null) {
-				currentPath=currentPath + "." + name.toString();
+                StringBuilder sb = new StringBuilder(currentPath);
+                sb.append(".");
+                sb.append(name.toString());
+				currentPath = sb.toString();
 			}	else {
-				currentPath=name.toString();
+				currentPath = name.toString();
 			}
 			context.put(CURRENT_PROPERTY_PATH, currentPath);
 		}

File core/src/test/java/com/opensymphony/xwork2/parameters/PerformanceTest.java

+package com.opensymphony.xwork2.parameters;
+
+import com.opensymphony.xwork2.SimpleAction;
+import com.opensymphony.xwork2.XWorkTestCase;
+import com.opensymphony.xwork2.ognl.OgnlUtil;
+
+import java.util.Map;
+import java.util.HashMap;
+
+public class PerformanceTest extends XWorkTestCase {
+    public void testPerformance() throws Exception {
+
+        SimpleAction action = new SimpleAction();
+        Map<String, Object> emptyMap = new HashMap();
+        XWorkParametersBinder binder = container.getInstance(XWorkParametersBinder.class);
+        OgnlUtil ognlUtil = container.getInstance(OgnlUtil.class);
+
+        int k = 15000;
+
+        long start = System.currentTimeMillis();
+        for (int i = 0; i < k; i++) {
+            binder.setProperty(emptyMap, action, "theProtectedMap['p0 p1']", "test");
+            binder.setProperty(emptyMap, action, "otherMap['my_hero'].name", "test");
+            binder.setProperty(emptyMap, action, "nestedAction.bean.name", "test");
+        }
+
+        long end = System.currentTimeMillis();
+        System.out.println("params: " + (end - start) / 1000);
+
+        action = new SimpleAction();
+        emptyMap = new HashMap();
+
+        start = System.currentTimeMillis();
+        for (int i = 0; i < k; i++) {
+            ognlUtil.setProperty("theProtectedMap['p0 p1']", "test", action, emptyMap);
+            ognlUtil.setProperty("otherMap['my_hero'].name", "test", action, emptyMap);
+            ognlUtil.setProperty("nestedAction.bean.name", "test", action, emptyMap);
+        }
+
+        end = System.currentTimeMillis();
+        System.out.println("ognl: " + (end - start) / 1000);
+    }
+}

File core/src/test/java/com/opensymphony/xwork2/parameters/bytecode/AccessorBytecodeUtilTest.java

+package com.opensymphony.xwork2.parameters.bytecode;
+
+import com.opensymphony.xwork2.XWorkTestCase;
+import com.opensymphony.xwork2.SimpleAction;
+
+public class AccessorBytecodeUtilTest extends XWorkTestCase {
+    private AccessorBytecodeUtil accessorBytecodeUtil;
+
+    public void testCreateSetter() throws Exception {
+        Setter setter = accessorBytecodeUtil.getSetter(SimpleAction.class, String.class, "name");
+
+        SimpleAction action = new SimpleAction();
+        setter.invoke(action, "Fyodor");
+
+        assertEquals("Fyodor", action.getName());
+    }
+
+    public void testCreateGetter() throws Exception {
+        Getter getter = accessorBytecodeUtil.getGetter(SimpleAction.class, "name");
+
+        SimpleAction action = new SimpleAction();
+        action.setName("Dostoevsky");
+        String result = (String) getter.invoke(action);
+
+        assertNotNull(result);
+        assertEquals("Dostoevsky", result);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        this.accessorBytecodeUtil = container.getInstance(AccessorBytecodeUtil.class);
+    }
+}