From 338e69e55d16b8b32efd69a0cb9fbb0d143cad24 Mon Sep 17 00:00:00 2001 From: Sander Hautvast Date: Fri, 4 Dec 2020 17:11:01 +0100 Subject: [PATCH] Revert "* removed server instance * agent now stores data in sqllite table (file) * other small improvements" This reverts commit a6f481fc --- .gitignore | 3 +- pom.xml | 6 +- src/main/java/perfix/Agent.java | 21 +- src/main/java/perfix/MethodInvocation.java | 22 ++ src/main/java/perfix/MethodNode.java | 69 +--- src/main/java/perfix/Registry.java | 138 +++---- .../perfix/instrument/ClassInstrumentor.java | 1 + .../java/perfix/instrument/Instrumentor.java | 18 +- .../perfix/instrument/JdbcInstrumentor.java | 6 +- .../instrument/ServletInstrumentor.java | 7 +- src/main/java/perfix/server/HTTPServer.java | 76 ++++ .../perfix/server/json/JSONSerializer.java | 21 ++ .../java/perfix/server/json/Serializer.java | 45 +++ .../json/SerializerCreationException.java | 10 + .../perfix/server/json/SerializerFactory.java | 5 + .../server/json/SynthSerializerFactory.java | 350 ++++++++++++++++++ 16 files changed, 635 insertions(+), 163 deletions(-) create mode 100644 src/main/java/perfix/MethodInvocation.java create mode 100644 src/main/java/perfix/server/HTTPServer.java create mode 100644 src/main/java/perfix/server/json/JSONSerializer.java create mode 100644 src/main/java/perfix/server/json/Serializer.java create mode 100644 src/main/java/perfix/server/json/SerializerCreationException.java create mode 100644 src/main/java/perfix/server/json/SerializerFactory.java create mode 100644 src/main/java/perfix/server/json/SynthSerializerFactory.java diff --git a/.gitignore b/.gitignore index 74d159f..af9d3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ .idea/ *.iml target/ -ui/node_modules -*.db \ No newline at end of file +ui/node_modules \ No newline at end of file diff --git a/pom.xml b/pom.xml index 85c5e77..689ada8 100644 --- a/pom.xml +++ b/pom.xml @@ -21,9 +21,9 @@ - org.xerial - sqlite-jdbc - 3.28.0 + org.nanohttpd + nanohttpd + 2.3.1 diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java index ba8bafd..55a20ca 100644 --- a/src/main/java/perfix/Agent.java +++ b/src/main/java/perfix/Agent.java @@ -1,6 +1,7 @@ package perfix; import perfix.instrument.Instrumentor; +import perfix.server.HTTPServer; import java.lang.instrument.Instrumentation; import java.util.ArrayList; @@ -8,33 +9,33 @@ import java.util.Collections; import java.util.List; import static java.util.Arrays.asList; +import static java.util.Arrays.stream; public class Agent { - public static final String DBFILE_PROPERTY = "perfix.db"; - public static final String DEFAULT_DBFILE = "perfix.db"; private static final String PORT_PROPERTY = "perfix.port"; private static final String INCLUDES_PROPERTY = "perfix.includes"; + + private static final String DEFAULT_PORT = "2048"; private static final String MESSAGE = " --- Perfix agent active"; + public static void premain(String agentArgs, Instrumentation instrumentation) { System.out.println(MESSAGE); - String dbFile = System.getProperty(DBFILE_PROPERTY); - if (dbFile == null) { - dbFile = DEFAULT_DBFILE; - } - System.out.println(" --- SQLite file written to " + dbFile); + + int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT)); Instrumentor.create(determineIncludes()).instrumentCode(instrumentation); + + new HTTPServer(port).start(); } private static List determineIncludes() { String includesPropertyValue = System.getProperty(INCLUDES_PROPERTY); - if (includesPropertyValue == null) { + if (includesPropertyValue==null){ System.out.println("WARNING: perfix.includes not set "); return Collections.emptyList(); - } - System.out.println(" --- Instrumenting packages: " + includesPropertyValue + ".*"); + } return new ArrayList<>(asList(includesPropertyValue.split(","))); } } diff --git a/src/main/java/perfix/MethodInvocation.java b/src/main/java/perfix/MethodInvocation.java new file mode 100644 index 0000000..92e4588 --- /dev/null +++ b/src/main/java/perfix/MethodInvocation.java @@ -0,0 +1,22 @@ +package perfix; + +/** + * contains start and stop time for method/query/servlet + */ +public class MethodInvocation { + private final long timestamp; + long duration; + + MethodInvocation(String name) { + timestamp = System.nanoTime(); + + } + + public long getDuration() { + return duration; + } + + public void registerEndingTime(long t1) { + duration = t1 - timestamp; + } +} diff --git a/src/main/java/perfix/MethodNode.java b/src/main/java/perfix/MethodNode.java index 6ed82e1..5be85f6 100644 --- a/src/main/java/perfix/MethodNode.java +++ b/src/main/java/perfix/MethodNode.java @@ -1,29 +1,30 @@ package perfix; - +import java.util.ArrayList; +import java.util.List; import java.util.Objects; public class MethodNode { - private final String name; - private final long timestamp; - private final String threadName; - private MethodNode parent; - private long duration; - private long invocationid; - + public final String name; + public final List children; + public MethodNode parent; + private MethodInvocation invocation; public MethodNode(String name) { this.name = name; - this.timestamp = System.nanoTime(); - this.threadName = Thread.currentThread().getName(); + this.children = new ArrayList<>(); + } + + public void addChild(MethodNode child) { + children.add(child); } public String getName() { return name; } - public long getId() { - return Objects.hash(Thread.currentThread().getId(), timestamp); + public List getChildren() { + return children; } @Override @@ -46,47 +47,11 @@ public class MethodNode { return Objects.hash(name); } - public long getParentId() { - if (parent == null) { - return 0; - } else { - return parent.getId(); - } + public MethodInvocation getInvocation() { + return invocation; } - public long getDuration() { - return duration; - } - - public void registerEndingTime(long t1) { - duration = t1 - timestamp; - } - - public long getTimestamp() { - return timestamp; - } - - public MethodNode getParent() { - return parent; - } - - public void setParent(MethodNode parent) { - this.parent = parent; - } - - public String getThreadName() { - return threadName; - } - - public void setInvocationId(long invocationid) { - this.invocationid = invocationid; - } - - public long getInvocationId() { - if (parent != null) { - return parent.getInvocationId(); - } else { - return getId(); - } + public void setInvocation(MethodInvocation invocation) { + this.invocation = invocation; } } diff --git a/src/main/java/perfix/Registry.java b/src/main/java/perfix/Registry.java index 73ddd83..b074ea0 100644 --- a/src/main/java/perfix/Registry.java +++ b/src/main/java/perfix/Registry.java @@ -2,65 +2,17 @@ package perfix; import perfix.instrument.Util; -import java.sql.*; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; -/** - * Handles start and stop of method invocations. Measures method/sql/url invocations. Stores individual measurements in sqlite. - */ -@SuppressWarnings("unused") //used from instrumented bytecode public class Registry { + private static final List callstack = new ArrayList<>(); private static final ThreadLocal currentMethod = new ThreadLocal<>(); - private static final String INSERT_REPORT = "insert into report (thread, id, parent_id, invocation_id, timestamp, name, duration) values (?,?,?,?,?,?,?)"; - private static final String CREATE_TABLE = "create table report(thread varchar(255), id int, parent_id int, invocation_id int, timestamp int, name varchar(255), duration integer)"; - private static final String SElECT_TABLE = "SELECT name FROM sqlite_master WHERE type='table' AND name='report';"; - private static BlockingQueue queue = new LinkedBlockingQueue<>(); - private static Connection connection; - - static { - initWorker(); - initDatabase(); - } - - private static void initWorker() { - ExecutorService executorService = Executors.newFixedThreadPool(1); - executorService.submit(() -> { - while (true) { - MethodNode methodNode = queue.take(); - - store(methodNode); - } - }); - } - - private static void initDatabase() { - try { - connection = DriverManager.getConnection("jdbc:sqlite:" + getSqliteFile()); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery(SElECT_TABLE); - if (!resultSet.next()) { - statement.execute(CREATE_TABLE); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - private static String getSqliteFile() { - String dbFileName = System.getProperty(Agent.DBFILE_PROPERTY); - if (dbFileName == null) { - return Agent.DEFAULT_DBFILE; - } else { - return dbFileName; - } - } - - public static MethodNode startJdbc(String name) { + @SuppressWarnings("unused") //used in generated code + public static MethodInvocation startJdbc(String name) { if (!Util.isFirstExecutionStarted()) { Util.startExecution(); return start(name); @@ -69,54 +21,72 @@ public class Registry { } } - public static MethodNode start(String name) { + @SuppressWarnings("unused") + public static MethodInvocation start(String name) { + MethodInvocation methodInvocation = new MethodInvocation(name); MethodNode newNode = new MethodNode(name); MethodNode parent = currentMethod.get(); if (parent != null) { - newNode.setParent(parent); - newNode.setInvocationId(parent.getInvocationId()); + parent.addChild(newNode); + newNode.parent = parent; + } else { + callstack.add(newNode); } currentMethod.set(newNode); - return newNode; + return methodInvocation; } @SuppressWarnings("unused") - public static void stopJdbc() { - MethodNode current = currentMethod.get(); - if (Util.isFirstExecutionStarted() && current != null) { - stop(); + public static void stopJdbc(MethodInvocation queryInvocation) { + if (Util.isFirstExecutionStarted() && queryInvocation != null) { + stop(queryInvocation); Util.endExecution(); } } @SuppressWarnings("unused") - public static void stop() { - MethodNode current = currentMethod.get(); - if (current != null) { - current.registerEndingTime(System.nanoTime()); - queue.add(current); - currentMethod.set(current.getParent()); + public static void stop(MethodInvocation methodInvocation) { + if (methodInvocation != null) { + methodInvocation.registerEndingTime(System.nanoTime()); } + MethodNode methodNode = currentMethod.get(); + methodNode.setInvocation(methodInvocation); + + currentMethod.set(methodNode.parent); } - private static void store(MethodNode methodNode) { - try { - PreparedStatement statement = connection.prepareStatement(INSERT_REPORT); - statement.setString(1, methodNode.getThreadName()); - statement.setLong(2, methodNode.getId()); - statement.setLong(3, methodNode.getParentId()); - statement.setLong(4, methodNode.getInvocationId()); - statement.setLong(5, methodNode.getTimestamp()); - statement.setString(6, methodNode.getName()); - statement.setLong(7, methodNode.getDuration()); - statement.executeUpdate(); - statement.close(); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } + public static SortedMap sortedMethodsByDuration() { + //walk the stack to group methods by their name + Map> methods = new ConcurrentHashMap<>(); + collectInvocationsPerMethodName(methods, callstack); + + //gather invocations by method name and calculate statistics + SortedMap sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); + methods.forEach((name, measurements) -> { + long totalDuration = measurements.stream() + .filter(Objects::nonNull) + .mapToLong(MethodInvocation::getDuration).sum(); + sortedByTotal.put(totalDuration, new Report(name, measurements.size(), totalDuration)); + }); + return sortedByTotal; + } + + private static void collectInvocationsPerMethodName(Map> invocations, List nodes) { + nodes.forEach(methodNode -> { + invocations.computeIfAbsent(methodNode.getName(), key -> new ArrayList<>()).add(methodNode.getInvocation()); + collectInvocationsPerMethodName(invocations, methodNode.children); + }); + + } + + public static List getCallStack() { + return callstack; + } + + public static void clear() { + callstack.clear(); } } diff --git a/src/main/java/perfix/instrument/ClassInstrumentor.java b/src/main/java/perfix/instrument/ClassInstrumentor.java index 7d2da6f..0aa361d 100644 --- a/src/main/java/perfix/instrument/ClassInstrumentor.java +++ b/src/main/java/perfix/instrument/ClassInstrumentor.java @@ -24,6 +24,7 @@ public class ClassInstrumentor extends Instrumentor { ClassInstrumentor(List includes, ClassPool classPool) { super(includes, classPool); try { + perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS); stringClass = classpool.get(JAVA_STRING); } catch (NotFoundException e) { diff --git a/src/main/java/perfix/instrument/Instrumentor.java b/src/main/java/perfix/instrument/Instrumentor.java index 3836187..0b03be7 100644 --- a/src/main/java/perfix/instrument/Instrumentor.java +++ b/src/main/java/perfix/instrument/Instrumentor.java @@ -9,6 +9,7 @@ import java.util.List; public abstract class Instrumentor { static final String JAVA_STRING = "java.lang.String"; static final String JAVA_HASHMAP = "java.util.HashMap"; + static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation"; static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1"; static final String JAVASSIST_RETURNVALUE = "$_"; @@ -16,12 +17,14 @@ public abstract class Instrumentor { final List includes; protected CtClass stringClass; protected CtClass hashMapClass; + protected CtClass perfixMethodInvocationClass; Instrumentor(List includes, ClassPool classPool) { this.includes = includes; this.classpool = classPool; try { + perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS); stringClass = classpool.get(JAVA_STRING); hashMapClass = classPool.get(JAVA_HASHMAP); @@ -49,8 +52,9 @@ public abstract class Instrumentor { /* record times at beginning and end of method body*/ void instrumentMethod(CtMethod methodToinstrument, String metricName) { try { - methodToinstrument.insertBefore("perfix.Registry.start(" + metricName + ");"); - methodToinstrument.insertAfter("perfix.Registry.stop();"); + methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); + methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");"); + methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);"); } catch (CannotCompileException e) { throw new RuntimeException(e); } @@ -62,8 +66,9 @@ public abstract class Instrumentor { * (measured) calls if not handled in a way to prevent this */ void instrumentJdbcCall(CtMethod methodToinstrument, String metricName) { try { - methodToinstrument.insertBefore("perfix.Registry.startJdbc(" + metricName + ");"); - methodToinstrument.insertAfter("perfix.Registry.stopJdbc();"); + methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); + methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(" + metricName + ");"); + methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);"); } catch (CannotCompileException e) { throw new RuntimeException(e); } @@ -72,8 +77,9 @@ public abstract class Instrumentor { /* record times at beginning and end of method body*/ void instrumentJdbcCall(CtMethod methodToinstrument) { try { - methodToinstrument.insertBefore("perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));"); - methodToinstrument.insertAfter("perfix.Registry.stopJdbc();"); + methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); + methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));"); + methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);"); } catch (CannotCompileException e) { throw new RuntimeException(e); } diff --git a/src/main/java/perfix/instrument/JdbcInstrumentor.java b/src/main/java/perfix/instrument/JdbcInstrumentor.java index fd3d6f3..0ed5771 100644 --- a/src/main/java/perfix/instrument/JdbcInstrumentor.java +++ b/src/main/java/perfix/instrument/JdbcInstrumentor.java @@ -28,7 +28,7 @@ public class JdbcInstrumentor extends Instrumentor { try { preparedStatementInterface.getDeclaredMethod("setSqlForPerfix"); } catch (NotFoundException e1) { -// e1.printStackTrace(); + e1.printStackTrace(); try { CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, "setSqlForPerfix", new CtClass[]{stringClass}, preparedStatementInterface); preparedStatementInterface.addMethod(setSqlForPerfix); @@ -94,7 +94,7 @@ public class JdbcInstrumentor extends Instrumentor { getDeclaredMethods(preparedStatementClass, "setString", "setObject", "setDate", "setTime", "setTimestamp") .forEach(method -> { try { - method.insertBefore("perfix.instrument.StatementText.set(_perfixSqlStatement,$1, \"'\"+$2+\"'\");"); + method.insertBefore("perfix.instrument.StatementText.set(_perfixSqlStatement,$1, \"\'\"+$2+\"\'\");"); } catch (CannotCompileException e) { throw new RuntimeException(e); } @@ -161,7 +161,7 @@ public class JdbcInstrumentor extends Instrumentor { } - private void addPerfixFields(CtClass preparedStatementClass) throws CannotCompileException { + private void addPerfixFields(CtClass preparedStatementClass) throws CannotCompileException, NotFoundException { // add a String field that will contain the statement CtField perfixSqlField = new CtField(statementTextClass, "_perfixSqlStatement", preparedStatementClass); perfixSqlField.setModifiers(Modifier.PRIVATE); diff --git a/src/main/java/perfix/instrument/ServletInstrumentor.java b/src/main/java/perfix/instrument/ServletInstrumentor.java index 969c203..35a8f98 100644 --- a/src/main/java/perfix/instrument/ServletInstrumentor.java +++ b/src/main/java/perfix/instrument/ServletInstrumentor.java @@ -26,10 +26,11 @@ public class ServletInstrumentor extends Instrumentor { try { stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> { try { - methodToInstrument.insertBefore("perfix.Registry.start($1.getRequestURI());"); - methodToInstrument.insertAfter("perfix.Registry.stop();"); + methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); + methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());"); + methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);"); } catch (CannotCompileException e) { - // ignore and return uninstrumented bytecode + } }); return bytecode(classToInstrument); diff --git a/src/main/java/perfix/server/HTTPServer.java b/src/main/java/perfix/server/HTTPServer.java new file mode 100644 index 0000000..fa21ddf --- /dev/null +++ b/src/main/java/perfix/server/HTTPServer.java @@ -0,0 +1,76 @@ +package perfix.server; + +import fi.iki.elonen.NanoHTTPD; +import perfix.Registry; +import perfix.server.json.Serializer; + +import java.io.IOException; +import java.util.ArrayList; + +public class HTTPServer extends NanoHTTPD { + + + public HTTPServer(int port) { + super(port); + } + + public void start() { + + try { + start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); + System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + getListeningPort() + "/"); + } catch (IOException ioe) { + System.err.println(" --- Couldn't start Perfix http server:\n" + ioe); + } + } + + @Override + public Response serve(IHTTPSession session) { + String uri = session.getUri(); + switch (uri) { + case "/report": + return perfixMetrics(); + case "/callstack": + return perfixCallstack(); + case "/clear": + return clear(); + default: + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "NOT FOUND"); + } + } + + private Response perfixMetrics() { + try { + return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(new ArrayList<>(Registry.sortedMethodsByDuration().values())))); + } catch (Exception e) { + e.printStackTrace(); + return newFixedLengthResponse(e.toString()); + } + } + + private Response addCors(Response response) { + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + return response; + } + + private Response perfixCallstack() { + try { + return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack()))); + } catch (Exception e) { + e.printStackTrace(); + return newFixedLengthResponse(e.toString()); + } + } + + private Response clear() { + Registry.clear(); + try { + return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack()))); + } catch (Exception e) { + e.printStackTrace(); + return newFixedLengthResponse(e.toString()); + } + } + +} diff --git a/src/main/java/perfix/server/json/JSONSerializer.java b/src/main/java/perfix/server/json/JSONSerializer.java new file mode 100644 index 0000000..7d11d1a --- /dev/null +++ b/src/main/java/perfix/server/json/JSONSerializer.java @@ -0,0 +1,21 @@ +package perfix.server.json; + +import java.util.Formatter; + +public abstract class JSONSerializer { + protected abstract String handle(T object); + + protected Formatter formatter = new Formatter(); + + public String toJSONString(T object) { + if (object == null) { + return ""; + } else if (object instanceof Number || object instanceof Boolean) { + return "" + object.toString(); + } else if (object instanceof CharSequence || object instanceof Character) { + return "\"" + object.toString() + "\""; + } else { + return handle(object); + } + } +} diff --git a/src/main/java/perfix/server/json/Serializer.java b/src/main/java/perfix/server/json/Serializer.java new file mode 100644 index 0000000..fa4f42a --- /dev/null +++ b/src/main/java/perfix/server/json/Serializer.java @@ -0,0 +1,45 @@ +package perfix.server.json; + +public class Serializer { + private static SerializerFactory instance = new SynthSerializerFactory(); + + public static String toJSONString(boolean b) { + return Boolean.toString(b); + } + + public static String toJSONString(short s) { + return Short.toString(s); + } + + public static String toJSONString(int i) { + return Integer.toString(i); + } + + public static String toJSONString(float f) { + return Float.toString(f); + } + + public static String toJSONString(double d) { + return Double.toString(d); + } + + public static String toJSONString(long l) { + return Long.toString(l); + } + + public static String toJSONString(char c) { + return "\"" + Character.toString(c) + "\""; + } + + @SuppressWarnings("unchecked") + public static String toJSONString(T o) { + if (o == null) { + return "null"; + } + return instance.createSerializer((Class) o.getClass()).toJSONString(o); + } + + public static void setInstance(SerializerFactory instance) { + Serializer.instance = instance; + } +} diff --git a/src/main/java/perfix/server/json/SerializerCreationException.java b/src/main/java/perfix/server/json/SerializerCreationException.java new file mode 100644 index 0000000..1743429 --- /dev/null +++ b/src/main/java/perfix/server/json/SerializerCreationException.java @@ -0,0 +1,10 @@ +package perfix.server.json; + +@SuppressWarnings("serial") +public class SerializerCreationException extends RuntimeException { + + public SerializerCreationException(Throwable t) { + super(t); + } + +} diff --git a/src/main/java/perfix/server/json/SerializerFactory.java b/src/main/java/perfix/server/json/SerializerFactory.java new file mode 100644 index 0000000..9e2346c --- /dev/null +++ b/src/main/java/perfix/server/json/SerializerFactory.java @@ -0,0 +1,5 @@ +package perfix.server.json; + +public interface SerializerFactory { + public JSONSerializer createSerializer(Class beanjavaClass); +} diff --git a/src/main/java/perfix/server/json/SynthSerializerFactory.java b/src/main/java/perfix/server/json/SynthSerializerFactory.java new file mode 100644 index 0000000..880acbf --- /dev/null +++ b/src/main/java/perfix/server/json/SynthSerializerFactory.java @@ -0,0 +1,350 @@ +package perfix.server.json; + +import javassist.*; + +import java.util.*; + +import static java.util.Arrays.asList; + +public class SynthSerializerFactory implements SerializerFactory { + private static final String STRING = "java.lang.String"; + private static final String BOOLEAN = "java.lang.Boolean"; + private static final String CHARACTER = "java.lang.Character"; + private static final String BYTE = "java.lang.Byte"; + private static final String DOUBLE = "java.lang.Double"; + private static final String FLOAT = "java.lang.Float"; + private static final String LONG = "java.lang.Long"; + private static final String SHORT = "java.lang.Short"; + private static final String INTEGER = "java.lang.Integer"; + + private final static Set wrappersAndString = new HashSet(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER, + STRING)); + + private static final String COLLECTION = "java.util.Collection"; + private static final String LIST = "java.util.List"; + private static final String SET = "java.util.Set"; + private static final List mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap"); + + private static final Map> serializers = new HashMap<>(); + private static final String ROOT_PACKAGE = "serializer."; + + private final ClassPool pool = ClassPool.getDefault(); + private final Map primitiveWrappers = new HashMap(); + private CtClass serializerBase; + + SynthSerializerFactory() { + init(); + } + + private static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) { + return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName()); + } + + void init() { + try { + serializerBase = pool.get(JSONSerializer.class.getName()); + + primitiveWrappers.put("int", pool.get(INTEGER)); + primitiveWrappers.put("short", pool.get(SHORT)); + primitiveWrappers.put("byte", pool.get(BYTE)); + primitiveWrappers.put("long", pool.get(LONG)); + primitiveWrappers.put("float", pool.get(FLOAT)); + primitiveWrappers.put("double", pool.get(DOUBLE)); + primitiveWrappers.put("boolean", pool.get(BOOLEAN)); + primitiveWrappers.put("char", pool.get(CHARACTER)); + } catch (NotFoundException e) { + throw new SerializerCreationException(e); + } + } + + public JSONSerializer createSerializer(Class beanjavaClass) { + try { + CtClass beanClass = pool.get(beanjavaClass.getName()); + + return createSerializer(beanClass); + } catch (NotFoundException e) { + throw new SerializerCreationException(e); + } + } + + @SuppressWarnings("unchecked") + private JSONSerializer createSerializer(CtClass beanClass) { + if (serializers.containsKey(createSerializerName(beanClass))) { + return (JSONSerializer) serializers.get(createSerializerName(beanClass)); + } + try { + return tryCreateSerializer(beanClass); + } catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException e) { + throw new SerializerCreationException(e); + } + } + + private JSONSerializer tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException, + IllegalAccessException { + CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase); + + addToJsonStringMethod(beanClass, serializerClass); + + JSONSerializer jsonSerializer = createSerializerInstance(serializerClass); + + serializers.put(createSerializerName(beanClass), jsonSerializer); + return jsonSerializer; + } + + /* + * create method source, compile it and add it to the class under construction + */ + private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws NotFoundException, CannotCompileException { + String body = createToJSONStringMethodSource(beanClass); + serializerClass.addMethod(CtNewMethod.make(body, serializerClass)); + } + + /* + * Creates the source, handling the for JSON different types of classes + */ + private String createToJSONStringMethodSource(CtClass beanClass) throws NotFoundException { + String source = "public String handle(Object object){\n"; + if (beanClass.isArray()) { + source += "\tObject[] array=(Object[])object;\n"; + source += handleArray(beanClass); + } else if (isCollection(beanClass)) { + source += "\tObject[] array=((java.util.Collection)object).toArray();\n"; + source += handleArray(beanClass); + } else if (isMap(beanClass)) { + source += handleMap(beanClass); + } else if (!isPrimitiveOrWrapperOrString(beanClass)) { + List getters = getGetters(beanClass); + if (shouldAddGetterCallers(getters)) { + source = addGetterCallers(beanClass, source, getters); + } + } else { + source += "\treturn \"\";}"; + } + return source; + } + + /* + * Any Collection is converted to an array, after which code is generated to handle the single elements. + * + * A subserializer is created for every single element, but most of the time it will be the same cached instance. + * + * The generated code fills a StringBuilder. The values are generated by the subserializers + */ + private String handleArray(CtClass beanClass) { + String source = "\tjava.util.StringJoiner result=new java.util.StringJoiner(\",\",\"[\",\"]\");\n"; + source += "\tfor (int i=0; i getters) throws NotFoundException { + int index = 0; + source += "\treturn "; + source += "\"{"; + for (CtMethod getter : getters) { + source = addPair(beanClass, source, getter); + if (index++ < getters.size() - 1) { + source += ","; + } + } + source += "}\";\n}"; + return source; + } + + @SuppressWarnings("unchecked") + private JSONSerializer createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException, + CannotCompileException { + return (JSONSerializer) serializerClass.toClass().newInstance(); + } + + /* + * custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes + * + * Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect + */ + public String createSerializerName(CtClass beanClass) { + return createSerializerName(beanClass.getName()); + } + + public String createSerializerName(String name) { + return ROOT_PACKAGE + name.replaceAll("\\[\\]", "Array") + "Serializer"; + } + + private boolean isCollection(CtClass beanClass) throws NotFoundException { + List interfaces = new ArrayList<>(asList(beanClass.getInterfaces())); + interfaces.add(beanClass); + boolean is = interfaces.stream().anyMatch(interfaze -> interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET)); + return is; + } + + private boolean isMap(CtClass beanClass) throws NotFoundException { + List interfaces = new ArrayList<>(asList(beanClass.getInterfaces())); + interfaces.add(beanClass); + return interfaces.stream().anyMatch(i -> mapInterfaces.contains(i.getName())); + } + + /* + * The JSON vernacular for key:value is pair... + */ + private String addPair(CtClass classToSerialize, String source, CtMethod getter) throws NotFoundException { + source += jsonKey(getter); + source += ":"; + source += jsonValue(classToSerialize, getter); + return source; + } + + /* + * derive property key from getter + */ + private String jsonKey(CtMethod getter) { + return "\\\"" + toFieldName(getter.getName()) + "\\\""; + } + + private String jsonValue(CtClass classToSerialize, CtMethod getter) throws NotFoundException { + String source = ""; + CtClass returnType = getter.getReturnType(); + + /* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */ + source = createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter, source, returnType); + + return source; + } + + private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod getter, String source, CtClass returnType) { + /* NB there does not seem to be auto(un))boxing nor generic types (or other jdk1.5 stuff) in javassist compileable code */ + + source += "\"+" + Serializer.class.getName() + ".toJSONString("; + + // cast because of lack of generics + source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()"; + + source += ")+\""; + return source; + } + + /* + * turns for example 'getValue' into 'value' + */ + private String toFieldName(String name) { + return name.substring(3, 4).toLowerCase() + (name.length() > 4 ? name.substring(4) : ""); + } + + public String regularClassname(String name) { + return name.replaceAll("\\$", "."); + } + + private String cast(String classToSerialize) { + return "(" + classToSerialize + ")"; + } + + /* + * Retrieves getter methods from a class + */ + private List getGetters(CtClass beanClass) { + List methods = new ArrayList(); + List fields = getAllFields(beanClass); + for (CtField field : fields) { + try { + CtMethod method = beanClass.getMethod(getGetterMethod(field), getDescription(field)); + if (Modifier.isPublic(method.getModifiers())) { + methods.add(method); + } + } catch (NotFoundException n) { + // ignore + } + } + return methods; + } + + private String getGetterMethod(CtField field) { + return "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); + } + + private List getAllFields(CtClass beanClass) { + try { + List allfields = new ArrayList<>(); + for (CtField field : beanClass.getDeclaredFields()) { + allfields.add(field); + } + if (beanClass.getSuperclass() != null) { + return getAllFields(beanClass.getSuperclass(), allfields); + } + return allfields; + } catch (NotFoundException e) { + throw new SerializerCreationException(e); + } + + } + + private List getAllFields(CtClass beanClass, List allfields) { + for (CtField field : beanClass.getDeclaredFields()) { + allfields.add(field); + } + + return allfields; + } + + /* + * is getter list is not empty then callers should be added + */ + boolean shouldAddGetterCallers(List getters) { + return !getters.isEmpty(); + } + + String getDescription(CtField field) throws NotFoundException { + if (field.getType().isArray()) { + return "()[" + innerClassName(field.getType().getName()) + ";"; + } else if (!field.getType().isPrimitive()) { + return "()" + innerClassName(field.getType().getName()) + ";"; + } else { + + return "()" + asPrimitive(field.getType().getName()); + } + } + + String asPrimitive(String name) { + switch (name) { + case "int": + return "I"; + case "byte": + return "B"; + case "float": + return "F"; + case "long": + return "J"; + case "boolean": + return "Z"; + case "char": + return "C"; + case "double": + return "D"; + case "short": + return "S"; + } + return ""; + } + + String innerClassName(String name) { + return "L" + name.replaceAll("\\.", "/").replaceAll("\\[\\]", ""); + } +}