Java Attach API是JDK 6中新增的一项功能,它提供了一种机制,允许运行在JVM中的Java进程与Agent程序进行动态依附。Attach API可以让应用程序在运行时动态连接到正在运行的JVM,并访问它的状态、执行代码和甚至修改它的状态或执行代码。它提供了一种标准的方式,使得开发者能够审查和修改某个正在运行的Java进程,而不必暴力地中断应用程序。
使用Attach API的过程,需要有两个部分共同配合:
-
JVM的部分:JVM需要开放Attach机制,使得外部的代码可以通过Attach接口连接到这个VM中。在打开这个机制时,需要在启动JVM时设置一个参数:
-Dcom.sun.management.jmxremote
。 -
Agent的部分:Agent则是一个Java代码,它被Attach到了某个JVM中,它可以通过使用Java Instrumentation技术,完全不停机地对JVM中的class字节码进行修改和替换,从而实现为应用程序注入一些新的行为。例如,可以监控和成批操作线程,甚至可以实现代码注入(通过修改已存在的代码)、代码替换或者是执行热插拔。
下面我们来看一下如何使用Attach API进行远程连接,并获取远程JVM中的运行状态。我们将分别从服务端和客户端两个角度进行讲解:
-
服务端环境
-
在JVM启动时添加如下参数,开启JMX远程连接:
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=8899 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
"
- 启动一个简单的Java应用程序,暴露出运行时MBean以供远程连接查看:
public class RemoteServer {
public static void main(String[] args) {
WatchService watchService = null;
try {
// create a WatchService instance
watchService = FileSystems.getDefault().newWatchService();
// create a Path object for current directory
Path path = Paths.get(".");
// register a watch service for the PATH object obtained above
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
// start an infinite loop
while (true) {
System.out.println("Current time:" + new Date() + ". Waiting for changes...");
// wait for a file system event to occur
WatchKey watchKey = watchService.take();
// fetch all the events from the watch key
List<WatchEvent<?>> events = watchKey.pollEvents();
// iterate over each of the events and process them
for (WatchEvent event : events) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context() + ".");
}
if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context() + ".");
}
if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context() + ".");
}
}
// reset the watch key and loop again
boolean valid = watchKey.reset();
if (!valid) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (watchService != null) {
try {
watchService.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
-
客户端环境
-
编写一个简单的Java应用程序,在使用LocalAttachProvider连接到远程JMX端口并获取其中的JMXServerConnection:
public class RemoteJmx {
private static final VirtualMachineDescriptor findProcess(String name) {
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
for (VirtualMachineDescriptor desc : vms) {
if (desc.displayName().endsWith(name)) {
return desc;
}
}
return null;
}
public static void main(String[] args) throws Exception {
String processName = "RemoteServer";
VirtualMachineDescriptor desc = findProcess(processName);
VirtualMachine vm = VirtualMachine.attach(desc);
String connectorAddr = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
if (connectorAddr == null) {
String agent = vm.getSystemProperties().getProperty("java.home") +
File.separator + "lib" + File.separator + "management-agent.jar";
vm.loadAgent(agent);
connectorAddr = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
}
JMXServiceURL url = new JMXServiceURL(connectorAddr);
try (JMXConnector connector = JMXConnectorFactory.connect(url)) {
MBeanServerConnection mbeanServer = connector.getMBeanServerConnection();
Set<ObjectName> mbeans = mbeanServer.queryNames(null, null);
for (ObjectName objectName : mbeans) {
System.out.println(objectName);
}
}
}
通过此方式,我们可以在客户端上连接到服务端的JMX端口,并获取其中的运行状态对象以便我们进行诊断调试。由于这种方式可以在应用程序运作时提供持续的诊断调试服务,因此在开发和生产环境中都有许多用处。
- 示例2:使用Attach API注入字节码并执行代码
在现代的分布式系统中,我们常常要使用协调器来维持不同的服务之间的一致性。你可能听说过Zookeeper或Etcd这样的产品。然而,有时候我们想要简化体系结构,做一些自己的东西。要实现分布式锁,我们可以尝试使用Java的attach API注入字节码,并且指定bytecode作为一个java运行时的agent。检视以下示例:
- 服务端环境
在服务端环境中执行下面的Java代码,以监听/distributed_lock路径的节点变化:
public class LockServer {
public static ZooKeeper zk = null;
public static final String ROOT_LOCK_PATH = "/distributed_lock";
public static final String SERVER_ID = "1";
public static final String ADDRESS = "localhost:7878";
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("USAGE: LockServer <zkQuorum> <thisServerAddr>");
System.exit(1);
}
String zkQuorum = args[0];
String thisServerAddr = args[1];
zk = new ZooKeeper(zkQuorum, 6000, null);
try {
zk.create(ROOT_LOCK_PATH, SERVER_ID.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
} catch (KeeperException | InterruptedException e) {
System.err.println(e.getMessage());
}
new LockEngine(zk, thisServerAddr).run();
}
}
在本例中,我们首先连接到本地的ZooKeeper节点并检查是否已在节点上设置一个路径锁。如果没有,则我们创建一个分布式锁路径(在本例中是/distributed_lock)。稍后,我们将使用LockEngine来运行我们的分布式锁服务。
请注意,LockEngine是异步事件驱动的,可以快速响应任务尝试获取和释放锁的请求。这里我们使用事件驱动技术可以保持代码的整洁性和可维护性。
- 客户端环境
接下来我们要使用客户端Attach的API来注入LockEngine。LockEngine以自己作为参数,并被注入到正在运行的Java虚拟机进程的字节码中。以下是将LockEngine注入到LockServer JVM进程中的Java代码:
public class LockClient {
private static void attachAgent(String jarFilePath, String targetJvmPid) throws Exception {
VirtualMachine vm = VirtualMachine.attach(targetJvmPid);
vm.loadAgent(jarFilePath);
vm.detach();
}
public static void main(String[] args) throws Exception {
String jarFilePath = "/path/to/lock-engine.jar";
String processName = "LockServer";
String targetJvmPid = findProcess(processName);
attachAgent(jarFilePath, targetJvmPid);
}
}
LockClient程序可以从目标JVM中远程注入LockEngine.jar,然后我们就可以从Zookeeper中获取分布式锁来等待锁的释放。例如,以下是在LockEngine中实现try-to-lock和unlock操作的示例Java代码:
public class LockEngine {
private ZooKeeper zkClient;
private final String serverId;
private String lockBaseNode = LockServer.ROOT_LOCK_PATH;
private String myLockPath;
private CountDownLatch lockAvailableSignal = new CountDownLatch(1);
private String serverAddress = LockServer.ADDRESS;
public LockEngine(ZooKeeper zk, String serverAddr) {
zkClient = zk;
serverId = LockServer.SERVER_ID;
serverAddress = serverAddr;
}
public boolean tryToLock() throws KeeperException, InterruptedException {
if (myLockPath != null) {
return true;
}
myLockPath = zkClient.create(lockBaseNode + "/" + serverId + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
List<String> sortedNode = zkClient.getChildren(lockBaseNode, false);
SortedSet<String> set = new TreeSet<String>();
for (String node : sortedNode) {
set.add(lockBaseNode + "/" + node);
}
String first = set.first();
SortedSet<String> lessThanMe = ((TreeSet<String>) set).headSet(myLockPath);
if (myLockPath.equals(first)) {
return true;
}
if (!lessThanMe.isEmpty()) {
String last = lessThanMe.last();
zkClient.exists(last, new LockWatcher(lockAvailableSignal, zkClient, myLockPath));
lockAvailableSignal.await();
}
return true;
}
public void unlock() throws Exception {
zkClient.delete(this.myLockPath, -1);
myLockPath = null;
lockAvailableSignal = new CountDownLatch(1);
}
}
这个锁定模块是一个图片完整的Java类,你可以选择提取和使用其中的代码,或在你的项目中直接使用Jar。如要使用该类,你需要在程序中注入它,如上所述。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java Attach API的作用是什么? - Python技术站