场景

在线上往往会遇到一些比较尴尬的异常,例如空指针。这种操作往往是某些情况校验不完善,客户输入了各种奇怪的内容导致的。当遇到这种情况的时候,修改都很方便,但是如何更新到线上是个问题了。为一个小问题,重新更换环境就动作有点大了,还得晚上派人值守。

更新方式

我们主要利用了2中java的外挂技术来完成这种不重启更新环境。这两种技术分别是javaagent以及 Vitural Machine attach。attach主要是为了把javaagent给attach到目标进程上。javaagent里主要写类的重新转化工作。

public class Transform implements ClassFileTransformer {

	@Override
	public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

		if (className.contains(Main.CLASS_NAME)) {//过滤类
			byte[] readAllBytes = null;
			try {
			//读取修改后的字节码
				readAllBytes = Files.readAllBytes(Paths.get("E:\\delete\\Nep.class"));
			} catch (IOException e) {
				e.printStackTrace();
			}
			return readAllBytes;
		}
		return null;
	}

}

当类加载或者重新转化的时候会经过ClassFileTransformer的 transform方法,返回null表示字节码没有任何修改,如果返回一个新的数组,就可以替换加载到内存的字节码了。

public static void agentmain(String args, Instrumentation ins) throws UnmodifiableClassException {
		//第二个参数表示是否能重新定义
		ins.addTransformer(new Transform(), true);
		List<Class> transformClass =new ArrayList<Class>();
		Class[] allLoadedClasses = ins.getAllLoadedClasses();
		for (Class clazz : allLoadedClasses) {
			if (clazz.getName() != null && clazz.getName().contains(CLASS_NAME)) {
				transformClass.add(clazz);
			}
		}
		if(!transformClass.isEmpty()){
			ins.retransformClasses(transformClass.toArray(new Class[0]));
		}
}

agentmain是一个入口函数。当agent加载以后,会从这里执行。Instrumentation 可以帮我们获取到所有加载到内存的类。然后根据要求筛选出合适的类进行重新转化。(省略掉attach代码) 利用这种方式。我们就可以把编译好的字节码准备好,然后以文件流的形式读取到内存中,进行动态的替换类的实现。

适用场景

重转换可能会更改方法体、常量池和属性。重转换不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系。在以后的版本中,可能会取消这些限制。在应用转换之前,类文件字节不会被检查、验证和安装。如果结果字节错误,此方法将抛出异常。

这里是jdk文档里说明的,你的类如果是加了字段,那就不行了。文档中说可能会取消这些限制,现在是可以新增或删除方法,必须是private static以及private final。替换的过程会STW。这个过程非常短暂,相比替换版本而言,这个已经是代价非常小了。