通过了与甲方半个月的热情合(si)做(bi),项目终于上线了,试运行正常后,下班准备放松一下。当你带着老婆,吃着火锅,还唱着歌,忽然甲方爸爸来电话说某个需求要稍微调整一下,而且不能停机,这个时候的表情想必是这样的:java
改需求还好作一些,但是要作到项目不停机改业务代码,这就比较麻烦。若是是在分布式环境,使用诸如负载均衡、金丝雀发布方式进行轮流替换、滚动更新代码便可,但在单jvm进程环境下,这种方法没法实现。git
稍微平复一下熊熊的吐嘈之魂,分析了甲方的需求并盘点了一下解决方案。原业务场景比较复杂,通过简化以下:将长期合做的渠道供应商由A切换为B。解决方案分两步,第一,修改全部代码中的供应商属性,第二,将全部修改过的类信息再次加载入jvm虚拟机。修改类信息,可使用jdk提供的字节码编程工具asm进行实现,加载修改事后的 .class文件可使用自定义类加载器。github
先定义一个MyConfig类,channel为渠道供应商类属性,固定值为A,原甲方需求翻译成编码需求就是在不停机状况下将channel属性由A改成B。web
public class MyConfig { /** * 渠道信息 */ public static final String channel = "A"; }
第一步,定义一个类转换器,用于修改.class的类信息。asm的树api中的ClassNode表示用于生成和转换已编译 Java 类,fields是类的属性集合,在transform方法中,能够经过fields元素的添加删除,实现操做目标类中定义的属性。spring
public class ConfigTransformer { private int fieldAccess; private String fieldName; private String fieldDesc; private String fieldValue; /** * 构造器 * * @param fieldAccess 属性修饰符 * @param fieldName 属性名 * @param fieldDesc 属性类型 * @param fieldValue 属性值 */ public ConfigTransformer(int fieldAccess, String fieldName, String fieldDesc, String fieldValue) { this.fieldAccess = fieldAccess; this.fieldName = fieldName; this.fieldDesc = fieldDesc; this.fieldValue = fieldValue; } /** * 执行类转换 * * @param cn */ public void transform(ClassNode cn) { //删除原属性 cn.fields.removeIf(fieldNode -> fieldNode.name.equals(fieldName)); //添加属性,并赋新值 cn.fields.add(new FieldNode(fieldAccess, fieldName, fieldDesc, null, fieldValue)); } }
引入asm工具的maven依赖编程
<!--asm字节码编程--> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-tree</artifactId> <version>7.0</version> </dependency>
第二步,定义一个类加载器MyClassLoader,用于加载class文件。api
public class MyClassLoader extends ClassLoader { private String path;//类加载类的路径 private String name;//类加载器的名称 /** * 让系统类加载器成为该类的父加载器 * * @param name * @param path */ public MyClassLoader(String name, String path) { super(); this.name = name; this.path = path; } /** * 指定父加载器 * * @param parent * @param name * @param path */ public MyClassLoader(ClassLoader parent, String name, String path) { super(parent); this.name = name; this.path = path; } /** * 重写findClass方法,父加载器找不到class文件时,经过该方法寻找文件并转化成流 * * @param name * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] date = readToByte(name); return this.defineClass(name, date, 0, date.length); } /** * .class文件转化为byte数组 * * @param name * @return */ private byte[] readToByte(String name) { InputStream is = null; byte[] returnData = null; name = name.replaceAll("\\.", "/"); String filePath = this.path + name + ".class"; File file = new File(filePath); ByteArrayOutputStream os = new ByteArrayOutputStream(); try { is = new FileInputStream(file); int tmp = 0; while ((tmp = is.read()) != -1) { os.write(tmp); } returnData = os.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); os.close(); } catch (Exception e) { e.printStackTrace(); } } return returnData; } }
封装类转换器和类加载器后,开始搭建web项目,这里使用springboot做为项目框架,并维护一个工厂ClassFactory单例,内含一个自定义类加载池。数组
public class ClassFactory { /** * 单例下维护一个自定义类加载池 */ private Map<String, ClassElement> classMap = new ConcurrentHashMap<>(); /** * 获取对应的Class对象 * @param name * @return */ public ClassElement getConfig(String name) { return classMap.get(name); } /** * 添加Class元素 * @param name * @param classElement * @return */ public boolean addClass(String name, ClassElement classElement) { classMap.put(name, classElement); return true; } /** * 移除Class元素 * @param name * @return */ public boolean removeClass(String name) { classMap.remove(name); return true; } private ClassFactory() { } public static ClassFactory getInstance() { return SingletonEnum.INSTANCE.getInstnce(); } /** * 枚举实现单例 */ public enum SingletonEnum { INSTANCE; private ClassFactory classFactory; SingletonEnum() { classFactory = new ClassFactory(); } public ClassFactory getInstnce() { return classFactory; } } } @Data @AllArgsConstructor public class ClassElement { /** * 类文件地址 */ private String path; /** * 类的class对象 */ private Class clzzz; }
使用一个InitHandler类,用于在spring容器启动时将目标类放入工厂。springboot
@Component public class InitHandler implements ApplicationContextAware { /** * 初始化工厂,将须要加载的类及路径包装存入工厂的自定义类加载池 * * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { try { //将目标类的.class文件读取并转移至特定目录,如myclasses/下,方便后续读取、解析、转换 InputStream input = this.getClass().getClassLoader().getResourceAsStream(MyConfig.class.getName().replace(".", "/") + ".class"); byte[] bytes = new byte[input.available()]; input.read(bytes); String path = "myclasses/"; FileTool.write(path + MyConfig.class.getName().replace(".", File.separator) + ".class", bytes, true, true); //将目标类的反射class对象放入自定义类加载池 ClassFactory.getInstance().addClass(MyConfig.class.getName(), new ClassElement(path, MyConfig.class)); } catch (Exception e) { e.printStackTrace(); } } }
定义一个Controller来模拟业务,获取渠道信息。app
@RestController @RequestMapping("asm") public class AsmController { /** * 获取业务配置信息,渠道信息 * * @return * @throws IllegalAccessException * @throws NoSuchFieldException */ @RequestMapping("/getconfig") public String getConfig() throws IllegalAccessException, NoSuchFieldException { //从类加载池中获取配置类MyConfig的Class对象 ClassElement classElement = ClassFactory.getInstance().getConfig(MyConfig.class.getName()); Class c = classElement.getClzzz(); //经过反射获取channel属性值 Field f = c.getField("channel"); String channelValue = (String) f.get(null); return "渠道信息为:" + channelValue; } }
项目启动后,调用http://localhost:8080/asm/getconfig,返回”渠道信息为:A“,表示如今的渠道信息为A,能够看下myclasses/com/config目录下的MyConfig.class文件,channel属性值为A,二者一致。
新建一个transForm方法,将channel属性转换成指定值,构建类分析器ClassReader及ClassNode,使用前面定义的ConfigTransformer进行类转换。
/** * 执行配置类转换 * * @param config * @throws IOException */ @RequestMapping("/transform") public void transForm(@RequestParam String config) throws IOException { //第一步,构建类分析器ClassReader ClassElement classElement = ClassFactory.getInstance().getConfig(MyConfig.class.getName()); FileInputStream io = new FileInputStream(classElement.getPath() + MyConfig.class.getName().replace(".", File.separator) + ".class"); ClassReader cr = new ClassReader(io); //第二步,构建树API ClassNode ClassNode cn = new ClassNode(); cr.accept(cn, 0); //第三步,进行类转换,这是最关键的一步,将静态属性channel的值替换为请示值 ConfigTransformer at = new ConfigTransformer(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "channel", "Ljava/lang/String;", config); at.transform(cn); //第四步,将转换成功的类生成byte流 ClassWriter cw = new ClassWriter(0); cn.accept(cw); byte[] toByte = cw.toByteArray(); //第五步,生成class文件,转换完成 FileTool.write(classElement.getPath() + MyConfig.class.getName().replace(".", File.separator) + ".class", toByte, true, true); }
字节码编程涉及一些指令操做,idea开发工具能够安装“ASM Bytecode Outline”插件,方便查看类的字节码指令,下面是MyConfig类的字节码相关信息:
// class version 52.0 (52) // access flags 0x21 public class com/config/MyConfig { // compiled from: MyConfig.java // access flags 0x19 public final static Ljava/lang/String; channel = "A" // access flags 0x1 public <init>()V L0 LINENUMBER 8 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this Lcom/config/MyConfig; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 }
调用http://localhost:8080/asm/transform?config=B,再次查看myclasses/com/config目录下的MyConfig.class文件,发现channel属性值已经被转换为B。
这时候再次调用业务接口http://localhost:8080/asm/getconfig,返回”渠道信息为:A“,并无变化,这是由于新转化的MyConfig.class尚未被加载,须要使用loadClass方法进行类加载处理。
/** * 加载配置类 * * @return * @throws Exception */ @RequestMapping("/loadclass") public String loadClass() throws Exception { //自定义类加载器读取并加载class文件,MyConfig.class ClassElement classElement = ClassFactory.getInstance().getConfig(MyConfig.class.getName()); MyClassLoader loader = new MyClassLoader(null, "myloader", classElement.getPath()); Class c = loader.loadClass(MyConfig.class.getName()); //类加载成功后,存放入类加载池 ClassFactory configFactory = ClassFactory.getInstance(); configFactory.removeClass(MyConfig.class.getName()); configFactory.addClass(MyConfig.class.getName(), new ClassElement(classElement.getPath(), c)); return "从新加载类" + c.getName() + "成功"; }
调用http://localhost:8080/asm/loadclass,返回“从新加载类com.config.MyConfig成功”,再次调用业务接口http://localhost:8080/asm/getconfig,返回“渠道信息为:B”,证实修改后的类已经加载成功。
补充说明:getConfig()方法里获取业务配置信息时,使用的是反射,而不是直接访问MyConfig.channel,这是由于java自定义类加载器的loadClass方法返回的是反射的Class对象,后续若是想使用新加载类生成对象,也必须使用反射里的newInstance()方法才能生效。
总结:想要实现不停机修改java服务的类信息,能够经过asm之类的字节码转换工具进行类信息修改,同时使用自定义类加载器进行加载,最后使用反射访问新的类信息。