diff --git a/eureka-client/src/main/java/com/netflix/discovery/provider/DiscoveryJerseyProvider.java b/eureka-client/src/main/java/com/netflix/discovery/provider/DiscoveryJerseyProvider.java
index 80c16be8bc..0a000c3ac6 100644
--- a/eureka-client/src/main/java/com/netflix/discovery/provider/DiscoveryJerseyProvider.java
+++ b/eureka-client/src/main/java/com/netflix/discovery/provider/DiscoveryJerseyProvider.java
@@ -16,6 +16,7 @@
package com.netflix.discovery.provider;
+import javax.annotation.Nullable;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
@@ -30,7 +31,7 @@
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
-import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.converters.wrappers.CodecWrappers.LegacyJacksonJson;
@@ -43,71 +44,92 @@
* A custom provider implementation for Jersey that dispatches to the
* implementation that serializes/deserializes objects sent to and from eureka
* server.
+ *
+ *
+ * This implementation allows users to plugin their own
+ * serialization/deserialization mechanism by reading the annotation provided by
+ * specifying the {@link Serializer} and dispatching it to that implementation.
+ *
*
* @author Karthik Ranganathan
*/
@Provider
-@Produces({"application/json", "application/xml"})
+@Produces("*/*")
@Consumes("*/*")
-public class DiscoveryJerseyProvider implements MessageBodyWriter, MessageBodyReader {
+public class DiscoveryJerseyProvider implements MessageBodyWriter, MessageBodyReader {
private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryJerseyProvider.class);
- private final EncoderWrapper jsonEncoder;
- private final DecoderWrapper jsonDecoder;
+ // Cache the serializers so that they don't have to be instantiated every time
+ private static ConcurrentHashMap serializers = new ConcurrentHashMap();
- // XML support is maintained for legacy/custom clients. These codecs are used only on the server side only, while
- // Eureka client is using JSON only.
- private final EncoderWrapper xmlEncoder;
- private final DecoderWrapper xmlDecoder;
+ private final EncoderWrapper encoder;
+ private final DecoderWrapper decoder;
public DiscoveryJerseyProvider() {
this(null, null);
}
- public DiscoveryJerseyProvider(EncoderWrapper jsonEncoder, DecoderWrapper jsonDecoder) {
- this.jsonEncoder = jsonEncoder == null ? CodecWrappers.getEncoder(LegacyJacksonJson.class) : jsonEncoder;
- this.jsonDecoder = jsonDecoder == null ? CodecWrappers.getDecoder(LegacyJacksonJson.class) : jsonDecoder;
- LOGGER.info("Using JSON encoding codec {}", this.jsonEncoder.codecName());
- LOGGER.info("Using JSON decoding codec {}", this.jsonDecoder.codecName());
+ public DiscoveryJerseyProvider(EncoderWrapper encoder, DecoderWrapper decoder) {
+ this.encoder = encoder == null ? CodecWrappers.getEncoder(LegacyJacksonJson.class) : encoder;
+ this.decoder = decoder == null ? CodecWrappers.getDecoder(LegacyJacksonJson.class) : decoder;
- if (jsonEncoder instanceof CodecWrappers.JacksonJsonMini) {
- throw new UnsupportedOperationException("Encoder: " + jsonEncoder.codecName() + "is not supported for the client");
+ if (encoder instanceof CodecWrappers.JacksonJsonMini) {
+ throw new UnsupportedOperationException("Encoder: " + encoder.codecName() + "is not supported for the client");
}
- this.xmlEncoder = CodecWrappers.getEncoder(CodecWrappers.XStreamXml.class);
- this.xmlDecoder = CodecWrappers.getDecoder(CodecWrappers.XStreamXml.class);
+ LOGGER.info("Using encoding codec {}", this.encoder.codecName());
+ LOGGER.info("Using decoding codec {}", this.decoder.codecName());
+ }
+
+ public EncoderWrapper getEncoder() {
+ return encoder;
+ }
- LOGGER.info("Using XML encoding codec {}", this.xmlEncoder.codecName());
- LOGGER.info("Using XML decoding codec {}", this.xmlDecoder.codecName());
+ public DecoderWrapper getDecoder() {
+ return decoder;
}
@Override
public boolean isReadable(Class serializableClass, Type type, Annotation[] annotations, MediaType mediaType) {
- return isSupported(mediaType) && isSupportedCharset(mediaType);
+ if ("application".equals(mediaType.getType()) && ("xml".equals(mediaType.getSubtype()) || "json".equals(mediaType.getSubtype()))) {
+ return checkForAnnotation(serializableClass);
+ }
+ return false;
}
@Override
public Object readFrom(Class serializableClass, Type type,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap headers, InputStream inputStream) throws IOException {
- DecoderWrapper decoder;
- if (MediaType.MEDIA_TYPE_WILDCARD.equals(mediaType.getSubtype())) {
- decoder = xmlDecoder;
- } else if ("json".equalsIgnoreCase(mediaType.getSubtype())) {
- decoder = jsonDecoder;
- } else {
- decoder = xmlDecoder; // default
+ if (decoder.support(mediaType)) {
+ try {
+ return decoder.decode(inputStream, serializableClass);
+ } catch (Throwable e) {
+ if (e instanceof Error) { // See issue: https://github.com/Netflix/eureka/issues/72 on why we catch Error here.
+ closeInputOnError(inputStream);
+ throw new WebApplicationException(createErrorReply(500, e, mediaType));
+ }
+ LOGGER.debug("Cannot parse request body", e);
+ throw new WebApplicationException(createErrorReply(400, "cannot parse request body", mediaType));
+ }
}
- try {
- return decoder.decode(inputStream, serializableClass);
- } catch (Throwable e) {
- if (e instanceof Error) { // See issue: https://github.com/Netflix/eureka/issues/72 on why we catch Error here.
- closeInputOnError(inputStream);
- throw new WebApplicationException(createErrorReply(500, e, mediaType));
+ // default to XML encoded with XStream
+ ISerializer serializer = getSerializer(serializableClass);
+ if (null != serializer) {
+ try {
+ return serializer.read(inputStream, serializableClass, mediaType);
+ } catch (Throwable e) {
+ if (e instanceof Error) { // See issue: https://github.com/Netflix/eureka/issues/72 on why we catch Error here.
+ closeInputOnError(inputStream);
+ throw new WebApplicationException(createErrorReply(500, e, mediaType));
+ }
+ LOGGER.debug("Cannot parse request body", e);
+ throw new WebApplicationException(createErrorReply(400, "cannot parse request body", mediaType));
}
- LOGGER.debug("Cannot parse request body", e);
- throw new WebApplicationException(createErrorReply(400, "cannot parse request body", mediaType));
+ } else {
+ LOGGER.error("No serializer available for serializable class: {}, de-serialization will fail.", serializableClass);
+ throw new WebApplicationException(createErrorReply(500, "No serializer available for serializable class: " + serializableClass, mediaType));
}
}
@@ -118,48 +140,86 @@ public long getSize(Object serializableObject, Class serializableClass, Type typ
@Override
public boolean isWriteable(Class serializableClass, Type type, Annotation[] annotations, MediaType mediaType) {
- return isSupported(mediaType);
+ return checkForAnnotation(serializableClass);
}
@Override
public void writeTo(Object serializableObject, Class serializableClass,
Type type, Annotation[] annotations, MediaType mediaType,
MultivaluedMap headers, OutputStream outputStream) throws IOException, WebApplicationException {
- EncoderWrapper encoder = "json".equalsIgnoreCase(mediaType.getSubtype()) ? jsonEncoder : xmlEncoder;
- // XML codec may not be available
- if (encoder == null) {
- throw new WebApplicationException(createErrorReply(400, "No codec available to serialize content type " + mediaType, mediaType));
+ if (encoder.support(mediaType)) {
+ encoder.encode(serializableObject, outputStream);
+ } else { // default
+ ISerializer serializer = getSerializer(serializableClass);
+ if (null != serializer) {
+ serializer.write(serializableObject, outputStream, mediaType);
+ } else {
+ LOGGER.error("No serializer available for serializable class: " + serializableClass
+ + ", serialization will fail.");
+ throw new IOException("No serializer available for serializable class: " + serializableClass);
+ }
}
-
- encoder.encode(serializableObject, outputStream);
}
- private boolean isSupported(MediaType mediaType) {
- if (MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
- return true;
- }
- if (MediaType.APPLICATION_XML_TYPE.isCompatible(mediaType)) {
- return xmlDecoder != null;
+ /**
+ * Checks for the {@link java.io.Serializable} annotation for the given class.
+ *
+ * @param serializableClass The class to be serialized/deserialized.
+ * @return true if the annotation is present, false otherwise.
+ */
+ private boolean checkForAnnotation(Class serializableClass) {
+ try {
+ Annotation annotation = serializableClass.getAnnotation(Serializer.class);
+ if (annotation != null) {
+ return true;
+ }
+ } catch (Throwable th) {
+ LOGGER.warn("Exception in checking for annotations", th);
}
return false;
}
/**
- * As content is cached, we expect both ends use UTF-8 always. If no content charset encoding is explicitly
- * defined, UTF-8 is assumed as a default.
- * As legacy clients may use ISO 8859-1 we accept it as well, although result may be unspecified if
- * characters out of ASCII 0-127 range are used.
+ * Gets the {@link Serializer} implementation for serializing/ deserializing
+ * objects.
+ *
+ *
+ * The implementation is cached after the first time instantiation and then
+ * returned.
+ *
+ *
+ * @param serializableClass - The class that is to be serialized/deserialized.
+ * @return The {@link Serializer} implementation for serializing/
+ * deserializing objects.
*/
- private static boolean isSupportedCharset(MediaType mediaType) {
- Map parameters = mediaType.getParameters();
- if (parameters == null || parameters.isEmpty()) {
- return true;
+ @Nullable
+ private static ISerializer getSerializer(@SuppressWarnings("rawtypes") Class serializableClass) {
+ ISerializer converter = null;
+ Annotation annotation = serializableClass.getAnnotation(Serializer.class);
+ if (annotation != null) {
+ Serializer payloadConverter = (Serializer) annotation;
+ String serializer = payloadConverter.value();
+ if (serializer != null) {
+ converter = serializers.get(serializableClass);
+ if (converter == null) {
+ try {
+ converter = (ISerializer) Class.forName(serializer).newInstance();
+ } catch (InstantiationException e) {
+ LOGGER.error("Error creating a serializer.", e);
+ } catch (IllegalAccessException e) {
+ LOGGER.error("Error creating a serializer.", e);
+ } catch (ClassNotFoundException e) {
+ LOGGER.error("Error creating a serializer.", e);
+ }
+ if (null != converter) {
+ serializers.put(serializableClass, converter);
+ }
+ }
+ }
+
}
- String charset = parameters.get("charset");
- return charset == null
- || "UTF-8".equalsIgnoreCase(charset)
- || "ISO-8859-1".equalsIgnoreCase(charset);
+ return converter;
}
private static Response createErrorReply(int status, Throwable cause, MediaType mediaType) {
diff --git a/eureka-client/src/test/java/com/netflix/discovery/provider/DiscoveryJerseyProviderTest.java b/eureka-client/src/test/java/com/netflix/discovery/provider/DiscoveryJerseyProviderTest.java
deleted file mode 100644
index a6c1c22be0..0000000000
--- a/eureka-client/src/test/java/com/netflix/discovery/provider/DiscoveryJerseyProviderTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2016 Netflix, 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 License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.netflix.discovery.provider;
-
-import javax.ws.rs.core.MediaType;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.netflix.appinfo.InstanceInfo;
-import com.netflix.discovery.converters.wrappers.CodecWrappers;
-import com.netflix.discovery.util.InstanceInfoGenerator;
-import org.apache.commons.io.output.ByteArrayOutputStream;
-import org.junit.Test;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-
-/**
- */
-public class DiscoveryJerseyProviderTest {
-
- private static final InstanceInfo INSTANCE = InstanceInfoGenerator.takeOne();
-
- private final DiscoveryJerseyProvider jerseyProvider = new DiscoveryJerseyProvider(
- CodecWrappers.getEncoder(CodecWrappers.JacksonJson.class),
- CodecWrappers.getDecoder(CodecWrappers.JacksonJson.class)
- );
-
- @Test
- public void testJsonEncodingDecoding() throws Exception {
- testEncodingDecoding(MediaType.APPLICATION_JSON_TYPE);
- }
-
- @Test
- public void testXmlEncodingDecoding() throws Exception {
- testEncodingDecoding(MediaType.APPLICATION_XML_TYPE);
- }
-
- @Test
- public void testDecodingWithUtf8CharsetExplicitlySet() throws Exception {
- Map params = new HashMap<>();
- params.put("charset", "UTF-8");
- testEncodingDecoding(new MediaType("application", "json", params));
- }
-
- private void testEncodingDecoding(MediaType mediaType) throws IOException {
- // Write
- assertThat(jerseyProvider.isWriteable(InstanceInfo.class, InstanceInfo.class, null, mediaType), is(true));
-
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- jerseyProvider.writeTo(INSTANCE, InstanceInfo.class, InstanceInfo.class, null, mediaType, null, out);
-
- // Read
- assertThat(jerseyProvider.isReadable(InstanceInfo.class, InstanceInfo.class, null, mediaType), is(true));
-
- ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
- InstanceInfo decodedInstance = (InstanceInfo) jerseyProvider.readFrom(InstanceInfo.class, InstanceInfo.class, null, mediaType, null, in);
-
- assertThat(decodedInstance, is(equalTo(INSTANCE)));
- }
-
- @Test
- public void testNonUtf8CharsetIsNotAccepted() throws Exception {
- Map params = new HashMap<>();
- params.put("charset", "ISO-8859");
- MediaType mediaTypeWithNonSupportedCharset = new MediaType("application", "json", params);
-
- assertThat(jerseyProvider.isReadable(InstanceInfo.class, InstanceInfo.class, null, mediaTypeWithNonSupportedCharset), is(false));
- }
-}
\ No newline at end of file