Avoid Java bottleneck by using explicit Charset for byte[] <-> String conversions. Repost of Rietveld issue 158803. Patch By: Jason Terk Review at http://gwt-code-reviews.appspot.com/1632803 Review by: skybrian@google.com git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10933 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java index 9d7c619..16fbe7a 100644 --- a/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java +++ b/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java
@@ -114,7 +114,7 @@ onAfterResponseSerialized(toReturn); try { - stream.write(toReturn.getBytes("UTF-8")); + stream.write(toReturn.getBytes(RPCServletUtils.CHARSET_UTF8)); } catch (IOException e) { throw new SerializationException("Unable to commit bytes", e); }
diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java index 05bf40d..d9d2178 100644 --- a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java +++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed 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 @@ -19,6 +19,8 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.concurrent.ConcurrentHashMap; import java.util.zip.GZIPOutputStream; import javax.servlet.ServletContext; @@ -31,6 +33,16 @@ * the RPC system. */ public class RPCServletUtils { + + public static final String CHARSET_UTF8_NAME = "UTF-8"; + + /** + * The UTF-8 Charset. Use this to avoid concurrency bottlenecks when + * converting between byte arrays and Strings. + * See http://code.google.com/p/google-web-toolkit/issues/detail?id=6398 + */ + public static final Charset CHARSET_UTF8 = Charset.forName(CHARSET_UTF8_NAME); + /** * Package protected for use in tests. */ @@ -40,11 +52,6 @@ private static final String ATTACHMENT = "attachment"; - /** - * Used both as expected request charset and encoded response charset. - */ - private static final String CHARSET_UTF8 = "UTF-8"; - private static final String CONTENT_DISPOSITION = "Content-Disposition"; private static final String CONTENT_ENCODING = "Content-Encoding"; @@ -64,10 +71,24 @@ private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256; /** + * Contains cached mappings from character set name to Charset. The + * null key maps to the default UTF-8 character set. + */ + private static final ConcurrentHashMap<String, Charset> CHARSET_CACHE = + new ConcurrentHashMap<String, Charset>(); + + /** + * Pre-populate the character set cache with UTF-8. + */ + static { + CHARSET_CACHE.put(CHARSET_UTF8_NAME, CHARSET_UTF8); + } + + /** * Returns <code>true</code> if the {@link HttpServletRequest} accepts Gzip * encoding. This is done by checking that the accept-encoding header * specifies gzip as a supported encoding. - * + * * @param request the request instance to test for gzip encoding acceptance * @return <code>true</code> if the {@link HttpServletRequest} accepts Gzip * encoding @@ -86,7 +107,7 @@ /** * Returns <code>true</code> if the response content's estimated UTF-8 byte * length exceeds 256 bytes. - * + * * @param content the contents of the response * @return <code>true</code> if the response content's estimated UTF-8 byte * length exceeds 256 bytes @@ -96,12 +117,38 @@ } /** + * Get the Charset for a named character set. Caches Charsets to work around + * a concurrency bottleneck in FastCharsetProvider. + * See http://code.google.com/p/google-web-toolkit/issues/detail?id=6398 + * @see {@link Charset#forName(String)} + * + * @param encoding the name of the Charset to get. If this is null + * the default UTF-8 character set will be returned. + * @return the named Charset. + */ + public static Charset getCharset(String encoding) { + + if (encoding == null) { + return CHARSET_UTF8; + } + + Charset charset = CHARSET_CACHE.get(encoding); + + if (charset == null) { + charset = Charset.forName(encoding); + CHARSET_CACHE.put(encoding, charset); + } + + return charset; + } + + /** * Returns true if the {@link java.lang.reflect.Method Method} definition on * the service is specified to throw the exception contained in the * InvocationTargetException or false otherwise. NOTE we do not check that the * type is serializable here. We assume that it must be otherwise the * application would never have been allowed to run. - * + * * @param serviceIntfMethod the method from the RPC request * @param cause the exception that the method threw * @return true if the exception's type is in the method's signature @@ -135,7 +182,7 @@ * Returns the content of an {@link HttpServletRequest} by decoding it using * <code>expectedCharSet</code>, or <code>UTF-8</code> if * <code>expectedCharSet</code> is <code>null</null>. - * + * * @param request the servlet request whose content we want to read * @param expectedContentType the expected content (i.e. 'type/subtype' only) * in the Content-Type request header, or <code>null</code> if no @@ -178,20 +225,18 @@ } out.write(buffer, 0, byteCount); } - String contentCharSet = expectedCharSet != null - ? expectedCharSet : CHARSET_UTF8; - return out.toString(contentCharSet); + return new String(out.toByteArray(), getCharset(expectedCharSet)); } finally { if (in != null) { in.close(); } } } - + /** * Returns the content of an {@link HttpServletRequest}, after verifying a * <code>gwt/x-gwt-rpc; charset=utf-8</code> content type. - * + * * @param request the servlet request whose content we want to read * @return the content of an {@link HttpServletRequest} by decoding it using * <code>UTF-8</code> @@ -202,13 +247,13 @@ */ public static String readContentAsGwtRpc(HttpServletRequest request) throws IOException, ServletException { - return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8); + return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8_NAME); } /** * Returns the content of an {@link HttpServletRequest} by decoding it using * the UTF-8 charset. - * + * * @param request the servlet request whose content we want to read * @return the content of an {@link HttpServletRequest} by decoding it using * the UTF-8 charset @@ -228,7 +273,7 @@ /** * Returns the content of an {@link HttpServletRequest} by decoding it using * the UTF-8 charset. - * + * * @param request the servlet request whose content we want to read * @param checkHeaders Specify 'true' to check the Content-Type header to see * that it matches the expected value 'text/x-gwt-rpc' and the @@ -246,7 +291,7 @@ @Deprecated public static String readContentAsUtf8(HttpServletRequest request, boolean checkHeaders) throws IOException, ServletException { - return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8); + return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8_NAME); } /** @@ -259,7 +304,7 @@ /** * Returns <code>true</code> if the request accepts gzip encoding and the the * response content's estimated UTF-8 byte length exceeds 256 bytes. - * + * * @param request the request associated with the response content * @param responseContent a string that will be * @return <code>true</code> if the request accepts gzip encoding and the the @@ -275,7 +320,7 @@ * Write the response content into the {@link HttpServletResponse}. If * <code>gzipResponse</code> is <code>true</code>, the response content will * be gzipped prior to being written into the response. - * + * * @param servletContext servlet context for this response * @param response response instance * @param responseContent a string containing the response content @@ -333,7 +378,7 @@ /** * Called when the servlet itself has a problem, rather than the invoked * third-party method. It writes a simple 500 message back to the client. - * + * * @param servletContext * @param response * @param failure @@ -349,7 +394,7 @@ response.setContentType("text/plain"); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); try { - response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes("UTF-8")); + response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes(CHARSET_UTF8)); } catch (IllegalStateException e) { // Handle the (unexpected) case where getWriter() was previously used response.getWriter().write(GENERIC_FAILURE_MSG); @@ -363,7 +408,7 @@ /** * Performs validation of the character encoding, ignoring case. - * + * * @param request the incoming request * @param expectedCharSet the expected charset of the request * @throws ServletException if requests encoding is not <code>null</code> and @@ -372,6 +417,8 @@ private static void checkCharacterEncodingIgnoreCase( HttpServletRequest request, String expectedCharSet) throws ServletException { + + assert (expectedCharSet != null); boolean encodingOkay = false; String characterEncoding = request.getCharacterEncoding(); if (characterEncoding != null) { @@ -409,6 +456,8 @@ private static void checkContentTypeIgnoreCase( HttpServletRequest request, String expectedContentType) throws ServletException { + + assert (expectedContentType != null); String contentType = request.getContentType(); boolean contentTypeIsOkay = false;
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java b/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java index 3b81cc9..7aa02db 100644 --- a/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java +++ b/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed 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 @@ -21,6 +21,7 @@ import com.google.gwt.user.client.rpc.SerializationException; import com.google.gwt.user.client.rpc.SerializationStreamReader; import com.google.gwt.user.client.rpc.SerializationStreamWriter; +import com.google.gwt.user.server.rpc.RPCServletUtils; import com.google.gwt.user.server.rpc.SerializationPolicy; import com.google.gwt.user.server.rpc.ServerCustomFieldSerializer; @@ -47,9 +48,6 @@ * Serialization utility class used by the server-side RPC code. */ public class SerializabilityUtil { - - public static final String DEFAULT_ENCODING = "UTF-8"; - /** * Comparator used to sort fields. */ @@ -222,7 +220,7 @@ /** * Return the concrete type that a generic type maps to, if known. - * + * * @param genericType The generic type to resolve. * @param resolvedTypes A map of generic types to actual types. * @return The actual type, which may be of any subclass of Type. @@ -244,11 +242,11 @@ /** * Determine the expected types for any instance type parameters. - * + * * This method also determines whether or not the instance can be assigned to * the expected type. We combine the tasks because they require traversing the * same data structures. - * + * * @param instanceClass The instance for which we want generic parameter types * @param expectedType The type we are expecting this instance to be * @param resolvedTypes The types that have been resolved to actual values @@ -269,14 +267,14 @@ if (resolvedTypes != null) { findInstanceParameters(instanceClass, resolvedTypes, expectedParameterTypes); } - + // With no expected type, the instance is assignable and we fall through } else { // First determine what type we are really expecting. The type may still // be a TypeVariable<?> at this time when we are deserializing class // fields or components of another data structure. Type actualType = findActualType(expectedType, resolvedTypes); - + // Try to match the instanceClass to the expected type, updating the // expectedParameterTypes so that we can track them back to the resolved // types once we determine what class to treat the instance as. In @@ -294,7 +292,7 @@ // and we are done. return null; } - + // Now that we know what class the instance should be, // get any remaining parameters using resolved types. if (resolvedTypes != null) { @@ -309,7 +307,7 @@ /** * Find the Class that a given type refers to. - * + * * @param type The type of interest * @return The Class that type represents */ @@ -353,7 +351,7 @@ /** * Returns the {@link Class} which can serialize the given instance type, or * <code>null</code> if this class has no custom field serializer. - * + * * Note that arrays never have custom field serializers. */ public static Class<?> hasCustomFieldSerializer(Class<?> instanceType) { @@ -384,7 +382,7 @@ * Returns the server-side {@link Class} which can serialize the given * instance type, or <code>null</code> if this class has no type-checking * custom field serializer. - * + * * Note that arrays never have custom field serializers. */ public static Class<?> hasServerCustomFieldSerializer(Class<?> instanceType) { @@ -413,10 +411,10 @@ /** * Remove all of the actual types that arose from the the given type. - * + * * This method should always be called after a corresponding call to * resolveTypes. - * + * * @param methodType The type we wish to assign this instance to * @param resolvedTypes The types that have been resolved to actual values */ @@ -428,10 +426,10 @@ * Find all the actual types we can from the information in the given type, * and put the mapping from TypeVariable objects to actual types into the * resolved types map. - * + * * The method releaseTypes should always be called after a call to this * method, unless the resolved types map is about to be discarded. - * + * * @param methodType The type we wish to assign this instance to * @param resolvedTypes The types that have been resolved to actual values */ @@ -464,12 +462,12 @@ /** * Loads a {@link CustomFieldSerializer} from a class that may implement that * interface. - * + * * @param customSerializerClass the Custom Field Serializer class - * + * * @return an instance the class provided if it implements * {@link CustomFieldSerializer} or {@code null} if it does not - * + * * @throws SerializationException if the load process encounters an unexpected * problem */ @@ -567,7 +565,7 @@ if (Throwable.class == field.getDeclaringClass()) { /** * Only serialize Throwable's detailMessage field; all others are ignored. - * + * * NOTE: Changing the set of fields that we serialize for Throwable will * necessitate a change to our JRE emulation's version of Throwable. */ @@ -612,7 +610,7 @@ } } else if (expectedType instanceof WildcardType) { WildcardType wildcardType = (WildcardType) expectedType; - + Type[] lowerBounds = wildcardType.getLowerBounds(); for (Type type : lowerBounds) { /* Require instance to be a superclass of type, or type itself. */ @@ -625,7 +623,7 @@ } boundClass = boundClass.getSuperclass(); } - + // We fail if the class does not meet any bound, as we should. if (boundClass == null) { return false; @@ -672,7 +670,7 @@ return true; } } - + Type[] interfaces = instanceClass.getGenericInterfaces(); for (Type interfaceType : interfaces) { Type[] localTypes = expectedParameterTypes.clone(); @@ -691,7 +689,7 @@ private static boolean findExpectedInstanceClassFromSuper( Type superType, Type expectedType, DequeMap<TypeVariable<?>, Type> resolvedTypes, Set<Class<?>> expectedInstanceClasses, Type[] expectedParameterTypes) { - + if (superType instanceof GenericArrayType) { // Can't use array types as supertypes or interfaces return false; @@ -723,18 +721,18 @@ Type[] upperBounds = wildcardType.getUpperBounds(); for (Type boundType : upperBounds) { if (findExpectedInstanceClassFromSuper(boundType, expectedType, - resolvedTypes, expectedInstanceClasses, expectedParameterTypes)) { + resolvedTypes, expectedInstanceClasses, expectedParameterTypes)) { return true; } } } - + return false; } /** * Attempt to find known type for TypeVariable type from an instance. - * + * * @param foundParameter The currently known parameter, which must be of type * TypeVariable * @param instanceType The instance that we need to check for information @@ -802,7 +800,7 @@ * Attempt to find the actual types for the generic parameters of an instance, * given the types we have resolved from the method signature or class field * declaration. - * + * * @param instanceClass The instance for which we want actual generic * parameter types. * @param resolvedTypes The types that have been resolved to actual values @@ -861,7 +859,7 @@ private static void generateSerializationSignature(Class<?> instanceType, CRC32 crc, SerializationPolicy policy) throws UnsupportedEncodingException { - crc.update(getSerializedTypeName(instanceType).getBytes(DEFAULT_ENCODING)); + crc.update(getSerializedTypeName(instanceType).getBytes(RPCServletUtils.CHARSET_UTF8)); if (excludeImplementationFromSerializationSignature(instanceType)) { return; @@ -882,8 +880,8 @@ * generate the signature. Otherwise, use all known fields. */ if ((clientFieldNames == null) || clientFieldNames.contains(field.getName())) { - crc.update(field.getName().getBytes(DEFAULT_ENCODING)); - crc.update(getSerializedTypeName(field.getType()).getBytes(DEFAULT_ENCODING)); + crc.update(field.getName().getBytes(RPCServletUtils.CHARSET_UTF8)); + crc.update(getSerializedTypeName(field.getType()).getBytes(RPCServletUtils.CHARSET_UTF8)); } } @@ -954,7 +952,7 @@ } } else if (methodType instanceof Class) { Class<?> classType = (Class<?>) methodType; - + // A type that is of instance Class, with TypeParameters, must be a raw // class, so strip off any parameters in the map. TypeVariable<?>[] classParams = classType.getTypeParameters();
diff --git a/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java b/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java index 27c6932..18928f2 100644 --- a/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java +++ b/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed 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 @@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; @@ -30,7 +31,6 @@ /** * Tests some of the methods in {@link RPCServletUtils}. - * */ public class RPCServletUtilsTest extends TestCase { @@ -69,7 +69,7 @@ public String getHeader(String name) { return null; } - + @SuppressWarnings("unused") @Override public ServletInputStream getInputStream() throws IOException { @@ -165,6 +165,31 @@ } /** + * RPCServletUtils#getCharset() should return the same instance for + * every invocation of a given encoding. + */ + public void testGetCharsetInstances() { + // Default UTF-8 character set. + assertSame(RPCServletUtils.getCharset(null), + RPCServletUtils.getCharset(null)); + assertSame(RPCServletUtils.getCharset("UTF-8"), + RPCServletUtils.getCharset("UTF-8")); + assertSame(RPCServletUtils.getCharset("US-ASCII"), + RPCServletUtils.getCharset("US-ASCII")); + assertSame(RPCServletUtils.getCharset("ISO-8859-1"), + RPCServletUtils.getCharset("ISO-8859-1")); + } + + /** + * Test that RPCServletUtils#getCharset() returns the correct + * default UTF-8 charachter set when passed a null encoding value. + */ + public void testGetDefaultCharset() { + assertEquals(Charset.forName("UTF-8"), RPCServletUtils.CHARSET_UTF8); + assertSame(RPCServletUtils.CHARSET_UTF8, RPCServletUtils.getCharset(null)); + } + + /** * Character type doesn't match UTF-8, but ignore it. */ public void testIgnoreCharacterEncoding() throws IOException,