使用 javassist 修改位元組碼實現 eureka-client 監聽服務啟動

flyleft發表於2019-02-26

專案中一些服務需要監聽其他微服務的啟動資訊,需要監聽到啟動後主動向其發請求拉取一些配置等。
可是eureka-client並未提供監聽其他服務啟動的事件,eureka-server倒是提供了事件,
可以在自己的eureka-server中監聽服務啟動,監聽後傳送服務啟動資訊到kafka這些訊息佇列,服務監聽kafka訊息,
這種方式要依賴訊息佇列: 原始碼
或者改造eureka-client, 由於需要改的程式碼不多,修改原始碼重新打成依賴自己維護不方便,這裡通過javassist直接修改jar裡的位元組碼實現。

使用javassist修改位元組碼

  • 檢視eureka裡原始碼,有個com.netflix.discovery.shared.Application類,addInstance方法在服務上線或更新,removeInstance方法在服務下線時呼叫,因此修改這倆方法,實現監聽服務上、下線。
  • 由於spring-boot自己實現的類載入機制,以spring-boot的jar形式執行javassist會掃描不到包,要通過insertClassPath新增掃描路徑。
  • 通過setBody修改方法體,分別新增me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaAddInstance($1);和me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaRemoveInstance($1);
  • 通過toClass覆蓋原有類後,通過類載入器重新載入。
public void init() {
        try {
            ClassPool classPool = new ClassPool(true);
            //新增com.netflix.discovery包的掃描路徑
            ClassClassPath classPath = new ClassClassPath(Applications.class);
            classPool.insertClassPath(classPath);
            //獲取要修改Application類
            CtClass ctClass = classPool.get(APPLICATION_PATH);
            //獲取addInstance方法
            CtMethod addInstanceMethod = ctClass.getDeclaredMethod("addInstance");
            //修改addInstance方法
            addInstanceMethod.setBody("{instancesMap.put($1.getId(), $1);"
                    + "synchronized (instances) {me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaAddInstance($1);" +
                    "instances.remove($1);instances.add($1);isDirty = true;}}");
            //獲取removeInstance方法
            CtMethod removeInstanceMethod = ctClass.getDeclaredMethod("removeInstance");
            //修改removeInstance方法
            removeInstanceMethod.setBody("{me.flyleft.eureka.client.event.EurekaEventHandler.getInstance().eurekaRemoveInstance($1);this.removeInstance($1, true);}");
            //覆蓋原有的Application類
            ctClass.toClass();
            //使用類載入器重新載入Application類
            classPool.getClassLoader().loadClass(APPLICATION_PATH);
            Class.forName(APPLICATION_PATH);
        } catch (Exception e) {
            throw new EurekaEventException(e);
        }
    }
複製程式碼
  • 放入main函式,在spring boot啟動前執行或者使用spring boot的事件,在spring bean初始化之前執行。(確保在eureka第一次執行之前執行即可)
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {

    public static void main(String[] args) {
        //先執行修改位元組碼程式碼
        EurekaEventHandler.getInstance().init();
        new SpringApplicationBuilder(EurekaClientApplication.class).web(true).run(args);
    }
}
複製程式碼

使用JDK中Observable和Observer實現觀察者,訂閱者模式

  • 傳送事件使用java.util.Observable的setChanged和notifyObservers
public class EurekaEventObservable extends Observable {
    public void sendEvent(EurekaEventPayload payload) {
        setChanged();
        notifyObservers(payload);
    }
}
複製程式碼
  • 接收事件使用使用java.util.Observer的update
public abstract class AbstractEurekaEventObserver implements Observer, EurekaEventService {
      @Override
        public void update(Observable o, Object arg) {
            if (arg instanceof EurekaEventPayload) {
                EurekaEventPayload payload = (EurekaEventPayload) arg;
                if (InstanceInfo.InstanceStatus.UP.name().equals(payload.getStatus())) {
                    LOGGER.info("Receive UP event, payload: {}", payload);
                } else {
                    LOGGER.info("Receive DOWN event, payload: {}", payload);
                }
                putPayloadInCache(payload);
                consumerEventWithAutoRetry(payload);
            }
        }
}
複製程式碼

使用RxJava實現自動重試。

接收到服務啟動去執行一些操作,如果執行失敗有異常則自動重試指定次數,每個一段事件重試一次,執行成功則不再執行

private void consumerEventWithAutoRetry(final EurekaEventPayload payload) {
    rx.Observable.just(payload)
            .map(t -> {
                // 此處為接收到服務啟動去執行的一些操作
                consumerEvent(payload);
                return payload;
            }).retryWhen(x -> x.zipWith(rx.Observable.range(1, retryTime),
            (t, retryCount) -> {
               //異常處理
                if (retryCount >= retryTime) {
                    if (t instanceof RemoteAccessException || t instanceof RestClientException) {
                        LOGGER.warn("error.eurekaEventObserver.fetchError, payload {}", payload, t);
                    } else {
                        LOGGER.warn("error.eurekaEventObserver.consumerError, payload {}", payload, t);
                    }
                }
                return retryCount;
            }).flatMap(y -> rx.Observable.timer(retryInterval, TimeUnit.SECONDS)))
            .subscribeOn(Schedulers.io())
            .subscribe((EurekaEventPayload payload1) -> {
            });
}
複製程式碼

新增手動重試失敗介面

自動重試失敗,可以手動重試,新增手動重試介面

@RestController
@RequestMapping(value = "/v1/eureka/events")
public class EurekaEventEndpoint {

    private EurekaEventService eurekaEventService;

    public EurekaEventEndpoint(EurekaEventService eurekaEventService) {
        this.eurekaEventService = eurekaEventService;
    }

    @Permission(permissionLogin = true)
    @ApiOperation(value = "獲取未消費的事件列表")
    @GetMapping
    public List<EurekaEventPayload> list(@RequestParam(value = "service", required = false) String service) {
        return eurekaEventService.unfinishedEvents(service);
    }

    @Permission(permissionLogin = true)
    @ApiOperation(value = "手動重試未消費成功的事件")
    @PostMapping("retry")
    public List<EurekaEventPayload> retry(@RequestParam(value = "id", required = false) String id,
                                          @RequestParam(value = "service", required = false) String service) {
        return eurekaEventService.retryEvents(id, service);
    }

}
複製程式碼

原始碼

相關文章