概述

VisualVM提供了强大的插件架构,允许开发者扩展其功能以满足特定的监控和分析需求。本章将详细介绍如何开发、部署和管理VisualVM插件。

VisualVM插件架构

插件系统概述

VisualVM基于NetBeans平台构建,采用模块化架构:

  • 模块系统:基于OSGi规范的模块化框架
  • API层:提供丰富的扩展点和服务接口
  • 插件管理:支持插件的动态加载、卸载和更新
  • 服务发现:自动发现和注册插件服务

核心组件

1. 应用程序类型(Application Types)

// CustomApplicationType.java
import org.graalvm.visualvm.application.Application;
import org.graalvm.visualvm.application.type.ApplicationType;
import org.graalvm.visualvm.application.type.ApplicationTypeFactory;
import org.graalvm.visualvm.application.jvm.Jvm;

public class CustomApplicationType extends ApplicationType {
    
    private static final String TYPE_NAME = "Custom Application";
    private static final String TYPE_VERSION = "1.0";
    private static final String TYPE_DESCRIPTION = "自定义应用程序类型";
    
    public CustomApplicationType(Application application) {
        super(application, TYPE_NAME, TYPE_VERSION, TYPE_DESCRIPTION);
    }
    
    @Override
    public String getName() {
        return TYPE_NAME;
    }
    
    @Override
    public String getVersion() {
        return TYPE_VERSION;
    }
    
    @Override
    public String getDescription() {
        return TYPE_DESCRIPTION;
    }
    
    // 工厂类
    public static class Factory extends ApplicationTypeFactory {
        
        @Override
        public ApplicationType createApplicationType(Application application) {
            // 检查应用程序是否符合自定义类型的条件
            if (isCustomApplication(application)) {
                return new CustomApplicationType(application);
            }
            return null;
        }
        
        private boolean isCustomApplication(Application application) {
            Jvm jvm = Jvm.getJVMFor(application);
            if (jvm != null && jvm.isGetSystemPropertiesSupported()) {
                String mainClass = jvm.getMainClass();
                // 根据主类名判断是否为自定义应用
                return mainClass != null && mainClass.contains("CustomApp");
            }
            return false;
        }
    }
}

2. 数据源(Data Sources)

// CustomDataSource.java
import org.graalvm.visualvm.core.datasource.DataSource;
import org.graalvm.visualvm.core.datasource.descriptor.DataSourceDescriptor;
import org.graalvm.visualvm.core.datasource.descriptor.DataSourceDescriptorFactory;
import org.graalvm.visualvm.application.Application;

public class CustomDataSource extends DataSource {
    
    private final Application application;
    private final String customData;
    
    public CustomDataSource(Application application, String customData) {
        super(application);
        this.application = application;
        this.customData = customData;
    }
    
    public Application getApplication() {
        return application;
    }
    
    public String getCustomData() {
        return customData;
    }
    
    // 描述符类
    public static class Descriptor extends DataSourceDescriptor<CustomDataSource> {
        
        private static final String IMAGE_PATH = "org/example/resources/custom_icon.png";
        
        public Descriptor(CustomDataSource dataSource) {
            super(dataSource, "Custom Data", "自定义数据源", null, 0);
        }
        
        @Override
        public String getName() {
            return "Custom: " + getDataSource().getCustomData();
        }
        
        @Override
        public String getDescription() {
            return "自定义数据源: " + getDataSource().getCustomData();
        }
    }
    
    // 描述符工厂
    public static class DescriptorFactory extends DataSourceDescriptorFactory<CustomDataSource> {
        
        @Override
        public DataSourceDescriptor<CustomDataSource> createDataSourceDescriptor(CustomDataSource dataSource) {
            return new Descriptor(dataSource);
        }
    }
}

3. 视图(Views)

// CustomView.java
import org.graalvm.visualvm.core.ui.DataSourceView;
import org.graalvm.visualvm.core.ui.components.DataViewComponent;
import org.graalvm.visualvm.application.Application;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class CustomView extends DataSourceView {
    
    private final Application application;
    private DataViewComponent dataViewComponent;
    private JLabel statusLabel;
    private JButton refreshButton;
    
    public CustomView(Application application) {
        super(application, "Custom View", null, 0, false);
        this.application = application;
    }
    
    @Override
    protected DataViewComponent createComponent() {
        // 创建主面板
        JPanel mainPanel = new JPanel(new BorderLayout());
        
        // 创建工具栏
        JPanel toolbarPanel = createToolbar();
        mainPanel.add(toolbarPanel, BorderLayout.NORTH);
        
        // 创建内容面板
        JPanel contentPanel = createContentPanel();
        mainPanel.add(contentPanel, BorderLayout.CENTER);
        
        // 创建状态面板
        JPanel statusPanel = createStatusPanel();
        mainPanel.add(statusPanel, BorderLayout.SOUTH);
        
        // 创建DataViewComponent
        DataViewComponent.MasterView masterView = new DataViewComponent.MasterView(
            "Custom Application Monitor", null, mainPanel);
        
        DataViewComponent.MasterViewConfiguration masterConfig = 
            new DataViewComponent.MasterViewConfiguration(false);
        
        dataViewComponent = new DataViewComponent(masterView, masterConfig);
        
        // 添加详细视图
        addDetailViews();
        
        return dataViewComponent;
    }
    
    private JPanel createToolbar() {
        JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT));
        
        refreshButton = new JButton("刷新");
        refreshButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                refreshData();
            }
        });
        
        JButton configButton = new JButton("配置");
        configButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                showConfigDialog();
            }
        });
        
        toolbar.add(refreshButton);
        toolbar.add(configButton);
        
        return toolbar;
    }
    
    private JPanel createContentPanel() {
        JPanel content = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        
        // 应用信息
        gbc.gridx = 0; gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.insets = new Insets(5, 5, 5, 5);
        content.add(new JLabel("应用程序:"), gbc);
        
        gbc.gridx = 1;
        content.add(new JLabel(application.getId()), gbc);
        
        // 自定义监控数据
        gbc.gridx = 0; gbc.gridy = 1;
        content.add(new JLabel("监控状态:"), gbc);
        
        statusLabel = new JLabel("正在加载...");
        gbc.gridx = 1;
        content.add(statusLabel, gbc);
        
        return content;
    }
    
    private JPanel createStatusPanel() {
        JPanel status = new JPanel(new FlowLayout(FlowLayout.LEFT));
        status.add(new JLabel("就绪"));
        return status;
    }
    
    private void addDetailViews() {
        // 添加性能详细视图
        JPanel performancePanel = createPerformancePanel();
        DataViewComponent.DetailsView performanceView = new DataViewComponent.DetailsView(
            "性能监控", null, 10, performancePanel, null);
        dataViewComponent.addDetailsView(performanceView, DataViewComponent.TOP_LEFT);
        
        // 添加配置详细视图
        JPanel configPanel = createConfigPanel();
        DataViewComponent.DetailsView configView = new DataViewComponent.DetailsView(
            "配置信息", null, 20, configPanel, null);
        dataViewComponent.addDetailsView(configView, DataViewComponent.TOP_RIGHT);
    }
    
    private JPanel createPerformancePanel() {
        JPanel panel = new JPanel(new BorderLayout());
        
        // 创建性能图表
        JPanel chartPanel = new JPanel();
        chartPanel.setPreferredSize(new Dimension(300, 200));
        chartPanel.setBorder(BorderFactory.createTitledBorder("性能趋势"));
        
        // 这里可以集成JFreeChart或其他图表库
        JLabel chartPlaceholder = new JLabel("性能图表", JLabel.CENTER);
        chartPanel.add(chartPlaceholder);
        
        panel.add(chartPanel, BorderLayout.CENTER);
        
        return panel;
    }
    
    private JPanel createConfigPanel() {
        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        
        gbc.gridx = 0; gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.insets = new Insets(2, 2, 2, 2);
        
        panel.add(new JLabel("监控间隔:"), gbc);
        gbc.gridx = 1;
        panel.add(new JLabel("5秒"), gbc);
        
        gbc.gridx = 0; gbc.gridy = 1;
        panel.add(new JLabel("数据保留:"), gbc);
        gbc.gridx = 1;
        panel.add(new JLabel("1小时"), gbc);
        
        return panel;
    }
    
    private void refreshData() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                statusLabel.setText("数据已刷新 - " + new java.util.Date());
            }
        });
    }
    
    private void showConfigDialog() {
        JOptionPane.showMessageDialog(dataViewComponent.getView(), 
            "配置对话框\n这里可以添加自定义配置选项", 
            "配置", JOptionPane.INFORMATION_MESSAGE);
    }
}

4. 视图提供者(View Providers)

// CustomViewProvider.java
import org.graalvm.visualvm.core.ui.DataSourceViewProvider;
import org.graalvm.visualvm.core.ui.DataSourceView;
import org.graalvm.visualvm.application.Application;
import org.graalvm.visualvm.application.type.ApplicationType;

public class CustomViewProvider extends DataSourceViewProvider<Application> {
    
    private static final int VIEW_POSITION = 100;
    
    @Override
    public boolean supportsViewFor(Application application) {
        // 检查是否支持该应用程序
        ApplicationType appType = ApplicationType.getApplicationTypeFor(application);
        return appType instanceof CustomApplicationType;
    }
    
    @Override
    public DataSourceView createView(Application application) {
        return new CustomView(application);
    }
    
    @Override
    public int getViewPosition(Application application) {
        return VIEW_POSITION;
    }
}

插件开发环境搭建

开发工具准备

  1. NetBeans IDE:推荐使用NetBeans进行插件开发
  2. VisualVM源码:从GitHub获取VisualVM源码
  3. Maven:用于构建和依赖管理
  4. JDK:Java 8或更高版本

项目结构

custom-visualvm-plugin/
├── pom.xml
├── src/
│   └── main/
│       ├── java/
│       │   └── org/
│       │       └── example/
│       │           └── visualvm/
│       │               ├── CustomApplicationType.java
│       │               ├── CustomDataSource.java
│       │               ├── CustomView.java
│       │               └── CustomViewProvider.java
│       └── resources/
│           ├── META-INF/
│           │   └── services/
│           │       ├── org.graalvm.visualvm.application.type.ApplicationTypeFactory
│           │       ├── org.graalvm.visualvm.core.datasource.descriptor.DataSourceDescriptorFactory
│           │       └── org.graalvm.visualvm.core.ui.DataSourceViewProvider
│           └── org/
│               └── example/
│                   └── resources/
│                       └── custom_icon.png
└── nbproject/
    └── project.xml

Maven配置

<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>org.example</groupId>
    <artifactId>custom-visualvm-plugin</artifactId>
    <version>1.0.0</version>
    <packaging>nbm</packaging>
    
    <name>Custom VisualVM Plugin</name>
    <description>自定义VisualVM插件示例</description>
    
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <netbeans.version>RELEASE120</netbeans.version>
        <visualvm.version>2.1.4</visualvm.version>
    </properties>
    
    <repositories>
        <repository>
            <id>netbeans</id>
            <name>NetBeans Repository</name>
            <url>https://netbeans.osuosl.org/content/repositories/releases/</url>
        </repository>
    </repositories>
    
    <dependencies>
        <!-- VisualVM Core -->
        <dependency>
            <groupId>org.graalvm.visualvm</groupId>
            <artifactId>visualvm-core</artifactId>
            <version>${visualvm.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- VisualVM Application -->
        <dependency>
            <groupId>org.graalvm.visualvm</groupId>
            <artifactId>visualvm-application</artifactId>
            <version>${visualvm.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- NetBeans Platform -->
        <dependency>
            <groupId>org.netbeans.api</groupId>
            <artifactId>org-netbeans-api-annotations-common</artifactId>
            <version>${netbeans.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.netbeans.api</groupId>
            <artifactId>org-openide-util</artifactId>
            <version>${netbeans.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.netbeans.api</groupId>
            <artifactId>org-openide-util-lookup</artifactId>
            <version>${netbeans.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- NetBeans Module Plugin -->
            <plugin>
                <groupId>org.apache.netbeans.utilities</groupId>
                <artifactId>nbm-maven-plugin</artifactId>
                <version>4.5</version>
                <extensions>true</extensions>
                <configuration>
                    <moduleType>normal</moduleType>
                    <codeNameBase>org.example.visualvm.custom</codeNameBase>
                    <publicPackages>
                        <publicPackage>org.example.visualvm.*</publicPackage>
                    </publicPackages>
                </configuration>
            </plugin>
            
            <!-- Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

服务注册

src/main/resources/META-INF/services/目录下创建服务注册文件:

# org.graalvm.visualvm.application.type.ApplicationTypeFactory
org.example.visualvm.CustomApplicationType$Factory

# org.graalvm.visualvm.core.datasource.descriptor.DataSourceDescriptorFactory
org.example.visualvm.CustomDataSource$DescriptorFactory

# org.graalvm.visualvm.core.ui.DataSourceViewProvider
org.example.visualvm.CustomViewProvider

高级插件功能

1. 自定义监控数据收集

// CustomDataCollector.java
import org.graalvm.visualvm.application.Application;
import org.graalvm.visualvm.application.jvm.Jvm;
import org.graalvm.visualvm.application.jvm.JvmFactory;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

public class CustomDataCollector {
    
    private final Application application;
    private final ScheduledExecutorService scheduler;
    private final List<DataCollectionListener> listeners;
    private volatile boolean collecting = false;
    
    public CustomDataCollector(Application application) {
        this.application = application;
        this.scheduler = Executors.newSingleThreadScheduledExecutor();
        this.listeners = new CopyOnWriteArrayList<>();
    }
    
    public void startCollection() {
        if (!collecting) {
            collecting = true;
            scheduler.scheduleAtFixedRate(this::collectData, 0, 5, TimeUnit.SECONDS);
        }
    }
    
    public void stopCollection() {
        collecting = false;
        scheduler.shutdown();
    }
    
    public void addListener(DataCollectionListener listener) {
        listeners.add(listener);
    }
    
    public void removeListener(DataCollectionListener listener) {
        listeners.remove(listener);
    }
    
    private void collectData() {
        try {
            CustomMetrics metrics = gatherMetrics();
            notifyListeners(metrics);
        } catch (Exception e) {
            // 处理收集错误
            System.err.println("数据收集失败: " + e.getMessage());
        }
    }
    
    private CustomMetrics gatherMetrics() {
        Jvm jvm = JvmFactory.getJVMFor(application);
        
        CustomMetrics metrics = new CustomMetrics();
        metrics.timestamp = System.currentTimeMillis();
        
        if (jvm != null) {
            // 收集JVM指标
            if (jvm.isMemoryMonitoringSupported()) {
                metrics.heapUsed = jvm.getHeapMemoryUsage().getUsed();
                metrics.heapMax = jvm.getHeapMemoryUsage().getMax();
            }
            
            if (jvm.isThreadMonitoringSupported()) {
                metrics.threadCount = jvm.getThreadCount();
            }
            
            // 收集自定义指标
            metrics.customValue = collectCustomMetric();
        }
        
        return metrics;
    }
    
    private double collectCustomMetric() {
        // 实现自定义指标收集逻辑
        // 例如:通过JMX、HTTP API等方式收集应用特定的指标
        return Math.random() * 100; // 示例数据
    }
    
    private void notifyListeners(CustomMetrics metrics) {
        for (DataCollectionListener listener : listeners) {
            try {
                listener.onDataCollected(metrics);
            } catch (Exception e) {
                System.err.println("通知监听器失败: " + e.getMessage());
            }
        }
    }
    
    // 自定义指标数据类
    public static class CustomMetrics {
        public long timestamp;
        public long heapUsed;
        public long heapMax;
        public int threadCount;
        public double customValue;
        
        @Override
        public String toString() {
            return String.format("CustomMetrics{timestamp=%d, heapUsed=%d, heapMax=%d, threadCount=%d, customValue=%.2f}",
                timestamp, heapUsed, heapMax, threadCount, customValue);
        }
    }
    
    // 数据收集监听器接口
    public interface DataCollectionListener {
        void onDataCollected(CustomMetrics metrics);
    }
}

2. 图表集成

// CustomChartPanel.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

public class CustomChartPanel extends JPanel {
    
    private final List<DataPoint> dataPoints;
    private final int maxDataPoints;
    private Color lineColor = Color.BLUE;
    private Color backgroundColor = Color.WHITE;
    private Color gridColor = Color.LIGHT_GRAY;
    
    public CustomChartPanel() {
        this(100); // 默认保留100个数据点
    }
    
    public CustomChartPanel(int maxDataPoints) {
        this.maxDataPoints = maxDataPoints;
        this.dataPoints = new CopyOnWriteArrayList<>();
        setPreferredSize(new Dimension(400, 200));
        setBackground(backgroundColor);
        
        // 添加右键菜单
        setupContextMenu();
    }
    
    public void addDataPoint(long timestamp, double value) {
        synchronized (dataPoints) {
            dataPoints.add(new DataPoint(timestamp, value));
            
            // 保持数据点数量在限制内
            while (dataPoints.size() > maxDataPoints) {
                dataPoints.remove(0);
            }
        }
        
        SwingUtilities.invokeLater(this::repaint);
    }
    
    public void clearData() {
        synchronized (dataPoints) {
            dataPoints.clear();
        }
        repaint();
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        
        int width = getWidth();
        int height = getHeight();
        int margin = 20;
        
        // 绘制背景
        g2d.setColor(backgroundColor);
        g2d.fillRect(0, 0, width, height);
        
        // 绘制网格
        drawGrid(g2d, width, height, margin);
        
        // 绘制数据线
        drawDataLine(g2d, width, height, margin);
        
        // 绘制坐标轴
        drawAxes(g2d, width, height, margin);
        
        g2d.dispose();
    }
    
    private void drawGrid(Graphics2D g2d, int width, int height, int margin) {
        g2d.setColor(gridColor);
        g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{2, 2}, 0));
        
        // 垂直网格线
        for (int i = 0; i <= 10; i++) {
            int x = margin + (width - 2 * margin) * i / 10;
            g2d.drawLine(x, margin, x, height - margin);
        }
        
        // 水平网格线
        for (int i = 0; i <= 10; i++) {
            int y = margin + (height - 2 * margin) * i / 10;
            g2d.drawLine(margin, y, width - margin, y);
        }
    }
    
    private void drawDataLine(Graphics2D g2d, int width, int height, int margin) {
        synchronized (dataPoints) {
            if (dataPoints.size() < 2) return;
            
            g2d.setColor(lineColor);
            g2d.setStroke(new BasicStroke(2));
            
            // 计算数据范围
            double minValue = dataPoints.stream().mapToDouble(p -> p.value).min().orElse(0);
            double maxValue = dataPoints.stream().mapToDouble(p -> p.value).max().orElse(100);
            double valueRange = maxValue - minValue;
            if (valueRange == 0) valueRange = 1;
            
            long minTime = dataPoints.get(0).timestamp;
            long maxTime = dataPoints.get(dataPoints.size() - 1).timestamp;
            long timeRange = maxTime - minTime;
            if (timeRange == 0) timeRange = 1;
            
            // 绘制数据线
            for (int i = 1; i < dataPoints.size(); i++) {
                DataPoint prev = dataPoints.get(i - 1);
                DataPoint curr = dataPoints.get(i);
                
                int x1 = margin + (int) ((prev.timestamp - minTime) * (width - 2 * margin) / timeRange);
                int y1 = height - margin - (int) ((prev.value - minValue) * (height - 2 * margin) / valueRange);
                
                int x2 = margin + (int) ((curr.timestamp - minTime) * (width - 2 * margin) / timeRange);
                int y2 = height - margin - (int) ((curr.value - minValue) * (height - 2 * margin) / valueRange);
                
                g2d.drawLine(x1, y1, x2, y2);
            }
        }
    }
    
    private void drawAxes(Graphics2D g2d, int width, int height, int margin) {
        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(1));
        
        // X轴
        g2d.drawLine(margin, height - margin, width - margin, height - margin);
        
        // Y轴
        g2d.drawLine(margin, margin, margin, height - margin);
        
        // 添加标签
        g2d.setFont(new Font("SansSerif", Font.PLAIN, 10));
        
        synchronized (dataPoints) {
            if (!dataPoints.isEmpty()) {
                double minValue = dataPoints.stream().mapToDouble(p -> p.value).min().orElse(0);
                double maxValue = dataPoints.stream().mapToDouble(p -> p.value).max().orElse(100);
                
                // Y轴标签
                g2d.drawString(String.format("%.1f", maxValue), 5, margin + 5);
                g2d.drawString(String.format("%.1f", minValue), 5, height - margin - 5);
            }
        }
    }
    
    private void setupContextMenu() {
        JPopupMenu contextMenu = new JPopupMenu();
        
        JMenuItem clearItem = new JMenuItem("清除数据");
        clearItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                clearData();
            }
        });
        
        JMenuItem colorItem = new JMenuItem("更改颜色");
        colorItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                Color newColor = JColorChooser.showDialog(CustomChartPanel.this, "选择线条颜色", lineColor);
                if (newColor != null) {
                    lineColor = newColor;
                    repaint();
                }
            }
        });
        
        contextMenu.add(clearItem);
        contextMenu.add(colorItem);
        
        setComponentPopupMenu(contextMenu);
    }
    
    // 数据点类
    private static class DataPoint {
        final long timestamp;
        final double value;
        
        DataPoint(long timestamp, double value) {
            this.timestamp = timestamp;
            this.value = value;
        }
    }
}

3. 配置管理

// PluginConfiguration.java
import java.util.prefs.Preferences;
import java.awt.Color;

public class PluginConfiguration {
    
    private static final String PREF_NODE = "org/example/visualvm/custom";
    private static final Preferences prefs = Preferences.userRoot().node(PREF_NODE);
    
    // 配置键
    private static final String KEY_MONITORING_INTERVAL = "monitoring.interval";
    private static final String KEY_DATA_RETENTION = "data.retention";
    private static final String KEY_CHART_COLOR = "chart.color";
    private static final String KEY_AUTO_REFRESH = "auto.refresh";
    private static final String KEY_ALERT_THRESHOLD = "alert.threshold";
    
    // 默认值
    private static final int DEFAULT_MONITORING_INTERVAL = 5; // 秒
    private static final int DEFAULT_DATA_RETENTION = 3600; // 秒
    private static final int DEFAULT_CHART_COLOR = Color.BLUE.getRGB();
    private static final boolean DEFAULT_AUTO_REFRESH = true;
    private static final double DEFAULT_ALERT_THRESHOLD = 80.0;
    
    // 获取配置值
    public static int getMonitoringInterval() {
        return prefs.getInt(KEY_MONITORING_INTERVAL, DEFAULT_MONITORING_INTERVAL);
    }
    
    public static void setMonitoringInterval(int interval) {
        prefs.putInt(KEY_MONITORING_INTERVAL, interval);
    }
    
    public static int getDataRetention() {
        return prefs.getInt(KEY_DATA_RETENTION, DEFAULT_DATA_RETENTION);
    }
    
    public static void setDataRetention(int retention) {
        prefs.putInt(KEY_DATA_RETENTION, retention);
    }
    
    public static Color getChartColor() {
        int rgb = prefs.getInt(KEY_CHART_COLOR, DEFAULT_CHART_COLOR);
        return new Color(rgb);
    }
    
    public static void setChartColor(Color color) {
        prefs.putInt(KEY_CHART_COLOR, color.getRGB());
    }
    
    public static boolean isAutoRefreshEnabled() {
        return prefs.getBoolean(KEY_AUTO_REFRESH, DEFAULT_AUTO_REFRESH);
    }
    
    public static void setAutoRefreshEnabled(boolean enabled) {
        prefs.putBoolean(KEY_AUTO_REFRESH, enabled);
    }
    
    public static double getAlertThreshold() {
        return prefs.getDouble(KEY_ALERT_THRESHOLD, DEFAULT_ALERT_THRESHOLD);
    }
    
    public static void setAlertThreshold(double threshold) {
        prefs.putDouble(KEY_ALERT_THRESHOLD, threshold);
    }
    
    // 重置所有配置
    public static void resetToDefaults() {
        try {
            prefs.clear();
        } catch (Exception e) {
            System.err.println("重置配置失败: " + e.getMessage());
        }
    }
    
    // 导出配置
    public static String exportConfiguration() {
        StringBuilder sb = new StringBuilder();
        sb.append("# Custom VisualVM Plugin Configuration\n");
        sb.append("monitoring.interval=").append(getMonitoringInterval()).append("\n");
        sb.append("data.retention=").append(getDataRetention()).append("\n");
        sb.append("chart.color=").append(getChartColor().getRGB()).append("\n");
        sb.append("auto.refresh=").append(isAutoRefreshEnabled()).append("\n");
        sb.append("alert.threshold=").append(getAlertThreshold()).append("\n");
        return sb.toString();
    }
}

插件构建与部署

构建插件

# 编译插件
mvn clean compile

# 构建NBM文件
mvn package

# 生成的插件文件位于 target/ 目录
# custom-visualvm-plugin-1.0.0.nbm

安装插件

  1. 通过VisualVM界面安装

    • 启动VisualVM
    • 选择 “工具” -> “插件”
    • 点击 “已下载” 标签页
    • 点击 “添加插件” 按钮
    • 选择生成的.nbm文件
    • 按照向导完成安装
  2. 手动安装

    # 将NBM文件复制到VisualVM插件目录
    cp target/custom-visualvm-plugin-1.0.0.nbm $VISUALVM_HOME/etc/
    

    插件分发

    1. 创建更新中心

    <!-- update-center.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <module_updates timestamp="1234567890">
    <module codenamebase="org.example.visualvm.custom"
            distribution="custom-visualvm-plugin-1.0.0.nbm"
            downloadsize="123456"
            homepage="https://example.org/plugin"
            license="Apache-2.0"
            moduleauthor="Example Developer"
            needsrestart="true"
            releasedate="2023/12/01">
        <manifest OpenIDE-Module="org.example.visualvm.custom"
                  OpenIDE-Module-Display-Category="Monitoring"
                  OpenIDE-Module-Implementation-Version="1.0.0"
                  OpenIDE-Module-Long-Description="自定义VisualVM监控插件,提供应用程序特定的监控功能。"
                  OpenIDE-Module-Name="Custom VisualVM Plugin"
                  OpenIDE-Module-Short-Description="自定义监控插件"
                  OpenIDE-Module-Specification-Version="1.0"/>
    </module>
    </module_updates>
    

2. 发布到GitHub Releases

# .github/workflows/release.yml
name: Release Plugin

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up JDK 8
      uses: actions/setup-java@v2
      with:
        java-version: '8'
        distribution: 'adopt'
    
    - name: Build plugin
      run: mvn clean package
    
    - name: Create Release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        draft: false
        prerelease: false
    
    - name: Upload NBM
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ./target/custom-visualvm-plugin-1.0.0.nbm
        asset_name: custom-visualvm-plugin-1.0.0.nbm
        asset_content_type: application/octet-stream

实践练习

练习1:创建简单监控插件

创建一个监控JVM线程数的简单插件:

// ThreadMonitorPlugin.java
import org.graalvm.visualvm.core.ui.DataSourceView;
import org.graalvm.visualvm.core.ui.components.DataViewComponent;
import org.graalvm.visualvm.application.Application;
import org.graalvm.visualvm.application.jvm.Jvm;
import org.graalvm.visualvm.application.jvm.JvmFactory;
import javax.swing.*;
import java.awt.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadMonitorPlugin extends DataSourceView {
    
    private final Application application;
    private JLabel threadCountLabel;
    private JLabel peakThreadCountLabel;
    private CustomChartPanel chartPanel;
    private ScheduledExecutorService scheduler;
    
    public ThreadMonitorPlugin(Application application) {
        super(application, "Thread Monitor", null, 0, false);
        this.application = application;
    }
    
    @Override
    protected DataViewComponent createComponent() {
        JPanel mainPanel = new JPanel(new BorderLayout());
        
        // 创建信息面板
        JPanel infoPanel = createInfoPanel();
        mainPanel.add(infoPanel, BorderLayout.NORTH);
        
        // 创建图表面板
        chartPanel = new CustomChartPanel();
        chartPanel.setBorder(BorderFactory.createTitledBorder("线程数趋势"));
        mainPanel.add(chartPanel, BorderLayout.CENTER);
        
        // 启动数据收集
        startDataCollection();
        
        DataViewComponent.MasterView masterView = new DataViewComponent.MasterView(
            "Thread Monitor", null, mainPanel);
        
        DataViewComponent.MasterViewConfiguration masterConfig = 
            new DataViewComponent.MasterViewConfiguration(false);
        
        return new DataViewComponent(masterView, masterConfig);
    }
    
    private JPanel createInfoPanel() {
        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        
        gbc.gridx = 0; gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.insets = new Insets(5, 5, 5, 5);
        
        panel.add(new JLabel("当前线程数:"), gbc);
        gbc.gridx = 1;
        threadCountLabel = new JLabel("--");
        panel.add(threadCountLabel, gbc);
        
        gbc.gridx = 0; gbc.gridy = 1;
        panel.add(new JLabel("峰值线程数:"), gbc);
        gbc.gridx = 1;
        peakThreadCountLabel = new JLabel("--");
        panel.add(peakThreadCountLabel, gbc);
        
        return panel;
    }
    
    private void startDataCollection() {
        scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(this::updateData, 0, 2, TimeUnit.SECONDS);
    }
    
    private void updateData() {
        Jvm jvm = JvmFactory.getJVMFor(application);
        if (jvm != null && jvm.isThreadMonitoringSupported()) {
            final int threadCount = jvm.getThreadCount();
            final int peakThreadCount = jvm.getPeakThreadCount();
            
            SwingUtilities.invokeLater(() -> {
                threadCountLabel.setText(String.valueOf(threadCount));
                peakThreadCountLabel.setText(String.valueOf(peakThreadCount));
                chartPanel.addDataPoint(System.currentTimeMillis(), threadCount);
            });
        }
    }
    
    @Override
    protected void willBeRemoved() {
        if (scheduler != null) {
            scheduler.shutdown();
        }
        super.willBeRemoved();
    }
}

练习2:集成外部监控API

创建一个集成外部监控API的插件:

// ExternalAPIMonitor.java
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

public class ExternalAPIMonitor {
    
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final String apiEndpoint;
    
    public ExternalAPIMonitor(String apiEndpoint) {
        this.apiEndpoint = apiEndpoint;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
        this.objectMapper = new ObjectMapper();
    }
    
    public CompletableFuture<MetricsData> fetchMetrics() {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiEndpoint + "/metrics"))
            .timeout(Duration.ofSeconds(5))
            .header("Accept", "application/json")
            .GET()
            .build();
        
        return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(this::parseResponse)
            .exceptionally(this::handleError);
    }
    
    private MetricsData parseResponse(HttpResponse<String> response) {
        try {
            if (response.statusCode() == 200) {
                JsonNode json = objectMapper.readTree(response.body());
                return new MetricsData(
                    json.get("cpu_usage").asDouble(),
                    json.get("memory_usage").asLong(),
                    json.get("request_count").asInt(),
                    json.get("error_rate").asDouble()
                );
            } else {
                throw new RuntimeException("HTTP " + response.statusCode());
            }
        } catch (Exception e) {
            throw new RuntimeException("解析响应失败", e);
        }
    }
    
    private MetricsData handleError(Throwable throwable) {
        System.err.println("获取指标失败: " + throwable.getMessage());
        return new MetricsData(0, 0, 0, 0); // 返回默认值
    }
    
    public static class MetricsData {
        public final double cpuUsage;
        public final long memoryUsage;
        public final int requestCount;
        public final double errorRate;
        
        public MetricsData(double cpuUsage, long memoryUsage, int requestCount, double errorRate) {
            this.cpuUsage = cpuUsage;
            this.memoryUsage = memoryUsage;
            this.requestCount = requestCount;
            this.errorRate = errorRate;
        }
    }
}

练习3:创建自定义报告生成器

开发一个能够生成性能报告的插件:

// PerformanceReportGenerator.java
import org.graalvm.visualvm.application.Application;
import org.graalvm.visualvm.application.jvm.Jvm;
import org.graalvm.visualvm.application.jvm.JvmFactory;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class PerformanceReportGenerator {
    
    private final Application application;
    private final Map<Long, PerformanceSnapshot> snapshots;
    private final ScheduledExecutorService scheduler;
    private volatile boolean collecting = false;
    
    public PerformanceReportGenerator(Application application) {
        this.application = application;
        this.snapshots = new ConcurrentHashMap<>();
        this.scheduler = Executors.newSingleThreadScheduledExecutor();
    }
    
    public void startCollection() {
        if (!collecting) {
            collecting = true;
            scheduler.scheduleAtFixedRate(this::collectSnapshot, 0, 30, TimeUnit.SECONDS);
        }
    }
    
    public void stopCollection() {
        collecting = false;
        scheduler.shutdown();
    }
    
    private void collectSnapshot() {
        try {
            Jvm jvm = JvmFactory.getJVMFor(application);
            if (jvm != null) {
                PerformanceSnapshot snapshot = new PerformanceSnapshot();
                snapshot.timestamp = System.currentTimeMillis();
                
                // 收集内存信息
                if (jvm.isMemoryMonitoringSupported()) {
                    snapshot.heapUsed = jvm.getHeapMemoryUsage().getUsed();
                    snapshot.heapMax = jvm.getHeapMemoryUsage().getMax();
                    snapshot.nonHeapUsed = jvm.getNonHeapMemoryUsage().getUsed();
                }
                
                // 收集线程信息
                if (jvm.isThreadMonitoringSupported()) {
                    snapshot.threadCount = jvm.getThreadCount();
                    snapshot.peakThreadCount = jvm.getPeakThreadCount();
                }
                
                // 收集类加载信息
                if (jvm.isClassMonitoringSupported()) {
                    snapshot.loadedClassCount = jvm.getLoadedClassCount();
                    snapshot.totalLoadedClassCount = jvm.getTotalLoadedClassCount();
                    snapshot.unloadedClassCount = jvm.getUnloadedClassCount();
                }
                
                snapshots.put(snapshot.timestamp, snapshot);
                
                // 保持最近1小时的数据
                long cutoff = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1);
                snapshots.entrySet().removeIf(entry -> entry.getKey() < cutoff);
            }
        } catch (Exception e) {
            System.err.println("收集性能快照失败: " + e.getMessage());
        }
    }
    
    public String generateReport() {
        StringBuilder report = new StringBuilder();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        
        report.append("# 性能分析报告\n\n");
        report.append("**应用程序**: ").append(application.getId()).append("\n");
        report.append("**生成时间**: ").append(dateFormat.format(new Date())).append("\n");
        report.append("**数据时间范围**: ").append(getDataTimeRange()).append("\n\n");
        
        if (snapshots.isEmpty()) {
            report.append("没有可用的性能数据。\n");
            return report.toString();
        }
        
        // 生成摘要统计
        generateSummaryStatistics(report);
        
        // 生成内存分析
        generateMemoryAnalysis(report);
        
        // 生成线程分析
        generateThreadAnalysis(report);
        
        // 生成类加载分析
        generateClassLoadingAnalysis(report);
        
        // 生成趋势分析
        generateTrendAnalysis(report);
        
        return report.toString();
    }
    
    private String getDataTimeRange() {
        if (snapshots.isEmpty()) return "无数据";
        
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
        long minTime = Collections.min(snapshots.keySet());
        long maxTime = Collections.max(snapshots.keySet());
        
        return dateFormat.format(new Date(minTime)) + " - " + dateFormat.format(new Date(maxTime));
    }
    
    private void generateSummaryStatistics(StringBuilder report) {
        report.append("## 摘要统计\n\n");
        
        List<PerformanceSnapshot> snapshotList = new ArrayList<>(snapshots.values());
        
        // 内存统计
        OptionalLong maxHeapUsed = snapshotList.stream().mapToLong(s -> s.heapUsed).max();
        OptionalDouble avgHeapUsed = snapshotList.stream().mapToLong(s -> s.heapUsed).average();
        
        report.append("**内存使用**:\n");
        report.append("- 最大堆内存使用: ").append(formatBytes(maxHeapUsed.orElse(0))).append("\n");
        report.append("- 平均堆内存使用: ").append(formatBytes((long)avgHeapUsed.orElse(0))).append("\n");
        
        // 线程统计
        OptionalInt maxThreads = snapshotList.stream().mapToInt(s -> s.threadCount).max();
        OptionalDouble avgThreads = snapshotList.stream().mapToInt(s -> s.threadCount).average();
        
        report.append("\n**线程统计**:\n");
        report.append("- 最大线程数: ").append(maxThreads.orElse(0)).append("\n");
        report.append("- 平均线程数: ").append(String.format("%.1f", avgThreads.orElse(0))).append("\n\n");
    }
    
    private void generateMemoryAnalysis(StringBuilder report) {
        report.append("## 内存分析\n\n");
        
        List<PerformanceSnapshot> snapshotList = new ArrayList<>(snapshots.values());
        snapshotList.sort(Comparator.comparing(s -> s.timestamp));
        
        if (snapshotList.size() >= 2) {
            PerformanceSnapshot first = snapshotList.get(0);
            PerformanceSnapshot last = snapshotList.get(snapshotList.size() - 1);
            
            long heapGrowth = last.heapUsed - first.heapUsed;
            long nonHeapGrowth = last.nonHeapUsed - first.nonHeapUsed;
            
            report.append("**内存增长趋势**:\n");
            report.append("- 堆内存增长: ").append(formatBytes(heapGrowth));
            if (heapGrowth > 0) {
                report.append(" ⚠️ 可能存在内存泄漏");
            }
            report.append("\n");
            
            report.append("- 非堆内存增长: ").append(formatBytes(nonHeapGrowth)).append("\n");
        }
        
        // 内存使用率分析
        double avgUsageRate = snapshotList.stream()
            .filter(s -> s.heapMax > 0)
            .mapToDouble(s -> (double)s.heapUsed / s.heapMax * 100)
            .average().orElse(0);
        
        report.append("\n**内存使用率**:\n");
        report.append("- 平均堆内存使用率: ").append(String.format("%.1f%%", avgUsageRate));
        if (avgUsageRate > 80) {
            report.append(" ⚠️ 内存使用率较高");
        }
        report.append("\n\n");
    }
    
    private void generateThreadAnalysis(StringBuilder report) {
        report.append("## 线程分析\n\n");
        
        List<PerformanceSnapshot> snapshotList = new ArrayList<>(snapshots.values());
        
        // 线程数变化分析
        int minThreads = snapshotList.stream().mapToInt(s -> s.threadCount).min().orElse(0);
        int maxThreads = snapshotList.stream().mapToInt(s -> s.threadCount).max().orElse(0);
        int peakThreads = snapshotList.stream().mapToInt(s -> s.peakThreadCount).max().orElse(0);
        
        report.append("**线程数统计**:\n");
        report.append("- 最小线程数: ").append(minThreads).append("\n");
        report.append("- 最大线程数: ").append(maxThreads).append("\n");
        report.append("- 历史峰值线程数: ").append(peakThreads).append("\n");
        
        int threadVariation = maxThreads - minThreads;
        if (threadVariation > 50) {
            report.append("\n⚠️ 线程数变化较大,可能存在线程池配置问题\n");
        }
        
        report.append("\n");
    }
    
    private void generateClassLoadingAnalysis(StringBuilder report) {
        report.append("## 类加载分析\n\n");
        
        List<PerformanceSnapshot> snapshotList = new ArrayList<>(snapshots.values());
        
        if (!snapshotList.isEmpty()) {
            PerformanceSnapshot latest = snapshotList.get(snapshotList.size() - 1);
            
            report.append("**类加载统计**:\n");
            report.append("- 当前已加载类数: ").append(latest.loadedClassCount).append("\n");
            report.append("- 总共加载类数: ").append(latest.totalLoadedClassCount).append("\n");
            report.append("- 已卸载类数: ").append(latest.unloadedClassCount).append("\n");
            
            if (latest.unloadedClassCount == 0 && latest.totalLoadedClassCount > 10000) {
                report.append("\n⚠️ 没有类被卸载,可能存在类加载器泄漏\n");
            }
        }
        
        report.append("\n");
    }
    
    private void generateTrendAnalysis(StringBuilder report) {
        report.append("## 趋势分析\n\n");
        
        List<PerformanceSnapshot> snapshotList = new ArrayList<>(snapshots.values());
        snapshotList.sort(Comparator.comparing(s -> s.timestamp));
        
        if (snapshotList.size() >= 3) {
            // 分析最近的趋势
            int recentCount = Math.min(10, snapshotList.size());
            List<PerformanceSnapshot> recentSnapshots = snapshotList.subList(
                snapshotList.size() - recentCount, snapshotList.size());
            
            // 内存趋势
            double memoryTrend = calculateTrend(recentSnapshots, s -> (double)s.heapUsed);
            report.append("**最近趋势**:\n");
            report.append("- 内存使用趋势: ");
            if (memoryTrend > 1000000) { // 1MB/sample
                report.append("上升 📈");
            } else if (memoryTrend < -1000000) {
                report.append("下降 📉");
            } else {
                report.append("稳定 ➡️");
            }
            report.append("\n");
            
            // 线程趋势
            double threadTrend = calculateTrend(recentSnapshots, s -> (double)s.threadCount);
            report.append("- 线程数趋势: ");
            if (threadTrend > 0.5) {
                report.append("上升 📈");
            } else if (threadTrend < -0.5) {
                report.append("下降 📉");
            } else {
                report.append("稳定 ➡️");
            }
            report.append("\n");
        }
        
        report.append("\n");
    }
    
    private double calculateTrend(List<PerformanceSnapshot> snapshots, 
                                  java.util.function.Function<PerformanceSnapshot, Double> valueExtractor) {
        if (snapshots.size() < 2) return 0;
        
        // 简单线性回归计算趋势
        double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
        int n = snapshots.size();
        
        for (int i = 0; i < n; i++) {
            double x = i;
            double y = valueExtractor.apply(snapshots.get(i));
            
            sumX += x;
            sumY += y;
            sumXY += x * y;
            sumX2 += x * x;
        }
        
        // 计算斜率
        double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
        return slope;
    }
    
    private String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
        return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
    }
    
    public void exportReport(File file) throws IOException {
        String report = generateReport();
        try (FileWriter writer = new FileWriter(file)) {
            writer.write(report);
        }
    }
    
    // 性能快照数据类
    private static class PerformanceSnapshot {
        long timestamp;
        long heapUsed;
        long heapMax;
        long nonHeapUsed;
        int threadCount;
        int peakThreadCount;
        int loadedClassCount;
        long totalLoadedClassCount;
        long unloadedClassCount;
    }
}

插件调试与测试

调试环境配置

1. NetBeans调试配置

<!-- nbproject/project.properties -->
# 调试配置
run.args.extra=-J-Xdebug -J-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000

# 日志配置
run.args.extra=${run.args.extra} -J-Djava.util.logging.config.file=logging.properties

# 开发模式
run.args.extra=${run.args.extra} -J-Dvisualvm.development.mode=true

2. 日志配置

# logging.properties
# 全局日志级别
.level=INFO

# 控制台处理器
handlers=java.util.logging.ConsoleHandler

# 控制台处理器配置
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter

# 自定义插件日志
org.example.visualvm.level=FINE
org.example.visualvm.handlers=java.util.logging.FileHandler
org.example.visualvm.useParentHandlers=false

# 文件处理器
java.util.logging.FileHandler.pattern=visualvm-plugin.log
java.util.logging.FileHandler.limit=1000000
java.util.logging.FileHandler.count=5
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter

单元测试

1. 测试框架配置

<!-- pom.xml 测试依赖 -->
<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.8.2</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>4.6.1</version>
        <scope>test</scope>
    </dependency>
    
    <!-- NetBeans Test Utilities -->
    <dependency>
        <groupId>org.netbeans.api</groupId>
        <artifactId>org-netbeans-modules-nbjunit</artifactId>
        <version>${netbeans.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2. 数据收集器测试

// CustomDataCollectorTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

class CustomDataCollectorTest {
    
    @Mock
    private Application mockApplication;
    
    @Mock
    private Jvm mockJvm;
    
    private CustomDataCollector collector;
    private AutoCloseable mocks;
    
    @BeforeEach
    void setUp() {
        mocks = MockitoAnnotations.openMocks(this);
        collector = new CustomDataCollector(mockApplication);
        
        // 配置模拟对象
        when(JvmFactory.getJVMFor(mockApplication)).thenReturn(mockJvm);
        when(mockJvm.isMemoryMonitoringSupported()).thenReturn(true);
        when(mockJvm.isThreadMonitoringSupported()).thenReturn(true);
    }
    
    @AfterEach
    void tearDown() throws Exception {
        collector.stopCollection();
        mocks.close();
    }
    
    @Test
    void testDataCollection() throws InterruptedException {
        // 配置模拟数据
        MemoryUsage heapUsage = new MemoryUsage(0, 1000000, 2000000, 4000000);
        when(mockJvm.getHeapMemoryUsage()).thenReturn(heapUsage);
        when(mockJvm.getThreadCount()).thenReturn(10);
        
        // 设置监听器
        CountDownLatch latch = new CountDownLatch(1);
        CustomDataCollector.DataCollectionListener listener = metrics -> {
            assertNotNull(metrics);
            assertEquals(1000000, metrics.heapUsed);
            assertEquals(4000000, metrics.heapMax);
            assertEquals(10, metrics.threadCount);
            latch.countDown();
        };
        
        collector.addListener(listener);
        collector.startCollection();
        
        // 等待数据收集
        assertTrue(latch.await(10, TimeUnit.SECONDS), "数据收集超时");
    }
    
    @Test
    void testErrorHandling() {
        // 模拟JVM异常
        when(JvmFactory.getJVMFor(mockApplication)).thenThrow(new RuntimeException("JVM错误"));
        
        // 验证异常处理
        assertDoesNotThrow(() -> {
            collector.startCollection();
            Thread.sleep(100); // 等待一次收集
        });
    }
    
    @Test
    void testListenerManagement() {
        CustomDataCollector.DataCollectionListener listener1 = mock(CustomDataCollector.DataCollectionListener.class);
        CustomDataCollector.DataCollectionListener listener2 = mock(CustomDataCollector.DataCollectionListener.class);
        
        // 添加监听器
        collector.addListener(listener1);
        collector.addListener(listener2);
        
        // 移除监听器
        collector.removeListener(listener1);
        
        // 验证只有listener2被调用
        // 这里需要触发数据收集并验证调用
    }
}

3. 视图组件测试

// CustomViewTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.netbeans.junit.NbTestCase;
import javax.swing.SwingUtilities;
import java.awt.Component;

class CustomViewTest extends NbTestCase {
    
    private Application mockApplication;
    private CustomView view;
    
    public CustomViewTest(String name) {
        super(name);
    }
    
    @BeforeEach
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mockApplication = mock(Application.class);
        when(mockApplication.getId()).thenReturn("test-app");
        
        SwingUtilities.invokeAndWait(() -> {
            view = new CustomView(mockApplication);
        });
    }
    
    @Test
    public void testViewCreation() throws Exception {
        SwingUtilities.invokeAndWait(() -> {
            DataViewComponent component = view.createComponent();
            assertNotNull("视图组件不应为null", component);
            
            Component viewComponent = component.getView();
            assertNotNull("视图不应为null", viewComponent);
        });
    }
    
    @Test
    public void testDataRefresh() throws Exception {
        SwingUtilities.invokeAndWait(() -> {
            // 测试数据刷新功能
            view.createComponent();
            // 这里可以测试刷新按钮的功能
        });
    }
}

集成测试

1. 插件加载测试

// PluginIntegrationTest.java
import org.netbeans.junit.NbModuleSuite;
import org.netbeans.junit.NbTestCase;
import junit.framework.Test;
import org.openide.util.Lookup;

public class PluginIntegrationTest extends NbTestCase {
    
    public PluginIntegrationTest(String name) {
        super(name);
    }
    
    public static Test suite() {
        return NbModuleSuite.createConfiguration(PluginIntegrationTest.class)
            .addTest("testPluginServices")
            .addTest("testViewProviderRegistration")
            .enableModules(".*")
            .gui(false)
            .suite();
    }
    
    public void testPluginServices() {
        // 测试服务注册
        CustomViewProvider provider = Lookup.getDefault().lookup(CustomViewProvider.class);
        assertNotNull("CustomViewProvider应该被注册", provider);
        
        CustomApplicationType.Factory factory = Lookup.getDefault().lookup(CustomApplicationType.Factory.class);
        assertNotNull("ApplicationTypeFactory应该被注册", factory);
    }
    
    public void testViewProviderRegistration() {
        // 测试视图提供者
        Collection<? extends DataSourceViewProvider> providers = 
            Lookup.getDefault().lookupAll(DataSourceViewProvider.class);
        
        boolean found = providers.stream()
            .anyMatch(p -> p instanceof CustomViewProvider);
        
        assertTrue("CustomViewProvider应该在查找中找到", found);
    }
}

性能测试

1. 内存泄漏测试

// MemoryLeakTest.java
import org.junit.jupiter.api.Test;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

class MemoryLeakTest {
    
    @Test
    void testDataCollectorMemoryLeak() {
        List<WeakReference<CustomDataCollector>> collectors = new ArrayList<>();
        
        // 创建多个收集器实例
        for (int i = 0; i < 100; i++) {
            Application app = mock(Application.class);
            CustomDataCollector collector = new CustomDataCollector(app);
            collector.startCollection();
            collector.stopCollection();
            
            collectors.add(new WeakReference<>(collector));
        }
        
        // 强制垃圾回收
        System.gc();
        System.runFinalization();
        System.gc();
        
        // 检查是否有内存泄漏
        long aliveCount = collectors.stream()
            .mapToLong(ref -> ref.get() != null ? 1 : 0)
            .sum();
        
        assertTrue(aliveCount < 10, "可能存在内存泄漏,存活对象数: " + aliveCount);
    }
}

2. 性能基准测试

// PerformanceBenchmarkTest.java
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;

class PerformanceBenchmarkTest {
    
    @Test
    void testDataCollectionPerformance() {
        Application app = mock(Application.class);
        Jvm jvm = mock(Jvm.class);
        
        when(JvmFactory.getJVMFor(app)).thenReturn(jvm);
        when(jvm.isMemoryMonitoringSupported()).thenReturn(true);
        when(jvm.getHeapMemoryUsage()).thenReturn(new MemoryUsage(0, 1000000, 2000000, 4000000));
        
        CustomDataCollector collector = new CustomDataCollector(app);
        
        // 性能测试
        long startTime = System.nanoTime();
        
        for (int i = 0; i < 1000; i++) {
            // 模拟数据收集
            collector.collectData(); // 假设有这个方法
        }
        
        long endTime = System.nanoTime();
        long duration = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
        
        assertTrue(duration < 1000, "数据收集性能测试失败,耗时: " + duration + "ms");
    }
}

插件部署最佳实践

版本管理

1. 语义化版本控制

<!-- pom.xml -->
<version>1.2.3</version>
<!-- 
1: 主版本号 - 不兼容的API变更
2: 次版本号 - 向后兼容的功能性新增
3: 修订号 - 向后兼容的问题修正
-->

2. 版本兼容性检查

// VersionCompatibility.java
public class VersionCompatibility {
    
    private static final String MIN_VISUALVM_VERSION = "2.1.0";
    private static final String MAX_VISUALVM_VERSION = "2.9.9";
    
    public static boolean isCompatible() {
        String currentVersion = getVisualVMVersion();
        return isVersionInRange(currentVersion, MIN_VISUALVM_VERSION, MAX_VISUALVM_VERSION);
    }
    
    private static String getVisualVMVersion() {
        // 获取当前VisualVM版本
        return System.getProperty("visualvm.version", "unknown");
    }
    
    private static boolean isVersionInRange(String version, String min, String max) {
        // 实现版本比较逻辑
        return compareVersions(version, min) >= 0 && compareVersions(version, max) <= 0;
    }
    
    private static int compareVersions(String v1, String v2) {
        String[] parts1 = v1.split("\\.");
        String[] parts2 = v2.split("\\.");
        
        int maxLength = Math.max(parts1.length, parts2.length);
        
        for (int i = 0; i < maxLength; i++) {
            int part1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0;
            int part2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0;
            
            if (part1 != part2) {
                return Integer.compare(part1, part2);
            }
        }
        
        return 0;
    }
}

自动化部署

1. GitHub Actions工作流

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  release:
    types: [ published ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 8
      uses: actions/setup-java@v3
      with:
        java-version: '8'
        distribution: 'temurin'
    
    - name: Cache Maven dependencies
      uses: actions/cache@v3
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    
    - name: Run tests
      run: mvn clean test
    
    - name: Generate test report
      uses: dorny/test-reporter@v1
      if: success() || failure()
      with:
        name: Maven Tests
        path: target/surefire-reports/*.xml
        reporter: java-junit
  
  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 8
      uses: actions/setup-java@v3
      with:
        java-version: '8'
        distribution: 'temurin'
    
    - name: Build plugin
      run: mvn clean package
    
    - name: Upload NBM to release
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: ./target/custom-visualvm-plugin-${{ github.event.release.tag_name }}.nbm
        asset_name: custom-visualvm-plugin-${{ github.event.release.tag_name }}.nbm
        asset_content_type: application/octet-stream
    
    - name: Update plugin center
      run: |
        # 更新插件中心的脚本
        ./scripts/update-plugin-center.sh ${{ github.event.release.tag_name }}

2. 插件中心更新脚本

#!/bin/bash
# scripts/update-plugin-center.sh

VERSION=$1
PLUGIN_NAME="custom-visualvm-plugin"
UPDATE_CENTER_REPO="your-org/visualvm-plugin-center"

if [ -z "$VERSION" ]; then
    echo "错误: 请提供版本号"
    exit 1
fi

# 克隆插件中心仓库
git clone https://github.com/${UPDATE_CENTER_REPO}.git plugin-center
cd plugin-center

# 更新插件信息
cat > plugins/${PLUGIN_NAME}.xml << EOF
<?xml version="1.0" encoding="UTF-8"?>
<module codenamebase="org.example.visualvm.custom"
        distribution="${PLUGIN_NAME}-${VERSION}.nbm"
        downloadsize="$(stat -c%s ../target/${PLUGIN_NAME}-${VERSION}.nbm)"
        homepage="https://github.com/your-org/custom-visualvm-plugin"
        license="Apache-2.0"
        moduleauthor="Your Name"
        needsrestart="true"
        releasedate="$(date +'%Y/%m/%d')">
    <manifest OpenIDE-Module="org.example.visualvm.custom"
              OpenIDE-Module-Display-Category="Monitoring"
              OpenIDE-Module-Implementation-Version="${VERSION}"
              OpenIDE-Module-Long-Description="自定义VisualVM监控插件"
              OpenIDE-Module-Name="Custom VisualVM Plugin"
              OpenIDE-Module-Short-Description="自定义监控插件"
              OpenIDE-Module-Specification-Version="${VERSION}"/>
</module>
EOF

# 提交更改
git add plugins/${PLUGIN_NAME}.xml
git commit -m "Update ${PLUGIN_NAME} to version ${VERSION}"
git push origin main

echo "插件中心已更新到版本 ${VERSION}"

用户文档

1. 安装指南

# Custom VisualVM Plugin 安装指南

## 系统要求

- VisualVM 2.1.0 或更高版本
- Java 8 或更高版本

## 安装方法

### 方法1: 通过VisualVM插件管理器

1. 启动VisualVM
2. 选择 "工具" -> "插件"
3. 在 "可用插件" 标签页中搜索 "Custom Plugin"
4. 选择插件并点击 "安装"
5. 按照向导完成安装
6. 重启VisualVM

### 方法2: 手动安装

1. 从 [Releases页面](https://github.com/your-org/custom-visualvm-plugin/releases) 下载最新的NBM文件
2. 在VisualVM中选择 "工具" -> "插件"
3. 切换到 "已下载" 标签页
4. 点击 "添加插件" 按钮
5. 选择下载的NBM文件
6. 按照向导完成安装

## 使用说明

安装完成后,插件会在以下位置出现:

- 应用程序树中会显示支持的应用程序类型
- 选择应用程序后,会在右侧面板中显示 "Custom View" 标签页
- 可以通过工具栏按钮进行配置和数据刷新

## 故障排除

### 插件未显示

1. 确认VisualVM版本兼容性
2. 检查插件是否正确安装
3. 查看VisualVM日志文件

### 性能问题

1. 调整监控间隔设置
2. 减少数据保留时间
3. 关闭不需要的监控功能

2. API文档

/**
 * Custom VisualVM Plugin API 文档
 * 
 * 本插件提供以下主要功能:
 * 
 * 1. 自定义应用程序类型识别
 * 2. 实时性能数据收集
 * 3. 可视化图表展示
 * 4. 性能报告生成
 * 
 * @author Your Name
 * @version 1.0.0
 * @since 2023-12-01
 */
public class PluginAPI {
    
    /**
     * 获取数据收集器实例
     * 
     * @param application 目标应用程序
     * @return 数据收集器实例
     * @throws IllegalArgumentException 如果应用程序不支持
     */
    public static CustomDataCollector getDataCollector(Application application) {
        // 实现代码
    }
    
    /**
     * 创建性能报告生成器
     * 
     * @param application 目标应用程序
     * @return 报告生成器实例
     */
    public static PerformanceReportGenerator createReportGenerator(Application application) {
        // 实现代码
    }
}

本章总结

关键要点

  1. VisualVM插件架构

    • 基于NetBeans平台的模块化架构
    • 应用程序类型、数据源、视图等核心组件
    • 服务发现和注册机制
  2. 插件开发环境

    • NetBeans IDE和Maven构建工具
    • 项目结构和依赖配置
    • 服务注册和模块描述
  3. 高级功能开发

    • 自定义数据收集器
    • 图表组件集成
    • 配置管理系统
    • 外部API集成
  4. 测试与调试

    • 单元测试和集成测试
    • 性能测试和内存泄漏检测
    • 调试环境配置
  5. 插件部署和分发

    • NBM文件构建和安装
    • 版本管理和兼容性检查
    • 自动化部署流程

最佳实践

  1. 设计原则

    • 遵循单一职责原则
    • 提供清晰的用户界面
    • 处理异常和错误情况
    • 版本兼容性考虑
  2. 性能考虑

    • 异步数据收集
    • 合理的刷新频率
    • 内存使用优化
    • 资源清理和释放
  3. 用户体验

    • 直观的界面设计
    • 丰富的配置选项
    • 有用的错误提示
    • 完善的文档
  4. 维护性

    • 模块化代码结构
    • 完善的测试覆盖
    • 自动化构建和部署
    • 持续集成和质量保证

下一章预告

下一章我们将学习VisualVM最佳实践与案例分析,包括: - 生产环境监控策略 - 性能问题诊断案例 - 监控数据分析方法 - 团队协作和知识分享

通过实际案例,您将学会如何在真实项目中有效使用VisualVM进行性能监控和问题诊断。