不停机上线服务_项目刚上线,甲方突然让不停机改需求……

        经过了与甲方半个月的热情合(si)作(bi),项目终于上线了,试运行正常后,下班准备放松一下。当你带着老婆,吃着火锅,还唱着歌,突然甲方爸爸来电话说某个需求要稍微调整一下,并且不能停机,这个时候的表情想必是这样的:

 4755d76be6dbd7b5714249f9dad8cedd.png

        改需求还好做一些,可是要做到项目不停机改业务代码,这就比较麻烦。如果是在分布式环境,使用诸如负载均衡、金丝雀发布方式进行轮流替换、滚动更新代码即可,但在单jvm进程环境下,这种方法无法实现。

        稍微平复一下熊熊的吐嘈之魂,分析了甲方的需求并盘点了一下解决方案。原业务场景比较复杂,经过简化如下:将长期合作的渠道供应商由A切换为B。解决方案分两步,第一,修改所有代码中的供应商属性,第二,将所有修改过的类信息再次加载入jvm虚拟机。修改类信息,可以使用jdk提供的字节码编程工具asm进行实现,加载修改过后的 .class文件可以使用自定义类加载器。

        先定义一个MyConfig类,channel为渠道供应商类属性,固定值为A,原甲方需求翻译成编码需求就是在不停机情况下将channel属性由A改为B。

 public class MyConfig {    /**     * 渠道信息     */    public static final String channel = "A";}

        第一步,定义一个类转换器,用于修改.class的类信息。asm的树api中的ClassNode表示用于生成和转换已编译 Java 类,fields是类的属性集合,在transform方法中,可以通过fields元素的添加删除,实现操作目标类中定义的属性。

 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依赖

 <dependency>    <groupId>org.ow2.asmgroupId>    <artifactId>asm-treeartifactId>    <version>7.0version>dependency>

        第二步,定义一个类加载器MyClassLoader,用于加载class文件。

 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@AllArgsConstructorpublic class ClassElement {    /**     * 类文件地址     */    private String path;    /**     * 类的class对象     */    private Class clzzz;}

        使用一个InitHandler类,用于在spring容器启动时将目标类放入工厂。

 @Componentpublic 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来模拟业务,获取渠道信息。

 @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,两者一致。

aeae0f80979616f3f08da9c6566cae26.png

        新建一个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 0x21public 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。

eb96196199c538c45fe7c5c18a2fd279.png

        这时候再次调用业务接口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之类的字节码转换工具进行类信息修改,同时使用自定义类加载器进行加载,最后使用反射访问新的类信息。

代码下载地址:https://github.com/kaccnGit/hotload
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值