阿里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);

你可能感兴趣的:(阿里Druid监控页面分析)