本文主要介绍Druid监控页面的生成流程及代码手法
监控效果图
以下是Druid自带的监控页面图,主要用于展示在DruidDataSource数据源当中存储的监控信息,这部分监控信息存储在内存中,通过json格式的数据展示到页面上。
页面分析
问题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);