【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

Spring Boot Admin 集成诊断利器 Arthas 实践

阿里巴巴云原生发布于 今天 02:14

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

作者 | 阿提说说
来源|阿里巴巴云原生公众号

前言

Arthas 是 Alibaba 开源的 Java 诊断工具,具有实时查看系统的运行状况;查看函数调用参数、返回值和异常;在线热更新代码;秒解决类冲突问题;定位类加载路径;生成热点;通过网页诊断线上应用。如今在各大厂都有广泛应用,也延伸出很多产品。

这里将介绍如何将 Arthas 集成进 Spring Boot 监控平台中。

SpringBoot Admin

为了方便,SpringBoot Admin 简称为 SBA(版本:1.5.x)。

1.5 版本的 SBA 如果要开发插件比较麻烦,需要下载 SBA 的源码包,再按照 Spring-boot-admin-server-ui-hystrix的形式 Copy 一份,由于 JS 使用的是 Angular,本人尝试了很久,虽然掌握了如何开发插件,奈何不会 Angular,遂放弃💀

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

版本:2.x 2.x 版本的 SBA 插件开发,官网有介绍如何开发,JS 使用 Vue,方便很多,由于我们项目还在使用 1.5,所以并没有使用该版本,请读者自行尝试。

不能使用 SBA 的插件进行集成,那还有什么办法呢?😅

SBA 集成

鄙人的办法是将 Arthas 的相关文件直接 Copy 到 Admin 服务中,这些文件都来自 Arthas-all 项目 Tunnel-server。

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

admin 目录结构

1. Arthas 目录

该包下存放的是所有 Arthas 的 Java 文件。

  • Endpoint 包下的文件可以都注释掉,没多大用。
  • ArthasController 这个文件是我自己新建的,用来获取所有注册到 Arthas 的客户端,这在后面是有用的。
  • 其他文件直接 Copy 过来就行。

@RequestMapping("/api/arthas")

@RestController

public class ArthasController {

@Autowired

private TunnelServer tunnelServer;

@RequestMapping(value = "/clients", method = RequestMethod.GET)

public Set<String> getClients() {

Map<String, AgentInfo> agentInfoMap = tunnelServer.getAgentInfoMap();

return agentInfoMap.keySet();

}

}

spring-boot-admin-server-ui

该文件建在 Resources.META-INF 下,Admin 会在启动的时候加载该目录下的文件。

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

2. Resources 目录

  • index.html 覆盖 SBA 原来的首页,在其中添加一个 Arthas 导航

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

<!DOCTYPE html>

<html class="no-js">

<head>

<meta charset="utf-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>Spring Boot Admin</title>

<meta name="description" content="">

<meta name="viewport" content="width=device-width">

<link rel="shortcut icon" type="image/x-icon" href="https://segmentfault.com/a/1190000039167747/img/favicon.png"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/a/1190000039167747/core.css"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/a/1190000039167747/all-modules.css"/>

</head>

<body>

<header class="navbar header--navbar desktop-only">

<div class="navbar-inner">

<div class="container-fluid">

<div class="spring-logo--container">

<a class="spring-logo" href="#"><span></span></a>

</div>

<div class="spring-logo--container">

<a class="spring-boot-logo" href="#"><span></span></a>

</div>

<ul class="nav pull-right">

<!--增加Arthas导航-->

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/a/1190000039167747/arthas/arthas.html">Arthas</a>

</li>

<li ng-repeat="view in mainViews" class="navbar-link" ng-class="{active: $state.includes(view.state)}">

<a ui-sref="{{view.state}}" ng-bind-html="view.title"></a>

</li>

</ul>

</div>

</div>

</header>

<div ui-view></div>

<footer class="footer">

<ul class="inline">

<li><a href="https://codecentric.github.io/spring-boot-admin/@[email protected]" target="_blank">Reference

Guide</a></li>

<li>-</li>

<li><a href="https://github.com/codecentric/spring-boot-admin" target="_blank">Sources</a></li>

<li>-</li>

<li>Code licensed under <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License

2.0</a></li>

</ul>

</footer>

<script type="text/javascript"></script>

<script type="text/javascript">

sbaModules = [];

</script>

<script type="text/javascript"></script>

<script type="text/javascript"></script>

<script type="text/javascript">

angular.element(document).ready(function () {

angular.bootstrap(document, sbaModules.slice(0), {

strictDi: true

});

});

</script>

</body>

</html>

  • Arthas.html

新建页面,用于显示 Arthas 控制台页面。

这个文件中有两个隐藏文本域,这两个用于连接 Arthas 服务端,在页面加载的时候会自动将 Admin 的 Url 赋值给 Ip。

<input type="hidden" id="ip" name="ip" value="127.0.0.1">

<input type="hidden" id="port" name="port" value="19898">

<!DOCTYPE html>

<html class="no-js">

<head>

<meta charset="utf-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>Spring Boot Admin</title>

<meta name="description" content="">

<meta name="viewport" content="width=device-width">

<link rel="shortcut icon" type="image/x-icon" href="https://segmentfault.com/img/favicon.png"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/core.css"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/all-modules.css"/>

<script></script>

<script></script>

<script></script>

<script></script>

<script></script>

<link href="https://segmentfault.com/a/1190000039167747/js/xterm.css" rel="stylesheet" />

<script type="text/javascript">

window.addEventListener('resize', function () {

var terminalSize = getTerminalSize();

ws.send(JSON.stringify({ action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows }));

xterm.resize(terminalSize.cols, terminalSize.rows);

});

</script>

</head>

<body>

<header class="navbar header--navbar desktop-only">

<div class="navbar-inner">

<div class="container-fluid">

<div class="spring-logo--container">

<a class="spring-logo" href="#"><span></span></a>

</div>

<div class="spring-logo--container">

<a class="spring-boot-logo" href="#"><span></span></a>

</div>

<ul class="nav pull-right">

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/a/1190000039167747/arthas.html">Arthas</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/">Applications</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/turbine">Turbine</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/events">Journal</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/about">About</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/logout"><i class="fa fa-2x fa-sign-out" aria-hidden="true"></i></a>

</li>

</ul>

</div>

</div>

</header>

<div ui-view>

<div class="container-fluid">

<form class="form-inline">

<input type="hidden" id="ip" name="ip" value="127.0.0.1">

<input type="hidden" id="port" name="port" value="19898">

Select Application:

<select id="selectServer"></select>

<button class="btn" onclick="startConnect()" type="button"><i class="fa fa-connectdevelop"></i> Connect</button>

<button class="btn" onclick="disconnect()" type="button"><i class="fa fa-search-minus"></i> Disconnect</button>

<button class="btn" onclick="release()" type="button"><i class="fa fa-search-minus"></i> Release</button>

</form>

<div id="terminal-card">

<div id="terminal"></div>

</div>

</div>

</div>

</body>

</html>

  • Arthas.js 存储页面控制的 js

var registerApplications = null;

var applications = null;

$(document).ready(function () {

reloadRegisterApplications();

reloadApplications();

});

/**

* 获取注册的arthas客户端

*/

function reloadRegisterApplications() {

var result = reqSync("/api/arthas/clients", "get");

registerApplications = result;

initSelect("#selectServer", registerApplications, "");

}

/**

* 获取注册的应用

*/

function reloadApplications() {

applications = reqSync("/api/applications", "get");

console.log(applications)

}

/**

* 初始化下拉选择框

*/

function initSelect(uiSelect, list, key) {

$(uiSelect).html('');

var server;

for (var i = 0; i < list.length; i++) {

server = list[i].toLowerCase().split("@");

if ("phantom-admin" === server[0]) continue;

$(uiSelect).append("<option value=" + list[i].toLowerCase() + ">" + server[0] + "</option>");

}

}

/**

* 重置配置文件

*/

function release() {

var currentServer = $("#selectServer").text();

for (var i = 0; i < applications.length; i++) {

serverId = applications[i].id;

serverName = applications[i].name.toLowerCase();

console.log(serverId + "/" + serverName);

if (currentServer === serverName) {

var result = reqSync("/api/applications/" +serverId+ "/env/reset", "post");

alert("env reset success");

}

}

}

function reqSync(url, method) {

var result = null;

$.ajax({

url: url,

type: method,

async: false, //使用同步的方式,true为异步方式

headers: {

'Content-Type': 'application/json;charset=utf8;',

},

success: function (data) {

// console.log(data);

result = data;

},

error: function (data) {

console.log("error");

}

});

return result;

}

  • Web-console.js

修改了连接部分代码,参考一下。

var ws;

var xterm;

/**有修改**/

$(function () {

var url = window.location.href;

var ip = getUrlParam('ip');

var port = getUrlParam('port');

var agentId = getUrlParam('agentId');

if (ip != '' && ip != null) {

$('#ip').val(ip);

} else {

$('#ip').val(window.location.hostname);

}

if (port != '' && port != null) {

$('#port').val(port);

}

if (agentId != '' && agentId != null) {

$('#selectServer').val(agentId);

}

// startConnect(true);

});

/** get params in url **/

function getUrlParam (name, url) {

if (!url) url = window.location.href;

name = name.replace(/[\[\]]/g, '\\$&');

var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),

results = regex.exec(url);

if (!results) return null;

if (!results[2]) return '';

return decodeURIComponent(results[2].replace(/\+/g, ' '));

}

function getCharSize () {

var tempDiv = $('<div />').attr({'role': 'listitem'});

var tempSpan = $('<div />').html('qwertyuiopasdfghjklzxcvbnm');

tempDiv.append(tempSpan);

$("html body").append(tempDiv);

var size = {

width: tempSpan.outerWidth() / 26,

height: tempSpan.outerHeight(),

left: tempDiv.outerWidth() - tempSpan.outerWidth(),

top: tempDiv.outerHeight() - tempSpan.outerHeight(),

};

tempDiv.remove();

return size;

}

function getWindowSize () {

var e = window;

var a = 'inner';

if (!('innerWidth' in window )) {

a = 'client';

e = document.documentElement || document.body;

}

var terminalDiv = document.getElementById("terminal-card");

var terminalDivRect = terminalDiv.getBoundingClientRect();

return {

width: terminalDivRect.width,

height: e[a + 'Height'] - terminalDivRect.top

};

}

function getTerminalSize () {

var charSize = getCharSize();

var windowSize = getWindowSize();

console.log('charsize');

console.log(charSize);

console.log('windowSize');

console.log(windowSize);

return {

cols: Math.floor((windowSize.width - charSize.left) / 10),

rows: Math.floor((windowSize.height - charSize.top) / 17)

};

}

/** init websocket **/

function initWs (ip, port, agentId) {

var protocol= location.protocol === 'https:' ? 'wss://' : 'ws://';

var path = protocol + ip + ':' + port + '/ws?method=connectArthas&id=' + agentId;

ws = new WebSocket(path);

}

/** init xterm **/

function initXterm (cols, rows) {

xterm = new Terminal({

cols: cols,

rows: rows,

screenReaderMode: true,

rendererType: 'canvas',

convertEol: true

});

}

/** 有修改 begin connect **/

function startConnect (silent) {

var ip = $('#ip').val();

var port = $('#port').val();

var agentId = $('#selectServer').val();

if (ip == '' || port == '') {

alert('Ip or port can not be empty');

return;

}

if (agentId == '') {

if (silent) {

return;

}

alert('AgentId can not be empty');

return;

}

if (ws != null) {

alert('Already connected');

return;

}

// init webSocket

initWs(ip, port, agentId);

ws.onerror = function () {

ws.close();

ws = null;

!silent && alert('Connect error');

};

ws.onclose = function (message) {

if (message.code === 2000) {

alert(message.reason);

}

};

ws.onopen = function () {

console.log('open');

$('#fullSc').show();

var terminalSize = getTerminalSize()

console.log('terminalSize')

console.log(terminalSize)

// init xterm

initXterm(terminalSize.cols, terminalSize.rows)

ws.onmessage = function (event) {

if (event.type === 'message') {

var data = event.data;

xterm.write(data);

}

};

xterm.open(document.getElementById('terminal'));

xterm.on('data', function (data) {

ws.send(JSON.stringify({action: 'read', data: data}))

});

ws.send(JSON.stringify({action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows}));

window.setInterval(function () {

if (ws != null && ws.readyState === 1) {

ws.send(JSON.stringify({action: 'read', data: ""}));

}

}, 30000);

}

}

function disconnect () {

try {

ws.close();

ws.onmessage = null;

ws.onclose = null;

ws = null;

xterm.destroy();

$('#fullSc').hide();

alert('Connection was closed successfully!');

} catch (e) {

alert('No connection, please start connect first.');

}

}

/** full screen show **/

function xtermFullScreen () {

var ele = document.getElementById('terminal-card');

requestFullScreen(ele);

}

function requestFullScreen (element) {

var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;

if (requestMethod) {

requestMethod.call(element);

} else if (typeof window.ActiveXObject !== "undefined") {

var wscript = new ActiveXObject("WScript.Shell");

if (wscript !== null) {

wscript.SendKeys("{F11}");

}

}

}

  • 其他文件

    • jquery-3.3.1.min.js 新加 Js
    • copy 过来的 js
    • popper-1.14.6.min.js
    • web-console.js
    • xterm.css
    • xterm.js

  • bootstrap.yml

# arthas端口

arthas:

server:

port: 9898

这样子,admin 端的配置完成了。

客户端配置

  • 在配置中心加入配置

#arthas服务端域名

arthas.tunnel-server = ws://admin域名/ws

#客户端id,应用名@随机值,js会截取前面的应用名

arthas.agent-id = ${spring.application.name}@${random.value}

#arthas开关,可以在需要调式的时候开启,不需要的时候关闭

spring.arthas.enabled = false

  • 需要自动 Attach 的应用中引入 Arthas-spring-boot-starter 需要对 Starter 进行部分修改,要将注册 Arthas 的部分移除,下面是修改后的文件。

这里是将修改后的文件重新打包成 Jar 包,上传到私服,但有些应用会有无法加载 ArthasConfigMap 的情况,可以将这两个文件单独放到项目的公共包中。

@EnableConfigurationProperties({ ArthasProperties.class })

public class ArthasConfiguration {

private static final Logger logger = LoggerFactory.getLogger(ArthasConfiguration.class);

@ConfigurationProperties(prefix = "arthas")

@ConditionalOnMissingBean

@Bean

public HashMap<String, String> arthasConfigMap() {

return new HashMap<String, String>();

}

}

@ConfigurationProperties(prefix = "arthas")

public class ArthasProperties {

private String ip;

private int telnetPort;

private int httpPort;

private String tunnelServer;

private String agentId;

/**

* report executed command

*/

private String statUrl;

/**

* session timeout seconds

*/

private long sessionTimeout;

private String home;

/**

* when arthas agent init error will throw exception by default.

*/

private boolean slientInit = false;

public String getHome() {

return home;

}

public void setHome(String home) {

this.home = home;

}

public boolean isSlientInit() {

return slientInit;

}

public void setSlientInit(boolean slientInit) {

this.slientInit = slientInit;

}

public String getIp() {

return ip;

}

public void setIp(String ip) {

this.ip = ip;

}

public int getTelnetPort() {

return telnetPort;

}

public void setTelnetPort(int telnetPort) {

this.telnetPort = telnetPort;

}

public int getHttpPort() {

return httpPort;

}

public void setHttpPort(int httpPort) {

this.httpPort = httpPort;

}

public String getTunnelServer() {

return tunnelServer;

}

public void setTunnelServer(String tunnelServer) {

this.tunnelServer = tunnelServer;

}

public String getAgentId() {

return agentId;

}

public void setAgentId(String agentId) {

this.agentId = agentId;

}

public String getStatUrl() {

return statUrl;

}

public void setStatUrl(String statUrl) {

this.statUrl = statUrl;

}

public long getSessionTimeout() {

return sessionTimeout;

}

public void setSessionTimeout(long sessionTimeout) {

this.sessionTimeout = sessionTimeout;

}

}

  • 实现开关效果

为了实现开关效果,还需要一个文件用来监听配置文件的改变。

我这里使用的是在 SBA 中改变环境变量,对应服务监听到变量改变,当监听 spring.arthas.enabled 为 true 的时候,注册 Arthas,到下面是代码。

@Component

public class EnvironmentChangeListener implements ApplicationListener<EnvironmentChangeEvent> {

@Autowired

private Environment env;

@Autowired

private Map<String, String> arthasConfigMap;

@Autowired

private ArthasProperties arthasProperties;

@Autowired

private ApplicationContext applicationContext;

@Override

public void onApplicationEvent(EnvironmentChangeEvent event) {

Set<String> keys = event.getKeys();

for (String key : keys) {

if ("spring.arthas.enabled".equals(key)) {

if ("true".equals(env.getProperty(key))) {

registerArthas();

}

}

}

}

private void registerArthas() {

DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

String bean = "arthasAgent";

if (defaultListableBeanFactory.containsBean(bean)) {

((ArthasAgent)defaultListableBeanFactory.getBean(bean)).init();

return;

}

defaultListableBeanFactory.registerSingleton(bean, arthasAgentInit());

}

private ArthasAgent arthasAgentInit() {

arthasConfigMap = StringUtils.removeDashKey(arthasConfigMap);

// 给配置全加上前缀

Map<String, String> mapWithPrefix = new HashMap<String, String>(arthasConfigMap.size());

for (Map.Entry<String, String> entry : arthasConfigMap.entrySet()) {

mapWithPrefix.put("arthas." + entry.getKey(), entry.getValue());

}

final ArthasAgent arthasAgent = new ArthasAgent(mapWithPrefix, arthasProperties.getHome(),

arthasProperties.isSlientInit(), null);

arthasAgent.init();

return arthasAgent;

}

}

结束

到此可以愉快的在 SBA 中调式应用了,看看最后的页面。

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

  • 调式流程

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

流程如下:

  1. 开启 Arthas
  2. 在 Select Application 中选择应用
  3. Connect 连接应用
  4. DisConnect 断开应用
  5. Release 释放配置文件

一些缺陷:

  • 使用 jar 包的方式引入应用,具有一定的侵略性,如果 Arthas 无法启动,会导致应用也无法启动。
  • 如果使用 Docker,需要适当调整 JVM 内存,防止开启 Arthas、调试的时候,内存炸了。
  • 没有使用 SBA 插件的方式集成如上集成仅供参考,请根据自己企业的情况来集成。

Arthas 有奖征文正在进行中!

为了让更多开发者开始用上 Arthas 这个 Java 诊断神器,Arthas 社区联合 JetBrains 推出 Arthas 有奖征文活动:聊聊这些年你和 Arthas 之间的那些事儿。活动仍在火热进行中,点击即可参与,欢迎大家踊跃投稿,参与即有可能获奖!

java容器docker云原生-cloud-nativearthas

阅读 53发布于 今天 02:14

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


阿里巴巴云原生

关注云原生技术趋势,输出最优质云原生内容

avatar

阿里巴巴云原生

阿里巴巴云原生公众号(ID:Alicloudnative)发布云原生技术最新资讯、汇集云原生技术最全内容,定期举办云原生活动、直播,阿里产品及用户最佳实践发布。与你并肩探索云原生技术点滴,分享你需要的云原生内容。

725 声望

57 粉丝

0 条评论

得票时间

avatar

阿里巴巴云原生

阿里巴巴云原生公众号(ID:Alicloudnative)发布云原生技术最新资讯、汇集云原生技术最全内容,定期举办云原生活动、直播,阿里产品及用户最佳实践发布。与你并肩探索云原生技术点滴,分享你需要的云原生内容。

725 声望

57 粉丝

宣传栏

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

作者 | 阿提说说
来源|阿里巴巴云原生公众号

前言

Arthas 是 Alibaba 开源的 Java 诊断工具,具有实时查看系统的运行状况;查看函数调用参数、返回值和异常;在线热更新代码;秒解决类冲突问题;定位类加载路径;生成热点;通过网页诊断线上应用。如今在各大厂都有广泛应用,也延伸出很多产品。

这里将介绍如何将 Arthas 集成进 Spring Boot 监控平台中。

SpringBoot Admin

为了方便,SpringBoot Admin 简称为 SBA(版本:1.5.x)。

1.5 版本的 SBA 如果要开发插件比较麻烦,需要下载 SBA 的源码包,再按照 Spring-boot-admin-server-ui-hystrix的形式 Copy 一份,由于 JS 使用的是 Angular,本人尝试了很久,虽然掌握了如何开发插件,奈何不会 Angular,遂放弃💀

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

版本:2.x 2.x 版本的 SBA 插件开发,官网有介绍如何开发,JS 使用 Vue,方便很多,由于我们项目还在使用 1.5,所以并没有使用该版本,请读者自行尝试。

不能使用 SBA 的插件进行集成,那还有什么办法呢?😅

SBA 集成

鄙人的办法是将 Arthas 的相关文件直接 Copy 到 Admin 服务中,这些文件都来自 Arthas-all 项目 Tunnel-server。

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

admin 目录结构

1. Arthas 目录

该包下存放的是所有 Arthas 的 Java 文件。

  • Endpoint 包下的文件可以都注释掉,没多大用。
  • ArthasController 这个文件是我自己新建的,用来获取所有注册到 Arthas 的客户端,这在后面是有用的。
  • 其他文件直接 Copy 过来就行。

@RequestMapping("/api/arthas")

@RestController

public class ArthasController {

@Autowired

private TunnelServer tunnelServer;

@RequestMapping(value = "/clients", method = RequestMethod.GET)

public Set<String> getClients() {

Map<String, AgentInfo> agentInfoMap = tunnelServer.getAgentInfoMap();

return agentInfoMap.keySet();

}

}

spring-boot-admin-server-ui

该文件建在 Resources.META-INF 下,Admin 会在启动的时候加载该目录下的文件。

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

2. Resources 目录

  • index.html 覆盖 SBA 原来的首页,在其中添加一个 Arthas 导航

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

<!DOCTYPE html>

<html class="no-js">

<head>

<meta charset="utf-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>Spring Boot Admin</title>

<meta name="description" content="">

<meta name="viewport" content="width=device-width">

<link rel="shortcut icon" type="image/x-icon" href="https://segmentfault.com/a/1190000039167747/img/favicon.png"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/a/1190000039167747/core.css"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/a/1190000039167747/all-modules.css"/>

</head>

<body>

<header class="navbar header--navbar desktop-only">

<div class="navbar-inner">

<div class="container-fluid">

<div class="spring-logo--container">

<a class="spring-logo" href="#"><span></span></a>

</div>

<div class="spring-logo--container">

<a class="spring-boot-logo" href="#"><span></span></a>

</div>

<ul class="nav pull-right">

<!--增加Arthas导航-->

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/a/1190000039167747/arthas/arthas.html">Arthas</a>

</li>

<li ng-repeat="view in mainViews" class="navbar-link" ng-class="{active: $state.includes(view.state)}">

<a ui-sref="{{view.state}}" ng-bind-html="view.title"></a>

</li>

</ul>

</div>

</div>

</header>

<div ui-view></div>

<footer class="footer">

<ul class="inline">

<li><a href="https://codecentric.github.io/spring-boot-admin/@[email protected]" target="_blank">Reference

Guide</a></li>

<li>-</li>

<li><a href="https://github.com/codecentric/spring-boot-admin" target="_blank">Sources</a></li>

<li>-</li>

<li>Code licensed under <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License

2.0</a></li>

</ul>

</footer>

<script type="text/javascript"></script>

<script type="text/javascript">

sbaModules = [];

</script>

<script type="text/javascript"></script>

<script type="text/javascript"></script>

<script type="text/javascript">

angular.element(document).ready(function () {

angular.bootstrap(document, sbaModules.slice(0), {

strictDi: true

});

});

</script>

</body>

</html>

  • Arthas.html

新建页面,用于显示 Arthas 控制台页面。

这个文件中有两个隐藏文本域,这两个用于连接 Arthas 服务端,在页面加载的时候会自动将 Admin 的 Url 赋值给 Ip。

<input type="hidden" id="ip" name="ip" value="127.0.0.1">

<input type="hidden" id="port" name="port" value="19898">

<!DOCTYPE html>

<html class="no-js">

<head>

<meta charset="utf-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>Spring Boot Admin</title>

<meta name="description" content="">

<meta name="viewport" content="width=device-width">

<link rel="shortcut icon" type="image/x-icon" href="https://segmentfault.com/img/favicon.png"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/core.css"/>

<link rel="stylesheet" type="text/css" href="https://segmentfault.com/all-modules.css"/>

<script></script>

<script></script>

<script></script>

<script></script>

<script></script>

<link href="https://segmentfault.com/a/1190000039167747/js/xterm.css" rel="stylesheet" />

<script type="text/javascript">

window.addEventListener('resize', function () {

var terminalSize = getTerminalSize();

ws.send(JSON.stringify({ action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows }));

xterm.resize(terminalSize.cols, terminalSize.rows);

});

</script>

</head>

<body>

<header class="navbar header--navbar desktop-only">

<div class="navbar-inner">

<div class="container-fluid">

<div class="spring-logo--container">

<a class="spring-logo" href="#"><span></span></a>

</div>

<div class="spring-logo--container">

<a class="spring-boot-logo" href="#"><span></span></a>

</div>

<ul class="nav pull-right">

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/a/1190000039167747/arthas.html">Arthas</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/">Applications</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/turbine">Turbine</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/events">Journal</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/about">About</a>

</li>

<li class="navbar-link ng-scope">

<a class="ng-binding" href="https://segmentfault.com/#/logout"><i class="fa fa-2x fa-sign-out" aria-hidden="true"></i></a>

</li>

</ul>

</div>

</div>

</header>

<div ui-view>

<div class="container-fluid">

<form class="form-inline">

<input type="hidden" id="ip" name="ip" value="127.0.0.1">

<input type="hidden" id="port" name="port" value="19898">

Select Application:

<select id="selectServer"></select>

<button class="btn" onclick="startConnect()" type="button"><i class="fa fa-connectdevelop"></i> Connect</button>

<button class="btn" onclick="disconnect()" type="button"><i class="fa fa-search-minus"></i> Disconnect</button>

<button class="btn" onclick="release()" type="button"><i class="fa fa-search-minus"></i> Release</button>

</form>

<div id="terminal-card">

<div id="terminal"></div>

</div>

</div>

</div>

</body>

</html>

  • Arthas.js 存储页面控制的 js

var registerApplications = null;

var applications = null;

$(document).ready(function () {

reloadRegisterApplications();

reloadApplications();

});

/**

* 获取注册的arthas客户端

*/

function reloadRegisterApplications() {

var result = reqSync("/api/arthas/clients", "get");

registerApplications = result;

initSelect("#selectServer", registerApplications, "");

}

/**

* 获取注册的应用

*/

function reloadApplications() {

applications = reqSync("/api/applications", "get");

console.log(applications)

}

/**

* 初始化下拉选择框

*/

function initSelect(uiSelect, list, key) {

$(uiSelect).html('');

var server;

for (var i = 0; i < list.length; i++) {

server = list[i].toLowerCase().split("@");

if ("phantom-admin" === server[0]) continue;

$(uiSelect).append("<option value=" + list[i].toLowerCase() + ">" + server[0] + "</option>");

}

}

/**

* 重置配置文件

*/

function release() {

var currentServer = $("#selectServer").text();

for (var i = 0; i < applications.length; i++) {

serverId = applications[i].id;

serverName = applications[i].name.toLowerCase();

console.log(serverId + "/" + serverName);

if (currentServer === serverName) {

var result = reqSync("/api/applications/" +serverId+ "/env/reset", "post");

alert("env reset success");

}

}

}

function reqSync(url, method) {

var result = null;

$.ajax({

url: url,

type: method,

async: false, //使用同步的方式,true为异步方式

headers: {

'Content-Type': 'application/json;charset=utf8;',

},

success: function (data) {

// console.log(data);

result = data;

},

error: function (data) {

console.log("error");

}

});

return result;

}

  • Web-console.js

修改了连接部分代码,参考一下。

var ws;

var xterm;

/**有修改**/

$(function () {

var url = window.location.href;

var ip = getUrlParam('ip');

var port = getUrlParam('port');

var agentId = getUrlParam('agentId');

if (ip != '' && ip != null) {

$('#ip').val(ip);

} else {

$('#ip').val(window.location.hostname);

}

if (port != '' && port != null) {

$('#port').val(port);

}

if (agentId != '' && agentId != null) {

$('#selectServer').val(agentId);

}

// startConnect(true);

});

/** get params in url **/

function getUrlParam (name, url) {

if (!url) url = window.location.href;

name = name.replace(/[\[\]]/g, '\\$&');

var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),

results = regex.exec(url);

if (!results) return null;

if (!results[2]) return '';

return decodeURIComponent(results[2].replace(/\+/g, ' '));

}

function getCharSize () {

var tempDiv = $('<div />').attr({'role': 'listitem'});

var tempSpan = $('<div />').html('qwertyuiopasdfghjklzxcvbnm');

tempDiv.append(tempSpan);

$("html body").append(tempDiv);

var size = {

width: tempSpan.outerWidth() / 26,

height: tempSpan.outerHeight(),

left: tempDiv.outerWidth() - tempSpan.outerWidth(),

top: tempDiv.outerHeight() - tempSpan.outerHeight(),

};

tempDiv.remove();

return size;

}

function getWindowSize () {

var e = window;

var a = 'inner';

if (!('innerWidth' in window )) {

a = 'client';

e = document.documentElement || document.body;

}

var terminalDiv = document.getElementById("terminal-card");

var terminalDivRect = terminalDiv.getBoundingClientRect();

return {

width: terminalDivRect.width,

height: e[a + 'Height'] - terminalDivRect.top

};

}

function getTerminalSize () {

var charSize = getCharSize();

var windowSize = getWindowSize();

console.log('charsize');

console.log(charSize);

console.log('windowSize');

console.log(windowSize);

return {

cols: Math.floor((windowSize.width - charSize.left) / 10),

rows: Math.floor((windowSize.height - charSize.top) / 17)

};

}

/** init websocket **/

function initWs (ip, port, agentId) {

var protocol= location.protocol === 'https:' ? 'wss://' : 'ws://';

var path = protocol + ip + ':' + port + '/ws?method=connectArthas&id=' + agentId;

ws = new WebSocket(path);

}

/** init xterm **/

function initXterm (cols, rows) {

xterm = new Terminal({

cols: cols,

rows: rows,

screenReaderMode: true,

rendererType: 'canvas',

convertEol: true

});

}

/** 有修改 begin connect **/

function startConnect (silent) {

var ip = $('#ip').val();

var port = $('#port').val();

var agentId = $('#selectServer').val();

if (ip == '' || port == '') {

alert('Ip or port can not be empty');

return;

}

if (agentId == '') {

if (silent) {

return;

}

alert('AgentId can not be empty');

return;

}

if (ws != null) {

alert('Already connected');

return;

}

// init webSocket

initWs(ip, port, agentId);

ws.onerror = function () {

ws.close();

ws = null;

!silent && alert('Connect error');

};

ws.onclose = function (message) {

if (message.code === 2000) {

alert(message.reason);

}

};

ws.onopen = function () {

console.log('open');

$('#fullSc').show();

var terminalSize = getTerminalSize()

console.log('terminalSize')

console.log(terminalSize)

// init xterm

initXterm(terminalSize.cols, terminalSize.rows)

ws.onmessage = function (event) {

if (event.type === 'message') {

var data = event.data;

xterm.write(data);

}

};

xterm.open(document.getElementById('terminal'));

xterm.on('data', function (data) {

ws.send(JSON.stringify({action: 'read', data: data}))

});

ws.send(JSON.stringify({action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows}));

window.setInterval(function () {

if (ws != null && ws.readyState === 1) {

ws.send(JSON.stringify({action: 'read', data: ""}));

}

}, 30000);

}

}

function disconnect () {

try {

ws.close();

ws.onmessage = null;

ws.onclose = null;

ws = null;

xterm.destroy();

$('#fullSc').hide();

alert('Connection was closed successfully!');

} catch (e) {

alert('No connection, please start connect first.');

}

}

/** full screen show **/

function xtermFullScreen () {

var ele = document.getElementById('terminal-card');

requestFullScreen(ele);

}

function requestFullScreen (element) {

var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;

if (requestMethod) {

requestMethod.call(element);

} else if (typeof window.ActiveXObject !== "undefined") {

var wscript = new ActiveXObject("WScript.Shell");

if (wscript !== null) {

wscript.SendKeys("{F11}");

}

}

}

  • 其他文件

    • jquery-3.3.1.min.js 新加 Js
    • copy 过来的 js
    • popper-1.14.6.min.js
    • web-console.js
    • xterm.css
    • xterm.js

  • bootstrap.yml

# arthas端口

arthas:

server:

port: 9898

这样子,admin 端的配置完成了。

客户端配置

  • 在配置中心加入配置

#arthas服务端域名

arthas.tunnel-server = ws://admin域名/ws

#客户端id,应用名@随机值,js会截取前面的应用名

arthas.agent-id = ${spring.application.name}@${random.value}

#arthas开关,可以在需要调式的时候开启,不需要的时候关闭

spring.arthas.enabled = false

  • 需要自动 Attach 的应用中引入 Arthas-spring-boot-starter 需要对 Starter 进行部分修改,要将注册 Arthas 的部分移除,下面是修改后的文件。

这里是将修改后的文件重新打包成 Jar 包,上传到私服,但有些应用会有无法加载 ArthasConfigMap 的情况,可以将这两个文件单独放到项目的公共包中。

@EnableConfigurationProperties({ ArthasProperties.class })

public class ArthasConfiguration {

private static final Logger logger = LoggerFactory.getLogger(ArthasConfiguration.class);

@ConfigurationProperties(prefix = "arthas")

@ConditionalOnMissingBean

@Bean

public HashMap<String, String> arthasConfigMap() {

return new HashMap<String, String>();

}

}

@ConfigurationProperties(prefix = "arthas")

public class ArthasProperties {

private String ip;

private int telnetPort;

private int httpPort;

private String tunnelServer;

private String agentId;

/**

* report executed command

*/

private String statUrl;

/**

* session timeout seconds

*/

private long sessionTimeout;

private String home;

/**

* when arthas agent init error will throw exception by default.

*/

private boolean slientInit = false;

public String getHome() {

return home;

}

public void setHome(String home) {

this.home = home;

}

public boolean isSlientInit() {

return slientInit;

}

public void setSlientInit(boolean slientInit) {

this.slientInit = slientInit;

}

public String getIp() {

return ip;

}

public void setIp(String ip) {

this.ip = ip;

}

public int getTelnetPort() {

return telnetPort;

}

public void setTelnetPort(int telnetPort) {

this.telnetPort = telnetPort;

}

public int getHttpPort() {

return httpPort;

}

public void setHttpPort(int httpPort) {

this.httpPort = httpPort;

}

public String getTunnelServer() {

return tunnelServer;

}

public void setTunnelServer(String tunnelServer) {

this.tunnelServer = tunnelServer;

}

public String getAgentId() {

return agentId;

}

public void setAgentId(String agentId) {

this.agentId = agentId;

}

public String getStatUrl() {

return statUrl;

}

public void setStatUrl(String statUrl) {

this.statUrl = statUrl;

}

public long getSessionTimeout() {

return sessionTimeout;

}

public void setSessionTimeout(long sessionTimeout) {

this.sessionTimeout = sessionTimeout;

}

}

  • 实现开关效果

为了实现开关效果,还需要一个文件用来监听配置文件的改变。

我这里使用的是在 SBA 中改变环境变量,对应服务监听到变量改变,当监听 spring.arthas.enabled 为 true 的时候,注册 Arthas,到下面是代码。

@Component

public class EnvironmentChangeListener implements ApplicationListener<EnvironmentChangeEvent> {

@Autowired

private Environment env;

@Autowired

private Map<String, String> arthasConfigMap;

@Autowired

private ArthasProperties arthasProperties;

@Autowired

private ApplicationContext applicationContext;

@Override

public void onApplicationEvent(EnvironmentChangeEvent event) {

Set<String> keys = event.getKeys();

for (String key : keys) {

if ("spring.arthas.enabled".equals(key)) {

if ("true".equals(env.getProperty(key))) {

registerArthas();

}

}

}

}

private void registerArthas() {

DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

String bean = "arthasAgent";

if (defaultListableBeanFactory.containsBean(bean)) {

((ArthasAgent)defaultListableBeanFactory.getBean(bean)).init();

return;

}

defaultListableBeanFactory.registerSingleton(bean, arthasAgentInit());

}

private ArthasAgent arthasAgentInit() {

arthasConfigMap = StringUtils.removeDashKey(arthasConfigMap);

// 给配置全加上前缀

Map<String, String> mapWithPrefix = new HashMap<String, String>(arthasConfigMap.size());

for (Map.Entry<String, String> entry : arthasConfigMap.entrySet()) {

mapWithPrefix.put("arthas." + entry.getKey(), entry.getValue());

}

final ArthasAgent arthasAgent = new ArthasAgent(mapWithPrefix, arthasProperties.getHome(),

arthasProperties.isSlientInit(), null);

arthasAgent.init();

return arthasAgent;

}

}

结束

到此可以愉快的在 SBA 中调式应用了,看看最后的页面。

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

  • 调式流程

【Java】Spring Boot Admin 集成诊断利器 Arthas 实践

流程如下:

  1. 开启 Arthas
  2. 在 Select Application 中选择应用
  3. Connect 连接应用
  4. DisConnect 断开应用
  5. Release 释放配置文件

一些缺陷:

  • 使用 jar 包的方式引入应用,具有一定的侵略性,如果 Arthas 无法启动,会导致应用也无法启动。
  • 如果使用 Docker,需要适当调整 JVM 内存,防止开启 Arthas、调试的时候,内存炸了。
  • 没有使用 SBA 插件的方式集成如上集成仅供参考,请根据自己企业的情况来集成。

Arthas 有奖征文正在进行中!

为了让更多开发者开始用上 Arthas 这个 Java 诊断神器,Arthas 社区联合 JetBrains 推出 Arthas 有奖征文活动:聊聊这些年你和 Arthas 之间的那些事儿。活动仍在火热进行中,点击即可参与,欢迎大家踊跃投稿,参与即有可能获奖!

以上是 【Java】Spring Boot Admin 集成诊断利器 Arthas 实践 的全部内容, 来源链接: utcz.com/a/112789.html

回到顶部