转载

线上问题解决方案之[偷梁换柱]

可以不会,但是得知道

作为一名默默无闻辛苦搬砖的程序员,搬砖和造轮子都不是最终目的。拜读过许多优秀的文章,心想自己有什么骚操作值得拿出来分享一下的,结合自己平时工作的遇到的问题,于是总结出这篇线上问题解决方案之 偷梁换柱 ,免升级解决线上问题

前言

在一个风雨交加的晚上,自己躺在床上心神不宁,似乎在暗示着什么。于是一整天的经历在脑海里像电影一样放映着,于是画面定格在了那一秒,下午的那一次 升级 ,What's wrong with that,到底有什么不对,于是赶紧打开自己的人脑debug模式,一行一行的去回忆的自己写的每一行代码。卧*,灵感到来突然之间,不知道是该感叹自己记忆的强大,还是该为这个 NullPointerException 而害怕。事故、绩效、年终奖、线上用户多个词同时映入在自己的脑海里,越想越心神不宁,写出去的代码泼出去的水,还有什么方式可以补救,于是又一番思绪涌上心头…

问题场景

假设线上有一个service(spring bean)的其中一个方法出现了空指针异常或者得到的并不是我们想要的结果,现在用一个ErrorService(自己程序里面的一个类模拟一下),假设这就是那个service,errorMethod方法是那个让我躺在床上久久不能入眠的那个方法,接下来通过代码展示一下罪魁祸首

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Random;
/**
 * 用来模拟线上异常service
 *
 * @author: ghb
 * @date 2020/1/4
 */
@Service
public class ErrorService {
    @Autowired
    protected PrintService printService;
    private int id;
    public ErrorService() {
        Random random = new Random();
        id = random.nextInt(100);
        System.out.println("调用ErrorService的构造方法");
    }
    /**
     * 这个是需要进行修改的目标方法
     */
    public String errorMethod(String msg) {
        return printService.print("[ErrorService]-print-" + msg + id);
    }
}
复制代码

先来看一下接口的返回结果>>>

线上问题解决方案之[偷梁换柱]

罪魁祸首已经找到 errorMethod 得到的结果并不是我想要的,于是提出以下问题:

提出问题

  • spring bean可以进行修改吗
  • 如何才能不升级分分钟就解决问题,避免担惊受怕
  • 做到掩人耳目、偷梁换柱需要具备哪些条件

解决方案

想着想着脑海里又浮现出复联4中复联大军PK灭霸的场景,面对 errorMethod ,我何德何能,到底谁才是带给我希望的美国队长和Iron Man 呢,形势岌岌可危,接下来有请他们上场

线上问题解决方案之[偷梁换柱]

groovy (Iron Man)

  • groovy跟java都是基于jvm的语言,可以在java项目中集成groovy并充分利用groovy的动态功能;
  • groovy兼容几乎所有的java语法,开发者完全可以将groovy当做java来开发,甚至可以不使用groovy的特有语法,仅仅通过引入groovy并使用它的动态能力;
  • groovy可以直接调用项目中现有的java类(通过import导入),通过构造函数构造对象并直接调用其方法并返回结果;

下面通过一段代码演示一下groovy 的stream遍历跟java中不同的地方,其他绝技会再其他的文章进行介绍

final personList = [
                new Person("Regina", "Fitzpatrick", 25),
                new Person("Abagail", "Ballard", 26),
                new Person("Lucian", "Walter", 30),
        ]
assertTrue(personList.stream().filter { it.age > 20 }.findAny().isPresent())
assertFalse(personList.stream().filter { it.age > 30 }.findAny().isPresent())
assertTrue(personList.stream().filter { it.age > 20 }.findAll().size() == 3)
assertTrue(personList.stream().filter { it.age > 30 }.findAll().isEmpty())
复制代码

官网请参考www.groovy-lang.org/

nacos (奇异博士)

线上问题解决方案之[偷梁换柱]

阿里巴巴在2018年7月份发布Nacos, Nacos 支持几乎所有主流类型的服务的发现、配置和管理Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

  • 服务发现和服务健康监测
  • 动态配置服务
  • 动态 DNS 服务
  • 服务及其元数据管理

Nacos具备服务优雅上下线和流量管理(API+后台管理页面),而Eureka的后台页面仅供展示,需要使用api操作上下线且不具备流量管理功能。Nacos具有分组隔离功能,一套Nacos集群可以支撑多项目、多环境。nacos具有Apollo大部分功能,最重要的是配置中心与注册中心打通,可以省去我们在微服务治理方面 的一些投入

因为这篇位置奇异博士只是作为配角出场,在此先不对其进行过多的介绍,这里只是提供它 传送门 的作用,先来了解一下spring cloud项目如何引入nacos

<!--配置中心-->
<dependency>
        <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
         <version>0.2.1.RELEASE</version>
 </dependency>
<!--服务注册与发现-->
 <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <version>0.2.1.RELEASE</version>
 </dependency>
复制代码
线上问题解决方案之[偷梁换柱]

传送门

这个dataId为 偷梁换柱 就是我们的 传送门 ,目的就是通过它来将我们用于干掉 ErrorService (灭霸)的那段代码传送过来,至于为什么选择它作为奇异博士还纯属个人喜好。那么我们来看看传送门内部长什么样吧

线上问题解决方案之[偷梁换柱]

传送门里面是一个list列表,里面的每一项对应着我们需要进行替换的 Service 的名称,已经知道了敌人是谁,那我们就得把打boss的秘密武器释放出来,那么欢迎 钢铁侠 上场

groovy class

import com.ghb.book.model.A
import com.ghb.book.service.ErrorService
import org.springframework.beans.factory.annotation.Autowired

class CorrectService extends ErrorService {
    private int id;
    /**
     * 这里注入一个spring bean并且调用它的get方法
     */
    @Autowired
    A a
    public CorrectService() {
        Random random = new Random();
        id = random.nextInt(100);
        System.out.println("调用CorrectService的构造方法");
    }
    @Override
    String errorMethod(String msg) {
        return printService.print("[CorrectService]-print" + msg + id);
    }
}
复制代码

先来介绍下这段代码,这是用groovy声明的一个类,其保存在nacos配置中心里面,为什么使用它保存我们的代码,因为作为配置中心,它支持各种格式文件的保存,并且历史版本可以支持代码回滚,之前一直使用 apollo 作为配置中心,值得遇到了 nacos ,我才发现原来曾经深刻认为的并不是我想要的…

线上问题解决方案之[偷梁换柱]

该类继承自ErrorService,重新了 errorMethod 方法,返回需要的正确的结果,现在它只是一个普通的类,里面为什么能注入 @Autowired 一个spring bean呢,就是因为那是钢铁侠吗,当然不是,如果剧情只有这么简单那岂不是很不过瘾

刚才奇异博士的 传送门 我们已经看到了,那么这段代码就是需要被传送的秘密武器了,接下来请观看奇异博士搞出传送门这个东西,到底修炼了什么功法

武功秘籍

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.ghb.book.service.RegisterBeanService;
import com.ghb.book.util.GroovyScriptFactory;
import com.ghb.book.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosConfigProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.Executor;

/**
 * @author: ghb
 * @date 2019/12/24
 */
@Component
@Slf4j
public class InitConfig {

    @Autowired
    private NacosConfigProperties nacosConfigProperties;
    @Autowired
    private RegisterBeanService registerBeanService;
    @PostConstruct
    public void init() throws NacosException {
        nacosConfigProperties.configServiceInstance()
                .addListener("偷梁换柱", "DEFAULT_GROUP", new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }
                    @Override
                    public void receiveConfigInfo(String s) {
                        log.info("监听到数据更新:{}", s);
                        JSONArray jsonArray = JSON.parseArray(s);
                        jsonArray.forEach(beanName -> {
                            try {
                                //惊奇队长上线 注册新的bean到spring容器中
                                register(String.valueOf(beanName));
                            } catch (NacosException e) {
                                log.info("偷梁换柱失败");
                            }
                        });
                    }
                });
    }

    /**
     * 1 从nacos中获取配置类
     * 2 解析类
     * 3 偷梁换柱
     *
     * @param beanName spring bean 命名
     * @throws NacosException e
     */
    public void register(String beanName) throws NacosException {
        //读取groovy配置
        String groovy = nacosConfigProperties.configServiceInstance()
                .getConfig(beanName, "groovy", 1000);
        //加载groovy类 并获取groovy class类类型
        Class groovyClass = GroovyScriptFactory.getInstance().parseClass(groovy);
        //注册bean
        Object bean = registerBeanService.registerBean(beanName, groovyClass);
        log.info("bean---{}", bean.getClass().getName());
    }
}
复制代码

在spring容器启动的时候,执行声明周期回调方法,通过 @PostConstruct 去添加一个监听器,去监听传送门里面的数据,这里这个传送门是代码里面的 偷梁换柱 ,如果监听到数据有变化,去遍历传送门里面的数据,然后将每一项交给美国队长,注册新的bean到spring容器中,关于如何解析一个groovy类本片文章先不做过多的赘述

  1. 监听偷梁换柱
  2. 根据 dataId 从nocos中读取配置好的groovy类
  3. 解析groovy类加载到JVM内存并且返回groovy类的类型
  4. 交给美国队长去注册groovy类到spring容器中

register bean(美国队长)

美国漫画中最“主旋律”的超级英雄非美国队长莫属。他用国名当做头衔,制服是红白蓝加上明亮的星星;一面同样颜色的盾牌就是他的武器。接下来期待下他的操作

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

/**
 * @author: ghb
 * @date 2020/1/4
 */
@Component
@Slf4j
public class RegisterBeanService implements ApplicationContextAware {
    ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public <T> T registerBean(String name, Class<T> clazz, Object... args) {
        ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        for (Object arg : args) {
            beanDefinitionBuilder.addConstructorArgValue(arg);
        }
        //bean定义
        BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
        //从spring容器中获取bean工厂
        BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) context.getBeanFactory();
        if (context.containsBean(name)) {
            //先从spring容器中获取该bean
            Object bean = context.getBean(name);
            if (bean.getClass().isAssignableFrom(clazz)) {
                log.info("bean:[{}]在系统中已存在,接下来对其进行替换操作", name);
                if (clazz.getGenericSuperclass() == bean.getClass()) {
                    log.info("新bean是被替换bean的子类,符合逻辑,开始替换操作.....");
                    beanFactory.removeBeanDefinition(name);
                    beanFactory.registerBeanDefinition(name, beanDefinition);
                } else {
                    throw new RuntimeException("偷梁换柱失败,非法操作");
                }
            } else {
                log.info("bean:[{}]系统中不存在存在,创建bean", name);
                beanFactory.registerBeanDefinition(name, beanDefinition);
            }
        }
        return applicationContext.getBean(name, clazz);
    }
}
复制代码

这段代码就是注册bean到spring容器中的核心操作了,这里只是一个用于实现功能的简化版的方法,我们来看看它到底干了什么

  1. 继承 ApplicationContextAware ,获得 ApplicationContext 对象
  2. 通过 BeanDefinitionBuilder 创建bean定义
  3. 先从 spring容器 中获取该bean
  4. 如果bean再系统中已存在,对其进行替换操作
  5. 如果不存在则创建bean

上述操作完成后我们已经实现了偷梁换柱并且灭霸最终被打败,弹响指的过程就是忘nacos配置中心中偷梁换柱中添加 errorService 的过程,当然只是使用nacos最为一个媒介,通过接口调用也是可以,我们来看看重新回归和平后的模样,终于守的云开见月明,一切问题都迎刃而解

线上问题解决方案之[偷梁换柱]

思考几个问题

  • CGLib动态代理
  • CorrectService中的A什么时候被注入的
  • spring bean生命周期
  • groovy如何使用

总结

经过上述操作,结合groovy、nacos和spring实现了对问题的动态修复,灭霸页最终被打败,从此再也不用担心自己线上写出来的bug了。在此个人只是提出了一种解决问题的方法和思路。代码也并不完善,如果存在任何错误,欢迎大家指正。

原文  https://juejin.im/post/5e104d06f265da5d3c6de188
正文到此结束
Loading...