Intellij IDEA调试SparkSQL在运行时生成的代码

插件下载地址:spark-debug-codegen.jar.zip

安装步骤

  1. 在plugin页面选择从本地磁盘安装插件,安装完后先不重启IDEA
    1.png
  2. 打开IDEA,在Help菜单编辑自定义属性(即IDEA的System.getProperties())
    2.png
  3. 增加一项属性spark.sql.codegen.debugFileSavePath代表要把SparkSQL的动态生成的代码保存在哪个目录,保存后重启IDEA
    3.png
  4. 在动态生成代码的目录生成generated.java文件,粘贴如下代码:

    package org.apache.spark.sql.catalyst.expressions; class GeneratedClass extends org.apache.spark.sql.catalyst.expressions.codegen.GeneratedClass {   public Object generate(Object[] references) {
        return "在本行打断点";
      }
    }
  5. 用IDEA打开generated.java文件,在第二行打断点,然后远程debug spark的程序,等待断点到达即可,当到达断点的时候插件会自动读取运行时的代码并更新到编辑器中4.png

注意事项

  1. 本插件在IDEA 2019.3.2下编译,低于此版本可能无法使用
  2. 需要修改spark的CodeGenerator类,在“evaluator.cook("generated.java", code.body)”之前加上“evaluator.setDebuggingInformation(true, true, true)”,代表编译的时候要加上源码、行数、变量信息,否则我们无法打断点
    5.png
  3. spark使用janino这个工具来编译java代码为字节码,这个工具有个机制,就是如果遇到非静态的private方法,会把这个方法重写为包级别的static方法,并修改方法的第一个参数传入“this”,导致IDEA调试这些方法时无法获取this查看变量的值,遇到这种情况可以把spark生成的private方法的private关键字去掉,重新编译spark。下面是个例子:6.png

插件原理

  1. SparkSQL动态生成的代码会缓存在CodeGenerator#cache里,当断点达到generated.class的第二行的时候,插件会自动Evaluate如下的代码来获取当前的动态代码

    java.lang.reflect.Method cacheMethod = org.apache.spark.sql.catalyst.expressions.codegen.CodeGenerator$.class.getDeclaredMethod("cache");
    cacheMethod.setAccessible(true);
    Object cache = cacheMethod.invoke(org.apache.spark.sql.catalyst.expressions.codegen.CodeGenerator$.MODULE$);
    java.lang.reflect.Method asMapMethod = cache.getClass().getMethod("asMap");
    asMapMethod.setAccessible(true);
    java.util.Map<org.apache.spark.sql.catalyst.expressions.codegen.CodeAndComment, Object> cacheMap = (java.util.Map) asMapMethod.invoke(cache);
    for (java.util.Map.Entry<org.apache.spark.sql.catalyst.expressions.codegen.CodeAndComment, Object> entry : cacheMap.entrySet()) {
      if (entry.getValue() == currentOuterClass) {
          return entry.getKey().body();
      }
    }
    return null; // 如果找不到GeneratedClass,则显示“无法加载代码“ // 如果找不到代码,则显示“无法加载代码“
  2. 如果是在GeneratedClass的内部类中debug,则会运行下面的代码来获取当前的动态代码

    Object currentOuterClass = (Object) this;
    boolean findGeneratedClass = false;
    String dontChange = "DONT_CHANGE";
    while (true) {// 找外部类直到找到 org.apache.spark.sql.catalyst.expressions.GeneratedClass
      boolean hasFindOuterClass = false;
      for (java.lang.reflect.Field declaredField : currentOuterClass.getClass().getDeclaredFields()) {
        if (declaredField.getName().startsWith("this$")) {
          hasFindOuterClass = true;
          declaredField.setAccessible(true);
          currentOuterClass = declaredField.get(currentOuterClass);
          if (currentOuterClass == null) { // 如果外部类未初始化,说明此时是从外部类step into进到内部类的构造方法,不需要改变代码
            return dontChange;
          }
          if (currentOuterClass.getClass().getName().equals("org.apache.spark.sql.catalyst.expressions.GeneratedClass")) {
            findGeneratedClass = true;
          }
          break;
        }
      }
      if (findGeneratedClass) {
        break;
      } else if (!hasFindOuterClass) { // 如果找不到GeneratedClass,则显示“无法加载代码“
        return null;
      }
    }
    
    if (!findGeneratedClass) { // 如果找不到GeneratedClass,则显示“无法加载代码“
      return dontChange;
    }
    java.lang.reflect.Method cacheMethod = org.apache.spark.sql.catalyst.expressions.codegen.CodeGenerator$.class.getDeclaredMethod("cache");
    cacheMethod.setAccessible(true);
    Object cache = cacheMethod.invoke(org.apache.spark.sql.catalyst.expressions.codegen.CodeGenerator$.MODULE$);
    java.lang.reflect.Method asMapMethod = cache.getClass().getMethod("asMap");
    asMapMethod.setAccessible(true);
    java.util.Map<org.apache.spark.sql.catalyst.expressions.codegen.CodeAndComment, Object> cacheMap = (java.util.Map) asMapMethod.invoke(cache);
    for (java.util.Map.Entry<org.apache.spark.sql.catalyst.expressions.codegen.CodeAndComment, Object> entry : cacheMap.entrySet()) {
      if (entry.getValue() == currentOuterClass) {
          return entry.getKey().body();
      }
    }
    return null; // 如果找不到GeneratedClass,则显示“无法加载代码“

添加新评论