From f427f64e63f87a890f2e158c45cee383e30641d2 Mon Sep 17 00:00:00 2001 From: Mengyi Zhou Date: Mon, 22 Feb 2016 14:03:10 +0800 Subject: [PATCH] fix bug serialize response containing special cases --- .../com/ctrip/zeus/client/GroupClient.java | 9 +- .../java/com/ctrip/zeus/client/SlbClient.java | 9 +- .../message/impl/DefaultResponseHandler.java | 45 ++++++--- .../message/impl/ErrorResponseHandler.java | 8 +- .../zeus/restful/resource/SlbResource.java | 20 ++-- .../model/handler/impl/ContentWriters.java | 7 +- .../ctrip/zeus/support/GenericSerializer.java | 85 ++++++++++++++-- .../com/ctrip/zeus/restful/ExceptionTest.java | 48 +-------- .../zeus/restful/ResponseWritingTest.java | 99 +++++++++++++++++++ 9 files changed, 238 insertions(+), 92 deletions(-) create mode 100644 src/test/java/com/ctrip/zeus/restful/ResponseWritingTest.java diff --git a/src/main/java/com/ctrip/zeus/client/GroupClient.java b/src/main/java/com/ctrip/zeus/client/GroupClient.java index 05755713..cf5c6faf 100644 --- a/src/main/java/com/ctrip/zeus/client/GroupClient.java +++ b/src/main/java/com/ctrip/zeus/client/GroupClient.java @@ -4,6 +4,7 @@ package com.ctrip.zeus.client; import com.ctrip.zeus.model.entity.Group; import com.ctrip.zeus.model.entity.GroupList; import com.ctrip.zeus.model.transform.DefaultJsonParser; +import com.ctrip.zeus.support.GenericSerializer; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; @@ -42,18 +43,14 @@ public class GroupClient extends AbstractRestClient { public Response add(Group group) { return getTarget().path("/api/group/new").request().headers(getDefaultHeaders()) .post(Entity.entity( - String.format(Group.JSON, group), - MediaType.APPLICATION_JSON - )); + GenericSerializer.writeJson(group), MediaType.APPLICATION_JSON)); } public Response update(Group group) { return getTarget().path("/api/group/update").request().headers(getDefaultHeaders()) .post(Entity.entity( - String.format(Group.JSON, group), - MediaType.APPLICATION_JSON - )); + GenericSerializer.writeJson(group), MediaType.APPLICATION_JSON)); } diff --git a/src/main/java/com/ctrip/zeus/client/SlbClient.java b/src/main/java/com/ctrip/zeus/client/SlbClient.java index 70a7b66e..77aa20ab 100644 --- a/src/main/java/com/ctrip/zeus/client/SlbClient.java +++ b/src/main/java/com/ctrip/zeus/client/SlbClient.java @@ -4,6 +4,7 @@ package com.ctrip.zeus.client; import com.ctrip.zeus.model.entity.Slb; import com.ctrip.zeus.model.entity.SlbList; import com.ctrip.zeus.model.transform.DefaultJsonParser; +import com.ctrip.zeus.support.GenericSerializer; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; @@ -42,17 +43,13 @@ public class SlbClient extends AbstractRestClient { public Response add(Slb slb) { return getTarget().path("/api/slb/new").request().headers(getDefaultHeaders()) .post(Entity.entity( - String.format(Slb.JSON, slb), - MediaType.APPLICATION_JSON - )); + GenericSerializer.writeJson(slb), MediaType.APPLICATION_JSON)); } public Response update(Slb slb) { return getTarget().path("/api/slb/update").request().headers(getDefaultHeaders()) .post(Entity.entity( - String.format(Slb.JSON, slb), - MediaType.APPLICATION_JSON - )); + GenericSerializer.writeJson(slb), MediaType.APPLICATION_JSON)); } public Response delete(Long slbId) { diff --git a/src/main/java/com/ctrip/zeus/restful/message/impl/DefaultResponseHandler.java b/src/main/java/com/ctrip/zeus/restful/message/impl/DefaultResponseHandler.java index deb61a2c..0de7331c 100644 --- a/src/main/java/com/ctrip/zeus/restful/message/impl/DefaultResponseHandler.java +++ b/src/main/java/com/ctrip/zeus/restful/message/impl/DefaultResponseHandler.java @@ -5,6 +5,8 @@ import com.ctrip.zeus.restful.message.Message; import com.ctrip.zeus.restful.message.ResponseHandler; import com.ctrip.zeus.restful.response.entity.SuccessMessage; import com.ctrip.zeus.support.GenericSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.ws.rs.core.MediaType; @@ -18,30 +20,39 @@ import java.util.Set; */ @Component("responseHandler") public class DefaultResponseHandler implements ResponseHandler { + Logger logger = LoggerFactory.getLogger(this.getClass()); + private static final Set acceptedMediaTypes = getDefault(); private static final MediaType defaultMediaType = MediaType.APPLICATION_JSON_TYPE; - public Message generateMessage(Object object, String type) throws Exception { + public Message generateMessage(Object object, MediaType type) throws Exception { ZeusResponse zr = new ZeusResponse(); if (object == null) return zr; if (object instanceof String) { object = new SuccessMessage().setMessage((String) object); } - if (type.equals(MediaType.APPLICATION_XML)) { - zr.setResponse(GenericSerializer.writeXml(object)); - } else { - try { - zr.setResponse(GenericSerializer.writeJson(object)); - } catch (Exception ex) { - if (object instanceof Serializable) { - zr.setResponse((Serializable) object); - } else { - throw new ValidationException("Response object cannot be serialized."); - } + + try { + if (type.equals(MediaType.APPLICATION_XML_TYPE)) { + zr.setResponse(GenericSerializer.writeXml(object).replace("%", "%%")); + return zr; + } + if (type.equals(MediaType.APPLICATION_JSON_TYPE)) { + zr.setResponse(GenericSerializer.writeJson(object).replace("%", "%%")); + return zr; + } + throw new ValidationException("Unaccepted media type " + type.getType() + "."); + } catch (Exception ex) { + if (object instanceof Serializable) { + zr.setResponse((Serializable) object); + return zr; + } else { + String error = "Response object cannot be serialized."; + logger.error(error, ex); + throw new ValidationException(error); } } - return zr; } @Override @@ -49,16 +60,18 @@ public class DefaultResponseHandler implements ResponseHandler { if (object == null) return Response.status(Response.Status.OK).type(mediaType).build(); if (mediaType != null && acceptedMediaTypes.contains(mediaType)) { - Message response = generateMessage(object, mediaType.toString()); + Message response = generateMessage(object, mediaType); return Response.status(response.getStatus()).entity(response.getResponse()) .type(mediaType).build(); } try { - Message response = generateMessage(object, defaultMediaType.toString()); + Message response = generateMessage(object, defaultMediaType); return Response.status(response.getStatus()).entity(response.getResponse()) .type(defaultMediaType).build(); } catch (Exception ex) { - throw new ValidationException("Unaccepted media type."); + String error = "Response cannot be serialized using application/json by default."; + logger.error(error, ex); + throw new ValidationException(error); } } diff --git a/src/main/java/com/ctrip/zeus/restful/message/impl/ErrorResponseHandler.java b/src/main/java/com/ctrip/zeus/restful/message/impl/ErrorResponseHandler.java index a653957d..7ad040d8 100644 --- a/src/main/java/com/ctrip/zeus/restful/message/impl/ErrorResponseHandler.java +++ b/src/main/java/com/ctrip/zeus/restful/message/impl/ErrorResponseHandler.java @@ -18,14 +18,14 @@ import javax.ws.rs.core.Response; public class ErrorResponseHandler implements ResponseHandler { private static final MediaType defaultMediaType = MediaType.APPLICATION_JSON_TYPE; - public Message generateMessage(Throwable object, String type, boolean printStackTrace) throws Exception { + public Message generateMessage(Throwable object, MediaType type, boolean printStackTrace) throws Exception { ErrorMessage em = ExceptionUtils.getErrorMessage(object, printStackTrace); ErrorResponse err = new ErrorResponse(); if (type.equals(MediaType.APPLICATION_XML)) { - err.setResponse(GenericSerializer.writeXml(em)); + err.setResponse(GenericSerializer.writeXml(em).replace("%", "%%")); } else { - err.setResponse(GenericSerializer.writeJson(em)); + err.setResponse(GenericSerializer.writeJson(em).replace("%", "%%")); } if (object instanceof NotFoundException) { err.setStatus(Response.Status.NOT_FOUND.getStatusCode()); @@ -55,7 +55,7 @@ public class ErrorResponseHandler implements ResponseHandler { if (mediaType == null || !MediaType.APPLICATION_XML_TYPE.equals(mediaType)) { mediaType = defaultMediaType; } - Message message = generateMessage((Throwable) object, mediaType.toString(), printStackTrace); + Message message = generateMessage((Throwable) object, mediaType, printStackTrace); return Response.status(message.getStatus()).entity(message.getResponse()) .type(mediaType).build(); } diff --git a/src/main/java/com/ctrip/zeus/restful/resource/SlbResource.java b/src/main/java/com/ctrip/zeus/restful/resource/SlbResource.java index cac4a133..7c3aa5e4 100644 --- a/src/main/java/com/ctrip/zeus/restful/resource/SlbResource.java +++ b/src/main/java/com/ctrip/zeus/restful/resource/SlbResource.java @@ -191,16 +191,16 @@ public class SlbResource { throw new Exception("Slb cannot be parsed."); } } - s.setName(s.getName().trim()); + s.setName(trimIfNotNull(s.getName())); for (VirtualServer virtualServer : s.getVirtualServers()) { - virtualServer.setName(virtualServer.getName().trim()); + virtualServer.setName(trimIfNotNull(virtualServer.getName())); for (Domain domain : virtualServer.getDomains()) { - domain.setName(domain.getName().trim()); + domain.setName(trimIfNotNull(domain.getName())); } } for (SlbServer slbServer : s.getSlbServers()) { - slbServer.setIp(slbServer.getIp().trim()); - slbServer.setHostName(slbServer.getHostName().trim()); + slbServer.setIp(trimIfNotNull(slbServer.getIp())); + slbServer.setHostName(trimIfNotNull(slbServer.getHostName())); } return s; } @@ -211,14 +211,22 @@ public class SlbResource { .setName(slb.getName()); } if ("NORMAL".equalsIgnoreCase(type)) { - return new Slb().setId(slb.getId()) + Slb result = new Slb().setId(slb.getId()) .setName(slb.getName()) .setNginxBin(slb.getNginxBin()) .setNginxConf(slb.getNginxConf()) .setNginxWorkerProcesses(slb.getNginxWorkerProcesses()) .setStatus(slb.getStatus()) .setVersion(slb.getVersion()); + for (SlbServer slbServer : slb.getSlbServers()) { + result.addSlbServer(slbServer); + } + return result; } return slb; } + + private String trimIfNotNull(String value) { + return value != null ? value.trim() : value; + } } diff --git a/src/main/java/com/ctrip/zeus/service/model/handler/impl/ContentWriters.java b/src/main/java/com/ctrip/zeus/service/model/handler/impl/ContentWriters.java index 2765d75f..da87e648 100644 --- a/src/main/java/com/ctrip/zeus/service/model/handler/impl/ContentWriters.java +++ b/src/main/java/com/ctrip/zeus/service/model/handler/impl/ContentWriters.java @@ -3,20 +3,21 @@ package com.ctrip.zeus.service.model.handler.impl; import com.ctrip.zeus.model.entity.Group; import com.ctrip.zeus.model.entity.Slb; import com.ctrip.zeus.model.entity.VirtualServer; +import com.ctrip.zeus.support.GenericSerializer; /** * Created by zhoumy on 2015/9/22. */ public class ContentWriters { public static String writeVirtualServerContent(VirtualServer vs) { - return String.format(VirtualServer.JSON, vs); + return GenericSerializer.writeJson(vs); } public static String writeGroupContent(Group g) { - return String.format(Group.JSON, g); + return GenericSerializer.writeJson(g); } public static String writeSlbContent(Slb s) { - return String.format(Slb.JSON, s); + return GenericSerializer.writeJson(s); } } \ No newline at end of file diff --git a/src/main/java/com/ctrip/zeus/support/GenericSerializer.java b/src/main/java/com/ctrip/zeus/support/GenericSerializer.java index c970d98b..74c3f9a8 100644 --- a/src/main/java/com/ctrip/zeus/support/GenericSerializer.java +++ b/src/main/java/com/ctrip/zeus/support/GenericSerializer.java @@ -1,23 +1,69 @@ package com.ctrip.zeus.support; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + /** * Created by zhoumy on 2015/4/3. */ public class GenericSerializer { + private static Logger logger = LoggerFactory.getLogger(GenericSerializer.class); + public static final String JSON = "%#.3s"; public static final String JSON_COMPACT = "%#s"; public static final String XML = "%.3s"; public static final String XML_COMPACT = "%s"; + private static Map Builders = new HashMap<>(); + private static Map Methods = new HashMap<>(); + private static LoadingCache> Ctors = CacheBuilder.newBuilder() + .build(new CacheLoader>() { + @Override + public Constructor load(Class aClass) throws Exception { + return aClass.getConstructor(boolean.class); + } + }); + public static String writeJson(Object object) { return writeJson(object, true); } public static String writeJson(Object object, boolean pretty) { - if (pretty) { - return String.format(JSON, object); - } else { - return String.format(JSON_COMPACT, object); + String pkg = object.getClass().getPackage().getName(); + pkg = pkg.substring(0, pkg.lastIndexOf(".entity")); + Class clazz = Builders.get(pkg + "#Json"); + if (clazz == null) { + try { + clazz = Class.forName(pkg + ".transform.DefaultJsonBuilder"); + Builders.put(pkg + "#Json", clazz); + } catch (ClassNotFoundException e) { + logger.error("Cannot find " + pkg + ".transform.DefaultJsonBuilder", e); + return ""; + } + } + try { + Object builder = Ctors.get(clazz).newInstance(!pretty); + Method m = Methods.get(clazz); + if (m == null) { + try { + m = clazz.getMethod("build", Class.forName(pkg + ".IEntity")); + Methods.put(clazz, m); + } catch (Exception e) { + logger.error("Cannot find method build(IEntity)", e); + } + } + return (String) m.invoke(builder, object); + } catch (Exception e) { + logger.error("Fail to build json data.", e); + return ""; } } @@ -26,10 +72,33 @@ public class GenericSerializer { } public static String writeXml(Object object, boolean pretty) { - if (pretty) { - return String.format(XML, object); - } else { - return String.format(XML_COMPACT, object); + String pkg = object.getClass().getPackage().getName(); + pkg = pkg.substring(0, pkg.lastIndexOf(".entity")); + Class clazz = Builders.get(pkg + "#Xml"); + if (clazz == null) { + try { + clazz = Class.forName(pkg + ".transform.DefaultXmlBuilder"); + Builders.put(pkg + "#Xml", clazz); + } catch (ClassNotFoundException e) { + logger.error("Cannot find " + pkg + ".transform.DefaultXmlBuilder", e); + return ""; + } + } + try { + Object builder = Ctors.get(clazz).newInstance(!pretty); + Method m = Methods.get(clazz); + if (m == null) { + try { + m = clazz.getMethod("buildXml", Class.forName(pkg + ".IEntity")); + Methods.put(clazz, m); + } catch (Exception e) { + logger.error("Cannot find method buildXml(IEntity)", e); + } + } + return (String) m.invoke(builder, object); + } catch (Exception e) { + logger.error("Fail to build xml data.", e); + return ""; } } } diff --git a/src/test/java/com/ctrip/zeus/restful/ExceptionTest.java b/src/test/java/com/ctrip/zeus/restful/ExceptionTest.java index b641bbcb..98a11633 100644 --- a/src/test/java/com/ctrip/zeus/restful/ExceptionTest.java +++ b/src/test/java/com/ctrip/zeus/restful/ExceptionTest.java @@ -1,5 +1,6 @@ package com.ctrip.zeus.restful; +import com.ctrip.zeus.AbstractServerTest; import com.ctrip.zeus.client.GroupClient; import com.ctrip.zeus.client.SlbClient; import com.ctrip.zeus.dal.core.*; @@ -7,21 +8,11 @@ import com.ctrip.zeus.exceptions.ValidationException; import com.ctrip.zeus.model.entity.*; import com.ctrip.zeus.restful.response.entity.ErrorMessage; import com.ctrip.zeus.restful.response.transform.DefaultJsonParser; -import com.ctrip.zeus.server.SlbAdminServer; import com.ctrip.zeus.util.IOUtils; -import com.ctrip.zeus.util.S; -import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException; -import org.codehaus.plexus.component.repository.exception.ComponentLookupException; -import org.junit.AfterClass; + import org.junit.Assert; -import org.junit.BeforeClass; import org.junit.Test; import org.unidal.dal.jdbc.DalException; -import org.unidal.dal.jdbc.datasource.DataSourceManager; -import org.unidal.dal.jdbc.transaction.TransactionManager; -import org.unidal.lookup.ContainerLoader; -import support.AbstractSpringTest; -import support.MysqlDbServer; import javax.annotation.Resource; import javax.ws.rs.core.Response; @@ -30,30 +21,12 @@ import java.io.*; /** * Created by zhoumy on 2015/4/2. */ -public class ExceptionTest extends AbstractSpringTest { - private static SlbAdminServer server; - private static MysqlDbServer mysqlDbServer; - +public class ExceptionTest extends AbstractServerTest { @Resource private GroupDao appDao; @Resource private SlbDao slbDao; - @BeforeClass - public static void setUpDb() throws Exception { - S.setPropertyDefaultValue("archaius.deployment.applicationId", "slb-admin"); - S.setPropertyDefaultValue("archaius.deployment.environment", "local"); - S.setPropertyDefaultValue("server.www.base-dir", new File("").getAbsolutePath() + "/src/main/www"); - S.setPropertyDefaultValue("server.temp-dir", new File("").getAbsolutePath() + "/target/temp"); - S.setPropertyDefaultValue("CONF_DIR", new File("").getAbsolutePath() + "/conf/test"); - - mysqlDbServer = new MysqlDbServer(); - mysqlDbServer.start(); - - server = new SlbAdminServer(); - server.start(); - } - @Test public void testDalNotFoundException() throws DalException { Assert.assertNull(appDao.findByName("notExistGroup", GroupEntity.READSET_FULL)); @@ -64,7 +37,7 @@ public class ExceptionTest extends AbstractSpringTest { public void testExceptionInterceptor() throws Exception { GroupClient ac = new GroupClient("http://127.0.0.1:8099"); Response appResponse = ac.add(new Group()); - Assert.assertEquals(500, appResponse.getStatus()); + Assert.assertEquals(400, appResponse.getStatus()); String appString = IOUtils.inputStreamStringify((InputStream) appResponse.getEntity()); ErrorMessage aem = DefaultJsonParser.parse(ErrorMessage.class, appString); @@ -73,7 +46,7 @@ public class ExceptionTest extends AbstractSpringTest { SlbClient sc = new SlbClient("http://127.0.0.1:8099"); Response slbResponse = sc.add(new Slb()); - Assert.assertEquals(500, slbResponse.getStatus()); + Assert.assertEquals(400, slbResponse.getStatus()); String slbString = IOUtils.inputStreamStringify((InputStream) slbResponse.getEntity()); ErrorMessage sem = DefaultJsonParser.parse(ErrorMessage.class, slbString); @@ -87,15 +60,4 @@ public class ExceptionTest extends AbstractSpringTest { System.out.println("message:\n" + em.getMessage()); System.out.println("*********************************************"); } - - @AfterClass - public static void tearDownDb() throws InterruptedException, ComponentLookupException, ComponentLifecycleException { - server.close(); - mysqlDbServer.stop(); - - DataSourceManager ds = ContainerLoader.getDefaultContainer().lookup(DataSourceManager.class); - ContainerLoader.getDefaultContainer().release(ds); - TransactionManager ts = ContainerLoader.getDefaultContainer().lookup(TransactionManager.class); - ContainerLoader.getDefaultContainer().release(ts); - } } diff --git a/src/test/java/com/ctrip/zeus/restful/ResponseWritingTest.java b/src/test/java/com/ctrip/zeus/restful/ResponseWritingTest.java new file mode 100644 index 00000000..b82f83d2 --- /dev/null +++ b/src/test/java/com/ctrip/zeus/restful/ResponseWritingTest.java @@ -0,0 +1,99 @@ +package com.ctrip.zeus.restful; + +import com.ctrip.zeus.model.entity.*; +import com.ctrip.zeus.restful.message.impl.DefaultResponseHandler; +import com.ctrip.zeus.restful.message.impl.ErrorResponseHandler; +import com.ctrip.zeus.support.GenericSerializer; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; + +/** + * Created by zhoumy on 2016/2/22. + */ +public class ResponseWritingTest { + + @Test + public void testSpecialCases() throws Exception { + System.out.println("************************* Test Special Cases Serialization *************************"); + + DefaultResponseHandler rh = new DefaultResponseHandler(); + System.out.printf(String.valueOf( + rh.generateMessage("%s", MediaType.APPLICATION_JSON_TYPE).getResponse())); + + ErrorResponseHandler erh = new ErrorResponseHandler(); + System.out.printf(String.valueOf( + erh.generateMessage(new Exception("%s"), MediaType.APPLICATION_JSON_TYPE, false).getResponse())); + } + + @Test + public void testFormat() throws Exception { + System.out.println("************************* Test Serialization Format *************************"); + + DefaultResponseHandler rh = new DefaultResponseHandler(); + System.out.printf(String.valueOf( + rh.generateMessage("json", MediaType.APPLICATION_JSON_TYPE).getResponse())); + + System.out.printf(String.valueOf( + rh.generateMessage("xml", MediaType.APPLICATION_XML_TYPE).getResponse())); + } + + @Test + public void testPerformance() throws Exception { + System.out.println("************************* Test Serialization Performance *************************"); + + Group group = new Group().setId(9837401263971292L).setName("performace").setAppId("000000").setSsl(false) + .setHealthCheck(new HealthCheck().setIntervals(2000).setFails(1).setPasses(1).setUri("/")) + .setLoadBalancingMethod(new LoadBalancingMethod().setType("roundrobin").setValue("test")); + for (int i = 0; i < 10; i++) { + group.addGroupVirtualServer(new GroupVirtualServer().setPath("/performace").setVirtualServer(new VirtualServer().setId(12345678891028L))); + } + for (int i = 0; i < 10; i++) { + group.addGroupServer(new GroupServer().setPort(80).setWeight(1).setMaxFails(1).setFailTimeout(30).setHostName("0").setIp("10.2.6.201")); + } + GroupList groupList = new GroupList(); + for (int i = 0; i < 100; i++) { + groupList.addGroup(group); + } + + final String JSON = "%#.3s"; + for (int i = 0; i < 150; i++) { + GenericSerializer.writeJson(groupList); + } + for (int i = 0; i < 150; i++) { + String.format(JSON, groupList); + } + + final String XML = "%.3s"; + for (int i = 0; i < 150; i++) { + GenericSerializer.writeXml(groupList); + } + for (int i = 0; i < 150; i++) { + String.format(XML, groupList); + } + + Long start = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + GenericSerializer.writeJson(groupList); + } + System.out.println("Using GenericSerializer to write json data takes " + (System.nanoTime() - start) / (1000 * 1000) + " ms."); + + start = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + String.format(JSON, groupList); + } + System.out.println("Using StringFormat to write json data takes " + (System.nanoTime() - start) / (1000 * 1000) + " ms."); + + start = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + GenericSerializer.writeXml(groupList); + } + System.out.println("Using GenericSerializer to write xml data takes " + (System.nanoTime() - start) / (1000 * 1000) + " ms."); + + start = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + String.format(XML, groupList); + } + System.out.println("Using StringFormat to write xml data takes " + (System.nanoTime() - start) / (1000 * 1000) + " ms."); + } +}