概述
源码地址
目前实现的功能
- 无侵入监控jdbc信息
- 无侵入监控Servlet
- 记录堆栈跟踪
- 集成了spring mvc的controller监控
- 分布式调用跟踪(待实现)
使用
拿到源码,打为jar包,在需要被监控的项目中加入启动参数,如下
-javaagent:F:\openSources\dwow\target\java-agent-dwow-1.0-RELEASE-jar-with-dependencies.jar
启动后被监控项目,随机访问,即可看到如下类似监控信息
mysql
preparedStatement statistics:{"success":true,"count":0,"startTime":1601102174682,"id":"-2147483642#1601102174682","endTime":1601102174757,"cosTime":75,"url":"jdbc:mysql://47.96.158.**:**/migration?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8","sql":"SELECT id,last_time,cron,target_name,source_table_name,create_time,target_table_name,source_name,category,select_condition,parent_id FROM job_info_config \n \n WHERE (parent_id IS NULL)"}
servlet
monitor data:{"args":[{"request":{}},{}],"method":"POST","respStatus":400,"bodyData":"{\t\"driverClassName\": \"org.h2.Driver\",\t\"password\": \"\",\t\"poolName\": \"test\",\t\"url\": \"jdbc:h2:mem:test10\",\t\"username\": \"sa\"}","urlData":"{\"a\":\"cc\"}","startTime":1601102274439,"id":"-2147483632#1601102274439","endTime":1601102274502,"cosTime":63,"url":"http://192.168.101.1/datasources/addBasic"}
实现
代码结构
所有监控类均实现Monitor,Monitor接口定义如下:
public interface Monitor {
/**
* 是否是监控目标
*
* @param className 格式com/github/A.class
* @return true 是
*/
boolean isTarget(String className);
/**
* 获取目标方法
*
* @param clz 目标方法所在类
* @param pool poll
* @return 目标方法
*/
CtMethod targetMethod(ClassPool pool,CtClass clz) throws NotFoundException;
/**
* 返回植入方法之前的代码
*
* @param oldMethodName 复制之前的方法名
* @return 返回植入方法之前的代码
*/
MethodInfo getMethodInfo(String oldMethodName);
Statistics begin(Object obj, Object... args);
void exception(Statistics statistics, Throwable t);
/**
* 原方法执行结果
*
* @param statistics 统计类
* @param result 源方法返回值
*/
Object end(Statistics statistics, Object result);
}
Statistics为存储统计信息的一个类,在agent的premain方法被执行时,会进入ClassFileTransformer#transform,在该方法实现的内部会调用每个具体的Monitor实现类的isTarget方法来判断当前加载的class是否需要被监控,如果需要就使用javaassit更改字节码,植入我们的监控逻辑。
ClassFileTransformer实现类的transform方法部分逻辑如下
//1.
Monitor monitor = null;
for (Monitor currentMonitor : monitorList) {
if (currentMonitor.isTarget(className)) {
monitor = currentMonitor;
flag = true;
break;
}
}
String clzName = className.replaceAll("/", ".");
if (flag) {
CtMethod method = monitor.targetMethod(pool, targetClz);
if (Objects.nonNull(method)) {
log.info("target {}#{} use monitor:{}", clzName, method.getName(), monitor.getClass().getName());
//2.
String newMethodName = method.getName() + AGENT_SUFFIX;
log.info("start copy new method : {}", newMethodName);
CtMethod newMethod = CtNewMethod.copy(method, newMethodName, targetClz, null);
targetClz.addMethod(newMethod);
CtClass throwable = pool.get(Throwable.class.getName());
//3.
MethodInfo methodInfo = monitor.getMethodInfo(newMethodName);
if (methodInfo.isNewInfo()) {
method.setBody(methodInfo.getNewBody());
} else {
method.setBody(methodInfo.getTryBody());
method.addCatch(methodInfo.getCatchBody(), throwable);
method.insertAfter(methodInfo.getFinallyBody(), true);
}
log.info("copy method{} end", method.getName());
return targetClz.toBytecode();
}
}
可以看见主要做了以下几件事
-
判断该类是否需要被监控
-
如果需要,则先复制一个方法,方法名为原方法名加上AGENT_SUFFIX组成
-
之后进行监控逻辑植入
-
先忽略if不成功的条件,这是原来遗留的逻辑,是分逻辑片段注入逻辑,即分别注入try代码块、catch代码块、finally代码块。
-
现在的逻辑是走if成立的分支,一次性构造好整个方法体,逻辑如下
public MethodInfo createBody(Monitor monitor, String oldMethodName) { int modifiers = monitor.getClass().getModifiers(); if (Modifier.isInterface(modifiers) || Modifier.isAbstract(modifiers)) { throw new IllegalStateException("The current class cannot be interface and abstract!"); } String clzName = monitor.getClass().getName(); //获取实例 String begin = clzName + " monitor = " + clzName + ".INSTANCE;\n"; String statisticName = Statistics.class.getName(); begin += statisticName + " statistic = monitor.begin($0,$args);"; String exception = "monitor.exception(statistic,t);"; String end = "result = monitor.end(statistic,result);"; String body = String.format(NEW_SOURCE0, begin, oldMethodName, exception, end); System.out.println(body); this.methodInfo.setNewBody(body); this.methodInfo.setNewInfo(true); return this.methodInfo; }
a. 首先是获取监控类实例,如监控类是JdbcMonitor,则获取的begin是com.xx.xx.JdbcMonitor.INSTANCE
b. 之后构造的begin值,就是调用监控类的begin方法逻辑
c. 随后构造异常,调用监控类的异常逻辑
d. end同理。之后再组装为一个大body,NEW_SOURCE0定义如下
private static final String NEW_SOURCE0 = "{\n" + // ThreadLocalUtil.class.getName() + ".set(\"0\");" + "%s\n" + "Object result = null;\n" + "try{\n" + " result=($w)$0.%s($$);\n" + "}catch(java.lang.Throwable t){\n" + " %s\n" + " throw t;\n" + "}finally{\n" + " %s\n" + "}\n" + " return ($r) result;\n" + "}";
当将占位符全部替换成功之后,整个body代码块的内容如下,以JdbcMonitor为例
{ com.github.mrlawrenc.attach.monitor.impl.JdbcMonitor monitor = com.github.mrlawrenc.attach.monitor.impl.JdbcMonitor.INSTANCE; com.github.mrlawrenc.attach.statistics.Statistics statistic = monitor.begin($0,$args); Object result = null; try{ //这个会调用原方法 result=($w)$0.connect$lawrence($$); }catch(java.lang.Throwable t){ monitor.exception(statistic,t); throw t; }finally{ result = monitor.end(statistic,result); } return ($r) result; }
可以发现,当调用if里的逻辑时,原方法的方法体已经被完全替换了,植入我们的监控逻辑。
在原方法调用时,第一步会调用我们监控方法的begin方法,之后再执行我们复制的原方法,这会返回原始结果,最终finally里面再传入原始结果,调用我们的end方法,这样就对我们原始方法进行了加强插桩,可以在各阶段掌握方法执行情况,出了异常我们监控方法也会调用监控的exception方法,也就会感知到异常。
-
Jdbc监控
由于接入java的mysql驱动是遵循jdbc规范,因此我们插桩到jdbc最上层的方法即可监控到所有的jdbc操作。
JDBCMonitor的isTarget方法如下,可以发现监控的类是com.mysql.cj.jdbc.NonRegisteringDriver
private static final String TARGET_CLZ = "com.mysql.cj.jdbc.NonRegisteringDriver";
@Override
public boolean isTarget(String className) {
return TARGET_CLZ.equals(className.replace("/", "."));
}
监控NonRegisteringDriver#connect,该方法会生成一个Connection连接对象
@Override
public CtMethod targetMethod(ClassPool pool, CtClass clz) throws NotFoundException {
return clz.getMethod("connect", "(Ljava/lang/String;Ljava/util/Properties;)Ljava/sql/Connection;");
}
因此我们对生成的连接对象进行代理,然后将我们的代理对象返回,JDBCMonitor的end方法如下
@Override
public Object end(Statistics current, Object obj) {
Object result = obj;
if (Objects.nonNull(obj) && obj instanceof Connection) {
current.setOldResult(obj);
result = proxyConnection((Connection) current.getOldResult());
current.setNewResult(result);
}
current.setEndTime(System.currentTimeMillis());
log.info("statistics:{}", JSONUtil.toJsonStr(current));
return result;
}
生成连接的代理对象如下
private static final String[] PROXY_CONNECTION_METHOD = new String[]{"prepareStatement"};
private static final String[] STATEMENT_METHOD = new String[]{"executeUpdate", "execute", "executeQuery", "getResultSet"};
public Connection proxyConnection(Connection connection) {
return (Connection) Proxy.newProxyInstance(JdbcMonitor.class.getClassLoader(), new Class[]{Connection.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
JdbcStatistics statistics = null;
if (Arrays.asList(PROXY_CONNECTION_METHOD).contains(method.getName())) {
statistics = GlobalUtil.createStatistics(JdbcStatistics.class);
statistics.setStartTime(System.currentTimeMillis());
}
Object result = method.invoke(connection, args);
if (Arrays.asList(PROXY_CONNECTION_METHOD).contains(method.getName()) && result instanceof PreparedStatement) {
if (Objects.nonNull(statistics)) {
statistics.setUrl(connection.getMetaData().getURL());
statistics.setSql(args[0].toString());
}
result = proxyStatement((PreparedStatement) result, statistics);
}
return result;
}
});
}
可以发现是使用jdk的动态代理生成的代理对象,并且指定方法做了处理,可以发现在真正的执行Connection的方法之前,先判断是否是prepareStatement方法,如果是才进行监控构造监控statistics对象,之后再执行真正的方法,拿到结果之后,还要进行判断如果结果是PreparedStatement的实例,还会存入监控信息(如url,sql等),之后对返回的PreparedStatement类型的result对象再进行代理
private PreparedStatement proxyStatement(PreparedStatement statement, JdbcStatistics statistics) {
return (PreparedStatement) Proxy.newProxyInstance(JdbcMonitor.class.getClassLoader()
, new Class[]{PreparedStatement.class}, (proxy, method, args) -> {
Object result = method.invoke(statement, args);
if (Arrays.asList(STATEMENT_METHOD).contains(method.getName())) {
System.out.println("method name:" + method.getName());
statistics.setEndTime(System.currentTimeMillis());
if (result instanceof ResultSet) {
ResultSet resultSet = (ResultSet) result;
statistics.setResultSet(resultSet);
//设置行数 完毕归位结果集指针
resultSet.last();
statistics.setCount(resultSet.getRow());
resultSet.beforeFirst();
} else if (result instanceof Integer || result instanceof Long) {
statistics.setCount((Long) result);
} else if (result instanceof Boolean) {
statistics.setSuccess((Boolean) result);
}
log.info("preparedStatement statistics:{}", JSONUtil.toJsonStr(statistics));
}
return result;
});
}
生成PreparedStatement代理对象,包装了结果集再返回方法执行结果。这个目的就是感知sql执行的结果,能拿到结果集对象,进入存储sql执行结果,并且还能进行耗时统计。
致此,一个jdbc的监控基本就实现了,主要是找到入口在class加载之前使用javaassist插入了监控代码,进行sql执行过程中的信息统计。
Servlet监控
servlet监控和jdbc监控类似,主要是找到入口类即可,其实实现大同小异。
监控目标类和目标方法定义如下
private static final String TARGET_CLZ = "javax.servlet.http.HttpServlet";
public static AbstractMonitor INSTANCE;
@Override
public void init() {
ServletMonitor.INSTANCE = this;
}
@Override
public boolean isTarget(String className) {
return TARGET_CLZ.equals(className.replace("/", "."));
}
@Override
public CtMethod targetMethod(ClassPool pool, CtClass clz) throws NotFoundException {
return clz.getDeclaredMethod("service", new CtClass[]{pool
.get("javax.servlet.http.HttpServletRequest"), pool.get("javax.servlet.http.HttpServletResponse")});
}
可以发现监控HttpServlet的service方法,入参是HttpServletRequest和HttpServletResponse
这个没什么代理,基本就是简单的监控逻辑插入,比较简单,需要注意的一点是,我们需要监控request参数,就需要读inputstream,而request不能重复读的,因此需要先拷贝一个request再进行其他操作。
Spring MVC监控
只要监控了controller就能对http的request进行监控。
堆栈跟踪
通过ThreadLocal和InheritableThreadLocal实现