* removed server instance

* agent now stores data in sqllite table (file)
* other small improvements
This commit is contained in:
Sander Hautvast 2019-12-19 14:36:27 +01:00
parent 2070a6328a
commit a6f481fc43
16 changed files with 163 additions and 635 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
*.iml *.iml
target/ target/
ui/node_modules ui/node_modules
*.db

View file

@ -21,9 +21,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nanohttpd</groupId> <groupId>org.xerial</groupId>
<artifactId>nanohttpd</artifactId> <artifactId>sqlite-jdbc</artifactId>
<version>2.3.1</version> <version>3.28.0</version>
</dependency> </dependency>
<dependency> <dependency>

View file

@ -1,7 +1,6 @@
package perfix; package perfix;
import perfix.instrument.Instrumentor; import perfix.instrument.Instrumentor;
import perfix.server.HTTPServer;
import java.lang.instrument.Instrumentation; import java.lang.instrument.Instrumentation;
import java.util.ArrayList; import java.util.ArrayList;
@ -9,25 +8,24 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
public class Agent { 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 PORT_PROPERTY = "perfix.port";
private static final String INCLUDES_PROPERTY = "perfix.includes"; private static final String INCLUDES_PROPERTY = "perfix.includes";
private static final String DEFAULT_PORT = "2048";
private static final String MESSAGE = " --- Perfix agent active"; private static final String MESSAGE = " --- Perfix agent active";
public static void premain(String agentArgs, Instrumentation instrumentation) { public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println(MESSAGE); System.out.println(MESSAGE);
String dbFile = System.getProperty(DBFILE_PROPERTY);
int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT)); if (dbFile == null) {
dbFile = DEFAULT_DBFILE;
}
System.out.println(" --- SQLite file written to " + dbFile);
Instrumentor.create(determineIncludes()).instrumentCode(instrumentation); Instrumentor.create(determineIncludes()).instrumentCode(instrumentation);
new HTTPServer(port).start();
} }
private static List<String> determineIncludes() { private static List<String> determineIncludes() {
@ -36,6 +34,7 @@ public class Agent {
System.out.println("WARNING: perfix.includes not set "); System.out.println("WARNING: perfix.includes not set ");
return Collections.emptyList(); return Collections.emptyList();
} }
System.out.println(" --- Instrumenting packages: " + includesPropertyValue + ".*");
return new ArrayList<>(asList(includesPropertyValue.split(","))); return new ArrayList<>(asList(includesPropertyValue.split(",")));
} }
} }

View file

@ -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;
}
}

View file

@ -1,30 +1,29 @@
package perfix; package perfix;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
public class MethodNode { public class MethodNode {
public final String name; private final String name;
public final List<MethodNode> children; private final long timestamp;
public MethodNode parent; private final String threadName;
private MethodInvocation invocation; private MethodNode parent;
private long duration;
private long invocationid;
public MethodNode(String name) { public MethodNode(String name) {
this.name = name; this.name = name;
this.children = new ArrayList<>(); this.timestamp = System.nanoTime();
} this.threadName = Thread.currentThread().getName();
public void addChild(MethodNode child) {
children.add(child);
} }
public String getName() { public String getName() {
return name; return name;
} }
public List<MethodNode> getChildren() { public long getId() {
return children; return Objects.hash(Thread.currentThread().getId(), timestamp);
} }
@Override @Override
@ -47,11 +46,47 @@ public class MethodNode {
return Objects.hash(name); return Objects.hash(name);
} }
public MethodInvocation getInvocation() { public long getParentId() {
return invocation; if (parent == null) {
return 0;
} else {
return parent.getId();
}
} }
public void setInvocation(MethodInvocation invocation) { public long getDuration() {
this.invocation = invocation; 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();
}
} }
} }

View file

@ -2,17 +2,65 @@ package perfix;
import perfix.instrument.Util; import perfix.instrument.Util;
import java.util.*; import java.sql.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentSkipListMap; 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 { public class Registry {
private static final List<MethodNode> callstack = new ArrayList<>();
private static final ThreadLocal<MethodNode> currentMethod = new ThreadLocal<>(); private static final ThreadLocal<MethodNode> 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 private static BlockingQueue<MethodNode> queue = new LinkedBlockingQueue<>();
public static MethodInvocation startJdbc(String name) { 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()) { if (!Util.isFirstExecutionStarted()) {
Util.startExecution(); Util.startExecution();
return start(name); return start(name);
@ -21,72 +69,54 @@ public class Registry {
} }
} }
@SuppressWarnings("unused") public static MethodNode start(String name) {
public static MethodInvocation start(String name) {
MethodInvocation methodInvocation = new MethodInvocation(name);
MethodNode newNode = new MethodNode(name); MethodNode newNode = new MethodNode(name);
MethodNode parent = currentMethod.get(); MethodNode parent = currentMethod.get();
if (parent != null) { if (parent != null) {
parent.addChild(newNode); newNode.setParent(parent);
newNode.parent = parent; newNode.setInvocationId(parent.getInvocationId());
} else {
callstack.add(newNode);
} }
currentMethod.set(newNode); currentMethod.set(newNode);
return methodInvocation; return newNode;
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static void stopJdbc(MethodInvocation queryInvocation) { public static void stopJdbc() {
if (Util.isFirstExecutionStarted() && queryInvocation != null) { MethodNode current = currentMethod.get();
stop(queryInvocation); if (Util.isFirstExecutionStarted() && current != null) {
stop();
Util.endExecution(); Util.endExecution();
} }
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static void stop(MethodInvocation methodInvocation) { public static void stop() {
if (methodInvocation != null) { MethodNode current = currentMethod.get();
methodInvocation.registerEndingTime(System.nanoTime()); if (current != null) {
} current.registerEndingTime(System.nanoTime());
MethodNode methodNode = currentMethod.get(); queue.add(current);
methodNode.setInvocation(methodInvocation); currentMethod.set(current.getParent());
}
currentMethod.set(methodNode.parent); }
}
private static void store(MethodNode methodNode) {
public static SortedMap<Long, Report> sortedMethodsByDuration() { try {
//walk the stack to group methods by their name PreparedStatement statement = connection.prepareStatement(INSERT_REPORT);
Map<String, List<MethodInvocation>> methods = new ConcurrentHashMap<>(); statement.setString(1, methodNode.getThreadName());
collectInvocationsPerMethodName(methods, callstack); statement.setLong(2, methodNode.getId());
statement.setLong(3, methodNode.getParentId());
//gather invocations by method name and calculate statistics statement.setLong(4, methodNode.getInvocationId());
SortedMap<Long, Report> sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); statement.setLong(5, methodNode.getTimestamp());
methods.forEach((name, measurements) -> { statement.setString(6, methodNode.getName());
long totalDuration = measurements.stream() statement.setLong(7, methodNode.getDuration());
.filter(Objects::nonNull) statement.executeUpdate();
.mapToLong(MethodInvocation::getDuration).sum(); statement.close();
sortedByTotal.put(totalDuration, new Report(name, measurements.size(), totalDuration)); } catch (Exception e) {
}); e.printStackTrace();
return sortedByTotal; throw new RuntimeException(e);
} }
private static void collectInvocationsPerMethodName(Map<String, List<MethodInvocation>> invocations, List<MethodNode> nodes) {
nodes.forEach(methodNode -> {
invocations.computeIfAbsent(methodNode.getName(), key -> new ArrayList<>()).add(methodNode.getInvocation());
collectInvocationsPerMethodName(invocations, methodNode.children);
});
}
public static List<MethodNode> getCallStack() {
return callstack;
}
public static void clear() {
callstack.clear();
} }
} }

View file

@ -24,7 +24,6 @@ public class ClassInstrumentor extends Instrumentor {
ClassInstrumentor(List<String> includes, ClassPool classPool) { ClassInstrumentor(List<String> includes, ClassPool classPool) {
super(includes, classPool); super(includes, classPool);
try { try {
perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
stringClass = classpool.get(JAVA_STRING); stringClass = classpool.get(JAVA_STRING);
} catch (NotFoundException e) { } catch (NotFoundException e) {

View file

@ -9,7 +9,6 @@ import java.util.List;
public abstract class Instrumentor { public abstract class Instrumentor {
static final String JAVA_STRING = "java.lang.String"; static final String JAVA_STRING = "java.lang.String";
static final String JAVA_HASHMAP = "java.util.HashMap"; 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_FIRST_ARGUMENT_NAME = "$1";
static final String JAVASSIST_RETURNVALUE = "$_"; static final String JAVASSIST_RETURNVALUE = "$_";
@ -17,14 +16,12 @@ public abstract class Instrumentor {
final List<String> includes; final List<String> includes;
protected CtClass stringClass; protected CtClass stringClass;
protected CtClass hashMapClass; protected CtClass hashMapClass;
protected CtClass perfixMethodInvocationClass;
Instrumentor(List<String> includes, ClassPool classPool) { Instrumentor(List<String> includes, ClassPool classPool) {
this.includes = includes; this.includes = includes;
this.classpool = classPool; this.classpool = classPool;
try { try {
perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
stringClass = classpool.get(JAVA_STRING); stringClass = classpool.get(JAVA_STRING);
hashMapClass = classPool.get(JAVA_HASHMAP); hashMapClass = classPool.get(JAVA_HASHMAP);
@ -52,9 +49,8 @@ public abstract class Instrumentor {
/* record times at beginning and end of method body*/ /* record times at beginning and end of method body*/
void instrumentMethod(CtMethod methodToinstrument, String metricName) { void instrumentMethod(CtMethod methodToinstrument, String metricName) {
try { try {
methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); methodToinstrument.insertBefore("perfix.Registry.start(" + metricName + ");");
methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");"); methodToinstrument.insertAfter("perfix.Registry.stop();");
methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
} catch (CannotCompileException e) { } catch (CannotCompileException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -66,9 +62,8 @@ public abstract class Instrumentor {
* (measured) calls if not handled in a way to prevent this */ * (measured) calls if not handled in a way to prevent this */
void instrumentJdbcCall(CtMethod methodToinstrument, String metricName) { void instrumentJdbcCall(CtMethod methodToinstrument, String metricName) {
try { try {
methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); methodToinstrument.insertBefore("perfix.Registry.startJdbc(" + metricName + ");");
methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(" + metricName + ");"); methodToinstrument.insertAfter("perfix.Registry.stopJdbc();");
methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);");
} catch (CannotCompileException e) { } catch (CannotCompileException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -77,9 +72,8 @@ public abstract class Instrumentor {
/* record times at beginning and end of method body*/ /* record times at beginning and end of method body*/
void instrumentJdbcCall(CtMethod methodToinstrument) { void instrumentJdbcCall(CtMethod methodToinstrument) {
try { try {
methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); methodToinstrument.insertBefore("perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));");
methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.startJdbc(perfix.instrument.StatementText.toString(_perfixSqlStatement));"); methodToinstrument.insertAfter("perfix.Registry.stopJdbc();");
methodToinstrument.insertAfter("perfix.Registry.stopJdbc(_perfixmethod);");
} catch (CannotCompileException e) { } catch (CannotCompileException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -28,7 +28,7 @@ public class JdbcInstrumentor extends Instrumentor {
try { try {
preparedStatementInterface.getDeclaredMethod("setSqlForPerfix"); preparedStatementInterface.getDeclaredMethod("setSqlForPerfix");
} catch (NotFoundException e1) { } catch (NotFoundException e1) {
e1.printStackTrace(); // e1.printStackTrace();
try { try {
CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, "setSqlForPerfix", new CtClass[]{stringClass}, preparedStatementInterface); CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, "setSqlForPerfix", new CtClass[]{stringClass}, preparedStatementInterface);
preparedStatementInterface.addMethod(setSqlForPerfix); preparedStatementInterface.addMethod(setSqlForPerfix);
@ -94,7 +94,7 @@ public class JdbcInstrumentor extends Instrumentor {
getDeclaredMethods(preparedStatementClass, "setString", "setObject", "setDate", "setTime", "setTimestamp") getDeclaredMethods(preparedStatementClass, "setString", "setObject", "setDate", "setTime", "setTimestamp")
.forEach(method -> { .forEach(method -> {
try { try {
method.insertBefore("perfix.instrument.StatementText.set(_perfixSqlStatement,$1, \"\'\"+$2+\"\'\");"); method.insertBefore("perfix.instrument.StatementText.set(_perfixSqlStatement,$1, \"'\"+$2+\"'\");");
} catch (CannotCompileException e) { } catch (CannotCompileException e) {
throw new RuntimeException(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 // add a String field that will contain the statement
CtField perfixSqlField = new CtField(statementTextClass, "_perfixSqlStatement", preparedStatementClass); CtField perfixSqlField = new CtField(statementTextClass, "_perfixSqlStatement", preparedStatementClass);
perfixSqlField.setModifiers(Modifier.PRIVATE); perfixSqlField.setModifiers(Modifier.PRIVATE);

View file

@ -26,11 +26,10 @@ public class ServletInstrumentor extends Instrumentor {
try { try {
stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> { stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> {
try { try {
methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass); methodToInstrument.insertBefore("perfix.Registry.start($1.getRequestURI());");
methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());"); methodToInstrument.insertAfter("perfix.Registry.stop();");
methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
} catch (CannotCompileException e) { } catch (CannotCompileException e) {
// ignore and return uninstrumented bytecode
} }
}); });
return bytecode(classToInstrument); return bytecode(classToInstrument);

View file

@ -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());
}
}
}

View file

@ -1,21 +0,0 @@
package perfix.server.json;
import java.util.Formatter;
public abstract class JSONSerializer<T> {
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);
}
}
}

View file

@ -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 <T> String toJSONString(T o) {
if (o == null) {
return "null";
}
return instance.createSerializer((Class<T>) o.getClass()).toJSONString(o);
}
public static void setInstance(SerializerFactory instance) {
Serializer.instance = instance;
}
}

View file

@ -1,10 +0,0 @@
package perfix.server.json;
@SuppressWarnings("serial")
public class SerializerCreationException extends RuntimeException {
public SerializerCreationException(Throwable t) {
super(t);
}
}

View file

@ -1,5 +0,0 @@
package perfix.server.json;
public interface SerializerFactory {
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass);
}

View file

@ -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<String> wrappersAndString = new HashSet<String>(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<String> mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap");
private static final Map<String, JSONSerializer<?>> serializers = new HashMap<>();
private static final String ROOT_PACKAGE = "serializer.";
private final ClassPool pool = ClassPool.getDefault();
private final Map<String, CtClass> primitiveWrappers = new HashMap<String, CtClass>();
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 <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
try {
CtClass beanClass = pool.get(beanjavaClass.getName());
return createSerializer(beanClass);
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializer(CtClass beanClass) {
if (serializers.containsKey(createSerializerName(beanClass))) {
return (JSONSerializer<T>) serializers.get(createSerializerName(beanClass));
}
try {
return tryCreateSerializer(beanClass);
} catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException e) {
throw new SerializerCreationException(e);
}
}
private <T> JSONSerializer<T> tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
IllegalAccessException {
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
addToJsonStringMethod(beanClass, serializerClass);
JSONSerializer<T> 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<CtMethod> 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<array.length; i++){\n";
source += "\t\tresult.add(" + Serializer.class.getName() + ".toJSONString(array[i]));\n";
source += "\t};\n\treturn result.toString();\n}";
return source;
}
private String handleMap(CtClass beanClass) {
String source = "StringBuilder result=new StringBuilder(\"{\");\n";
source += "\tfor (java.util.Iterator entries=((java.util.Map)object).entrySet().iterator();entries.hasNext();){\n";
source += "\t\tjava.util.Map.Entry entry=(java.util.Map.Entry)entries.next();\n";
source += "\t\tresult.append(\"\\\"\"+entry.getKey().toString()+\"\\\"\");\n";
source += "\t\tresult.append(\": \");\n";
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(entry.getValue()));\n";
source += "\t\tresult.append(\", \");\n";
source += "\t};\n";
source += "\tresult.setLength(result.length()-2);\n";
source += "\tresult.append(\"}\");\n";
source += "\treturn result.toString();\n";
source += "}";
return source;
}
/*
* If the class contains fields for which public getters are available, then these will be called in the generated code.
*/
private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> 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 <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException,
CannotCompileException {
return (JSONSerializer<T>) 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<CtClass> 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<CtClass> 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<CtMethod> getGetters(CtClass beanClass) {
List<CtMethod> methods = new ArrayList<CtMethod>();
List<CtField> 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<CtField> getAllFields(CtClass beanClass) {
try {
List<CtField> 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<CtField> getAllFields(CtClass beanClass, List<CtField> 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<CtMethod> 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("\\[\\]", "");
}
}