网页调试Java代码

背景

低代码平台中的后端流主要处理数据、执行业务逻辑和与数据库或其他后端服务进行交互的一系列步骤。

但是在页面上,看不到后端执行步骤,所以提出这一想法,在网页debug Java代码。

过程

实现调试之前已知逻辑流是一个类,逻辑流中的节点是类中的每个方法,而且每个逻辑流在被调用的时候都会有一个属于自己的上下文环境 ActionContext ,这个 ActionContext中包含当前逻辑流调用时候的所有参数信息,那么想实现在页面调试Java代码就简单了,在执行每个方法前,将线程暂停,并将 ActionContext中的信息返回页面,页面发送指令到后端,进行对应的操作即可。

实现

  • 使用 WebSocket来前后端交互
  • 使用锁将线程暂停

最刚开始尝试过使用 wait或者用线程的 notify,因为文章整理跟当时做功能已经有段时间了,已经忘记当时为什么这几种方案有什么问题了,只记得当时这几种方案都有问题。。。
后面使用的是 LockSupport,关于此工具的使用可以看

  1. 首先在每个类中生成属于每个线程自己的 debugActionId

  2. 其次生成代码的时候在每个节点方法开始的第一句,插入一条记录当前方法和当前线程的信息。
    DebugUtil.park(debugActionId.get(), "Start", 3056069844134422L);

  3. 然后在页面调用过程中,如果当前节点ID在调试列表中,使用工具类 LockSupport.park()暂停线程,并把当前逻辑流的参数发送给页面

    image-fpep-abau.png

  4. 页面通过 WebSocket将操作类型发送给后端,后端继续相应的操作。
    整体操作比较简单,复杂的是功能实现以后得调试,跟契合当前平台的返回数据格式,因为上下文环境中返回的数据格式并不是页面上的格式,所以需要拼装成页面上熟悉的数据格式。

下面附上工具代码

Click here for code - DebugUtil
publi class DebugUtil {
  // 存储debug的信息
  private static final Map<Long, DebugContext> debugContextMap = new ConcurrentHashMap<>();
  // 如果是逻辑流互相调用 记录调用逻辑流服务节点的id
  private static final Map<Long, Map<String, Object>> nextNodeMap = new ConcurrentHashMap<>();
  // 断点列表
  private static final Set<Long> breakPointList = new CopyOnWriteArraySet<>();
  // 记录下一步的所有的节点id
  private static final Set<Long> nextStepNodeIdSet = new CopyOnWriteArraySet<>();
  // 记录流的debugActionId
  private static final List<Long> debugActionIdList = new CopyOnWriteArrayList<>();
  private static WebSocketServer webSocketServer;
  private static final Map<Long, DebugThread> threadMap = new ConcurrentHashMap<>();
  //private static Thread thread;
  // 是否忽略断点 用给终止调试用 true: 忽略所有断点
  private static boolean ignore = false;
  // 标记运行到这里的节点id
  private static Long runToNodeId;
  @Resource
  private ApplicationContext applicationContext;

  @PostConstruct
  public void init() {
      DebugUtil.webSocketServer = applicationContext.getBean(WebSocketServer.class);
  }

  public static void park(Long debugActionId, String nodeName, Long nodeId) {
      if (longIsNotEmpty(debugActionId) && debugActionIdList.contains(debugActionId) && debugContextMap.get(debugActionId) != null) {
          DebugContext debugContext = debugContextMap.get(debugActionId);
          debugContext.setCurrentNodeName(nodeName);
          debugContext.setCurrentNodeId(nodeId);
          // 如果有标记的节点id 证明是"运行到这里 或者 步出" 那需要判断是否匹配当前的节点 忽略其他断点
          if ((longIsNotEmpty(runToNodeId))) {
              if (nodeId.equals(runToNodeId)) {
                  runToNodeId = 0L;
                  doPark(debugActionId);
              }
          } else {
              // 是正常的debug
              // 如果没有忽略断点而且 节点集合 包含当前节点 停止
              if (!ignore && (breakPointList.contains(nodeId) || nextStepNodeIdSet.contains(nodeId))) {
                  doPark(debugActionId);
              }
          }
          if ("End".equalsIgnoreCase(nodeName)) {
              handleEnd(debugActionId);
              threadMap.remove(debugActionId);
          }
      }
  }

  private static void doPark(Long debugActionId) {
      sendMsg(debugActionId);
      nextStepNodeIdSet.clear();
      LockSupport.park();
  }

  /**
   * 断点放开
   */
  public static void unPark(Long debugActionId) {
      if (!threadMap.containsKey(debugActionId) && !longIsNotEmpty(runToNodeId)) {
          Map<String, Object> errMsgMap = new LinkedHashMap<>();
          errMsgMap.put("result", "error");
          errMsgMap.put("msg", "调试线程出现异常, 请中断重试");
          webSocketServer.sendMessage(errMsgMap);
          handleClose();
      }
      if (!CollectionUtils.isEmpty(debugActionIdList)) {
          if (longIsNotEmpty(debugActionId)) {
              DebugContext debugContext = debugContextMap.get(debugActionId);
              // 如果这次放开断点放开的是结束节点 从Util里删除这个流的数据
              DebugThread thread = threadMap.get(debugActionId);
              if (debugContext != null && thread != null) {
                  if (debugContext.getCurrentNodeName().startsWith("End")) {
                      handleEnd(debugActionId);
                      threadMap.remove(debugActionId);
                  }
              }
              try {
                  LockSupport.unpark(thread.getThread());
              } catch (NullPointerException exception) {
                  exception.printStackTrace();
              }
          }
      }
  }

  /**
   * 断点放开 全部
   */
  public static void unPark() {
      if (!CollectionUtils.isEmpty(threadMap)) {
          for (DebugThread thread : threadMap.values()) {
              LockSupport.unpark(thread.getThread());
          }
      }
  }

  /**
   * 终止调试
   */
  public static void stopDebug() {
      ignore = true;
      unPark();
  }

  /**
   * 添加开启debug的线程
   */
  public static void addDebugThread(Long actionId, DebugThread thread, ActionContext context) {
      Long debugActionId = thread.getCId();
      String actionName = thread.getName();
      System.out.printf("添加debug线程, debugActionId: %s, actionName: %s%n", debugActionId, actionName);
      // actionNameMap 和 debugContextMap 不为空 证明这是被调用的逻辑流 不需要覆盖线程信息
      //if (CollectionUtils.isEmpty(debugActionIdList) && CollectionUtils.isEmpty(debugContextMap)) {
      //DebugUtil.thread = thread;
      threadMap.put(debugActionId, thread);
      //}
      debugActionIdList.add(debugActionId);
      debugContextMap.putIfAbsent(debugActionId, new DebugContext(actionId, actionName, thread.getParentId(), context));
  }

  /**
   * 添加开启断点的节点id
   */
  public static void addBreakPoint(Long nodeId) {
      System.out.printf("添加断点, nodeId: %s%n", nodeId);
      breakPointList.add(nodeId);
  }

  public static void addBreakPoint(List<Long> nodeIdList) {
      System.out.printf("添加断点(列表) nodeIdList: %s%n", nodeIdList.stream().map(String::valueOf).collect(Collectors.joining(",")));
      breakPointList.clear();
      breakPointList.addAll(nodeIdList);
  }

  /**
   * 移除断点
   */
  public static void removeBreakPoint(Long nodeId) {
      System.out.printf("删除断点, nodeId: %s%n", nodeId);
      breakPointList.remove(nodeId);
  }

  /**
   * 运行到这里
   */
  public static void runToThis(Long debugActionId, Long nodeId) {
      runToNodeId = nodeId;
      unPark(debugActionId);
  }

  public static void addNextStepNodeId(Long debugActionId, List<Long> nodeIdList) {
      nextStepNodeIdSet.addAll(nodeIdList);
      unPark(debugActionId);
  }

  /**
   * 一个流走完后 清空debug信息
   */
  public static void handleEnd(Long debugActionId) {
      if (debugContextMap.containsKey(debugActionId) && debugActionIdList.contains(debugActionId)) {
          DebugContext debugContext = debugContextMap.get(debugActionId);
          Long actionId = debugContext.getActionId();
          String actionName = debugContext.getActionName();
          log.info(" #END#  debugAction: {}, action:{}:{}, 结束, 清空消息", debugActionId, actionId, actionName);
          debugActionIdList.remove(debugActionId);
          debugContextMap.remove(debugActionId);
          threadMap.remove(debugActionId);
          nextStepNodeIdSet.remove(debugActionId);
          if (CollectionUtils.isEmpty(debugActionIdList)) {
              log.info("当前无断点 全部调试结束");
              ignore = false;
          }
          sendMsg(debugActionId);
      }
  }

  /**
   * 调试过程中更改参数值
   */
  public static void changeParam(Long debugActionId, Map<String, Object> paramMap) {
      paramMap.forEach((key, value) -> debugContextMap.get(debugActionId).getContext().putValue(key, value));
      sendMsg(debugActionId);
  }

  public static void markNextNodeId(Long debugActionId, Long currentServerId, Long nextNodeId) {
      Map<String, Object> innerNodeMap = new HashMap<>();
      innerNodeMap.put("currentServerId", currentServerId);
      innerNodeMap.put("nextNodeId", nextNodeId);
      nextNodeMap.put(debugActionId, innerNodeMap);
  }

  public static void handleException() {
      log.info("出现异常, 调试结束");
      if (!CollectionUtils.isEmpty(debugActionIdList)) {
          log.info("清空debug信息 发送消息");
          handleClose();
          sendMsg(null);
      }
  }

  private static void sendMsg(Long debugActionId) {
      Map<String, Object> resultMap = new LinkedHashMap<>();
      List<Map<String, Object>> debugActionList = new ArrayList<>();
      if (longIsNotEmpty(debugActionId)) {
          resultMap.put("currentDebugActionId", debugActionId);
      }
      debugActionIdList.forEach(currentDebugActionId -> {
          Map<String, Object> varMap = new LinkedHashMap<>();
          DebugContext context = debugContextMap.get(currentDebugActionId);
          Map<String, Object> variablesMap = mapCopy(context.getContext().getVariables());
          variablesMap.remove("Entities");
          handleLogicList(variablesMap);
          varMap.put("currentNodeId", context.getCurrentNodeId());
          varMap.put("currentNodeName", context.getCurrentNodeName());
          varMap.put("debugActionId", currentDebugActionId);
          varMap.put("actionId", context.getActionId());
          varMap.put("actionName", context.getActionName());
          varMap.put("variables", variablesMap);
          varMap.put("currentServerId", 0L);
          varMap.put("nextNodeId", 0L);
          Long parentId = context.getParentId();
          // 获取爸爸的信息
          if (longIsNotEmpty(parentId)) {
              if (nextNodeMap.containsKey(parentId)) {
                  varMap.putAll(nextNodeMap.get(parentId));
              }
          }
          varMap.putIfAbsent("parentId", longIsNotEmpty(parentId) ? parentId : 0L);
          debugActionList.add(varMap);
      });
      resultMap.put("actionList", debugActionList);
      webSocketServer.sendMessage(resultMap);
  }

  /**
   * 处理WebSocket结束 一般debug结束会调用
   */
  public static void handleClose() {
      ignore = true;
      unPark();
      runToNodeId = null;
      //thread = null;
      //ignore = false;
      debugContextMap.clear();
      debugActionIdList.clear();
      breakPointList.clear();
      nextStepNodeIdSet.clear();
      log.info("debug completed");
  }

  private static boolean longIsNotEmpty(Long value) {
      return value != null && value != 0L;
  }

  private static void handleLogicList(Map<String, Object> variablesMap) {
      variablesMap.forEach((key, value) -> {
          if (value instanceof Map) {
              handleLogicList((Map<String, Object>) value);
          }
          if (value != null && "outPutModel".equalsIgnoreCase(value.getClass().getSimpleName())) {
              Map<String, Object> fieldMap = new HashMap<>();
              variablesMap.put(key, fieldMap);
              Field[] fields = value.getClass().getDeclaredFields();
              for (Field field : fields) {
                  try {
                      String fieldName = field.getName();
                      field.setAccessible(true);
                      Object fieldValue = field.get(value);
                      if (Map.class.equals(field.getType())) {
                          Map map = (Map) fieldValue;
                          handleLogicList(map);
                          fieldMap.putAll(map);
                      } else if (LogicList.class.equals(field.getType())) {
                          LogicList logicList = (LogicList) fieldValue;
                          fieldMap.put(fieldName, generateLogicListMap(logicList));
                      } else {
                          fieldMap.put(fieldName, fieldValue);
                      }
                  } catch (Exception exception) {
                      throw new RuntimeException("获取对象的属性异常:" + exception.getMessage());
                  }
              }
          }
          if (value instanceof LogicList) {
              LogicList<?> list = (LogicList<?>) value;
              variablesMap.put(key, generateLogicListMap(list));
          }
      });
  }

  @NotNull
  private static Map<String, Object> generateLogicListMap(LogicList<?> list) {
      Map<String, Object> listMap = new HashMap<>();
      listMap.put("Current", list.getCurrent());
      listMap.put("List", list);
      listMap.put("Empty", list.getEmpty());
      listMap.put("Length", list.getLength());
      listMap.put("CurrentRowNumber", list.getCurrentRowNumber() == -1 ? 0 : list.getCurrentRowNumber());
      return listMap;
  }

  private static Map<String, Object> mapCopy(Map<String, Object> variablesMap) {
      Map<String, Object> resultMap = new HashMap<>();
      variablesMap.forEach((key, value) -> {
          if (value instanceof Map) {
              resultMap.put(key, mapCopy((Map) ((HashMap) value).clone()));
          } else if (value instanceof LogicList) {
              LogicList cloneList = (LogicList) ((LogicList) value).clone();
              cloneList.forEach(item -> {
                  if (item instanceof Map) {
                      int index = cloneList.indexOf(item);
                      cloneList.set(index, mapCopy((Map) ((HashMap) item).clone()));
                  }
              });
              resultMap.put(key, cloneList);
          } else {
              resultMap.put(key, value);
          }
      });
      return resultMap;
  }
}
Click here for code - WebSocket
public class WebSocketServer {
    private static Session session;
    @Autowired
    private ObjectMapper objectMapper;

    @OnOpen
    public void onOpen(Session session) {
        WebSocketServer.session = session;
        log.info("ws connect");
    }

    /**
     * 连接关闭
     * 调用的方法
     */
    @OnClose
    public void onClose() {
        DebugUtil.handleClose();
        session = null;
        log.info("ws disconnect");
    }

    public void sendMessage(Object obj) {
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(objectMapper.writeValueAsString(obj));
                log.info("send message success, msg: " + objectMapper.writeValueAsString(obj));
            } catch (Exception ex) {
                log.error("there has an exception when closing the ws", ex);
            }
        } else {
            log.info("ws connection lost");
        }
    }

    @OnMessage
    public void onMessage(String msg) throws JsonProcessingException {
        if (!StringUtils.isEmpty(msg)) {
            DebugParam debugParam = new ObjectMapper().readValue(msg, DebugParam.class);
            Long debugActionId = debugParam.getDebugActionId();
            switch (debugParam.getOperationEnum()) {
                case CONTINUE:
                    DebugUtil.unPark(debugActionId);
                    break;
                case PARAM:
                    DebugUtil.changeParam(debugActionId, debugParam.getParamMap());
                    break;
                case ADD_BREAK_POINT:
                    DebugUtil.addBreakPoint(debugParam.getNodeId());
                    break;
                case REMOVE_BREAK_POINT:
                    DebugUtil.removeBreakPoint(debugParam.getNodeId());
                    break;
                case STOP:
                    DebugUtil.stopDebug();
                    break;
                case RUN_TO:
                    DebugUtil.runToThis(debugActionId, debugParam.getNodeId());
                    break;
                case NEXT_STEP:
                    DebugUtil.addNextStepNodeId(debugActionId, debugParam.getNodeIdList());
                    break;
                default:
                    throw new IllegalArgumentException("未知的操作类型" + debugParam.getOperationEnum());
            }
        }
    }

    @OnError
    public void onError(Throwable t) {
        //什么都不想打印都去掉就好了
        log.info("出现未知错误 ");
        t.printStackTrace();
    }
}