无疆 不朽凡人 侠行天下 斗罗大陆 全职高手 雪鹰领主 择天记 民国之文豪崛起 我真是大明星 绝世武魂 元气少年 俗人回档 重生完美时代 天道图书馆 万界天尊 超凡传 圣墟 捉蛊记 劫天运 神藏 我的姐姐是大明星 帝师 天域神座 我是至尊 惊悚乐园 武道宗师 银狐 未来天王 极道天魔 修真聊天群 元龙 万古仙穹 盛唐风华 一念永恒 重生豪门之强势归来 绝世战魂 完美世界 三生三世十里桃花 大魏宫廷 放开那个女巫 灵域 赘婿 太古神王 神门 山野杂家 异常生物见闻录 逆鳞 逆流纯真年代 牧神记 微微一笑很倾城 文化入侵异世界 电影的世界 校花的贴身高手 逆向恋爱游戏 宰执天下 异界全职业大师 橙红年代 苗疆蛊事 疯巫妖的实验日志 凌天传说 流氓高手 光荣之路 重生之嫡女有毒 大圣传 唐砖 死神逃学日记 武动乾坤 最强弃少 王牌进化 从前有座灵剑山 极品家丁 医妃独步天下 九鼎记 神印王座 黑道特种兵 唐骑 战神领主 大明望族 盛世嫡妃 重生之大涅磐 逍遥兵王 末日蟑螂 斗战狂潮 傲世九重天 一品江山 最终信仰 傲剑凌云 暧昧高手 战争之王 特种教师 百炼成仙 校园全能高手 乱世宏图 五行天 山楂树之恋 冠军之光 亵渎 复活 摄政大明 九仙图 符皇 盖世帝尊 王者归来 暗黑破坏神之毁灭 造化之门 家园 网游之天谴修罗 无限道武者路 寻找前世之旅 执魔 阴阳鬼医 雪中悍刀行 猛龙过江 上品寒士 悟空传 武神天下 冒牌大英雄 张三丰异界游 重生之神级学霸 地球游戏场

手游服务端代码热部署

对于一个部署在生产环境的游戏项目来说,我们希望当代码出现bug的时候,可以不用重启游戏进程而达到动态修改代码的目的——

这就是代码热部署!

我们先来看一下,不同编程语言是如何实现代码热部署的。

使用Javascript写过网页的童鞋们都清楚,修复代码后直接刷新一下浏览器,就可以执行最新的脚本;游戏客户端脚本Lua也是如此,在游戏运行过程中可以动态卸载已加载的文件再重新加载,神不知鬼不觉就把bug修复了;Erlang在游戏服务端应用也相当广泛,函数式语言尽可能不使用全局性变量,状态都是用函数参数保存,天然无状态的特性使得代码热部署变得相当简单……

了解过jvm的朋友们都知道,jvm使用类加载器和类的全路径名称结合起来标识一个类,以此来保证同一个类只能被一个类加载器所加载。因此,我们无法要求jvm主动卸载一个类而修改类代码,除非使用新的自定义类加载器。

利用JDK5的Instrumentation提供的接口,我们可以动态改变JVM已加载的类的方法体。

在JDK5 中,Instrumentation 要求在运行前利用命令行参数或者系统参数来设置代理类。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

下边演示热部署的操作过程。

1.Instrumentation的运行方式都是通过代理的方式实现的,并且代理要放在一个独立的jar包。若需要在JVM启动后再启动代理的话,要求代理类必须有一个静态的agentmain()方法,其函数申明如下:

public static void agentmain(String args,Instrumentation inst);
2.定义代理类

package agent;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;

public class MyAgent {

	//该方法支持在JVM 启动后再启动代理,对应清单的Agent-Class:属性
	public static void agentmain(String args,Instrumentation inst){
		try{
			System.err.println("传进来的参数为"+args);
			File f = new File(args);
			byte[] targetClassFile = new byte[(int)f.length()];
			DataInputStream dis = new DataInputStream(new FileInputStream(f));
			dis.readFully(targetClassFile);
			dis.close();
			
			DynamicClassLoader myLoader = new DynamicClassLoader();
			Class targetClazz = myLoader.findClass(targetClassFile);
			System.err.println("目标class类全路径为"+targetClazz.getName());
			ClassDefinition clazzDef = new ClassDefinition(Class.forName(targetClazz.getName()), targetClassFile);
			inst.redefineClasses(clazzDef);
			
			System.err.println("重新定义"+args+"完成!!");
			
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}
3.自定义类加载器,用于动态加载类文件

package agent;

public class DynamicClassLoader extends ClassLoader{
	public Class<?> findClass(byte[] b) throws ClassNotFoundException { 

		return defineClass(null, b, 0, b.length); 
	} 
}

4.编写清单文件Manifest.MF。该清单格式有严格的要求,详情请参考官方文档“Notes on Manifest and Signature Files”一节。

Manifest-Version: 1.0
Agent-Class: agent.MyAgent
Can-Redefine-Classes: true

这些属性的意义可以在jdk的文档中查阅

5.整个项目的文件结构如下


6.使用Eclipse,将该工程打成jar包为myagent.jar(要包括清单文件)

7.新建另外一个项目,用于演示代码。定义热部署的逻辑方法

import java.lang.management.ManagementFactory;

import com.sun.tools.attach.VirtualMachine;


public class UpdateNormalClass {

	public static boolean updateClass(String className){
		try{
			//拿到当前jvm的进程id
			String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
			VirtualMachine vm = VirtualMachine.attach(pid);
			String[] classStr = className.split("\\.");
			String path = "./class/"+classStr[classStr.length-1]+".class";
			System.err.println("path=="+path);
			vm.loadAgent("./agent/myagent.jar",path);//path参数即agentmain()方法的第一个参数
			return true;
		}catch(Exception e){
			e.printStackTrace();
		}

		return true;
	}
}
注意:VirtualMachine类的导入需要jdk/lib下的tools.jar包,所以要把该包导入到项目环境

8.定义测试类。将该文件编译后,放在src同目录的class文件下

public class Person {

	public String toString(){
		return "热部署后:hello,world....";
	}
}
9.修改Person类(不需要class文件),用于作对比
public class Person {

	public String toString(){
		return "热部署前:hello,everyone";
	}
}
10.编写测试方法

public class Entry {

	public static void main(String[] args) throws Exception {
		final Person p = new Person();  //内存只有一个实例对象
		new Thread(
				new Runnable(){
					@Override
					public void run() {
						while(true){
							try{
								Thread.sleep(1000);
								System.err.println(p);
							}catch(Exception e){

							}
						}
					}
				}
				).start();
		Thread.sleep(3000); //主线程先暂停一会,形成对比效果
		UpdateNormalClass.updateClass("Person");

	}

}

11.运行结果如下,奇迹发生了……


可以看到,Person类的热更新代码生效了。即使是已经初始化的对象!!
12.需要注意的是,该热更方式只支持修改方法内部定义和常量池。这些对于修改线上环境的bug已经足矣。即使要修改的类包含在jar包里面,也可以修改Instrumentation接口可以在jdk文档看到详细信息


总结:

1.通过Instrumentation的代理方式,可以动态改变jvm已加载的类文件的方法体

2.若需要动态在类文件增加或减少方法的话,就需要采用一些迂回的方式(代理类+单例模式

posted on 2015-09-07 22:56 叶哓飞 阅读(...) 评论(...) 编辑 收藏

导航

公告