JavaAgent和javassist构建的监控服务

Mars 2020年09月26日 16次浏览

概述

源码地址

monitor分支

目前实现的功能

  1. 无侵入监控jdbc信息
  2. 无侵入监控Servlet
  3. 记录堆栈跟踪
  4. 集成了spring mvc的controller监控
  5. 分布式调用跟踪(待实现)

使用

拿到源码,打为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();
    }
}

可以看见主要做了以下几件事

  1. 判断该类是否需要被监控

  2. 如果需要,则先复制一个方法,方法名为原方法名加上AGENT_SUFFIX组成

  3. 之后进行监控逻辑植入

    • 先忽略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实现