From: adinn Date: Thu, 8 Apr 2010 08:12:32 +0000 (+0000) Subject: fixes for JASSIST-42 and JASSIST-97 X-Git-Tag: rel_3_17_1_ga~128 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bd1c47e0f555ada2d250a52f4a3b6697107486b0;p=javassist.git fixes for JASSIST-42 and JASSIST-97 git-svn-id: http://anonsvn.jboss.org/repos/javassist/trunk@522 30ef5769-5b8d-40dd-aea6-55b5d6557bb3 --- diff --git a/build.xml b/build.xml index 2a4fac13..729b197a 100644 --- a/build.xml +++ b/build.xml @@ -104,6 +104,17 @@ + + + + + + + + + + + This factory generates a class that extends the given super class and implements * the given interfaces. The calls of the methods inherited from the super class are * forwarded and then invoke() is called on the method handler - * associated with the generated class. The calls of the methods from the interfaces - * are also forwarded to the method handler. + * associated with instances of the generated class. The calls of the methods from + * the interfaces are also forwarded to the method handler. * *

For example, if the following code is executed, * *

      * ProxyFactory f = new ProxyFactory();
      * f.setSuperclass(Foo.class);
    - * MethodHandler mi = new MethodHandler() {
    - *     public Object invoke(Object self, Method m, Method proceed,
    - *                          Object[] args) throws Throwable {
    - *         System.out.println("Name: " + m.getName());
    - *         return proceed.invoke(self, args);  // execute the original method.
    - *     }
    - * };
      * f.setFilter(new MethodFilter() {
      *     public boolean isHandled(Method m) {
      *         // ignore finalize()
    @@ -61,6 +54,13 @@ import javassist.bytecode.*;
      *     }
      * });
      * Class c = f.createClass();
    + * MethodHandler mi = new MethodHandler() {
    + *     public Object invoke(Object self, Method m, Method proceed,
    + *                          Object[] args) throws Throwable {
    + *         System.out.println("Name: " + m.getName());
    + *         return proceed.invoke(self, args);  // execute the original method.
    + *     }
    + * };
      * Foo foo = (Foo)c.newInstance();
      * ((ProxyObject)foo).setHandler(mi);
      * 
@@ -86,28 +86,17 @@ import javassist.bytecode.*; * execute the following code: * *
    - * MethodHandler mi2 = ... ;    // another handler
    - * ((ProxyObject)foo).setHandler(mi2);
    - * 
- * - *

You can also specify the default method handler: - * - *

    - * ProxyFactory f2 = new ProxyFactory();
    - * f2.setSuperclass(Foo.class);
    - * f2.setHandler(mi);            // set the default handler
    - * Class c2 = f2.createClass();
    + * MethodHandler mi = ... ;    // alternative handler
    + * ((ProxyObject)foo).setHandler(mi);
      * 
* - *

The default handler is implicitly attached to an instance of the generated class - * c2. Calling setHandler on the instance is not necessary - * unless another method handler must be attached to the instance. - * - *

The following code is an example of method handler. It does not execute - * anything except invoking the original method: + *

If setHandler is never called for a proxy instance then it will + * employ the default handler which proceeds by invoking the original method. + * The behaviour of the default handler is identical to the following + * handler: * *

    - * class SimpleHandler implements MethodHandler {
    + * class EmptyHandler implements MethodHandler {
      *     public Object invoke(Object self, Method m,
      *                          Method proceed, Object[] args) throws Exception {
      *         return proceed.invoke(self, args);
    @@ -115,22 +104,71 @@ import javassist.bytecode.*;
      * }
      * 
* + *

A proxy factory caches and reuses proxy classes by default. It is possible to reset + * this default globally by setting static field {@link ProxyFactory#useCache} to false. + * Caching may also be configured for a specific factory by calling instance method + * {@link ProxyFactory#setUseCache(boolean)}. It is strongly recommended that new clients + * of class ProxyFactory enable caching. Failure to do so may lead to exhaustion of + * the heap memory area used to store classes. + * + *

Caching is automatically disabled for any given proxy factory if deprecated instance + * method {@link ProxyFactory#setHandler(MethodHandler)} is called. This method was + * used to specify a default handler which newly created proxy classes should install + * when they create their instances. It is only retained to provide backward compatibility + * with previous releases of javassist. Unfortunately,this legacy behaviour makes caching + * and reuse of proxy classes impossible. The current programming model expects javassist + * clients to set the handler of a proxy instance explicitly by calling method + * {@link ProxyObject#setHandler(MethodHandler)} as shown in the sample code above. New + * clients are strongly recommended to use this model rather than calling + * {@link ProxyFactory#setHandler(MethodHandler)}. + * *

A proxy object generated by ProxyFactory is serializable - * if its super class or interfaces implement a java.io.Serializable. - * However, a serialized proxy object will not be compatible with future releases. + * if its super class or any of its interfaces implement java.io.Serializable. + * However, a serialized proxy object may not be compatible with future releases. * The serialization support should be used for short-term storage or RMI. * + *

For compatibility with older releases serialization of proxy objects is implemented by + * adding a writeReplace method to the proxy class. This allows a proxy to be serialized + * to a conventional {@link java.io.ObjectOutputStream} and deserialized from a corresponding + * {@link java.io.ObjectInputStream}. However this method suffers from several problems, the most + * notable one being that it fails to serialize state inherited from the proxy's superclass. + *

+ * An alternative method of serializing proxy objects is available which fixes these problems. It + * requires inhibiting generation of the writeReplace method and instead using instances of + * {@link javassist.util.proxy.ProxyObjectOutputStream} and {@link javassist.util.proxy.ProxyObjectInputStream} + * (which are subclasses of {@link java.io.ObjectOutputStream} and {@link java.io.ObjectInputStream}) + * to serialize and deserialize, respectively, the proxy. These streams recognise javassist proxies and ensure + * that they are serialized and deserialized without the need for the proxy class to implement special methods + * such as writeReplace. Generation of the writeReplace method can be disabled globally by setting static field + * {@link ProxyFactory#useWriteReplace} to false. Alternatively, it may be + * configured per factory by calling instance method {@link ProxyFactory#setUseWriteReplace(boolean)}. + * * @see MethodHandler * @since 3.1 * @author Muga Nishizawa * @author Shigeru Chiba + * @author Andrew Dinn */ public class ProxyFactory { private Class superClass; private Class[] interfaces; private MethodFilter methodFilter; - private MethodHandler handler; + private MethodHandler handler; // retained for legacy usage + private List signatureMethods; + private byte[] signature; + private String classname; + private String superName; + private String packageName; private Class thisClass; + /** + * per factory setting initialised from current setting for useCache but able to be reset before each create call + */ + private boolean factoryUseCache; + /** + * per factory setting initialised from current setting for useWriteReplace but able to be reset before each create call + */ + private boolean factoryWriteReplace; + /** * If the value of this variable is not null, the class file of @@ -147,7 +185,8 @@ public class ProxyFactory { private static final String HOLDER = "_methods_"; private static final String HOLDER_TYPE = "[Ljava/lang/reflect/Method;"; - private static final String METHOD_FILTER_FIELD = "_method_filter"; + private static final String FILTER_SIGNATURE_FIELD = "_filter_signature"; + private static final String FILTER_SIGNATURE_TYPE = "[B"; private static final String HANDLER = "handler"; private static final String NULL_INTERCEPTOR_HOLDER = "javassist.util.proxy.RuntimeSupport"; private static final String DEFAULT_INTERCEPTOR = "default_interceptor"; @@ -159,95 +198,132 @@ public class ProxyFactory { private static final String HANDLER_GETTER = "getHandler"; private static final String HANDLER_GETTER_TYPE = "()" + HANDLER_TYPE; + private static final String SERIAL_VERSION_UID_FIELD = "serialVersionUID"; + private static final String SERIAL_VERSION_UID_TYPE = "J"; + private static final int SERIAL_VERSION_UID_VALUE = -1; + /** * If true, a generated proxy class is cached and it will be reused * when generating the proxy class with the same properties is requested. * The default value is true. * + * Note that this value merely specifies the initial setting employed by any newly created + * proxy factory. The factory setting may be overwritten by calling factory instance method + * {@link #setUseCache(boolean)} + * * @since 3.4 */ - public static boolean useCache = true; + public static volatile boolean useCache = true; - private static WeakHashMap proxyCache = new WeakHashMap(); + /** + * If true, a generated proxy class will implement method writeReplace enabling + * serialization of its proxies to a conventional ObjectOutputStream. this (default) + * setting retains the old javassist behaviour which has the advantage that it + * retains compatibility with older releases and requires no extra work on the part + * of the client performing the serialization. However, it has the disadvantage that + * state inherited from the superclasses of the proxy is lost during serialization. + * if false then serialization/deserialization of the proxy instances will preserve + * all fields. However, serialization must be performed via a {@link ProxyObjectOutputStream} + * and deserialization must be via {@link ProxyObjectInputStream}. Any attempt to serialize + * proxies whose class was created with useWriteReplace set to false via a normal + * {@link java.io.ObjectOutputStream} will fail. + * + * Note that this value merely specifies the initial setting employed by any newly created + * proxy factory. The factory setting may be overwritten by calling factory instance method + * {@link #setUseWriteReplace(boolean)} + * + * @since 3.4 + */ + public static volatile boolean useWriteReplace = true; + + /* + * methods allowing individual factory settings for factoryUseCache and factoryWriteReplace to be reset + */ /** - * store details of a specific proxy created using a given method filter and handler + * test whether this factory uses the proxy cache + * @return true if this factory uses the proxy cache otherwise false */ - static class ProxyDetails { - MethodFilter filter; - MethodHandler handler; - Class proxyClass; + public boolean isUseCache() + { + return factoryUseCache; + } - ProxyDetails(MethodFilter filter, MethodHandler handler, Class proxyClass) - { - this.filter = filter; - this.handler = handler; - this.proxyClass = proxyClass; + /** + * configure whether this factory should use the proxy cache + * @param useCache true if this factory should use the proxy cache and false if it should not use the cache + * @throws RuntimeException if a default interceptor has been set for the factory + */ + public void setUseCache(boolean useCache) + { + // we cannot allow caching to be used if the factory is configured to install a default interceptor + // field into generated classes + if (handler != null && useCache) { + throw new RuntimeException("caching cannot be enabled if the factory default interceptor has been set"); } + factoryUseCache = useCache; } /** - * collect together details of all proxies associated with a given classloader which are constructed - * from a given superclass and set of interfaces + * test whether this factory installs a writeReplace method in created classes + * @return true if this factory installs a writeReplace method in created classes otherwise false */ - static class ProxySet { - String classes; // key constructed from super/interfaces names - List proxyDetails; // hold details of all proxies with given super/interfaces + public boolean isUseWriteReplace() + { + return factoryWriteReplace; + } - public ProxySet(String key) - { - classes = key; - proxyDetails = new ArrayList(); - } + /** + * configure whether this factory should add a writeReplace method to created classes + * @param useWriteReplace true if this factory should add a writeReplace method to created classes and false if it + * should not add a writeReplace method + */ + public void setUseWriteReplace(boolean useWriteReplace) + { + factoryWriteReplace = useWriteReplace; + } + + private static WeakHashMap proxyCache = new WeakHashMap(); + /** + * determine if a class is a javassist proxy class + * @param cl + * @return true if the class is a javassist proxy class otherwise false + */ + public static boolean isProxyClass(Class cl) + { + // all proxies implement ProxyObject. nothing else should. + return (ProxyObject.class.isAssignableFrom(cl)); + } + + /** + * used to store details of a specific proxy class in the second tier of the proxy cache. this entry + * will be located in a hashmap keyed by the unique identifying name of the proxy class. the hashmap is + * located in a weak hashmap keyed by the classloader common to all proxy classes in the second tier map. + */ + static class ProxyDetails { /** - * retrieve an entry from the set with the given filter and handler - * @param filter - * @param handler - * @return the proxy details or null if it is not found + * the unique signature of any method filter whose behaviour will be met by this class. each bit in + * the byte array is set if the filter redirects the corresponding super or interface method and clear + * if it does not redirect it. */ - public ProxyDetails lookup(MethodFilter filter, MethodHandler handler) - { - Iterator iterator = proxyDetails.iterator(); - while (iterator.hasNext()) { - ProxyDetails details = (ProxyDetails)iterator.next(); - if (details.filter == filter && details.handler == handler) { - return details; - } - } - return null; - } - + byte[] signature; /** - * add details of a new proxy to the set - * @param details must not contain the same filter and handler as any existing entry in the set + * a hexadecimal string representation of the signature bit sequence. this string also forms part + * of the proxy class name. */ - public void add(ProxyDetails details) - { - proxyDetails.add(details); - } - + WeakReference proxyClass; /** - * remove details of an existing proxy from the set - * @param details must be in the set + * a flag which is true this class employs writeReplace to perform serialization of its instances + * and false if serialization must employ of a ProxyObjectOutputStream and ProxyObjectInputStream */ - public void remove(ProxyDetails details) - { - proxyDetails.remove(details); - } + boolean isUseWriteReplace; - static String getKey(Class superClass, Class[] interfaces) { - StringBuffer sbuf = new StringBuffer(); - if (superClass != null) - sbuf.append(superClass.getName()); - sbuf.append(':'); - if (interfaces != null) { - int len = interfaces.length; - for (int i = 0; i < len; i++) - sbuf.append(interfaces[i].getName()).append(','); - } - - return sbuf.toString(); + ProxyDetails(byte[] signature, Class proxyClass, boolean isUseWriteReplace) + { + this.signature = signature; + this.proxyClass = new WeakReference(proxyClass); + this.isUseWriteReplace = isUseWriteReplace; } } @@ -259,8 +335,12 @@ public class ProxyFactory { interfaces = null; methodFilter = null; handler = null; + signature = null; + signatureMethods = null; thisClass = null; writeDirectory = null; + factoryUseCache = useCache; + factoryWriteReplace = useWriteReplace; } /** @@ -268,6 +348,8 @@ public class ProxyFactory { */ public void setSuperclass(Class clazz) { superClass = clazz; + // force recompute of signature + signature = null; } /** @@ -282,6 +364,8 @@ public class ProxyFactory { */ public void setInterfaces(Class[] ifs) { interfaces = ifs; + // force recompute of signature + signature = null; } /** @@ -296,29 +380,89 @@ public class ProxyFactory { */ public void setFilter(MethodFilter mf) { methodFilter = mf; + // force recompute of signature + signature = null; } /** - * Generates a proxy class. + * Generates a proxy class using the current filter. */ public Class createClass() { + if (signature == null) { + computeSignature(methodFilter); + } + return createClass1(); + } + + /** + * Generates a proxy class using the supplied filter. + */ + public Class createClass(MethodFilter filter) { + computeSignature(filter); + return createClass1(); + } + + /** + * Generates a proxy class with a specific signature. + * access is package local so ProxyObjectInputStream can use this + * @param signature + * @return + */ + Class createClass(byte[] signature) + { + this.signature = signature; + return createClass1(); + } + + private Class createClass1() { if (thisClass == null) { ClassLoader cl = getClassLoader(); synchronized (proxyCache) { - if (useCache) + if (factoryUseCache) createClass2(cl); else createClass3(cl); } } - return thisClass; + // don't retain any unwanted references + Class result = thisClass; + thisClass = null; + + return result; + } + + 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(); } private void createClass2(ClassLoader cl) { - String key = ProxySet.getKey(superClass, interfaces); - WeakReference reference; - ProxySet set; + 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 @@ -326,41 +470,40 @@ public class ProxyFactory { */ // synchronized (proxyCache) { HashMap cacheForTheLoader = (HashMap)proxyCache.get(cl); + ProxyDetails details; if (cacheForTheLoader == null) { cacheForTheLoader = new HashMap(); proxyCache.put(cl, cacheForTheLoader); } - reference = (WeakReference)cacheForTheLoader.get(key); - if (reference != null) { - set = (ProxySet)reference.get(); - } else { - set = null; - } - if (set == null) { - set = new ProxySet(key); - reference = new WeakReference(set); - cacheForTheLoader.put(key, reference); - } - ProxyDetails details = set.lookup(methodFilter, handler); - if (details == null) { - createClass3(cl); - details = new ProxyDetails(methodFilter, handler, thisClass); - set.add(details); - } else { - thisClass = details.proxyClass; + details = (ProxyDetails)cacheForTheLoader.get(key); + if (details != null) { + WeakReference reference = details.proxyClass; + thisClass = (Class)reference.get(); + if (thisClass != null) { + return; + } } + createClass3(cl); + details = new ProxyDetails(signature, thisClass, factoryWriteReplace); + cacheForTheLoader.put(key, details); // } } private void createClass3(ClassLoader cl) { + // we need a new class so we need a new class name + allocateClassName(); + try { ClassFile cf = make(); if (writeDirectory != null) FactoryHelper.writeFile(cf, writeDirectory); thisClass = FactoryHelper.toClass(cf, cl, getDomain()); - setField(DEFAULT_INTERCEPTOR, handler); - setField(METHOD_FILTER_FIELD, methodFilter); + 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); @@ -381,12 +524,8 @@ public class ProxyFactory { } } - static MethodFilter getFilter(Class clazz) { - return (MethodFilter)getField(clazz, METHOD_FILTER_FIELD); - } - - static MethodHandler getHandler(Class clazz) { - return (MethodHandler)getField(clazz, DEFAULT_INTERCEPTOR); + static byte[] getFilterSignature(Class clazz) { + return (byte[])getField(clazz, FILTER_SIGNATURE_FIELD); } private static Object getField(Class clazz, String fieldName) { @@ -516,9 +655,22 @@ public class ProxyFactory { * Sets the default invocation handler. This invocation handler is shared * among all the instances of a proxy class unless another is explicitly * specified. + * @deprecated since 3.12 + * use of this method is incompatible with proxy class caching. + * instead clients should call method {@link ProxyObject#setHandler(MethodHandler)} to set the handler + * for each newly created proxy instance. + * calling this method will automatically disable caching of classes created by the proxy factory. */ public void setHandler(MethodHandler mi) { + // if we were using the cache and the handler is non-null then we must stop caching + if (factoryUseCache && mi != null) { + factoryUseCache = false; + // clear any currently held class so we don't try to reuse it or set its handler field + thisClass = null; + } handler = mi; + // this retains the behaviour of the old code which resets any class we were holding on to + // this is probably not what is wanted setField(DEFAULT_INTERCEPTOR, handler); } @@ -529,64 +681,135 @@ public class ProxyFactory { } private ClassFile make() throws CannotCompileException { - String superName, classname; - if (interfaces == null) - interfaces = new Class[0]; - - if (superClass == null) { - superClass = OBJECT_TYPE; - superName = superClass.getName(); - classname = interfaces.length == 0 ? superName - : interfaces[0].getName(); - } - else { - superName = superClass.getName(); - classname = superName; - } - - if (Modifier.isFinal(superClass.getModifiers())) - throw new CannotCompileException(superName + " is final"); - - classname = makeProxyName(classname); - if (classname.startsWith("java.")) - classname = "org.javassist.tmp." + classname; - ClassFile cf = new ClassFile(false, classname, superName); cf.setAccessFlags(AccessFlag.PUBLIC); setInterfaces(cf, interfaces); ConstPool pool = cf.getConstPool(); - FieldInfo finfo = new FieldInfo(pool, DEFAULT_INTERCEPTOR, HANDLER_TYPE); - finfo.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC); - cf.addField(finfo); + // 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); - FieldInfo finfo3 = new FieldInfo(pool, METHOD_FILTER_FIELD, - "Ljavassist/util/proxy/MethodFilter;"); + // 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); - HashMap allMethods = getMethods(superClass, interfaces); - int size = allMethods.size(); + // 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); - int s = overrideMethods(cf, pool, classname, allMethods); + int s = overrideMethods(cf, pool, classname); addMethodsHolder(cf, pool, classname, s); addSetter(classname, cf, pool); addGetter(classname, cf, pool); - try { - cf.addMethod(makeWriteReplace(pool)); - } - catch (DuplicateMemberException e) { - // writeReplace() is already declared in the super class/interfaces. + if (factoryWriteReplace) { + try { + cf.addMethod(makeWriteReplace(pool)); + } + catch (DuplicateMemberException e) { + // writeReplace() is already declared in the super class/interfaces. + } } - thisClass = null; + thisClass = null; return cf; } + private void checkClassAndSuperName() + { + if (interfaces == null) + interfaces = new Class[0]; + + if (superClass == null) { + superClass = OBJECT_TYPE; + } + superName = superClass.getName(); + + if (Modifier.isFinal(superClass.getModifiers())) + throw new RuntimeException(superName + " is final"); + packageName = getPackageName(superName); + if (packageName.startsWith("java.")) + packageName = "org.javassist.tmp." + packageName; + } + + private void allocateClassName() + { + classname = makeProxyName(superName); + if (classname.startsWith("java.")) + classname = "org.javassist.tmp." + classname; + } + + private static Comparator sorter = new Comparator() { + + public int compare(Object o1, Object o2) { + Map.Entry e1 = (Map.Entry)o1; + Map.Entry e2 = (Map.Entry)o2; + String key1 = (String)e1.getKey(); + String key2 = (String)e2.getKey(); + return key1.compareTo(key2); + } + }; + + private void computeSignature(MethodFilter filter) // throws CannotCompileException + { + checkClassAndSuperName(); + + HashMap allMethods = getMethods(superClass, interfaces); + signatureMethods = new ArrayList(allMethods.entrySet()); + Collections.sort(signatureMethods, sorter); + int l = signatureMethods.size(); + int maxBytes = ((l + 7) >> 3); + signature = new byte[maxBytes]; + for (int idx = 0; idx < l; idx++) + { + Map.Entry e = (Map.Entry)signatureMethods.get(idx); + Method m = (Method)e.getValue(); + int mod = m.getModifiers(); + if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod) + && isVisible(mod, packageName, m) && filter.isHandled(m)) { + setBit(signature, idx); + } + } + } + + private boolean testBit(byte[] signature, int idx) + { + int byteIdx = idx >> 3; + if (byteIdx > signature.length) { + return false; + } else { + int bitIdx = idx & 0x7; + int mask = 0x1 << bitIdx; + int sigByte = signature[byteIdx]; + return ((sigByte & mask) != 0); + } + } + + 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); + } + } + private static void setInterfaces(ClassFile cf, Class[] interfaces) { String setterIntf = ProxyObject.class.getName(); String[] list; @@ -616,6 +839,9 @@ public class ProxyFactory { code.addIconst(size * 2); code.addAnewarray("java.lang.reflect.Method"); code.addPutstatic(classname, HOLDER, HOLDER_TYPE); + // also need to set serial version uid + code.addLconst(-1L); + code.addPutstatic(classname, SERIAL_VERSION_UID_FIELD, SERIAL_VERSION_UID_TYPE); code.addOpcode(Bytecode.RETURN); minfo.setCodeAttribute(code.toCodeAttribute()); cf.addMethod(minfo); @@ -650,13 +876,11 @@ public class ProxyFactory { cf.addMethod(minfo); } - private int overrideMethods(ClassFile cf, ConstPool cp, String className, - HashMap allMethods) + private int overrideMethods(ClassFile cf, ConstPool cp, String className) throws CannotCompileException { - String prefix = makeUniqueName("_d", allMethods); - Set entries = allMethods.entrySet(); - Iterator it = entries.iterator(); + String prefix = makeUniqueName("_d", signatureMethods); + Iterator it = signatureMethods.iterator(); int index = 0; while (it.hasNext()) { Map.Entry e = (Map.Entry)it.next(); @@ -664,10 +888,12 @@ public class ProxyFactory { Method meth = (Method)e.getValue(); int mod = meth.getModifiers(); if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod) - && isVisible(mod, className, meth)) + && isVisible(mod, packageName, meth)) + if (testBit(signature, index)) if (methodFilter == null || methodFilter.isHandled(meth)) - override(className, meth, prefix, index++, + override(className, meth, prefix, index, keyToDesc(key), cf, cp); + index++; } return index; @@ -699,25 +925,26 @@ public class ProxyFactory { ConstPool cp, String classname) throws CannotCompileException { Constructor[] cons = SecurityActions.getDeclaredConstructors(superClass); + // legacy: if we are not caching then we need to initialise the default handler + boolean doHandlerInit = !factoryUseCache; for (int i = 0; i < cons.length; i++) { Constructor c = cons[i]; int mod = c.getModifiers(); if (!Modifier.isFinal(mod) && !Modifier.isPrivate(mod) - && isVisible(mod, classname, c)) { - MethodInfo m = makeConstructor(thisClassName, c, cp, superClass); + && isVisible(mod, packageName, c)) { + MethodInfo m = makeConstructor(thisClassName, c, cp, superClass, doHandlerInit); cf.addMethod(m); } } } - private static String makeUniqueName(String name, HashMap hash) { - Set keys = hash.keySet(); - if (makeUniqueName0(name, keys.iterator())) + private static String makeUniqueName(String name, List sortedMethods) { + if (makeUniqueName0(name, sortedMethods.iterator())) return name; for (int i = 100; i < 999; i++) { String s = name + i; - if (makeUniqueName0(s, keys.iterator())) + if (makeUniqueName0(s, sortedMethods.iterator())) return s; } @@ -726,7 +953,8 @@ public class ProxyFactory { private static boolean makeUniqueName0(String name, Iterator it) { while (it.hasNext()) { - String key = (String)it.next(); + Map.Entry e = (Map.Entry)it.next(); + String key = (String)e.getKey(); if (key.startsWith(name)) return false; } @@ -735,22 +963,21 @@ public class ProxyFactory { } /** - * Returns true if the method is visible from the class. + * Returns true if the method is visible from the package. * * @param mod the modifiers of the method. */ - private static boolean isVisible(int mod, String from, Member meth) { + private static boolean isVisible(int mod, String fromPackage, 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) + if (fromPackage == null) return q == null; else - return p.equals(q); + return fromPackage.equals(q); } } @@ -804,7 +1031,7 @@ public class ProxyFactory { } private static MethodInfo makeConstructor(String thisClassName, Constructor cons, - ConstPool cp, Class superClass) { + ConstPool cp, Class superClass, boolean doHandlerInit) { String desc = RuntimeSupport.makeDescriptor(cons.getParameterTypes(), Void.TYPE); MethodInfo minfo = new MethodInfo(cp, "", desc); @@ -812,12 +1039,19 @@ public class ProxyFactory { setThrows(minfo, cp, cons.getExceptionTypes()); Bytecode code = new Bytecode(cp, 0, 0); - code.addAload(0); - code.addGetstatic(thisClassName, DEFAULT_INTERCEPTOR, HANDLER_TYPE); - code.addPutfield(thisClassName, HANDLER, HANDLER_TYPE); - code.addGetstatic(thisClassName, DEFAULT_INTERCEPTOR, HANDLER_TYPE); - code.addOpcode(Opcode.IFNONNULL); - code.addIndex(10); + // legacy: if we are not using caching then we initialise the instance's handler + // from the class's static default interceptor and skip the next few instructions if + // it is non-null + if (doHandlerInit) { + code.addAload(0); + code.addGetstatic(thisClassName, DEFAULT_INTERCEPTOR, HANDLER_TYPE); + code.addPutfield(thisClassName, HANDLER, HANDLER_TYPE); + code.addGetstatic(thisClassName, DEFAULT_INTERCEPTOR, HANDLER_TYPE); + code.addOpcode(Opcode.IFNONNULL); + code.addIndex(10); + } + // if caching is enabled then we don't have a handler to initialise so this else branch will install + // the handler located in the static field of class RuntimeSupport. code.addAload(0); code.addGetstatic(NULL_INTERCEPTOR_HOLDER, DEFAULT_INTERCEPTOR, HANDLER_TYPE); code.addPutfield(thisClassName, HANDLER, HANDLER_TYPE); @@ -1056,7 +1290,7 @@ public class ProxyFactory { FactoryHelper.unwrapDesc[index]); } } - else + else code.addCheckcast(type.getName()); } diff --git a/src/main/javassist/util/proxy/ProxyObjectInputStream.java b/src/main/javassist/util/proxy/ProxyObjectInputStream.java new file mode 100644 index 00000000..8296d895 --- /dev/null +++ b/src/main/javassist/util/proxy/ProxyObjectInputStream.java @@ -0,0 +1,82 @@ +package javassist.util.proxy; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; + +/** + * An input stream class which knows how to deserialize proxies created via {@link ProxyFactory} and + * serializedo via a {@link ProxyObjectOutputStream}. It must be used when deserialising proxies created + * from a proxy factory configured with {@link ProxyFactory#useWriteReplace} set to false. + */ +public class ProxyObjectInputStream extends ObjectInputStream +{ + /** + * create an input stream which can be used to deserialize an object graph which includes proxies created + * using class ProxyFactory. the classloader used to resolve proxy superclass and interface names + * read from the input stream will default to the current thread's context class loader or the system + * classloader if the context class loader is null. + * @param in + * @throws java.io.StreamCorruptedException whenever ObjectInputStream would also do so + * @throws IOException whenever ObjectInputStream would also do so + * @throws SecurityException whenever ObjectInputStream would also do so + * @throws NullPointerException if in is null + */ + public ProxyObjectInputStream(InputStream in) throws IOException + { + super(in); + loader = Thread.currentThread().getContextClassLoader(); + if (loader == null) { + loader = ClassLoader.getSystemClassLoader(); + } + } + + /** + * Reset the loader to be + * @param loader + */ + public void setClassLoader(ClassLoader loader) + { + if (loader != null) { + this.loader = loader; + } else { + loader = ClassLoader.getSystemClassLoader(); + } + } + + protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException { + boolean isProxy = readBoolean(); + if (isProxy) { + String name = (String)readObject(); + Class superClass = loader.loadClass(name); + int length = readInt(); + Class[] interfaces = new Class[length]; + for (int i = 0; i < length; i++) { + name = (String)readObject(); + interfaces[i] = loader.loadClass(name); + } + length = readInt(); + byte[] signature = new byte[length]; + read(signature); + ProxyFactory factory = new ProxyFactory(); + // we must always use the cache and never use writeReplace when using + // ProxyObjectOutputStream and ProxyObjectInputStream + factory.setUseCache(true); + factory.setUseWriteReplace(false); + factory.setSuperclass(superClass); + factory.setInterfaces(interfaces); + Class proxyClass = factory.createClass(signature); + return ObjectStreamClass.lookup(proxyClass); + } else { + return super.readClassDescriptor(); + } + } + + /** + * the loader to use to resolve classes for proxy superclass and interface names read + * from the stream. defaults to the context class loader of the thread which creates + * the input stream or the system class loader if the context class loader is null. + */ + private ClassLoader loader; +} \ No newline at end of file diff --git a/src/main/javassist/util/proxy/ProxyObjectOutputStream.java b/src/main/javassist/util/proxy/ProxyObjectOutputStream.java new file mode 100644 index 00000000..d06153cd --- /dev/null +++ b/src/main/javassist/util/proxy/ProxyObjectOutputStream.java @@ -0,0 +1,54 @@ +package javassist.util.proxy; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; + +/** + * An input stream class which knows how to serialize proxies created via {@link ProxyFactory}. It must + * be used when serialising proxies created from a proxy factory configured with + * {@link ProxyFactory#useWriteReplace} set to false. Subsequent deserialization of the serialized data + * must employ a {@link ProxyObjectInputStream} + */ +public class ProxyObjectOutputStream extends ObjectOutputStream +{ + /** + * create an output stream which can be used to serialize an object graph which includes proxies created + * using class ProxyFactory + * @param out + * @throws IOException whenever ObjectOutputStream would also do so + * @throws SecurityException whenever ObjectOutputStream would also do so + * @throws NullPointerException if out is null + */ + public ProxyObjectOutputStream(OutputStream out) throws IOException + { + super(out); + } + + protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException { + Class cl = desc.forClass(); + if (ProxyFactory.isProxyClass(cl)) { + writeBoolean(true); + Class superClass = cl.getSuperclass(); + Class[] interfaces = cl.getInterfaces(); + byte[] signature = ProxyFactory.getFilterSignature(cl); + String name = superClass.getName(); + writeObject(name); + // we don't write the marker interface ProxyObject + writeInt(interfaces.length - 1); + for (int i = 0; i < interfaces.length; i++) { + Class interfaze = interfaces[i]; + if (interfaze != ProxyObject.class) { + name = interfaces[i].getName(); + writeObject(name); + } + } + writeInt(signature.length); + write(signature); + } else { + writeBoolean(false); + super.writeClassDescriptor(desc); + } + } +} diff --git a/src/main/javassist/util/proxy/RuntimeSupport.java b/src/main/javassist/util/proxy/RuntimeSupport.java index ea7cbfe9..817ab4ca 100644 --- a/src/main/javassist/util/proxy/RuntimeSupport.java +++ b/src/main/javassist/util/proxy/RuntimeSupport.java @@ -206,10 +206,6 @@ public class RuntimeSupport { if (proxy instanceof ProxyObject) methodHandler = ((ProxyObject)proxy).getHandler(); - if (methodHandler == null) - methodHandler = ProxyFactory.getHandler(clazz); - - return new SerializedProxy(clazz, ProxyFactory.getFilter(clazz), - methodHandler); + return new SerializedProxy(clazz, ProxyFactory.getFilterSignature(clazz), methodHandler); } } diff --git a/src/main/javassist/util/proxy/SerializedProxy.java b/src/main/javassist/util/proxy/SerializedProxy.java index 5092e992..cddfab40 100644 --- a/src/main/javassist/util/proxy/SerializedProxy.java +++ b/src/main/javassist/util/proxy/SerializedProxy.java @@ -31,11 +31,11 @@ import java.security.ProtectionDomain; class SerializedProxy implements Serializable { private String superClass; private String[] interfaces; - private MethodFilter filter; + private byte[] filterSignature; private MethodHandler handler; - SerializedProxy(Class proxy, MethodFilter f, MethodHandler h) { - filter = f; + SerializedProxy(Class proxy, byte[] sig, MethodHandler h) { + filterSignature = sig; handler = h; superClass = proxy.getSuperclass().getName(); Class[] infs = proxy.getInterfaces(); @@ -80,9 +80,9 @@ class SerializedProxy implements Serializable { ProxyFactory f = new ProxyFactory(); f.setSuperclass(loadClass(superClass)); f.setInterfaces(infs); - f.setFilter(filter); - f.setHandler(handler); - return f.createClass().newInstance(); + ProxyObject proxy = (ProxyObject)f.createClass(filterSignature).newInstance(); + proxy.setHandler(handler); + return proxy; } catch (ClassNotFoundException e) { throw new java.io.InvalidClassException(e.getMessage()); diff --git a/src/test/test/javassist/proxy/ProxyCacheGCTest.java b/src/test/test/javassist/proxy/ProxyCacheGCTest.java index 8f583a37..062872b7 100644 --- a/src/test/test/javassist/proxy/ProxyCacheGCTest.java +++ b/src/test/test/javassist/proxy/ProxyCacheGCTest.java @@ -4,6 +4,7 @@ import javassist.*; import javassist.util.proxy.MethodFilter; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; +import javassist.util.proxy.ProxyObject; import junit.framework.TestCase; /** @@ -92,13 +93,13 @@ public class ProxyCacheGCTest extends TestCase MethodFilter filter = (MethodFilter)javaFilterClass.newInstance(); // ok, now create a factory and a proxy class and proxy from that factory - factory.setHandler(handler); factory.setFilter(filter); factory.setSuperclass(javaTargetClass); // factory.setSuperclass(Object.class); Class proxyClass = factory.createClass(); Object target = proxyClass.newInstance(); + ((ProxyObject)target).setHandler(handler); } catch (Exception e) { e.printStackTrace(); fail("cannot create proxy " + e); diff --git a/src/test/test/javassist/proxy/ProxyFactoryCompatibilityTest.java b/src/test/test/javassist/proxy/ProxyFactoryCompatibilityTest.java new file mode 100644 index 00000000..069d0c9c --- /dev/null +++ b/src/test/test/javassist/proxy/ProxyFactoryCompatibilityTest.java @@ -0,0 +1,114 @@ +package test.javassist.proxy; + +import javassist.*; +import javassist.util.proxy.MethodFilter; +import javassist.util.proxy.MethodHandler; +import javassist.util.proxy.ProxyFactory; +import javassist.util.proxy.ProxyObject; +import junit.framework.TestCase; + +import java.lang.reflect.Method; + +/** + * test which checks that it is still possible to use the old style proxy factory api + * to create proxy classes which set their own handler. it checks that caching is + * automatically disabled if this legacy api is used. it also exercises the new style + * api, ensuring that caching works correctly with this model. + */ +public class ProxyFactoryCompatibilityTest extends TestCase +{ + private ClassPool basePool; + MethodFilter filter; + MethodHandler handler; + + protected void setUp() + { + basePool = ClassPool.getDefault(); + filter = new MethodFilter() { + public boolean isHandled(Method m) { + return !m.getName().equals("finalize"); + } + }; + + handler = new MethodHandler() { + public Object invoke(Object self, Method m, Method proceed, + Object[] args) throws Throwable { + System.out.println("calling: " + m.getName()); + return proceed.invoke(self, args); // execute the original method. + } + }; + } + + public void testFactoryCompatibility() throws Exception + { + // create a factory which, by default, uses caching + ProxyFactory factory = new ProxyFactory(); + factory.setSuperclass(TestClass.class); + factory.setInterfaces(new Class[] { TestInterface.class}); + factory.setFilter(filter); + + // create the same class twice and check that it is reused + Class proxyClass1 = factory.createClass(); + System.out.println("created first class " + proxyClass1.getName()); + TestClass proxy1 = (TestClass)proxyClass1.newInstance(); + ((ProxyObject) proxy1).setHandler(handler); + proxy1.testMethod(); + assertTrue(proxy1.isTestCalled()); + + Class proxyClass2 = factory.createClass(); + System.out.println("created second class " + proxyClass2.getName()); + TestClass proxy2 = (TestClass)proxyClass2.newInstance(); + ((ProxyObject) proxy2).setHandler(handler); + proxy2.testMethod(); + assertTrue(proxy2.isTestCalled()); + + assertTrue(proxyClass1 == proxyClass2); + + // create a factory which, by default, uses caching then set the handler so it creates + // classes which do not get cached. + ProxyFactory factory2 = new ProxyFactory(); + factory.setSuperclass(TestClass.class); + factory.setInterfaces(new Class[] { TestInterface.class}); + factory.setFilter(filter); + factory.setHandler(handler); + + // create the same class twice and check that it is reused + Class proxyClass3 = factory.createClass(); + System.out.println("created third class " + proxyClass3.getName()); + TestClass proxy3 = (TestClass)proxyClass3.newInstance(); + proxy3.testMethod(); + assertTrue(proxy3.isTestCalled()); + + Class proxyClass4 = factory.createClass(); + System.out.println("created fourth class " + proxyClass4.getName()); + TestClass proxy4 = (TestClass)proxyClass4.newInstance(); + proxy4.testMethod(); + assertTrue(proxy4.isTestCalled()); + + assertTrue(proxyClass3 != proxyClass4); + } + + /** + * test class used as the super for the proxy + */ + public static class TestClass { + private boolean testCalled = false; + public void testMethod() + { + // record the call + testCalled = true; + } + public boolean isTestCalled() + { + return testCalled; + } + } + + /** + * test interface used as an interface implemented by the proxy + */ + public static interface TestInterface { + public void testMethod(); + } + +} \ No newline at end of file diff --git a/src/test/test/javassist/proxy/ProxySerializationTest.java b/src/test/test/javassist/proxy/ProxySerializationTest.java new file mode 100644 index 00000000..e79bcb48 --- /dev/null +++ b/src/test/test/javassist/proxy/ProxySerializationTest.java @@ -0,0 +1,151 @@ +package test.javassist.proxy; + +import javassist.util.proxy.*; +import junit.framework.TestCase; + +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Test to ensure that serialization and deserialization of javassist proxies via + * {@link javassist.util.proxy.ProxyObjectOutputStream} and @link javassist.util.proxy.ProxyObjectInputStream} + * reuses classes located in the proxy cache. This tests the fixes provided for JASSIST-42 and JASSIST-97. + */ +public class ProxySerializationTest extends TestCase +{ + public void testSerialization() + { + ProxyFactory factory = new ProxyFactory(); + factory.setSuperclass(TestClass.class); + factory.setInterfaces(new Class[] {TestInterface.class}); + + factory.setUseWriteReplace(true); + Class proxyClass = factory.createClass(new TestFilter()); + + MethodHandler handler = new TestHandler(); + + // first try serialization using writeReplace + + try { + String name = "proxytest_1"; + Constructor constructor = proxyClass.getConstructor(String.class); + TestClass proxy = (TestClass)constructor.newInstance(name); + ((ProxyObject)proxy).setHandler(handler); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bos); + out.writeObject(proxy); + out.close(); + byte[] bytes = bos.toByteArray(); + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream in = new ObjectInputStream(bis); + TestClass newProxy = (TestClass)in.readObject(); + // inherited fields should have been deserialized + assert(newProxy.getName() == null); + // since we are reading into the same JVM the new proxy should have the same class as the old proxy + assert(newProxy.getClass() == proxy.getClass()); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + + // second try serialization using proxy object output/input streams + + factory.setUseWriteReplace(false); + proxyClass = factory.createClass(new TestFilter()); + + try { + String name = "proxytest_2"; + Constructor constructor = proxyClass.getConstructor(String.class); + TestClass proxy = (TestClass)constructor.newInstance(name); + ((ProxyObject)proxy).setHandler(handler); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ProxyObjectOutputStream out = new ProxyObjectOutputStream(bos); + out.writeObject(proxy); + out.close(); + byte[] bytes = bos.toByteArray(); + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ProxyObjectInputStream in = new ProxyObjectInputStream(bis); + TestClass newProxy = (TestClass)in.readObject(); + // inherited fields should have been deserialized + assert(proxy.getName() == newProxy.getName()); + // since we are reading into the same JVM the new proxy should have the same class as the old proxy + assert(newProxy.getClass() == proxy.getClass()); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + public static class TestFilter implements MethodFilter, Serializable + { + public boolean isHandled(Method m) { + if (m.getName().equals("getName")) { + return true; + } + return false; + } + + public boolean equals(Object o) + { + if (o instanceof TestFilter) { + // all test filters are equal + return true; + } + + return false; + } + + public int hashCode() + { + return TestFilter.class.hashCode(); + } + } + + public static class TestHandler implements MethodHandler, Serializable + { + public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable + { + return proceed.invoke(self, args); + } + public boolean equals(Object o) + { + if (o instanceof TestHandler) { + // all test handlers are equal + return true; + } + + return false; + } + + public int hashCode() + { + return TestHandler.class.hashCode(); + } + } + + public static class TestClass implements Serializable + { + public String name; + + public TestClass() + { + } + + public TestClass(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + } + + public static interface TestInterface + { + public String getName(); + } +}