场景
在线上往往会遇到一些比较尴尬的异常,例如空指针。这种操作往往是某些情况校验不完善,客户输入了各种奇怪的内容导致的。当遇到这种情况的时候,修改都很方便,但是如何更新到线上是个问题了。为一个小问题,重新更换环境就动作有点大了,还得晚上派人值守。
更新方式
我们主要利用了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。这个过程非常短暂,相比替换版本而言,这个已经是代价非常小了。