写那么多年Java,还不知道啥是Java agent的必须看一下!

简介: 作者信息:张帅,花名洵澈,负责物流表达和履约相关研发工作。热衷于中间件相关技术。

原文链接

引言

在本篇文章中,我会通过几个简单的程序来说明agent的使用,最后在实战替换我会通过asm字节码框架来实现一个小工具,用于在程序运行中采集指定方法的参数和返回值。有关asm字节码的内容不是本文的重点,不会过多的分解,不明白的同学可以自己的Google下。

简介

Java代理提供了一种在加载字节码时,对字节码进行修改的方式。他共有两种方式执行,一种是在main方法执行之前,通过premain来实现,另一种是在程序运行中,通过attach api来实现。

在介绍agent之前,先给大家简单说下一个Instrumentation。它是JDK1.5提供的API,用于拦截类加载事件,通过字节码进行修改,它的主要方法如下:

public interface Instrumentation {
    //注册一个转换器,类加载事件会被注册的转换器所拦截
     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    //重新触发类加载
     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    //直接替换类的定义
     void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;
}

主要

premain是在main方法之前运行的方法,也是最常见的agent方式。运行时需要将agent程序打包成jar包,并在启动时添加命令来执行,如下文所示:
java -javaagent:agent.jar=xunche HelloWorld

premain共提供以下2种重载方法,Jvm启动时会先尝试使用第一种方法,若没有会使用第二种方法:

public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);

一个简单的例子

下面我们通过一个程序来简单说明下premain的使用,首先我们准备下测试代码,测试代码比较简单,运行main方法并输出hello world。

package org.xunche.app;
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

接下来我们看下agent的代码,运行premain方法并输出我们预期的参数。

package org.xunche.agent;
public class HelloAgent {
  public static void premain(String args) {
    System.out.println("Hello Agent:  " + args);
  }
}

为了能够使agent能够运行,我们需要将META-INF / MANIFEST.MF文件中的Premain- Class为我们编写的agent路径,然后通过以下方式将其打包成jar包,当然你也可以使用idea直接替换jar包。

echo 'Premain-Class: org.xunche.agent.HelloAgent' > manifest.mf
javac org/xunche/agent/HelloAgent.java
javac org/xunche/app/HelloWorld.java
jar cvmf manifest.mf hello-agent.jar org/

接下来,我们编译下并运行下测试代码,这里为了测试简单,我将编译后的类和agent的jar包放在同级目录下

java -javaagent:hello-agent.jar=xunche org/xunche/app/HelloWorld

可以看到输出结果如下,agent中的premain方法有延续main方法执行

Hello Agent: xunche
Hello World

稍微复杂点的例子

通过上面的例子,是否对agent有个简单的了解呢?

下面我们来看一个稍微复杂点,我们通过agent来实现一个方法监控的功能。思路大致是这样的,若是非jdk的方法,我们通过asm在方法的执行入口和执行出口处,植入几行记录最佳的代码,当方法结束后,通过合并来获取方法的耗时。

首先还是看下测试代码,逻辑很简单,main方法执行时调用say Hi方法,输出hi,xunche,并随机睡眠中断。

package org.xunche.app;
public class HelloXunChe {
    public static void main(String[] args) throws InterruptedException {
        HelloXunChe helloXunChe = new HelloXunChe();
        helloXunChe.sayHi();
    }
    public void sayHi() throws InterruptedException {
        System.out.println("hi, xunche");
        sleep();
    }
    public void sleep() throws InterruptedException {
        Thread.sleep((long) (Math.random() * 200));
    }
}

接下来我们替换asm来植入我们自己的代码,在jvm加载类的时候,为类的每个方法加上统计方法调用耗时的代码,代码如下,这里的asm我使用了jdk自带的,当然你也可以使用官方的asm类库。

package org.xunche.agent;
import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class TimeAgent {
    public static void premain(String args, Instrumentation instrumentation) {
        instrumentation.addTransformer(new TimeClassFileTransformer());
    }
    private static class TimeClassFileTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun") || className.startsWith("com/sun")|| className.startsWith("org/xunche/agent")) {
                //return null或者执行异常会执行原来的字节码
                return null;
            }
            System.out.println("loaded class: " + className);
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        }
    }
    public static class TimeClassVisitor extends ClassVisitor {
        public TimeClassVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor);
        }
        @Override
        public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions);
            return new TimeAdviceAdapter(Opcodes.ASM5, methodVisitor, methodAccess, methodName, methodDesc);
        }
    }
    public static class TimeAdviceAdapter extends AdviceAdapter {
        private String methodName;
        protected TimeAdviceAdapter(int api, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) {
            super(api, methodVisitor, methodAccess, methodName, methodDesc);
            this.methodName = methodName;
        }
        @Override
        protected void onMethodEnter() {
            //在方法入口处植入
            if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) {
                return;
            }
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(".");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "org/xunche/agent/TimeHolder", "start", "(Ljava/lang/String;)V", false);
        }
        @Override
        protected void onMethodExit(int i) {
            //在方法出口植入
            if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) {
                return;
            }
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(".");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitVarInsn(ASTORE, 1);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(": ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKESTATIC, "org/xunche/agent/TimeHolder", "cost", "(Ljava/lang/String;)J", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    }
}

的上述代码略长,ASM的部分可以略过。我们通过instrumentation.addTransformer注册一个转换器,转换器重写了变换方法,方法入参中的classfileBuffer表示的是原始的字节码,方法返回值表示的是真正要进行加载的字节码。

onMethodEnter方法中的代码含义是调用TimeHolder的start方法并初始化当前的方法名。

onMethodExit方法中的代码含义是调用TimeHolder的成本方法并合并当前的方法名,并打印成本方法的返回值。

下面来看下TimeHolder的代码:

package org.xunche.agent;
import java.util.HashMap;
import java.util.Map;
public class TimeHolder {
    private static Map<String, Long> timeCache = new HashMap<>();
    public static void start(String method) {
        timeCache.put(method, System.currentTimeMillis());
    }
    public static long cost(String method) {
        return System.currentTimeMillis() - timeCache.get(method);
    }
}

至此之后,agent的代码编写完成,有关asm的部分不是本章的重点,日后再单独推出发表有关asm的文章。后的代码是怎样的。可以看到,与最开始的测试代码排序,每个方法都加入了我们统计方法耗时的代码。

package org.xunche.app;
import org.xunche.agent.TimeHolder;
public class HelloXunChe {
    public HelloXunChe() {
    }
    public static void main(String[] args) throws InterruptedException {
        TimeHolder.start(args.getClass().getName() + "." + "main");
        HelloXunChe helloXunChe = new HelloXunChe();
        helloXunChe.sayHi();
        HelloXunChe helloXunChe = args.getClass().getName() + "." + "main";
        System.out.println(helloXunChe + ": " + TimeHolder.cost(helloXunChe));
    }
    public void sayHi() throws InterruptedException {
        TimeHolder.start(this.getClass().getName() + "." + "sayHi");
        System.out.println("hi, xunche");
        this.sleep();
        String var1 = this.getClass().getName() + "." + "sayHi";
        System.out.println(var1 + ": " + TimeHolder.cost(var1));
    }
    public void sleep() throws InterruptedException {
        TimeHolder.start(this.getClass().getName() + "." + "sleep");
        Thread.sleep((long)(Math.random() * 200.0D));
        String var1 = this.getClass().getName() + "." + "sleep";
        System.out.println(var1 + ": " + TimeHolder.cost(var1));
    }
}

代理基础

上面的premain是通过agetn在应用启动前,对字节码进行修改,来实现我们想要的功能。实际上jdk提供了attach api,通过这个api,我们可以访问已经启动的Java进程。并通过agentmain方法来拦截类加载。下面我们来通过实战来具体说明下agentmain。

实战

本次实战的目标是实现一个小工具,其目标是能远程采集已经在运行中的Java进程的方法调用信息。听起来像不像BTrace,实际上 BTrace也是 这么实现的。只不过因为时间关系,本次的实战代码写的比较简陋,大家不必关注细节,看下实现的思路就好。

具体的实现思路如下:

agent对指定类的方法进行字节码的修改,采集方法的入参和返回值。并通过socket将请求和返回发送到服务端
服务端通过attach api访问运行中的Java进程,并加载agent,使agent程序能对目标进程实施
服务端加载agent时指定需要采集的类和方法
服务端开启一个端口,接受目标进程的请求信息

老规矩,先看测试代码,测试代码很简单,每隔100ms运行一次sayHi方法,并随机随身睡觉。

package org.xunche.app;
public class HelloTraceAgent {
    public static void main(String[] args) throws InterruptedException {
        HelloTraceAgent helloTraceAgent = new HelloTraceAgent();
        while (true) {
            helloTraceAgent.sayHi("xunche");
            Thread.sleep(100);
        }
    }
    public String sayHi(String name) throws InterruptedException {
        sleep();
        String hi = "hi, " + name + ", " + System.currentTimeMillis();
        return hi;
    }
    public void sleep() throws InterruptedException {
        Thread.sleep((long) (Math.random() * 200));
    }
}

接下看agent代码,思路同等监控方法耗时差不多,在方法出口处,通过asm植入采集方法入参和返回值的代码,并通过发件人将信息通过socket发送到服务端,代码如下:

package org.xunche.agent;
import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class TraceAgent {
    public static void agentmain(String args, Instrumentation instrumentation) throws ClassNotFoundException, UnmodifiableClassException {
        if (args == null) {
            return;
        }
        int index = args.lastIndexOf(".");
        if (index != -1) {
            String className = args.substring(0, index);
            String methodName = args.substring(index + 1);
            //目标代码已经加载,需要重新触发加载流程,才会通过注册的转换器进行转换
            instrumentation.addTransformer(new TraceClassFileTransformer(className.replace(".", "/"), methodName), true);
            instrumentation.retransformClasses(Class.forName(className));
        }
    }
    public static class TraceClassFileTransformer implements ClassFileTransformer {
        private String traceClassName;
        private String traceMethodName;
        public TraceClassFileTransformer(String traceClassName, String traceMethodName) {
            this.traceClassName = traceClassName;
            this.traceMethodName = traceMethodName;
        }
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            //过滤掉Jdk、agent、非指定类的方法
            if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun")
                    || className.startsWith("com/sun") || className.startsWith("org/xunche/agent") || !className.equals(traceClassName)) {
                //return null会执行原来的字节码
                return null;
            }
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            reader.accept(new TraceVisitor(className, traceMethodName, writer), ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        }
    }
    public static class TraceVisitor extends ClassVisitor {
        private String className;
        private String traceMethodName;
        public TraceVisitor(String className, String traceMethodName, ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor);
            this.className = className;
            this.traceMethodName = traceMethodName;
        }
        @Override
        public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions);
            if (traceMethodName.equals(methodName)) {
                return new TraceAdviceAdapter(className, methodVisitor, methodAccess, methodName, methodDesc);
            }
            return methodVisitor;
        }
    }
    private static class TraceAdviceAdapter extends AdviceAdapter {
        private final String className;
        private final String methodName;
        private final Type[] methodArgs;
        private final String[] parameterNames;
        private final int[] lvtSlotIndex;
        protected TraceAdviceAdapter(String className, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) {
            super(Opcodes.ASM5, methodVisitor, methodAccess, methodName, methodDesc);
            this.className = className;
            this.methodName = methodName;
            this.methodArgs = Type.getArgumentTypes(methodDesc);
            this.parameterNames = new String[this.methodArgs.length];
            this.lvtSlotIndex = computeLvtSlotIndices(isStatic(methodAccess), this.methodArgs);
        }
        @Override
        public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
            for (int i = 0; i < this.lvtSlotIndex.length; ++i) {
                if (this.lvtSlotIndex[i] == index) {
                    this.parameterNames[i] = name;
                }
            }
        }
        @Override
        protected void onMethodExit(int opcode) {
            //排除构造方法和静态代码块
            if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) {
                return;
            }
            if (opcode == RETURN) {
                push((Type) null);
            } else if (opcode == LRETURN || opcode == DRETURN) {
                dup2();
                box(Type.getReturnType(methodDesc));
            } else {
                dup();
                box(Type.getReturnType(methodDesc));
            }
            Type objectType = Type.getObjectType("java/lang/Object");
            push(lvtSlotIndex.length);
            newArray(objectType);
            for (int j = 0; j < lvtSlotIndex.length; j++) {
                int index = lvtSlotIndex[j];
                Type type = methodArgs[j];
                dup();
                push(j);
                mv.visitVarInsn(ALOAD, index);
                box(type);
                arrayStore(objectType);
            }
            visitLdcInsn(className.replace("/", "."));
            visitLdcInsn(methodName);
            mv.visitMethodInsn(INVOKESTATIC, "org/xunche/agent/Sender", "send", "(Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V", false);
        }
        private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) {
            int[] lvtIndex = new int[paramTypes.length];
            int nextIndex = isStatic ? 0 : 1;
            for (int i = 0; i < paramTypes.length; ++i) {
                lvtIndex[i] = nextIndex;
                if (isWideType(paramTypes[i])) {
                    nextIndex += 2;
                } else {
                    ++nextIndex;
                }
            }
            return lvtIndex;
        }
        private static boolean isWideType(Type aType) {
            return aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE;
        }
        private static boolean isStatic(int access) {
            return (access & 8) > 0;
        }
    }
}

SpringLocalVariableTableParameterNameNameDiscoverer,注意的同学可以自己研究下。接下来看下Sender中级代码:

public class Sender {
    private static final int SERVER_PORT = 9876;
    public static void send(Object response, Object[] request, String className, String methodName) {
        Message message = new Message(response, request, className, methodName);
        try {
            Socket socket = new Socket("localhost", SERVER_PORT);
            socket.getOutputStream().write(message.toString().getBytes());
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static class Message {
        private Object response;
        private Object[] request;
        private String className;
        private String methodName;
        public Message(Object response, Object[] request, String className, String methodName) {
            this.response = response;
            this.request = request;
            this.className = className;
            this.methodName = methodName;
        }
        @Override
        public String toString() {
            return "Message{" +
                    "response=" + response +
                    ", request=" + Arrays.toString(request) +
                    ", className='" + className + '\'' +
                    ", methodName='" + methodName + '\'' +
                    '}';
        }
    }
}

Sender中的代码不复杂,一看就懂,就不多说了。下面我们来看下服务端的代码,服务端要实现开启一个端口监听,接受请求信息,并使用attach api加载agent。

package org.xunche.app;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class TraceAgentMain {
    private static final int SERVER_PORT = 9876;
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        new Server().start();
        //attach的进程
        VirtualMachine vm = VirtualMachine.attach("85241");
        //加载agent并指明需要采集信息的类和方法
        vm.loadAgent("trace-agent.jar", "org.xunche.app.HelloTraceAgent.sayHi");
        vm.detach();
    }
    private static class Server implements Runnable {
        @Override
        public void run() {
            try {
                ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
                while (true) {
                    Socket socket = serverSocket.accept();
                    InputStream input = socket.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(input));
                    System.out.println("receive message:" + reader.readLine());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public void start() {
            Thread thread = new Thread(this);
            thread.start();
        }
    }
}

运行上面的程序,可以看到服务端收到了org.xunche.app.HelloTraceAgent.sayHi的请求和返回信息。

receive message:Message{response=hi, xunche, 1581599464436, request=[xunche], className='org.xunche.app.HelloTraceAgent', methodName='sayHi'}

小结

和通过agentmain实现了一个收集运行时方法调用信息的小工具,当然根据篇幅和时间问题,代码写的比较随意,大家多体会实际上,agent的作用远不止文章中介绍的这些,像 BTrace,arm,springloaded等中也都有用到agent。

转载自Hollis

相关文章
|
4月前
|
Java 关系型数据库 MySQL
GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现
在 2024 OpenTelemetry Community Day 会议中,阿里云可观测工程师张乎兴(望陶)和饶子昊(铖朴)为大家带来了《GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现》的演讲分享,介绍阿里云在相关领域的探索方案,本文是相关分享对应的中文整理。
252 18
|
3月前
|
数据采集 人工智能 监控
【Azure 应用程序见解】Application Insights Java Agent 3.1.0的使用实验,通过修改单个URL的采样率来减少请求及依赖项的数据采集
【Azure 应用程序见解】Application Insights Java Agent 3.1.0的使用实验,通过修改单个URL的采样率来减少请求及依赖项的数据采集
|
6月前
|
监控 Java Maven
揭秘Java Agent技术:解锁Java工具开发的新境界
作为JDK提供的关键机制,Java Agent技术不仅为Java工具的开发者提供了一个强大的框架,还为性能监控、故障诊断和动态代码修改等领域带来了革命性的变革。本文旨在全面解析Java Agent技术的应用场景以及实现方式,特别是静态加载模式和动态加载模式这两种关键模式。
1132 0
|
6月前
|
消息中间件 运维 前端开发
java云his系统源码 B/S版+saas智慧医院云his系统源码 二甲医院应用多年 运行稳定
Java云HIS系统,SaaS模式,适用于二甲医院,稳定运行多年。包括B/S版电子病历,支持四级标准,具有自主知识产权。系统涵盖挂号、收费、病历、药物管理、统计报表、综合维护及运营运维功能。前端使用Angular+Nginx,后端基于Java+Spring系列,数据库为MySQL+MyCat,缓存Redis+J2Cache,集成RabbitMQ、XxlJob等。电子病历系统支持在线创建、管理和打印,采用SpringBoot、Mybatis-Plus等技术,提供一体化解决方案。系统优势在于模板化、配置化设计,便于使用和扩展。
80 1
java云his系统源码 B/S版+saas智慧医院云his系统源码  二甲医院应用多年 运行稳定
|
6月前
|
监控 Java 数据库
Zabbix【部署 05】 Docker部署Zabbix Server Agent Agent2 Web interface及 Java-Gate-Way(详细启动脚本及踩坑记录)不定时更新
Zabbix【部署 05】 Docker部署Zabbix Server Agent Agent2 Web interface及 Java-Gate-Way(详细启动脚本及踩坑记录)不定时更新
525 0
|
监控 Java API
Java Agent入门实战(三)-JVM Attach原理与使用
Java Agent入门实战(三)-JVM Attach原理与使用
|
存储 弹性计算 安全
Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题
从 Java Agent 报错开始,到 JVM 原理,到 glibc 线程安全,再到 pthread tls,逐步探究 Java Agent 诡异报错。
225 5
Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题
|
安全 Java jenkins
Jenkins 解决Jenkins下java无法运行slave-agent jnlp程序连接Windows Slave主机
Jenkins 解决Jenkins下java无法运行slave-agent jnlp程序连接Windows Slave主机
324 0
|
5天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
4天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
下一篇
无影云桌面