锁定老帖子 主题:远程执行小工具
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2013-02-04
最后修改:2013-02-05
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 噢啦,就写到这了,希望思路是清晰的,也希望能帮助到正需要的人 补充,我将代码从项目中剥离了出来,由于Netty Server代码有继承结构,懒得剥离了,对这个小工具没有影响请忽略 另外,服务端在加载类之前会对类的常量池做一个修改,替换了System类来重定向日志输出,这里不详细介绍了,参考的周志明大神的《深入理解java虚拟机》一书中的例子代码,具体请参考那本书 附件为代码,maven建的工程,要的同学可以下载参考,有哪写的不好的请多指点 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2013-02-05
没看到你的ClassLoader的使用代码阿,服务器端加载的类是如何管理的,什么时候可以卸载掉?
|
|
返回顶楼 | |
发表时间:2013-02-05
楼主是不是怕别人窥窃你的代码阿,呵呵,最好还是把代码和sample放出来比较好
|
|
返回顶楼 | |
发表时间:2013-02-05
iamiwell 写道 楼主是不是怕别人窥窃你的代码阿,呵呵,最好还是把代码和sample放出来比较好
呵呵,那到不是,只是代码和业务稍有关联,得剥离出来,而且我以为代码都贴出来会理不清思路,一会我整理下代码,附件上传上来 |
|
返回顶楼 | |
发表时间:2013-02-05
iamiwell 写道 没看到你的ClassLoader的使用代码阿,服务器端加载的类是如何管理的,什么时候可以卸载掉?
这个。。。没考虑卸载,有什么好办法指点下吗?ClassLoader的使用代码一会上传 |
|
返回顶楼 | |
发表时间:2013-02-05
mark then learn
|
|
返回顶楼 | |
发表时间:2013-02-05
可以参考下groovy之类的动态语言对class的管理,例如采用WeakReference,每个类配置一个独立的ClassLoader,当不想使用的时候,就可以直接卸载掉了
|
|
返回顶楼 | |
发表时间:2013-02-05
iamiwell 写道 可以参考下groovy之类的动态语言对class的管理,例如采用WeakReference,每个类配置一个独立的ClassLoader,当不想使用的时候,就可以直接卸载掉了
多谢,有时间搞搞看,代码我已经整理好附件上传了 |
|
返回顶楼 | |
发表时间:2013-02-05
其实你的JavaClassExecuter里面,已经是每次new 一个ClassLoader的
现在通过修改常量池指向一个代理的System类,不用做指令修改,思路比较特别,有自己的想法 |
|
返回顶楼 | |
发表时间:2013-02-06
楼主这个方法不错,值得借鉴,感谢分享!
|
|
返回顶楼 | |