Java 程序员必须知道:深入了解 Instrument
一. 前提
很早之前就了解到目前主流的APM
开源框架如Pinpoint
、SkyWalking
等等都是通过java.lang.instrument
包提供的字节码增强功能来实现的。趁着对这块的热情还没消退,抽时间分析一下java.lang.instrument
包的使用方式,记录下来写成一个系列的文章。本系列博文针对的是JDK11,其他版本的JDK可能不适合。
二. instrument简介
java.lang.instrument
包的结构如下:
java.lang.instrument
- ClassDefinition
- ClassFileTransformer
- IllegalClassFormatException
- Instrumentation
- UnmodifiableClassException
- UnmodifiableModuleException
其中,核心功能由接口java.lang.instrument.Instrumentation
提供,这里可以通过Instrumentation
类的API注释来理解一下什么是instrument
:
Instrumentation
类提供控制Java语言程序代码的服务。Instrumentation
可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定工具的目的。由于插入的字节码是附加的,这些更变不会修改原来程序的状态或者行为。通过这种方式实现的良性工具包括监控代理、分析器、覆盖分析程序和事件日志记录程序等等。
也就是说,java.lang.instrument
包的最大功能就是可以在已有的类上附加(修改)字节码来实现增强的逻辑,如果良性使用当然不会影响程序的正常行为,如果恶性使用就可能产生一些负面的影响(其实很多商用Java程序如IntelliJ IDEA的License的破解都可以基于Instrumentation
的功能实现,前提是找到程序认证License的入口)。
1.1 instrument原理
instrument
的底层实现依赖于JVMTI
,也就是JVM Tool Interface
,它是JVM暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。JVMTIAgent
是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load
)、代理通过attach形式加载(agent on attach
)和代理卸载(agent on unload
)功能的动态库。而instrument agent
可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent)
,也就是专门为java语言编写的插桩服务提供支持的代理。因为涉及到源码分析,笔者暂时没能力展开,可以详细阅读参考资料中你假笨大神的那篇专门分析JVM相关源码实现的文章。
其中,VM启动时加载Agent可以使用命令行参数-javaagent:yourAgent.jar的形式实现。
三. Instrumentation接口详解
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
注册ClassFileTransformer
实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer
实例,相当于它们通过了redefineClasses
方法进行重定义。布尔值参数canRetransform
决定这里被重定义的类是否能够通过retransformClasses
方法进行回滚。
void addTransformer(ClassFileTransformer transformer)
相当于addTransformer(transformer, false)
,也就是通过ClassFileTransformer
实例重定义的类不能进行回滚。
boolean removeTransformer(ClassFileTransformer transformer)
移除(反注册)ClassFileTransformer实例。
boolean isRetransformClassesSupported()
返回当前JVM配置是否支持类重新转换的特性。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer
的列表中进行处理,想深入理解建议阅读API注释。
boolean isRedefineClassesSupported()
返回当前JVM配置是否支持重定义类(修改类的字节码)的特性。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
注册ClassFileTransformer
实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer
实例,相当于它们通过了redefineClasses
方法进行重定义。布尔值参数canRetransform
决定这里被重定义的类是否能够通过retransformClasses
方法进行回滚。
void addTransformer(ClassFileTransformer transformer)
相当于addTransformer(transformer, false)
,也就是通过ClassFileTransformer
实例重定义的类不能进行回滚。
boolean removeTransformer(ClassFileTransformer transformer)
移除(反注册)ClassFileTransformer实例。
boolean isRetransformClassesSupported()
返回当前JVM配置是否支持类重新转换的特性。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer
的列表中进行处理,想深入理解建议阅读API注释。
boolean isRedefineClassesSupported()
返回当前JVM配置是否支持重定义类(修改类的字节码)的特性。
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException
重定义类,也就是对已经加载的类进行重定义,ClassDefinition类型的入参包括了对应的类型Class<?>对象和字节码文件对应的字节数组。
其他功能:
-
boolean isModifiableClass(Class<?> theClass)
:判断对应类是否被修改过。 -
Class[] getAllLoadedClasses()
:获取所有已经被加载的类。 -
Class[] getInitiatedClasses(ClassLoader loader)
:获取所有已经被初始化过了的类。 -
long getObjectSize(Object objectToSize)
:获取某个对象的(字节)大小,注意嵌套对象或者对象中的属性引用需要另外单独计算。 -
void appendToBootstrapClassLoaderSearch(JarFile jarfile)
:将某个jar加入到Bootstrap Classpath里优先其他jar被加载。 -
void appendToSystemClassLoaderSearch(JarFile jarfile)
:将某个jar加入到Classpath里供AppClassloard去加载。 -
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)
:设置某些native方法的前缀,主要在找native方法的时候做规则匹配。 -
boolean isNativeMethodPrefixSupported()
:是否支持设置native方法的前缀。 -
void redefineModule(...)
:重定义Module。 -
boolean isModifiableModule(Module module)
:判断指定Module是否重定义过。
四. 如何使用Instrumentation
Instrumentation
类在API注释中有十分简洁的使用方式描述:
有两种方式可以获取Instrumentation接口的实例:
- JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
- JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。
首先我们知道Instrumentation的实现类是sun.instrument.InstrumentationImpl
,在JDK9之后,由于模块权限控制,不可能通过反射构造其实例,一般情况下反射做不到的东西只能通过JVM实现。而且根据上面简洁的API注释我们是无法得知如何使用Instrumentation
。其实,premain
对应的就是VM启动时的Instrument Agent
加载,也就是上文提到的agent on load
,而agentmain
对应的是VM运行时的Instrument Agent
加载,也就是上文提到的agent on attach
。两种加载形式所加载的Instrument Agent
都关注同一个JVMTI事件 – ClassFileLoadHook
事件,而这个事件是在读取字节码文件之后回调时用。换言之,premain和agentmain
方式的回调时机都是类文件字节码读取之后(或者说是类加载之后)。
实际上,premain
和agentmain
两种方式最终的目的都是为了回调Instrumentation
实例并且激活sun.instrument.InstrumentationImpl#transform()
从而回调注册到Instrumentation
中的ClassFileTransformer
实现字节码修改,本质功能上没有很大区别。两者的非本质功能的区别如下:
-
premain
需要通过命令行使用外部代理jar包;而agentmain
则可以通过attach
机制直接附着到目标VM中加载代理,也就是使用agentmain
方式下,操作attach的程序和被代理的程序可以是完全不同的两个程序。 -
premain
方式回调到ClassFileTransformer
中的类是虚拟机加载的所有类,这个是由于代理加载的顺序比较靠前决定的,在开发者逻辑看来就是:所有类首次加载并且进入程序main()方法之前,premain
方法会被激活,然后所有被加载的类都会执行ClassFileTransformer
列表中的回调。 -
agentmain
方式由于是采用attach
机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)
让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer
列表中的回调。 -
premain
方式是JDK1.5引入的,而agentmain
方式是JDK1.6引入的,也就是JDK1.6之后可以自行选择使用premain
或者agentmain
。
4.1 premain使用方式
premain
方式依赖独立的javaagent
,也就是单独建立一个项目编写好代码之后打成jar包供另一个使用程序通过代理形式引入。简单的步骤如下:
①编写premain函数,也就是编写一个普通的Java类,包含下面两个方法的其中之一。
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
②通过指定Agent运行。
java -javaagent:代理Jar包的路径 [=传入premain的参数] yourTarget.jar
简单例子如下:
新建一个premain-agent
的项目,新建一个类club.throwable.permain.PermainAgent
如下:
public class PermainAgent {
private static Instrumentation INST;
public static void premain(String agentArgs, Instrumentation inst) {
INST = inst;
process();
}
private static void process() {
INST.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> clazz,
ProtectionDomain protectionDomain,
byte[] byteCode) throws IllegalClassFormatException {
System.out.println(String.format("Process by ClassFileTransformer,target class = %s", className));
return byteCode;
}
}
);
}
}
引入Maven
插件maven-jar-plugin
:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>club.throwable.permain.PermainAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
通过mvn package
命令打包即可得到premain-agent.jar
(笔者发现该插件未支持JDK11,所以降级到JDK8)。接着可以使用该代理Jar:
// 这个是样品类
public class HelloSample {
public void sayHello(String name) {
System.out.println(String.format("%s say hello!", name));
}
}
// main函数,vm参数:-javaagent:I:J-Projectsinstrument-samplepremain-agenttargetpremain-agent.jar
public class PermainMain {
public static void main(String[] args) throws Exception{
}
}
// 输出结果
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders$1
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders$Cache
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders$2
Process by ClassFileTransformer,target class = com/intellij/rt/execution/application/AppMainV2$Agent
Process by ClassFileTransformer,target class = com/intellij/rt/execution/application/AppMainV2
Process by ClassFileTransformer,target class = com/intellij/rt/execution/application/AppMainV2$1
Process by ClassFileTransformer,target class = java/lang/reflect/InvocationTargetException
Process by ClassFileTransformer,target class = java/net/InetAddress$1
Process by ClassFileTransformer,target class = java/lang/ClassValue
// ... 省略大量其他输出
实际上,如果我们要定制功能需要排除掉一些java.lang包和sun包的类,当然这里仅仅作为演示所以无伤大雅。
4.2 agentmain使用方式
agentmain
的使用方式和permain
十分相似,包括编写MANIFEST.MF
和生成代理Jar包。但是,它并不需要通过-javaagent
命令行形式引入代理Jar,而是在运行时通过attach工具激活指定代理即可。简单的步骤如下:
①编写premain
函数,也就是编写一个普通的Java类,包含下面两个方法的其中之一。
public static void agentmain(String agentArgs, Instrumentation inst); [1]
public static void agentmain(String agentArgs); [2]
①的回调优先级会比②高,也就是[1]和[2]同时存在的情况下,只有①会被回调。而agentArgs
是agentmain
函数得到的程序参数,通过com.sun.tools.attach.VirtualMachine#loadAgent(var1,var2)
中的var2传入,var1就是代理Jar的绝对路径。
②代理服务打包为Jar。
Agent一般是一个普通的Java服务,只是需要编写agentmain函数,并且该Jar包的manifest(也就是MANIFEST.MF文件)属性中需要加入Agent-Class来指定步骤1中编写好agentmain函数的那个Java类。
③通过attach工具直接加载Agent,执行attach的程序和需要被代理的程序可以是两个完全不同的程序。
// 列出所有VM实例
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// attach目标VM
VirtualMachine.attach(descriptor.id());
// 目标VM加载Agent
VirtualMachine#loadAgent("代理Jar路径","命令参数");
举个简单的例子:编写agentmain函数的类如下:
public class AgentmainAgent {
private static Instrumentation INST;
public static void agentmain(String agentArgs, Instrumentation inst) {
INST = inst;
process();
}
private static void process() {
INST.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> clazz,
ProtectionDomain protectionDomain,
byte[] byteCode) throws IllegalClassFormatException {
System.out.println(String.format("Agentmain process by ClassFileTransformer,target class = %s", className));
return byteCode;
}
}
, true);
try {
INST.retransformClasses(Class.forName("club.throwable.instrument.AgentTargetSample"));
}
catch (Exception e) {
e.printStackTrace();
}
}
}
更改Maven插件maven-jar-plugin的配置,然后通过mvn pacakge打包:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestEntries>
<!-- 主要改这个配置项 -->
<Agent-Class>club.throwable.permain.PermainAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
负责attach工作的程序AgentmainAttachMain:
public class AgentmainAttachMain {
public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor descriptor : list) {
if (descriptor.displayName().endsWith("AgentTargetSample")) {
VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());
virtualMachine.loadAgent("I:\J-Projects\instrument-sample\premain-agent\target\premain-agent.jar", "arg1");
virtualMachine.detach();
}
}
}
}
被代理的目标程序AgentTargetSample:
public class AgentTargetSample {
public void sayHello(String name) {
System.out.println(String.format("%s say hello!", name));
}
public static void main(String[] args) throws Exception {
AgentTargetSample sample = new AgentTargetSample();
for (; ; ) {
Thread.sleep(1000);
sample.sayHello(Thread.currentThread().getName());
}
}
}
接着先启动AgentTargetSample,然后再启动AgentmainAttachMain:
main say hello!
main say hello!
main say hello!
main say hello!
main say hello!
main say hello!
main say hello!
Agentmain process by ClassFileTransformer,target class = club/throwable/instrument/AgentTargetSample
main say hello!
main say hello!
main say hello!
PS:如果没有找到
VirtualMachineDescriptor
或者VirtualMachine
,只需要把${JAVA_HONE}/lib/tools.jar
拷贝到${JAVA_HONE}/jre/lib
目录下即可。
五. Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:
- premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
- 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新类和老类的父类必须相同。
- 新类和老类实现的接口数也要相同,并且是相同的接口。
- 新类和老类访问符必须一致。
- 新类和老类字段数和字段名要一致。
- 新类和老类新增或删除的方法必须是private static/final修饰的。
- 可以修改方法体。
除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。
六. 小结
本文仅仅简单分析instrument
的原理和基本使用,可以体会到instrument
让Java具有了更强的动态控制、解释能力,从而让Java语言变得更加灵活多变。在JDK1.6之后,使用Instrumentation
,开发者可以构建一个独立于应用程序的代理程序,用来监测和协助运行在JVM上的程序,可以远程重新转换指定JVM实例里面的已经加载的类,这一点实现从开发者角度来看就像是从JVM级别支持了AOP编程。
写在最后
- 第一:看完点赞,感谢您的认可;
- ...
- 第二:随手转发,分享知识,让更多人学习到;
- ...
- 第三:记得点关注,每天更新的!!!
- ...
推荐阅读
-
Java 程序员必须知道:深入了解 Instrument
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)