Apache Druid源碼導讀--Google guice DI框架

緣起

在大數據應用組件中,有兩款OLAP引擎應用廣泛,一款是偏離線處理的Kylin,另一個是偏實時的Druid。Kylin是一款國人開源的優秀離線OLAP引擎,基本上是Hadoop領域離線OLAP事實標準,在離線報表,指標分析領域應用廣泛。而Apache Druid則在實時OLAP領域獨領風騷,優異的性能、高可用、易擴展。Kylin的實現細節網上資料很多,而Druid很少,最近打算研究下源碼,寫幾篇閱讀導讀,記錄於此。

閱讀Druid源碼的第一個障礙莫過於Google Guice這個小巧的DI框架了。不瞭解Guice很難閱讀Druid源碼,而Guice在國內應用偏少,文章也少,無疑加大了難度。我把Druid中使用的guice擴展單獨抽出來,放在了https://github.com/Skycrab/guice-module下,對druid源碼註釋放在了https://github.com/Skycrab/druid-comment

本文主要介紹下Google Guice以及Druid中實現的guice擴展模塊。

Google Guice介紹

Guice是Google開源的一個小巧的依賴注入框架。
下面主要介紹下與Spring的對比,以及guice幾個核心的能力。

與Spring的對比

Guice與Spring沒有直接競爭關係,Spring是複雜的技術棧,而Guice只專注於依賴注入。

Guice與Spring的表現方式也稍微有所區別。Guice覺得基於xml的方式過於隱晦,而自動注入(AutoWired)又過於靈活,所以Guice基於代碼綁定實現,較爲剋制。

而基於Module的方式讓Guice獲得了巨大的靈活性與可複用性,可以簡單理解爲多個xml裝配,但更加強大,可複用。

Example

看下面這個例子(git地址)

@Slf4j
public class GuiceExample {
    public static void main( String[] args ) throws Exception {
        Injector injector = Guice.createInjector(new Module() {
            @Override
            public void configure(Binder binder) {
                binder.bind(ProduceService.class).to(KafkaPrduceService.class);
                binder.bind(String.class).annotatedWith(Names.named("server")).toInstance("localhost:9002");
                binder.bind(String.class).annotatedWith(Names.named("topic")).toInstance("test");
            }
        });

        ProduceService produce = injector.getInstance(ProduceService.class);
        produce.produce("hello guice");
    }

    public interface ProduceService {
        void produce(Object msg);
    }

    @Singleton
    public static class KafkaPrduceService implements ProduceService {
        private String server;
        private String topic;

        @Inject
        public KafkaPrduceService(@Named("server") String server, @Named("topic") String topic) {
            this.server = server;
            this.topic = topic;
        }
        @Override
        public void produce(Object msg) {
            log.info("produce {}-{}-{}", server, topic, msg);
        }
    }
}

我們看到guice中的綁定關係是在Module中維護的,可以簡單當做是spring的xml文件。Singleton代表該服務是單例的,通過@Inject注入需要的bean,如果需要的bean沒有綁定,會通過默認構造函數實例化。
其它基本介紹可參考guice文檔

覆蓋已有綁定關係

Apache Druid好多模塊是可以自定義替換的,一方面通過spi機制+ClassLoader加載擴展模塊實現模塊熱插拔,另一方面通過Guice覆蓋綁定關係將新實現注入到框架。下面要介紹的就是guice的覆蓋綁定關係能力。
看下面這個例子

/**
 * Created by yihaibo on 2019-09-29.
 * Guice模塊綁定覆蓋
 */
public class GuiceOverrideExample {
  public static void main(String[] args) {
    List<Module> builtIns = ImmutableList.of(binder -> {
      binder.bind(Service.class).to(BuiltinService.class);
    });
//    List<Module> customs = ImmutableList.of();
    List<Module> customs = ImmutableList.of(binder -> {
      binder.bind(Service.class).to(CustomService.class);
    });
    Injector injector = Guice.createInjector(Modules.override(builtIns).with(customs));
    FrameWork frameWork = injector.getInstance(FrameWork.class);
    frameWork.start();
  }
  
  public static class FrameWork {
    private Service service;
    @Inject
    public FrameWork(Service service) {
      this.service = service;
    }
    public void start() {
      this.service.run();
    }
  }
  
  public interface Service {
    void run();
  }
  
  public static class BuiltinService implements Service {
    @Override
    public void run() {
      System.out.println("BuiltinService");
    }
  }

  public static class CustomService implements Service {
    @Override
    public void run() {
      System.out.println("CustomService");
    }
  }
}

我們看到框架本來綁定的是BuiltinService,現在我們需要替換成CustomService,只需要Modules.override即可覆蓋綁定關係。

默認綁定

在做基礎庫的時候,有時會依賴一些服務,但這些服務很可能被用戶自定義,這時可以使用guice的默認綁定功能。
看下面代碼

@Slf4j
public class GuiceOptionalBinderExample {
public static void main(String[] args) {
  Injector injector = Guice.createInjector(new FrameWorkModule(), new Module() {
    @Override
    public void configure(Binder binder) {
      //覆蓋框架默認實現
      //如果需要傳遞參數,可以1.Inject 2.使用Provider
      OptionalBinder.newOptionalBinder(binder, Emit.class).setBinding().to(kafkaEmit.class);
    }
  });

  TestService testService = injector.getInstance(TestService.class);
  testService.test();
}

public static class TestService {
  private Emit emit;
  @Inject
  public TestService(Emit emit) {
    this.emit = emit;
  }
  public void test() {
    this.emit.emit("start TestService");
  }
}

//-------應用代碼
public static class kafkaEmit implements Emit {
  @Override
  public void emit(Object object) {
    log.info("kafkaEmit emit");
  }
}

//-------庫代碼
public static class FrameWorkModule implements Module {
  @Override
  public void configure(Binder binder) {
    //庫默認實現
    OptionalBinder.newOptionalBinder(binder, Emit.class).setDefault().to(HttpEmit.class);
  }
}

public interface Emit {
  void emit(Object object);
}

public static class HttpEmit implements Emit {
  @Override
  public void emit(Object object) {
    log.info("HttpEmit emit");
  }
}
}

Apache Druid中Guice模塊

在Druid中有幾個通用的Guice擴展,不瞭解會對代碼閱讀產生影響。

模塊 功能
guice-lifecycle 實現生命週期託管,實現服務start、stop方法
guice-jsonconfig Properties配置文件bean自動裝配
guice-jersey-jetty 內嵌jetty的jersey Restful

guice-lifecycle

LifecycleModule提供服務託管能力,提供了4級服務優先級,框架會自動調用start和stop方法。該功能在druid應用非常廣泛,是應用啓動的原點。

/**
 * Guice 生命週期管理
 */
public class GuiceLifecycleExample
{
    public static void main( String[] args ) throws Exception
    {
        final Injector injector = Guice.createInjector(new LifecycleModule());
        final Bootstrap bootstrap = injector.getInstance(Bootstrap.class);
        bootstrap.run();
    }

    public static class Bootstrap{
        // 必須主動注入ManageLifecycle,否則需要通過Lifecycle.addHandler主動註冊
        private PrintLifecycle printLifecycle;
        private Lifecycle lifecycle;
        @Inject
        public Bootstrap(Lifecycle lifecycle, PrintLifecycle printLifecycle) {
            this.lifecycle = lifecycle;
            this.printLifecycle = printLifecycle;
        }
        public void run() throws Exception {
            System.out.println("Bootstrap run");
            lifecycle.start();
            lifecycle.join();
        }
    }
    
    @ManageLifecycle
    @Slf4j
    public static class PrintLifecycle {
        @LifecycleStart
        public void start() {
            System.out.println("PrintLifecycle start");
        }

        @LifecycleStop
        public void stop()
        {
            System.out.println("PrintLifecycle stop");
        }
    }
}

guice-jsonconfig

jsonconfig提供了配置文件bean自動裝配,並支持validation註解校驗。Druid中讀取配置功能都使用了該功能。

public class GuiceJsonConfigExample {
  public static void main( String[] args ) {
    Injector injector = Guice.createInjector(new JsonConfigModule(), new Module() {
      @Override
      public void configure(Binder binder) {
        JsonConfigProvider.bind(binder, "druid.server", DruidServerConfig.class);
      }

      @Provides
      @Singleton
      /**
       * Properties代碼注入需注意必須爲string
       */
      Properties provideProperties() {
        Properties props = new Properties();
        props.put("druid.server.hostname", "0.0.0.0");
        props.put("druid.server.port", "3333");
        return props;
      }
    });

    DruidServerConfig druidServerConfig = injector.getInstance(DruidServerConfig.class);

    System.out.println(druidServerConfig.port);
    System.out.println(druidServerConfig.hostname);
  }

  public static class DruidServerConfig {
    @JsonProperty
    @NotNull
    public String hostname = null;
    @JsonProperty @Min(1025) public int port = 8080;
  }
}

guice-jersey-jetty

jersey-jetty提供了內嵌jetty的jersey Restful。Druid中http接口都由jersey支持,由Jetty充當servlet容器。

/**
 * Created by yihaibo on 2019-10-08.
 * jersey restful簡單列子
 */
@Slf4j
public class GuiceJerseryJettyExample {
  public static void main(String[] args) throws Exception {
    Injector injector = Guice.createInjector(new JsonConfigModule(), new JettyServerModule(), new Module() {
      @Override
      public void configure(Binder binder) {
        Jerseys.addResource(binder, IndexResource.class);
      }

      @Provides @Singleton
      Properties provideProperties() {
        Properties props = new Properties();
        props.put("server.http.host", "0.0.0.0");
        props.put("server.http.port", "9000");
        return props;
      }
    });

    JerseyJettyServer jerseyJettyServer = injector.getInstance(JerseyJettyServer.class);
    jerseyJettyServer.start();
    Thread.currentThread().join();
  }

  @Singleton
  @Path("/index")
  public static class IndexResource {
    private ServerConfig serverConfig;
    @Inject
    public IndexResource(ServerConfig serverConfig) {
      this.serverConfig = serverConfig;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public ServerConfig doGet(@Context final HttpServletRequest req) {
      return serverConfig;
    }
  }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章