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

能够不会,可是得知道java

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


前言

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

问题场景

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

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);
    }
}
复制代码

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

罪魁祸首已经找到errorMethod获得的结果并非我想要的,因而提出如下问题:api

提出问题

  • 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中不一样的地方,其余绝技会再其余的文章进行介绍app

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/dom

nacos (奇异博士)

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

  • 服务发现和服务健康监测
  • 动态配置服务
  • 动态 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> 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了。在此我的只是提出了一种解决问题的方法和思路。代码也并不完善,若是存在任何错误,欢迎你们指正。

相关文章
相关标签/搜索