今天想给项目写个远程执行的小工具
1.客户端动态编译要远程执行的代码
2.通过网络将编译好的字节码传输到服务端
3.服务端留一个类装载器的接口
4.对客户端传输过来的字节码做一定修改(复杂了的不好改,修改常量池还是不难实现的,比如需要输出信息到客户端,却又想用System.out输出,修改常量池就好了,不然System.out只能输出在服务端)
5.用自定义的ClassLoader将要执行的类装载到jvm,然后执行,输出信息返回给客户端
这个工具类还是比较强大的(不过也很危险,看怎么用了),可以看到服务端的任何类的变量,也可以执行清除缓存之类的操作。
以前写过这种小玩意儿,不过是在有web容器的环境下,
现在的项目是基于netty的长连接应用,不过也好搞定,把原来代码拿来改了个把小时搞定
首先写个netty server用来接收要执行的字节码(它要跟随应用Server一同启动,也就是说同jvm)
代码太多容易打乱思路,只贴出主要代码(decode):
class HotSwapPipelineFactory implements ChannelPipelineFactory {
private SimpleChannelHandler messageReceivedHandler = new SimpleChannelHandler() {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
byte[] classByte = (byte[]) e.getMessage(); // decode后的字节码byte数组
// execute内部用自定义ClassLoader加载进jvm然后通过反射执行,返回值为一个String,是返回给客户端的信息,这部分代码就不贴出来了
String resultMsg = JavaClassExecuter.execute(classByte);
byte[] resultByte = resultMsg.getBytes(Charset.forName(Constants.UTF8_CHARSET));
ChannelBuffer buffer = ChannelBuffers.buffer(resultByte.length);
buffer.writeBytes(resultByte);
e.getChannel().write(buffer);
}
};
@Override
public ChannelPipeline getPipeline() throws Exception {
return addHandlers(Channels.pipeline());
}
public ChannelPipeline addHandlers(ChannelPipeline pipeline) {
if (null == pipeline) {
return null;
}
// 这个decoder主要应对消息不完整的情况,虽然是小工具也认真对待吧
pipeline.addLast("hotSwapDecoder", new FrameDecoder() {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
if (buffer.readableBytes() >= 4) {
buffer.markReaderIndex(); // 标记ReaderIndex
int msgBodyLen = buffer.readInt(); // 前四个字节存放消息(字节码)的长度
if (buffer.readableBytes() >= msgBodyLen) {
ChannelBuffer dst = ChannelBuffers.buffer(msgBodyLen);
buffer.readBytes(dst, msgBodyLen);
return dst.array(); // 这就是完整的字节码byte数组了
} else {
buffer.resetReaderIndex();
return null;
}
}
return null;
}
});
pipeline.addLast("hotSwapHandler", messageReceivedHandler);
return pipeline;
}
再写个netty的client发送字节码,代码很简单我就只贴出关键部分吧:
// Connection established successfully
Channel channel = future.getChannel();
channel.setInterestOps(Channel.OP_READ_WRITE);
// 编译参数
List<String> otherArgs = Arrays.asList("-classpath", HotSwapClient.class.getProtectionDomain().getCodeSource().getLocation().toString());
// 编译
byte[] classByte = JavacTool.callJavac(otherArgs, "com.XXX.HotSwap");
ChannelBuffer buffer = ChannelBuffers.buffer(classByte.length + 4);
buffer.writeInt(classByte.length);
buffer.writeBytes(classByte);
channel.write(buffer);
主要想说下动态编译那一块,把以前的代码拿出来,发现当时的自己真山炮啊,先调用编译器接口将java文件编译到硬盘上,再从硬盘读出来,放个小屁何必脱裤子呢,于是今天改了下,编译后直接返回byte[],以下是完整代码:
public class JavacTool {
// java文件的存放路径
public final static String JAVA_FILES_PATH = System.getProperty("user.dir") + "/src/test/java/";
private final static JavacTool JAVAC_TOOL = new JavacTool();
/**
* @param classNames 类的全限定名称
* @return
*/
public static byte[] callJavac(String... classNames) {
return callJavac(null, classNames);
}
/**
* @param otherArgs 其他参数,已有参数包括"-verbose"
* @param classNames 类的全限定名称
* @return
*/
public static byte[] callJavac(List<String> otherArgs, String... classNames) {
// standardJavaFileManager实际类型 : com.sun.tools.javac.file.JavacFileManager
javax.tools.StandardJavaFileManager standardJavaFileManager = null;
ClassFileManager fileManager = null;
try {
// compiler实际类型:com.sun.tools.javac.api.JavacTool
javax.tools.JavaCompiler javac = javax.tools.ToolProvider.getSystemJavaCompiler();
standardJavaFileManager = javac.getStandardFileManager(null, null, null);
fileManager = JAVAC_TOOL.new ClassFileManager(standardJavaFileManager);
for (int i = 0; i < classNames.length; ++i) {
classNames[i] = JAVA_FILES_PATH + classNames[i].replace(".", "/") + ".java";
}
Iterable<? extends javax.tools.JavaFileObject> iterable = standardJavaFileManager.getJavaFileObjects(classNames);
// 相当于命令行调用javac时的参数
List<String> args = new ArrayList<String>();
args.add("-verbose");
if (otherArgs != null) {
for (String arg : otherArgs) {
args.add(arg);
}
}
CompilationTask javacTaskImpl = javac.getTask(null, fileManager, null, args, null, iterable);
// 编译,调用com.sun.tools.javac.main.compile(String[], Context, List<JavaFileObject>, Iterable<? extends Processor>)
if (javacTaskImpl.call()) {
return fileManager.getJavaClassObject().getBytes();
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (standardJavaFileManager != null)
try {
standardJavaFileManager.close();
} catch (IOException e) {
e.printStackTrace();
}
if (fileManager != null)
try {
fileManager.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
// 编译器内部会回调这个内部类的getJavaFileForOutput方法
class ClassFileManager extends ForwardingJavaFileManager<javax.tools.StandardJavaFileManager> {
private JavaClassObject jclassObject;
public JavaClassObject getJavaClassObject() {
return jclassObject;
}
protected ClassFileManager(StandardJavaFileManager fileManager) {
super(fileManager);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
jclassObject = new JavaClassObject(className, kind);
return jclassObject;
}
}
// 这个内部类大有用处哇,编译器内部会回调openOutputStream()这个被重写的方法,拿到你定义的输出流,将字节码写入
class JavaClassObject extends SimpleJavaFileObject {
protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();
public JavaClassObject(String name, JavaFileObject.Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}
public byte[] getBytes() {
return bos.toByteArray();
}
@Override
public OutputStream openOutputStream() throws IOException {
return bos;
}
}
}
再写一个这样的ClassLoader,就差不多了(主要是把defineClass开放出来,注意指定父类装载器,利用双亲委派的规则来访问项目中的所有类)
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
super(HotSwapClassLoader.class.getClassLoader());
}
public Class<?> loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}
}
要注意的是服务端将类加载到jvm后需要通过反射执行(我是写死了直接执行main方法)
比如:
Method method = clazz.getMethod("main", new Class[] { String[].class });
method.invoke(null, new Object[] { null });
我是这样使用的:
1.在当前的项目中新建一个要远程执行的类(因为这个类在你的项目中,所以编译期间你项目中所有的类对它都是可见的)
2.调用上面的netty客户端代码向远程服务器发送就可以舒服的等待返回执行结果了
下面是个小例子:
比如我的项目中有个Season类,我想看看当前服务器的Season,于是我写一个这样的远程执行代码:
public class HotSwap {
public static void main(String[] args) {
System.out.println("test:" + Season.getSeason());
}
}
执行结果:
2013-02-04 23:35:58 [com.futurefleet.framework.concurrency.NamedThreadFactory]-[INFO] new thread created : HotSwapClient_Worker-thread-1, group active count : 1
2013-02-04 23:35:58 [com.futurefleet.framework.concurrency.NamedThreadFactory]-[INFO] new thread created : HotSwapClient_Boss-thread-1, group active count : 2
channelConnected
[解析开始时间 /Users/fengjiachun/Documents/workspace/pandabusGateway/src/test/java/com/futurefleet/tools/hotswap/HotSwap.java]
[解析已完成时间 9ms]
[正在装入 /Users/fengjiachun/Documents/workspace/XXX/target/classes/com/futurefleet/framework/util/Season.class]
[正在装入 java/lang/Object.class(java/lang:Object.class)]
[正在装入 java/lang/String.class(java/lang:String.class)]
[正在检查 com.futurefleet.tools.hotswap.HotSwap]
[正在装入 java/lang/Enum.class(java/lang:Enum.class)]
[正在装入 java/lang/System.class(java/lang:System.class)]
[正在装入 java/io/PrintStream.class(java/io:PrintStream.class)]
[正在装入 java/io/FilterOutputStream.class(java/io:FilterOutputStream.class)]
[正在装入 java/io/OutputStream.class(java/io:OutputStream.class)]
[正在装入 java/lang/StringBuilder.class(java/lang:StringBuilder.class)]
[正在装入 java/lang/AbstractStringBuilder.class(java/lang:AbstractStringBuilder.class)]
[正在装入 java/lang/CharSequence.class(java/lang:CharSequence.class)]
[正在装入 java/io/Serializable.class(java/io:Serializable.class)]
[正在装入 java/lang/Comparable.class(java/lang:Comparable.class)]
[正在装入 java/lang/StringBuffer.class(java/lang:StringBuffer.class)]
[已写入 string:///com/futurefleet/tools/hotswap/HotSwap.class from JavaClassObject]
[总时间 557ms]
test:WINTER
噢啦,就写到这了,希望思路是清晰的,也希望能帮助到正需要的人
补充,我将代码从项目中剥离了出来,完整的代码到我在论坛里发的帖子中可以下载
http://www.iteye.com/topic/1128989#2386832