通過了與甲方半個月的熱情合(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之類的字節碼轉換工具進行類信息修改,同時使用自定義類加載器進行加載,最後使用反射訪問新的類信息。