`
chenjingbo
  • 浏览: 456376 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

housemd源码解析

 
阅读更多

1.1准备工作

1.1.1 java agent , java attach api, VirtualMachine 等

java agent代理和 virtualMachine的知识,可以参考 http://blog.csdn.net/qyongkang/article/details/7765255  大概有连续6篇文章.看完的话大概能明白几个概念了.

还有一个简单的例子 http://chenjingbo.iteye.com/blog/1966733  含金量不高 ,不过也可以将就看看.

 

1.1.2 scala 学习

housemd大部分的代码都是用scala写的.所以至少能读懂里面大概什么内容. 拿来主义,看这个吧 http://chenjingbo.iteye.com/blog/1974335

 

1.1.3  jenv

housemd下载是通过jenv的.很方便,一行命令搞定.当然使用jenv使用之前需要安装jenv.也很简单.这个不需要学习,只需要安装一下就好了 ,具体如下 https://github.com/linux-china/jenv/wiki/Chinese-Introduction 

 

1.1.4 yascli

这个包.是一个scala的命令行开发包.具体的地址是 https://github.com/CSUG/yascli

 老实说,我没有完全读透立面的代码,就当一个简单的工具看了.

1.1.5 asm

这个字节码修改工具,如果只是看看housemd的主流程,那么就不需要仔细研究了.如果想看trace命令这种需要修改字节码实现的,是如何工作的,那再仔细学习吧.

 

1.2  正文

1.2.1 housemd的官方地址

housemd的地址在 https://github.com/CSUG/HouseMD 具体的用法就不在这个文档里说明了.看一下 https://github.com/CSUG/HouseMD/wiki/UserGuideCN 就已经非常清楚明白了.

 

1.2.2程序入口

Housemd的使用,最开始当然是在终端里里输入 

terminal 写道
housemd $PID

 直接看 housemd命令对应的代码.

#!/bin/sh
if [ -z "$JAVA_HOME" ];
then
    echo "Please set JAVA_HOME to JDK 6+!"
    exit 1
else
    ROOT=`dirname  "$0"`
    if [ -f $JAVA_HOME/lib/tools.jar ]; then
            BOOT_CLASSPATH=-Xbootclasspath/a:$JAVA_HOME/lib/tools.jar
    fi
    $JAVA_HOME/bin/java $BOOT_CLASSPATH -jar $ROOT/housemd_2.9.2-0.2.6.min.jar "$@"
fi

 这里非常简单,只是把tools.jar设置成boot_classpath,然后执行执行 java -jar 命令运行housemd对应的jar. 既然如此,直接看对应的METAINF.MF .

Manifest-Version: 1.0
Implementation-Title: housemd
Implementation-Version: 0.2.6
Specification-Vendor: com.github.zhongl
Implementation-Vendor: com.github.zhongl
Implementation-Vendor-Id: com.github.zhongl
Specification-Title: housemd
Agent-Class: com.github.zhongl.housemd.duck.Duck
Specification-Version: 0.2.6
Main-Class: com.github.zhongl.housemd.house.House
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Signature-Version: 0.2.6

 这个文件,后续还需要用到.这里我们需要关心的是 

写道
Main-Class: com.github.zhongl.housemd.house.House

 这样,我们就找到对应的入口class了.

 

 1.2.3 House.scala

这个类里核心代码如下

 

def run() {
    if (printVersion()) {//如果只是打印版本,直接打印版本号
      println("v" + version)
      return
    }
//如果是windows系统,直接返回说不支持.
    if (ManagementFactory.getOperatingSystemMXBean.getName.toLowerCase.contains("window")) {
      throw new IllegalStateException("Sorry, Windows is not supported now.")
    }
    try {
//创建一个内置的unix terminal 这个都是scala的独有类,不多说了.功能就是内置unix terminal.
      val terminal = new NoInterruptUnixTerminal()
      terminal.init()
      val sout = terminal.wrapOutIfNeeded(System.out)
      val sin = terminal.wrapInIfNeeded(System.in)
      val vm = VirtualMachine.attach(pid())

      val mobilephone = new Mobilephone(port(), {
        case PickUp              => info("connection established on " + port())
        case ListenTo(earphone)  => earphone(sout)
        case SpeakTo(microphone) => microphone(sin)
        case BreakOff(reason)    => error("connection breaked causeby"); error(reason)
        case HangUp              => terminal.restore(); silentClose(errorDetailWriter); info("bye")

      })

      info("Welcome to HouseMD " + version)
//核心代码在这里.下面会仔细说明.
      vm.loadAgent(agentJarFile, agentOptions mkString " ")
      vm.detach()

      mobilephone.start()
    } catch {
      case e: Throwable => error(e); silentClose(errorDetailWriter)
    }
  }
一些非主要的功能,上面已经通过注释说明了.其中核心的代码是 

 

 

val vm = VirtualMachine.attach(pid()) //pid()就是获取传入的pid
      vm.loadAgent(agentJarFile, agentOptions mkString " ")
      vm.detach()
 可以看到,他也是通过VirtualMachine load 对应的agent来实现功能的..我们再仔细看一下对应的参数.

 

我们知道 VirtualMachine.loadAgent 方法,第一个参数是对应agent的jar包全路径,第二个参数是给agent传入的参数.

 

private lazy val agentJarFile = sourceOf(Manifest.classType(getClass))
 housemd一共就一个jar包,也就是说,当前的class所在的jar地址就是对应agent所在的地址..sourceOf方法就是获取当前类所在的jar包地址.

 

 

private lazy val agentOptions = agentJarFile :://agent所在的jar包全路径
                                  classNameOf[Telephone] :: //Telephone对应的全额类目.这个类后续会具体说
                                  port() :://内置的terminal 开启的服务的端口号.
                                  classNameOf[Trace] :://后面的6个就是housemd对应支持的命令,每个命令对应一个类.
                                  classNameOf[Loaded] ::
                                  classNameOf[Env] ::
                                  classNameOf[Inspect] ::
                                  classNameOf[Prop] ::
                                  classNameOf[Resources] ::
                                  Nil  //what's this 
 后面的参数比较多.里面的参数内容,我大概都解释了.每个参数中间用空格隔开.这里的参数内容解释,后面都会用到.

 

好了.上面把程序入口的代码都解释了一遍.里面核心的就是attach agent了.下面,就需要看对应的agent class在哪呢.查看上面的METAINF.MF ,可以看到
写道
Agent-Class: com.github.zhongl.housemd.duck.Duck
 OK.下面继续看对应的agent class

1.2.4 Duck

这个类就是对应的代理类了.具体的代码如下

 

ublic class Duck {
    public static void agentmain(String arguments, Instrumentation instrumentation) throws Exception {
        String[] parts = arguments.split("\\s+", 4);//把传入的参数用空格分割,总共分为4部分.(每个参数对应的说明看1.2.3的说明.已经很清楚了)
        URL agentJar = new File(parts[0]).toURI().toURL();//agent所在jar,转成URL
        String telephoneClassName = parts[1];//这个类的用处后面会仔细说.
        int port = Integer.parseInt(parts[2]);//terminal的监听端口号.给telephone类使用的.

       //创建一个classloader
        ClassLoader classLoader = new URLClassLoader(new URL[]{agentJar}) {
            @Override
            protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
                Class<?> loadedClass = findLoadedClass(name);
                if (loadedClass != null) return loadedClass;

                try {
                    Class<?> aClass = findClass(name);
                    if (resolve) resolveClass(aClass);
                    return aClass;
                } catch (Exception e) {
                    return super.loadClass(name, resolve);
                }
            }
        };
       //把所有commond对应的类,通过上面创建的classloader给loader进来.
        Class<?>[] commandClasses = loadClasses(parts[3].split("\\s+"), classLoader);

       //通过反射,创建一个telephoneClass的实例.注意这里传入的参数.instrumentation, port, commandClasses
        Runnable executor = (Runnable) classLoader.loadClass(telephoneClassName)
                .getConstructor(Instrumentation.class, int.class, Class[].class)
                .newInstance(instrumentation, port, commandClasses);

        //创建幽灵线程,执行telephoneClass.
        Thread thread = new Thread(executor, "HouseMD-Duck");
        thread.setDaemon(true);
        thread.start();
    }
   //简单的load 对应的class.
    private static Class<?>[] loadClasses(String[] classNames, ClassLoader classLoader) throws ClassNotFoundException {
        Class<?>[] classes = (Class<?>[]) Array.newInstance(Class.class, classNames.length);
        for (int i = 0; i < classes.length; i++) {
            classes[i] = Class.forName(classNames[i], false, classLoader);
        }
        return classes;
    }

}

 终于是java代码,.内牛满面..

大部分的代码解释,我都已经在上面注释了..唯一没有解释的就是telephoneClass了.仔细读一下1.2.3的章节,可以知道 telephoneClass对应就是 com.github.zhongl.housemd.duck.Telephone .这个agent里可以说其他什么都没有做,就是启动了一个Telephone的线程.

 

1.2.4 Telephone

这个类,主要的功能是 把Instrumentation对象与命令行联系起来.源码如下

 

class Telephone(inst: Instrumentation, port: Int, classes: Array[Class[Command]]) extends Runnable {
 
 
  def run() {
//链接到之前创建的teminal里获取数据
    val socket = new Socket("localhost", port)
    val reader = new ConsoleReader(socket.getInputStream, socket.getOutputStream)
    val history = new FileHistory(new File("/tmp/housemd/.history"))
    reader.setHistory(history)
 
    try {
//Shell类后面再说.这里就是创建了一个Shell
      new Shell(name = "housemd", description = "a runtime diagnosis tool of jvm.", reader = reader) {
        private val lastCommand = new Last(out)
 
        override protected def commands = Quit :: helpCommand :: lastCommand :: classes
          .map { toCommand(_, PrintOut(reader)) }
          .toList
      //处理异常的情况(日志接口).
        override def error(a: Any) {
          super.error(a)
          if (a.isInstanceOf[Throwable]) lastCommand.keep(a.asInstanceOf[Throwable])
        }
 
      } main (Array.empty[String])
    } finally {
      TerminalFactory.reset()
      history.flush()
      socket.shutdownOutput()
      socket.shutdownInput()
      socket.close()
    }
  }
//反射创建对应的Commond对象
  private def toCommand(c: Class[Command], out: PrintOut) = {
    val I = classOf[Instrumentation]
    val O = classOf[PrintOut]
    val constructors = c.getConstructors
    val args = constructors(0).getParameterTypes map {
      case I => inst
      case O => out
      case x => throw new IllegalStateException("Unsupport parameter type: " + x)
    }
    constructors(0).newInstance(args: _*).asInstanceOf[Command]
  }
 
}

  这里覆盖了 commands方法.新增了我们需要的命令.分别是helpCommand,lastCommand 还有之前从Duck里传入的命令.也就是从House.scala里传给Duck的

classNameOf[Trace] ::
                                  classNameOf[Loaded] ::
                                  classNameOf[Env] ::
                                  classNameOf[Inspect] ::
                                  classNameOf[Prop] ::
                                  classNameOf[Resources] ::

 然后通过反射创建对应的实例.这样,这个Shell支持的命令就有了.

 Telphone类一句话概括代码就是创建了一个Shell对象.然后调用了对应的main方法.那么代码继续下去,就应该看Shell对象的main方法了.

 

1.2.5 Shell

 直接看源码

abstract class Shell(
  name: String,
  description: String,
  reader: ConsoleReader = new ConsoleReader(System.in, System.out))
  extends Command(name, description, PrintOut(reader)) with Suite {

//可以看到main方法只是调用了interact方法.
  final def main(arguments: Array[String]) { interact() }

  override final def run() {}

  protected def prompt: String = name + "> "

  override protected def decorate(list: String) = "\n" + list + "\n"

//这个才是该类的主方法.
  private def interact() {
    reader.setPrompt(prompt)//就是设置这个shell的前缀是  housemd>
    reader.addCompleter(DefaultCompleter) //设置一个completer,貌似和主流程没关系
  //这个方法会获取从命令行里传来的命令,然后做解析,并输出.
    @tailrec
    def parse(line: String) {
      if (line == null) return
      val array = line.trim.split("\\s+") //用空格隔开,解析命令
      try {
//一个很有趣的回调写法..我还是非常不习惯scala的语法啊.
//核心的逻辑在run方法
        if (!array.head.isEmpty) run(array.head, array.tail) { name => println("Unknown command: " + name) }
      } catch {
//quit命令直接作为异常处理了..
        case e: QuitException => return
//记录错误日志.
        case t                => error(t)
      }
//处理完一个命令,继续等待.
      parse(reader.readLine())
    }
 
    parse(reader.readLine())

    reader.shutdown()
  }

  object Quit extends Command("quit", "terminate the process.", PrintOut(reader)) {
    def run() { throw new QuitException }
  }

  class QuitException extends Exception

 //*** 下面是Completer相关的代码,和主流程无关,就不多解释了 ***//

}

 可以看到,这个类,处理了一些边缘逻辑,比如设置命令头,获取命令,退出命令实现,等等.但是最核心的处理命令的逻辑封装在 

 run(array.head, array.tail)

 当中. 这个方法定义在 Suite类中,.Shell类继承的Suite类.下面,继续看Suite类的run!

 

1.2.6 Suite

这个类,也是一个封装类,基本没有核心的逻辑,里面定义了helpCommand.这里就不多解释了..核心的还是run方法.

def run(command: String, arguments: Array[String])(handleUnknown: String => Unit) {
//还记得在Shell类里看到的回调吗.
    commands find {_.name == command} match {
      case Some(c) => c.parse(arguments); c.run()
      case None    => handleUnknown(command)
    }
  }

 可以看到,这个run方法也很简单,直接调用了对应Commond的parse方法和run方法.那么,下面我们就需要看Commond类了..

 

1.2.7 Command

这个类,是所有命令的父类.之前在suite里可以看到,他调用了 parse方法和run方法.其中 parse方法是解析对应的option和parameter的.run方法就是 Runnable接口中定义的run接口.对应每一个命令都会实现对应的run接口.先说parse方法

 

def parse(arguments: Array[String]) {
//把之前的values给清除
    values.clear() // reset last state

//这个方法 也很容易看懂,如果匹配 -开头的,则调用 addOption 如果是参数,则addParameter.这里有递归的处理,很容易看懂,不多解释了.不管是otpion 还是parameter,都是放在values里
    @tailrec
    def read(list: List[String])(implicit index: Int = 0) {
      list match {
        case head :: tail if (head.matches("-[a-zA-Z-]+")) => read(addOption(head, tail))
        case head :: tail                                  => read(addParameter(index, list))(index + 1)
        case Nil                                           => // end recusive
      }
    }

    read(arguments.toList)
  }

 看完parse方法,就需要看对应的run方法了..对应的run方法在每个命令里都有不同.我估计我应该不会每个命令都讲.先讲一个简单的loaded命令吧. 看对应的run方法.

 

1.2.8 Loaded

loaded 命令的功能是查看类加载路径 ..可以跟一个-h参数,可以打印出对应的class loader的层级. 大概的功能如下

loaded 写道
housemd> loaded -h JuwlCommonMTopServiceImpl
com.taobao.juwl.iserver.service.top.impl.JuwlCommonMTopServiceImpl -> /home/admin/juwliserver/target/juwliserver.war/WEB-INF/lib/juwl-iserver-service-1.0-SNAPSHOT.jar
- com.taobao.tomcat.classloader.TomcatWebAppClassLoader@55f49969
- org.apache.catalina.loader.StandardClassLoader@6b12da40
- sun.misc.Launcher$AppClassLoader@4aad3ba4
- sun.misc.Launcher$ExtClassLoader@3326b249

 BootClassLoader不是java实现,所以这里看不到.

看对应的代码

class Loaded(val inst: Instrumentation, out: PrintOut)
  extends Command("loaded", "display loaded classes information.", out)
          with ClassSimpleNameCompleter {

  private val tab = "\t"

  private val hierarchyable   = flag("-h" :: "--classloader-hierarchies" :: Nil, "display classloader hierarchies of loaded class.")
  private val classSimpleName = parameter[String]("name", "class name without package name.")

  override def run() {
    val k = classSimpleName()
//获取所有loaded class,然后做过滤
    val matched = inst.getAllLoadedClasses filter { simpleNameOf(_) == k }
    if (matched.isEmpty) println("No matched class")
//打印匹配到的
    else matched foreach {
      c =>
        println(c.getName + " -> " + sourceOf(Manifest.classType(c)))
//如果有-h 参数,打印对应的classloader 
        if (hierarchyable()) layout(Option(c.getClassLoader))
    }
  }

//按照双亲委派模型,自下而上打印对应的classloader .
  @tailrec
  private def layout(cl: Option[ClassLoader], lastIndents: String = "- ") {
    cl match {
      case Some(loader) =>
        val indents = tab + lastIndents
        println(indents + getOrForceToNativeString(loader))
        layout(Option(loader.getParent), indents)
      case None         =>
    }
  }

}

 到这里为止,其实已经把housemd一个主流程完全讲通,看一下housemd所有支持的命令

写道
housemd> help

quit terminate the process.
help display this infomation.
last show exception stack trace of last error.
trace display or output infomation of method invocaton.
loaded display loaded classes information.
env display system env.
inspect display fields of a class.
prop display system properties.
resources list all source paths can loaded from every classloader by resource full name.

 

 其实上面的命令可以分为三种.

写道
1 功能型命令 quit,help,last .只是提供housmd命令本身功能的.
2 一般性命令(为了区分第三种随意取的名字). loaded env prop resources
3 需要修改字节码的命令 trace inspect

 功能型命令,不多说了.上面解释的代码里都可以直接看到..基本就是写死的. 一般性的命令,已经解析了loaded命令了,其他的几个都是类似的,获取对应的信息,然后做一个匹配打印.不一一解释了.下面会解析trace命令(inspect命令和trace命令很类似,看源码就知道了).看看需要修改字节码的实现是怎么样的.本质上还是通过instrumentation修改对应的方法而已.

 

1.2.9 Trace

与看loaded命令类似,直接看对应的run方法,但是通观整个Trace类,可以发现压根没有run方法.继续看,可以发现对应的run方法在他的父类TransformCommand中.直接看TransformCommand的run方法

override def run() {
    val delegate = hook
//创建一个钩子.
    val h = new Hook {
      val intervalMillis = interval().toMillis
      var last           = 0L

      def heartbeat(now: Long) {
        if (now - last > intervalMillis) {
          delegate.heartbeat(now)
          last = now
        }
      }

      def finalize(throwable: Option[Throwable]) {
        delegate.finalize(throwable)
      }

      def enterWith(context: Context) {
        delegate.enterWith(context)
      }

      def exitWith(context: Context) {
        delegate.exitWith(context)
      }
    }

    transform(inst, filter, timeout(), overLimit(), this, h)
  }

 可以看到.这里除了创建一个钩子,就是调用了transform方法.transform 在Transform 类中,

def apply(inst: Instrumentation, filter: Filter, timeout: Seconds, overLimit: Int, log: Loggable, hook: Hook) {
    implicit val i = inst
    implicit val t = timeout
    implicit val l = log
    implicit val h = hook
//获取所有loaded的类,然后过滤一些不支持的类.
    val candidates = inst.getAllLoadedClasses filter {
      c =>
//还是非常看不惯随时定义,随时使用.还有回调的方式
        @inline
        def skipClass(description: String)(cause: Class[_] => Boolean) =
          if (cause(c)) {log.warn("Skip %1$s %2$s" format(c, description)); false } else true

        @inline
        def isNotBelongsHouseMD = skipClass("belongs to HouseMD") { _.getName.startsWith("com.github.zhongl.housemd") }

        @inline
        def isNotInterface = skipClass("") { _.isInterface }

        @inline
        def isNotFromBootClassLoader = skipClass("loaded from bootclassloader") { isFromBootClassLoader }

        filter(c) && isNotBelongsHouseMD && isNotInterface && isNotFromBootClassLoader
    }

    if (candidates.isEmpty) {
      log.println("No matched class")
    } else {
     //这个就是修改对应字节码的方法,这个方法,最主要是asm相关的知识.这里也不多说了.
      val probeDecorator = classFileTransformer(filter, candidates)
     //修改对应的字类.
      inst.addTransformer(probeDecorator, true)
//该方法会对上面 addTransformer里设置成true的class重新load.
      probe(candidates, advice(overLimit))
      //执行对应的方法.这里就不多解释了.就是分别处理对应的option,然后生成不同的输出结果
      try {handleAdviceEvent} finally {
    //执行完毕以后,把字节码重新恢复.
        inst.removeTransformer(probeDecorator)
        reset(candidates)
      }

    }

  }

 

1.3 结尾

housemd一个非常简陋的解析,差不多告一段落了.。如果有兴趣,可以到 https://github.com/zhenghui/housemd4j  看对应java版本的实现。

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics