線上問題解決方案之[偷樑換柱]

能夠不會,可是得知道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了。在此我的只是提出了一種解決問題的方法和思路。代碼也並不完善,若是存在任何錯誤,歡迎你們指正。

相關文章
相關標籤/搜索