Статьи

Другой взгляд на плагины JConsole


Чарли Хант, один из гуру производительности Sun, находится в городе (где «город» равен «Прага, Чешская Республика»), и мы изучали
плагин JDop JDK для JConsole с целью его переноса на модуль NetBeans. Позвольте мне сказать вам, это было нелегко понять, по крайней мере, на начальном этапе. JTop состоит из класса, расширяющего JConsolePlugin , который интегрируется с JConsole, а также JPanel , который обеспечивает пользовательский интерфейс. В итоге мы полностью сбросили класс JConsolePlugin . Там не было ничего, что нам нужно от этого.

Затем мы переместили большую часть контента из другого класса примера, то есть JPanel , в TopComponent , который интегрируется с платформой NetBeans. Сначала мы просто хотели вызвать JPanel из TopComponent, а затем добавить его в TopComponent . Я думаю, что это, вероятно, не удалось, потому что JPanel могли быть созданы (по крайней мере) дважды. Итак, мы переместили все в TopComponent , запустили платформу NetBeans, и это было:

Мы также установили его в Visual VM :

… и в среду IDE NetBeans:

В последнем случае мы столкнулись с множеством проблем, поскольку, как мы в конечном итоге обнаружили, в Visual Web Pack есть менеджер безопасности, который конфликтует с менеджером безопасности, установленным в tools.jar , где находится API-интерфейс JConsole. Мы тщетно пытались установить политики безопасности в разных местах, прежде чем обнаружили виновника, а затем просто исключили Visual Web Pack из IDE. Тогда плагин JTop работал без проблем.

А поскольку оригинальный плагин JTop использует классный новый класс JDK6 SwingWorker, TopComponent автоматически обновляется в фоновом режиме, а информация о потоках постоянно обновляется.

Вот полный TopComponent. Вы увидите, что мы на самом деле не добавляли код самостоятельно. Нам пришлось жестко кодировать сервер и номер порта, в настоящее время, кажется, нет очевидного способа обнаружить их. Оригинальный плагин JTop, интегрированный с JConsole, так что JConsole предоставил эту информацию. Теперь, когда JConsole больше не отображается, эту информацию нужно каким-то образом получить. Итак, в настоящее время мы жестко закодировали это.

Вот большая часть кода, за исключением стандартного кода TopComponent, созданного мастером компонента Windows, в конце этого фрагмента:

final class JTopTopComponent extends TopComponent {

private static JTopTopComponent instance;
private static final String PREFERRED_ID = "JTopTopComponent";
private MBeanServerConnection server;
private ThreadMXBean tmbean;
private MyTableModel tmodel;
private String hostname;
private int port;

public JTopTopComponent() {

try {
initComponents();
setName(NbBundle.getMessage(JTopTopComponent.class, "CTL_JTopTopComponent"));
setToolTipText(NbBundle.getMessage(JTopTopComponent.class, "HINT_JTopTopComponent"));

//Here we hardcode localhost and port:
main(new String[]{"localhost:1090"});

//Here's the table, from the original plugin
tmodel = new MyTableModel();
JTable table = new JTable(tmodel);
table.setPreferredScrollableViewportSize(new Dimension(500, 300));

// Set the renderer to format Double
table.setDefaultRenderer(Double.class, new DoubleRenderer());
// Add some space
table.setIntercellSpacing(new Dimension(6, 3));
table.setRowHeight(table.getRowHeight() + 4);

// Create the scroll pane and add the table to it.
JScrollPane scrollPane = new JScrollPane(table);

// Add the scroll pane to this panel.
add(scrollPane);


} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}

}

public void main(String[] args) throws Exception {
// Validate the input arguments
if (args.length != 1) {
usage();
}

String[] arg2 = args[0].split(":");
if (arg2.length != 2) {
usage();
}
hostname = arg2[0];
port = -1;
try {
port = Integer.parseInt(arg2[1]);
} catch (NumberFormatException x) {
usage();
}
if (port < 0) {
usage();
}

//Making the connection:
System.setProperty("netbeans.security.nocheck", "true");
MBeanServerConnection serverMBean = connect(hostname, port);
setMBeanServerConnection(serverMBean);

// A timer task to update GUI per each interval
TimerTask timerTask = new TimerTask() {

public void run() {
// Schedule the SwingWorker to update the GUI
newSwingWorker().execute();
}
};

// refresh every 2 seconds
Timer timer = new Timer("JTop Sampling thread");
timer.schedule(timerTask, 0, 2000);

}

// SwingWorker responsible for updating the GUI
//
// It first gets the thread and CPU usage information as a
// background task done by a worker thread so that
// it will not block the event dispatcher thread.
//
// When the worker thread finishes, the event dispatcher
// thread will invoke the done() method which will update
// the UI.
class Worker extends SwingWorker<List<Map.Entry<Long, ThreadInfo>>, Object> {

private MyTableModel tmodel;

Worker(MyTableModel tmodel) {
this.tmodel = tmodel;
}

// Get the current thread info and CPU time
public List<Map.Entry<Long, ThreadInfo>> doInBackground() {
return getThreadList();
}

// fire table data changed to trigger GUI update
// when doInBackground() is finished
protected void done() {
try {
// Set table model with the new thread list
tmodel.setThreadList(get());
// refresh the table model
tmodel.fireTableDataChanged();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
}

private MBeanServerConnection getMBeanServerConnection() {
MBeanServerConnection serverMBean = connect(hostname, port);
return serverMBean;
}

// Return a new SwingWorker for UI update
public SwingWorker<?, ?> newSwingWorker() {
return new Worker(tmodel);
}

// Set the MBeanServerConnection object for communicating
// with the target VM
public void setMBeanServerConnection(MBeanServerConnection mbs) {
this.server = mbs;
try {
this.tmbean = newPlatformMXBeanProxy(server,
THREAD_MXBEAN_NAME,
ThreadMXBean.class);
} catch (IOException e) {
e.printStackTrace();
}
if (!tmbean.isThreadCpuTimeSupported()) {
System.err.println("This VM does not support thread CPU time monitoring");
} else {
tmbean.setThreadCpuTimeEnabled(true);
}
}

class MyTableModel extends AbstractTableModel {

private String[] columnNames = {"ThreadName",
"CPU(sec)",
"State"
};
// List of all threads. The key of each entry is the CPU time
// and its value is the ThreadInfo object with no stack trace.
private List<Map.Entry<Long, ThreadInfo>> threadList =
Collections.EMPTY_LIST;

public MyTableModel() {
}

public int getColumnCount() {
return columnNames.length;
}

public int getRowCount() {
return threadList.size();
}

public String getColumnName(int col) {
return columnNames[col];
}

public Object getValueAt(int row, int col) {
Map.Entry<Long, ThreadInfo> me = threadList.get(row);
switch (col) {
case 0:
// Column 0 shows the thread name
return me.getValue().getThreadName();
case 1:
// Column 1 shows the CPU usage
long ns = me.getKey().longValue();
double sec = ns / 1000000000;
return new Double(sec);
case 2:
// Column 2 shows the thread state
return me.getValue().getThreadState();
default:
return null;
}
}

public Class getColumnClass(int c) {
return getValueAt(0, c).getClass();
}

void setThreadList(List<Map.Entry<Long, ThreadInfo>> list) {
threadList = list;
}
}

/**
* Get the thread list with CPU consumption and the ThreadInfo
* for each thread sorted by the CPU time.
*/
private List<Map.Entry<Long, ThreadInfo>> getThreadList() {
// Get all threads and their ThreadInfo objects
// with no stack trace
long[] tids = tmbean.getAllThreadIds();
ThreadInfo[] tinfos = tmbean.getThreadInfo(tids);

// build a map with key = CPU time and value = ThreadInfo
SortedMap<Long, ThreadInfo> map = new TreeMap<Long, ThreadInfo>();
for (int i = 0; i < tids.length; i++) {
long cpuTime = tmbean.getThreadCpuTime(tids[i]);
// filter out threads that have been terminated
if (cpuTime != -1 && tinfos[i] != null) {
map.put(new Long(cpuTime), tinfos[i]);
}
}

// build the thread list and sort it with CPU time
// in decreasing order
Set<Map.Entry<Long, ThreadInfo>> set = map.entrySet();
List<Map.Entry<Long, ThreadInfo>> list =
new ArrayList<Map.Entry<Long, ThreadInfo>>(set);
Collections.reverse(list);
return list;
}

/**
* Format Double with 4 fraction digits
*/
class DoubleRenderer extends DefaultTableCellRenderer {

NumberFormat formatter;

public DoubleRenderer() {
super();
setHorizontalAlignment(JLabel.RIGHT);
}

public void setValue(Object value) {
if (formatter == null) {
formatter = NumberFormat.getInstance();
formatter.setMinimumFractionDigits(4);
}
setText((value == null) ? "" : formatter.format(value));
}
}

public static MBeanServerConnection connect(String hostname, int port) {
// Create an RMI connector client and connect it to
// the RMI connector server
String urlPath = "/jndi/rmi://" + hostname + ":" + port + "/jmxrmi";
MBeanServerConnection server = null;
try {
JMXServiceURL url = new JMXServiceURL("rmi", "", 0, urlPath);
JMXConnector jmxc = JMXConnectorFactory.connect(url);
server = jmxc.getMBeanServerConnection();
} catch (MalformedURLException e) {
// should not reach here
} catch (IOException e) {
System.err.println("\nCommunication error: " + e.getMessage());
System.exit(1);
}
return server;
}

private static void usage() {
System.out.println("Usage: java JTop <hostname>:<port>");
System.exit(1);
}
//From here on, it's just standard TopComponent boilerplate code:
...
...
...

И это все! (Кстати, больше нет причин для папки META-INF / services с ее содержимым.) Если вы знаете оригинальный плагин JTop, вы узнаете большую часть кода выше. Основной метод указывает, что оригинал должен был работать как отдельное приложение, а также как новая вкладка в JConsole. Вот почему в приведенном выше коде вообще есть метод main. Возможно, нам следует переименовать это в другое, и тогда наша работа здесь закончена.