阿里Druid监控页面分析

本文主要介绍Druid监控页面的生成流程及代码手法

监控效果图

以下是Druid自带的监控页面图,主要用于展示在DruidDataSource数据源当中存储的监控信息,这部分监控信息存储在内存中,通过json格式的数据展示到页面上。


Druid监控页面

页面分析

问题1: 页面是如何展示出来的?

步骤一: 先找到资源文件

查看源码jar包可以知道,页面被存放在support文件目录下的http.resources和monitor文件夹中


源码资源目录

我们知道展示html页面最原生的方式就是PrintWriter.print(String)相应的html文本内容到浏览器上,但习惯了使用SpringMVC框架后可能会思维定式的想怎么配置jsp路径,由框架来完成资源展示

步骤二: 找到相关类

通过追溯监控页面的开启的使用方法可以知道,由StatViewServlet WebStatFilter两个类实现了页面展示的特性。

步骤三: 分析类代码逻辑

StatViewServlet与SpringMVC的DispatcherServlet类似,直接继承了HttpServlet进行了方法重写

1. init方法重写
public void init() throws ServletException {
        // 初始化权限属性
        initAuthEnv();
}
  • 从servletConfig中获取username
String paramUserName = getInitParameter(PARAM_NAME_USERNAME);
 if (!StringUtils.isEmpty(paramUserName)) {
      this.username = paramUserName;
}

  • 从servletConfig中获取password
String paramPassword = getInitParameter(PARAM_NAME_PASSWORD);
 if (!StringUtils.isEmpty(paramPassword)) {
      this.password = paramPassword;
}
  • 从servletConfig中获取远程ip地址
String paramRemoteAddressHeader = getInitParameter(PARAM_REMOTE_ADDR);
 if (!StringUtils.isEmpty(paramRemoteAddressHeader)) {
      this.remoteAddressHeader = paramRemoteAddressHeader;
}
  • 从servletConfig获取ip白名单,以,隔开的IP地址
try {
     String param = getInitParameter(PARAM_NAME_ALLOW);
      if (param != null && param.trim().length() != 0) {
          param = param.trim();
          String[] items = param.split(",");
          for (String item : items) {
               if (item == null || item.length() == 0) {
                   continue;
               }
               IPRange ipRange = new IPRange(item);
               allowList.add(ipRange);
           }
      }
 } catch (Exception e) {
     String msg = "initParameter config error, allow : " + getInitParameter(PARAM_NAME_ALLOW);
     LOG.error(msg, e);
}
  • 从servletConfig获取ip黑名单,以,隔开的IP地址
try {
     String param = getInitParameter(PARAM_NAME_DENY);
     if (param != null && param.trim().length() != 0) {
          param = param.trim();
         String[] items = param.split(",");

        for (String item : items) {
            if (item == null || item.length() == 0) {
                 continue;
             }
             IPRange ipRange = new IPRange(item);
             denyList.add(ipRange);
           }
       }
 } catch (Exception e) {
    String msg = "initParameter config error, deny : " + getInitParameter(PARAM_NAME_DENY);
      LOG.error(msg, e);
 }

此处的IPRange对象包含三个属性:ip地址(自定义对象IPAddress)、子网掩码(IPAddress)、和继承的网络前缀(int)

  • 从servletConfig获取重置属性(boolean字串类型)
try {
      String param = getInitParameter(PARAM_NAME_RESET_ENABLE);
      if (param != null && param.trim().length() != 0) {
          param = param.trim();
          boolean resetEnable = Boolean.parseBoolean(param);
          statService.setResetEnable(resetEnable);
     }
} catch (Exception e) {
   String msg = "initParameter config error, resetEnable : " + getInitParameter(PARAM_NAME_RESET_ENABLE);
   LOG.error(msg, e);
 }
  • 从servletConfig获取jmx相关连接信息,包含url、username、password等信息,如果包含相关参数,初始化一条jmx连接
// 获取jmx的连接配置信息
String param = readInitParam(PARAM_NAME_JMX_URL);
if (param != null) {
   jmxUrl = param;
   jmxUsername = readInitParam(PARAM_NAME_JMX_USERNAME);
   jmxPassword = readInitParam(PARAM_NAME_JMX_PASSWORD);
   try {
        // 初始化jmx连接
        initJmxConn();
       } catch (IOException e) {
           LOG.error("init jmx connection error", e);
      }
}
2. service方法重写

service方法由StatViewServlet的父类ResourceServlet重写

  • 根据request取得contextPath、servletPath和RequestURI
// 上下文路径,比如 ""
String contextPath = request.getContextPath();
// servlet路径,比如 /druid
String servletPath = request.getServletPath();
// 请求路径,比如 /druid/index.html
String requestURI = request.getRequestURI();
// 设置编码格式
response.setCharacterEncoding("utf-8");

if (contextPath == null) { // root context
   contextPath = "";
}
// 获取servlet路径
String uri = contextPath + servletPath;
// 获得约定的对应的资源路径为 "" 或者如 /index.html
String path = requestURI.substring(contextPath.length() + servletPath.length());
  • 判断是否有相应的访问权限,比如是否在白名单或者黑名单中;如果没有相应的权限跳转到/nopermit.html页面
 if (!isPermittedRequest(request)) {
     path = "/nopermit.html";
     returnResourceFile(path, uri, response);
     return;
}
  • 如果是登录页面,确认可以登录success或者登录错误error
if ("/submitLogin".equals(path)) {
    // request中获取username和password与servlet当中的进行验证
   String usernameParam = request.getParameter(PARAM_NAME_USERNAME);
   String passwordParam = request.getParameter(PARAM_NAME_PASSWORD);
    if (username.equals(usernameParam) && password.equals(passwordParam)) {
      // 校验通过,向session当中设置username标识符
       request.getSession().setAttribute(SESSION_USER_KEY, username);
       response.getWriter().print("success");
       } else {
      // 不通过输出error
         response.getWriter().print("error");
       }
       return;
}
  • 如果是要求登录的情况(即username!=null的情况),session中没有记录,请求中也没有相应的username和password通过校验,以及不是css、js、img等前端资源,则将页面重定向到登录页面
 if (isRequireAuth() //
     && !ContainsUser(request)//
     && !checkLoginParam(request)//
     && !("/login.html".equals(path) //
          || path.startsWith("/css")//
          || path.startsWith("/js") //
          || path.startsWith("/img"))) {
         if (contextPath.equals("") || contextPath.equals("/")) {
             response.sendRedirect("/druid/login.html");
         } else {
           if ("".equals(path)) {
               response.sendRedirect("druid/login.html");
           } else {
               response.sendRedirect("login.html");
            }
         }
        return;
}
  • 如果为根路径,则将页面重定向到主页index.html
if ("".equals(path)) {
    if (contextPath.equals("") || contextPath.equals("/")) {
        response.sendRedirect("/druid/index.html");
      } else {
          response.sendRedirect("druid/index.html");
      }
      return;
}
if ("/".equals(path)) {
   response.sendRedirect("index.html");
   return;
}
  • 如果是以.json结尾的请求,则将该部分交由方法process处理,返回对应的json字符串
 if (path.contains(".json")) {
     String fullUrl = path;
     if (request.getQueryString() != null && request.getQueryString().length() > 0) {
        fullUrl += "?" + request.getQueryString();
      }
     response.getWriter().print(process(fullUrl));
     return;
}
  • 具体的页面资源请求交由方法returnResourceFile处理
// find file in resources path
returnResourceFile(path, uri, response);
protected void returnResourceFile(String fileName, String uri, HttpServletResponse response) 
 throws ServletException, IOException {
        // 获取文件路径,如 http/resources/index.html
        String filePath = getFilePath(fileName);
        if (filePath.endsWith(".html")) {
            response.setContentType("text/html; charset=utf-8");
        }
        // 如果是页面请求,在输出流中写入对应的byte数组
        if (fileName.endsWith(".jpg")) {
            byte[] bytes = Utils.readByteArrayFromResource(filePath);
            if (bytes != null) {
                response.getOutputStream().write(bytes);
            }
            return;
        }
        // 采用线程上下文classloader读取流,入参为相对路径,不以/开头
        String text = Utils.readFromResource(filePath);
        if (text == null) {
            response.sendRedirect(uri + "/index.html");
            return;
        }
        // 根据文件类型的不同,设置不同的contentType
        if (fileName.endsWith(".css")) {
            response.setContentType("text/css;charset=utf-8");
        } else if (fileName.endsWith(".js")) {
            response.setContentType("text/javascript;charset=utf-8");
        }
        // 写回response
        response.getWriter().write(text);
    }
Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,147评论 19 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,395评论 11 349
  • 公众号回复“VM”获取激活密钥公众号名称:暧玩游戏 注意是ai不是暖 软件简介 V...
    搞事团阅读 3,443评论 0 0
  • 目光所及的世界 是有海的地方 看不到的不会是尽头 海浪奔走 沙在瞩目 感慨如云 哀叹的边界 郁郁葱葱 响彻天空的色...
    鱼虫它阅读 171评论 2 3
  • 2018年5月1日 星期二 晴 又坐了三天的板凳,粗粗算一下大概一天十小时!腰骨PP都坐痛了……但一个字:值! ...
    Maggieyip阅读 235评论 0 0