diff --git a/.gitignore b/.gitignore index af9d3d9..74d159f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .idea/ *.iml target/ -ui/node_modules \ No newline at end of file +ui/node_modules +*.db \ No newline at end of file diff --git a/pom.xml b/pom.xml index 689ada8..85c5e77 100644 --- a/pom.xml +++ b/pom.xml @@ -21,9 +21,9 @@ - org.nanohttpd - nanohttpd - 2.3.1 + org.xerial + sqlite-jdbc + 3.28.0 diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java index 55a20ca..ba8bafd 100644 --- a/src/main/java/perfix/Agent.java +++ b/src/main/java/perfix/Agent.java @@ -1,7 +1,6 @@ package perfix; import perfix.instrument.Instrumentor; -import perfix.server.HTTPServer; import java.lang.instrument.Instrumentation; import java.util.ArrayList; @@ -9,33 +8,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); - - int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT)); + String dbFile = System.getProperty(DBFILE_PROPERTY); + if (dbFile == null) { + dbFile = DEFAULT_DBFILE; + } + System.out.println(" --- SQLite file written to " + dbFile); 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 deleted file mode 100644 index 92e4588..0000000 --- a/src/main/java/perfix/MethodInvocation.java +++ /dev/null @@ -1,22 +0,0 @@ -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 5be85f6..6ed82e1 100644 --- a/src/main/java/perfix/MethodNode.java +++ b/src/main/java/perfix/MethodNode.java @@ -1,30 +1,29 @@ package perfix; -import java.util.ArrayList; -import java.util.List; + import java.util.Objects; public class MethodNode { - public final String name; - public final List children; - public MethodNode parent; - private MethodInvocation invocation; + private final String name; + private final long timestamp; + private final String threadName; + private MethodNode parent; + private long duration; + private long invocationid; + public MethodNode(String name) { this.name = name; - this.children = new ArrayList<>(); - } - - public void addChild(MethodNode child) { - children.add(child); + this.timestamp = System.nanoTime(); + this.threadName = Thread.currentThread().getName(); } public String getName() { return name; } - public List getChildren() { - return children; + public long getId() { + return Objects.hash(Thread.currentThread().getId(), timestamp); } @Override @@ -47,11 +46,47 @@ public class MethodNode { return Objects.hash(name); } - public MethodInvocation getInvocation() { - return invocation; + public long getParentId() { + if (parent == null) { + return 0; + } else { + return parent.getId(); + } } - public void setInvocation(MethodInvocation invocation) { - this.invocation = 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(); + } } } diff --git a/src/main/java/perfix/Registry.java b/src/main/java/perfix/Registry.java index b074ea0..73ddd83 100644 --- a/src/main/java/perfix/Registry.java +++ b/src/main/java/perfix/Registry.java @@ -2,17 +2,65 @@ package perfix; import perfix.instrument.Util; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListMap; +import java.sql.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +/** + * 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';"; - @SuppressWarnings("unused") //used in generated code - public static MethodInvocation startJdbc(String name) { + 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) { if (!Util.isFirstExecutionStarted()) { Util.startExecution(); return start(name); @@ -21,72 +69,54 @@ public class Registry { } } - @SuppressWarnings("unused") - public static MethodInvocation start(String name) { - MethodInvocation methodInvocation = new MethodInvocation(name); + public static MethodNode start(String name) { MethodNode newNode = new MethodNode(name); MethodNode parent = currentMethod.get(); if (parent != null) { - parent.addChild(newNode); - newNode.parent = parent; - } else { - callstack.add(newNode); + newNode.setParent(parent); + newNode.setInvocationId(parent.getInvocationId()); } currentMethod.set(newNode); - return methodInvocation; + return newNode; } @SuppressWarnings("unused") - public static void stopJdbc(MethodInvocation queryInvocation) { - if (Util.isFirstExecutionStarted() && queryInvocation != null) { - stop(queryInvocation); + public static void stopJdbc() { + MethodNode current = currentMethod.get(); + if (Util.isFirstExecutionStarted() && current != null) { + stop(); Util.endExecution(); } } @SuppressWarnings("unused") - public static void stop(MethodInvocation methodInvocation) { - if (methodInvocation != null) { - methodInvocation.registerEndingTime(System.nanoTime()); + public static void stop() { + MethodNode current = currentMethod.get(); + if (current != null) { + current.registerEndingTime(System.nanoTime()); + queue.add(current); + currentMethod.set(current.getParent()); } - MethodNode methodNode = currentMethod.get(); - methodNode.setInvocation(methodInvocation); - - currentMethod.set(methodNode.parent); } - 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(); + 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); + } } } diff --git a/src/main/java/perfix/instrument/ClassInstrumentor.java b/src/main/java/perfix/instrument/ClassInstrumentor.java index 0aa361d..7d2da6f 100644 --- a/src/main/java/perfix/instrument/ClassInstrumentor.java +++ b/src/main/java/perfix/instrument/ClassInstrumentor.java @@ -24,7 +24,6 @@ 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 0b03be7..3836187 100644 --- a/src/main/java/perfix/instrument/Instrumentor.java +++ b/src/main/java/perfix/instrument/Instrumentor.java @@ -9,7 +9,6 @@ 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 = "$_"; @@ -17,14 +16,12 @@ 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); @@ -52,9 +49,8 @@ public abstract class Instrumentor { /* record times at beginning and end of method body*/ void instrumentMethod(CtMethod methodToinstrument, String metricName) { try { - methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); - methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");"); - methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);"); + methodToinstrument.insertBefore("perfix.Registry.start(" + metricName + ");"); + methodToinstrument.insertAfter("perfix.Registry.stop();"); } catch (CannotCompileException e) { throw new RuntimeException(e); } @@ -66,9 +62,8 @@ public abstract class Instrumentor { * (measured) calls if not handled in a way to prevent this */ void instrumentJdbcCall(CtMethod methodToinstrument, String metricName) { try { - methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); - methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(" + metricName + ");"); - methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);"); + methodToinstrument.insertBefore("perfix.Registry.startJdbc(" + metricName + ");"); + methodToinstrument.insertAfter("perfix.Registry.stopJdbc();"); } catch (CannotCompileException e) { throw new RuntimeException(e); } @@ -77,9 +72,8 @@ public abstract class Instrumentor { /* record times at beginning and end of method body*/ void instrumentJdbcCall(CtMethod methodToinstrument) { try { - methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); - methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));"); - methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);"); + methodToinstrument.insertBefore("perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));"); + methodToinstrument.insertAfter("perfix.Registry.stopJdbc();"); } 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 77d385a..f7eae88 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, NotFoundException { + private void addPerfixFields(CtClass preparedStatementClass) throws CannotCompileException { // 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 35a8f98..969c203 100644 --- a/src/main/java/perfix/instrument/ServletInstrumentor.java +++ b/src/main/java/perfix/instrument/ServletInstrumentor.java @@ -26,11 +26,10 @@ public class ServletInstrumentor extends Instrumentor { try { stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> { try { - methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); - methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());"); - methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);"); + methodToInstrument.insertBefore("perfix.Registry.start($1.getRequestURI());"); + methodToInstrument.insertAfter("perfix.Registry.stop();"); } 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 deleted file mode 100644 index fa21ddf..0000000 --- a/src/main/java/perfix/server/HTTPServer.java +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 7d11d1a..0000000 --- a/src/main/java/perfix/server/json/JSONSerializer.java +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index fa4f42a..0000000 --- a/src/main/java/perfix/server/json/Serializer.java +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 1743429..0000000 --- a/src/main/java/perfix/server/json/SerializerCreationException.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 9e2346c..0000000 --- a/src/main/java/perfix/server/json/SerializerFactory.java +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 880acbf..0000000 --- a/src/main/java/perfix/server/json/SynthSerializerFactory.java +++ /dev/null @@ -1,350 +0,0 @@ -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("\\[\\]", ""); - } -}