輕量級DI框架Guice使用詳解

throwable發表於2022-02-22

背景

在日常寫一些小工具或者小專案的時候,有依賴管理和依賴注入的需求,但是Spring(Boot)體系作為DI框架過於重量級,於是需要調研一款微型的DI框架。GuiceGoogle出品的一款輕量級的依賴注入框架,使用它有助於解決專案中的依賴注入問題,提高了可維護性和靈活性。相對於重量級的Spring(Boot)體系,Guice專案只有一個小於1MB的核心模組,如果核心需求是DI(其實Guice也提供了很低層次的AOP實現),那麼Guice應該會是一個合適的候選方案。

在查詢Guice相關資料的時候,見到不少介紹文章吐槽Guice過於簡陋,需要在Module中註冊介面和實現的連結關係,顯得十分簡陋。原因是:Guice是極度精簡的DI實現,沒有提供Class掃描和自動註冊的功能。下文會提供一些思路去實現ClassPath下的Bean自動掃描方案

依賴引入與入門示例

Guice5.x版本後整合了低版本的擴充套件類庫,目前使用其所有功能只需要引入一個依賴即可,當前(2022-02前後)最新版本依賴為:

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>5.1.0</version>
</dependency>

一個入門例子如下:

public class GuiceDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new DemoModule());
        Greeter first = injector.getInstance(Greeter.class);
        Greeter second = injector.getInstance(Greeter.class);
        System.out.printf("first hashcode => %s\n", first.hashCode());
        first.sayHello();
        System.out.printf("second hashcode => %s\n", second.hashCode());
        second.sayHello();
    }

    @Retention(RUNTIME)
    public @interface Count {

    }

    @Retention(RUNTIME)
    public @interface Message {

    }

    @Singleton
    public static class Greeter {

        private final String message;

        private final Integer count;

        @Inject
        public Greeter(@Message String message,
                       @Count Integer count) {
            this.message = message;
            this.count = count;
        }

        public void sayHello() {
            for (int i = 1; i <= count; i++) {
                System.out.printf("%s,count => %d\n", message, i);
            }
        }
    }

    public static class DemoModule extends AbstractModule {

        @Override
        public void configure() {
//            bind(Greeter.class).in(Scopes.SINGLETON);
        }

        @Provides
        @Count
        public static Integer count() {
            return 2;
        }

        @Provides
        @Count
        public static String message() {
            return "vlts.cn";
        }
    }
}

執行main方法控制檯輸出:

first hashcode => 914507705
vlts.cn,count => 1
vlts.cn,count => 2
second hashcode => 914507705
vlts.cn,count => 1
vlts.cn,count => 2

Greeter類需要註冊為單例,Guice中註冊的例項如果不顯式指定為單例,預設都是原型(Prototype,每次重新構造一個新的例項)。Guice註冊一個單例目前來看主要有三種方式:

  • 方式一:在類中使用註解@Singleton(使用Injector#getInstance()會懶載入單例)
@Singleton
public static class Greeter {
    ......
}
  • 方式二:註冊繫結關係的時候顯式指定ScopeScopes.SINGLETON
public static class DemoModule extends AbstractModule {

    @Override
    public void configure() {
        bind(Greeter.class).in(Scopes.SINGLETON);
        // 如果Greeter已經使用了註解@Singleton可以無需指定in(Scopes.SINGLETON),僅bind(Greeter.class)即可
    }    
}
  • 方式三:組合使用註解@Provides@Singleton,效果類似於Spring中的@Bean註解
public static class SecondModule extends AbstractModule {

    @Override
    public void configure() {
        // config module
    }

    @Provides
    @Singleton
    public Foo foo() {
        return new Foo();
    }
}

public static class Foo {

}

上面的例子中,如果Greeter類不使用@Singleton,同時註釋掉bind(Greeter.class).in(Scopes.SINGLETON);,那麼執行main方法會發現兩次從注入器中獲取到的例項的hashCode不一致,也就是兩次從注入器中獲取到的都是重新建立的例項(hashCode不相同):

Guice中所有單例預設是懶載入的,理解為單例初始化使用了懶漢模式,可以通過ScopedBindingBuilder#asEagerSingleton()標記單例為飢餓載入模式,可以理解為切換單例載入模式為餓漢模式

Guice注入器初始化

Guice注入器介面Injector是其核心API,類比為Spring中的BeanFactoryInjector初始化依賴於一或多個模組(com.google.inject.Module)的實現。初始化Injector的示例如下:

public class GuiceInjectorDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(
                new FirstModule(),
                new SecondModule()
        );
        //  injector.getInstance(Foo.class);
    }

    public static class FirstModule extends AbstractModule {

        @Override
        public void configure() {
            // config module
        }
    }

    public static class SecondModule extends AbstractModule {

        @Override
        public void configure() {
            // config module
        }
    }
}

Injector支援基於當前例項建立子Injector例項,類比於Spring中的父子IOC容器:

public class GuiceChildInjectorDemo {

    public static void main(String[] args) throws Exception {
        Injector parent = Guice.createInjector(
                new FirstModule()
        );
        Injector childInjector = parent.createChildInjector(new SecondModule());
    }

    public static class FirstModule extends AbstractModule {

        @Override
        public void configure() {
            // config module
        }
    }

    public static class SecondModule extends AbstractModule {

        @Override
        public void configure() {
            // config module
        }
    }
}

Injector例項會繼承父Injector例項的所有狀態(所有繫結、Scope、攔截器和轉換器等)。

Guice心智模型

心智模型(Mental Model)的概念來自於認知心理學,心智模型指的是指認知主體運用概念對自身體驗進行判斷與分類的一種慣性化的心理機制或既定的認知框架

Guice在認知上可以理解為一個map(文件中表示為map[^guice-map]),應用程式程式碼可以通過這個map宣告和獲取應用程式內的依賴元件。這個Guice Map每一個Map.Entry有兩個部分:

  • Guice KeyGuice Map中的鍵,用於獲取該map中特定的值
  • ProviderGuice Map中的值,用於建立應用於應用程式內的(元件)物件

這個抽象的Guice Map有點像下面這樣的結構:

// com.google.inject.Key => com.google.inject.Provider
private final ConcurrentMap<Key<?>, Provider<?>> guiceMap = new ConcurrentHashMap<>();

Guice Key用於標識Guice Map中的一個依賴元件,這個鍵是全域性唯一的,由com.google.inject.Key定義。鑑於Java裡面沒有形參(也就是方法的入參列表或者返回值只有順序和型別,沒有名稱),所以很多時候在構建Guice Key的時候既需要依賴元件的型別,無法唯一確定元件型別的時候(例如一些定義常量的場景,只要滿足常量的場景,對於類例項也是可行的),需要額外增加一個自定義註解用於生成組合的唯一標識Type + Annotation(Type)。例如:

  • @Message String相當於Key<String>
  • @Count int相當於Key<Integer>
public class GuiceMentalModelDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new EchoModule());
        EchoService echoService = injector.getInstance(EchoService.class);
    }

    @Qualifier
    @Retention(RUNTIME)
    public @interface Count {

    }

    @Qualifier
    @Retention(RUNTIME)
    public @interface Message {

    }

    public static class EchoModule extends AbstractModule {

        @Override
        public void configure() {
            bind(EchoService.class).in(Scopes.SINGLETON);
        }

        @Provides
        @Message
        public String messageProvider() {
            return "foo";
        }

        @Provides
        @Count
        public Integer countProvider() {
            return 10087;
        }
    }

    public static class EchoService {

        private final String messageValue;

        private final Integer countValue;

        @Inject
        public EchoService(@Message String messageValue, @Count Integer countValue) {
            this.messageValue = messageValue;
            this.countValue = countValue;
        }
    }
}

Guice注入器建立單例的處理邏輯類似於:

String messageValue = injector.getInstance(Key.get(String.class, Message.class));
Integer countValue = injector.getInstance(Key.get(Integer.class, Count.class));
EchoService echoService = new EchoService(messageValue, countValue);

這裡的註解@ProvidesGuice中的實現對應於Provider介面,該介面的定義十分簡單:

interface Provider<T> {

    /** Provides an instance of T.**/
    T get();
}

Guice Map中所有的值都可以理解為一個Provider的實現,例如上面的例子可以理解為:

// messageProvider.get() => 'foo'
Provider<String> messageProvider = () -> EchoModule.messageProvider();
// countProvider.get() => 10087
Provider<Integer> countProvider = () -> EchoModule.countProvider();

依賴搜尋和建立的過程也是根據條件建立Key例項,然後在Guice Map中定位唯一的於Provider,然後通過該Provider完成依賴元件的例項化,接著完成後續的依賴注入動作。這個過程在Guice文件中使用了一個具體的表格進行說明,這裡貼一下這個表格:

Guice DSL語法 對應的模型
bind(key).toInstance(value) instance bindingmap.put(key,() -> value)
bind(key).toProvider(provider) provider bindingmap.put(key, provider)
bind(key).to(anotherKey) linked bindingmap.put(key, map.get(anotherKey))
@Provides Foo provideFoo(){...} provider method bindingmap.put(Key.get(Foo.class), module::provideFoo)

Key例項的建立有很多衍生方法,可以滿足單具體型別、具體型別加註解等多種例項化方式。依賴注入使用@Inject註解,支援成員變數和構造注入,一個介面由多個實現的場景可以通過內建@Named註解或者自定義註解指定具體注入的實現,但是需要在構建繫結的時候通過@Named註解或者自定義註解標記具體的實現。例如:

public class GuiceMentalModelDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bind(MessageProcessor.class)
                        .annotatedWith(Names.named("firstMessageProcessor"))
                        .to(FirstMessageProcessor.class)
                        .in(Scopes.SINGLETON);
                bind(MessageProcessor.class)
                        .annotatedWith(Names.named("secondMessageProcessor"))
                        .to(SecondMessageProcessor.class)
                        .in(Scopes.SINGLETON);
            }
        });
        MessageClient messageClient = injector.getInstance(MessageClient.class);
        messageClient.invoke("hello world");
    }

    interface MessageProcessor {

        void process(String message);
    }

    public static class FirstMessageProcessor implements MessageProcessor {

        @Override
        public void process(String message) {
            System.out.println("FirstMessageProcessor process message => " + message);
        }
    }

    public static class SecondMessageProcessor implements MessageProcessor {

        @Override
        public void process(String message) {
            System.out.println("SecondMessageProcessor process message => " + message);
        }
    }

    @Singleton
    public static class MessageClient {

        @Inject
        @Named("secondMessageProcessor")
        private MessageProcessor messageProcessor;

        public void invoke(String message) {
            messageProcessor.process(message);
        }
    }
}

// 控制檯輸出:SecondMessageProcessor process message => hello world

@Named註解這裡可以換成任意的自定義註解實現,不過注意自定義註解需要新增元註解@javax.inject.Qualifier,最終的效果是一致的,內建的@Named就能滿足大部分的場景。最後,每個元件註冊到Guice中,該元件的所有依賴會形成一個有向圖,注入該元件的時候會遞迴注入該元件自身的所有依賴,這個遍歷注入流程遵循深度優先Guice會校驗元件的依賴有向圖的合法性,如果該有向圖是非法的,會丟擲CreationException異常。

Guice支援的繫結

Guice提供AbstractModule抽象模組類給使用者繼承,覆蓋configure()方法,通過bind()相關API建立繫結。

Guice中的Binding其實就是前面提到的Mental Model中Guice Map中的鍵和值的對映關係,Guice提供多種註冊這個繫結關係的API

這裡僅介紹最常用的繫結型別:

  • Linked Binding
  • Instance Binding
  • Provider Binding
  • Constructor Binding
  • Untargeted Binding
  • Multi Binding
  • JIT Binding

Linked Binding

Linked Binding用於對映一個型別和此型別的實現型別,使用起來如下:

 bind(介面型別.class).to(實現型別.class);

具體例子:

public class GuiceLinkedBindingDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bind(Foo.class).to(Bar.class).in(Scopes.SINGLETON);
            }
        });
        Foo foo = injector.getInstance(Foo.class);
    }

    interface Foo {

    }

    public static class Bar implements Foo {

    }
}

Linked Binding常用於這種一個介面一個實現的場景。目標型別上新增了@Singleton註解,那麼程式設計式註冊繫結時候可以無需呼叫in(Scopes.SINGLETON)

Instance Binding

Instance Binding用於對映一個型別和此型別的實現型別例項,也包括常量的繫結。以前一小節的例子稍微改造成Instance Binding的模式如下:

final Bar bar = new Bar();
bind(Foo.class).toInstance(bar);

# 或者新增Named註解
bind(Foo.class).annotatedWith(Names.named("bar")).toInstance(bar);

# 常量繫結
bindConstant().annotatedWith(Names.named("key")).to(value);

可以基於這種方式進行常量的繫結,例如:

public class GuiceInstanceBindingDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bind(String.class).annotatedWith(Names.named("host")).toInstance("localhost");
                bind(Integer.class).annotatedWith(Names.named("port")).toInstance(8080);
                bindConstant().annotatedWith(Protocol.class).to("HTTPS");
                bind(HttpClient.class).to(DefaultHttpClient.class).in(Scopes.SINGLETON);
            }
        });
        HttpClient httpClient = injector.getInstance(HttpClient.class);
        httpClient.print();
    }

    @Qualifier
    @Retention(RUNTIME)
    public @interface Protocol {

    }

    interface HttpClient {

        void print();
    }

    public static class DefaultHttpClient implements HttpClient {

        @Inject
        @Named("host")
        private String host;

        @Inject
        @Named("port")
        private Integer port;

        @Inject
        @Protocol
        private String protocol;

        @Override
        public void print() {
            System.out.printf("host => %s, port => %d, protocol => %s\n", host, port, protocol);
        }
    }
}
// 輸出結果:host => localhost, port => 8080, protocol => HTTPS

Provider Binding

Provider Binding,可以指定某個型別和該型別的Provider實現型別進行繫結,有點像設計模式中的簡單工廠模式,可以類比為Spring中的FactoryBean介面。舉個例子:

public class GuiceProviderBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bind(Key.get(Foo.class)).toProvider(FooProvider.class).in(Scopes.SINGLETON);
            }
        });
        Foo s1 = injector.getInstance(Key.get(Foo.class));
        Foo s2 = injector.getInstance(Key.get(Foo.class));
    }

    public static class Foo {

    }

    public static class FooProvider implements Provider<Foo> {

        private final Foo foo = new Foo();

        @Override
        public Foo get() {
            System.out.println("Get Foo from FooProvider...");
            return foo;
        }
    }
}
// Get Foo from FooProvider...

這裡也要注意,如果標記Provider為單例,那麼在Injector中獲取建立的例項,只會呼叫一次get()方法,也就是懶載入

@Provides註解是Provider Binding一種特化模式,可以在自定義的Module實現中新增使用了@Provides註解的返回對應型別例項的方法,這個用法跟Spring裡面的@Bean註解十分相似。一個例子如下:

public class GuiceAnnotationProviderBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {

            }

            @Singleton
            @Provides
            public Foo fooProvider() {
                System.out.println("init Foo from method fooProvider()...");
                return new Foo();
            }
        });
        Foo s1 = injector.getInstance(Key.get(Foo.class));
        Foo s2 = injector.getInstance(Key.get(Foo.class));
    }

    public static class Foo {

    }
}
// init Foo from method fooProvider()...

Constructor Binding

Constructor Binding需要顯式繫結某個型別到其實現型別的一個明確入參型別的建構函式,目標建構函式不需要使用@Inject註解。例如:

public class GuiceConstructorBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                try {
                    bind(Key.get(JdbcTemplate.class))
                            .toConstructor(DefaultJdbcTemplate.class.getConstructor(DataSource.class))
                            .in(Scopes.SINGLETON);
                } catch (NoSuchMethodException e) {
                    addError(e);
                }
            }
        });
        JdbcTemplate instance = injector.getInstance(JdbcTemplate.class);
    }

    interface JdbcTemplate {

    }

    public static class DefaultJdbcTemplate implements JdbcTemplate {

        public DefaultJdbcTemplate(DataSource dataSource) {
            System.out.println("init JdbcTemplate,ds => " + dataSource.hashCode());
        }
    }

    public static class DataSource {

    }
}
// init JdbcTemplate,ds => 1420232606

這裡需要使用者捕獲和處理獲取建構函式失敗丟擲的NoSuchMethodException異常。

Untargeted Binding

Untargeted Binding用於註冊繫結沒有目標(實現)型別的特化場景,一般是沒有實現介面的普通型別,在沒有使用@Named註解或者自定義註解繫結的前提下可以忽略to()呼叫。但是如果使用了@Named註解或者自定義註解進行繫結,to()呼叫一定不能忽略。例如:

public class GuiceUnTargetedBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bind(Foo.class).in(Scopes.SINGLETON);
                bind(Bar.class).annotatedWith(Names.named("bar")).to(Bar.class).in(Scopes.SINGLETON);
            }
        });
    }

    public static class Foo {

    }

    public static class Bar {

    }
}

Multi Binding

Multi Binding也就是多(例項)繫結,使用特化的Binder代理完成,這三種Binder代理分別是:

  • Multibinder:可以簡單理解為Type => Set<TypeImpl>,注入型別為Set<Type>
  • MapBinder:可以簡單理解為(KeyType, ValueType) => Map<KeyType, ValueTypeImpl>,注入型別為Map<KeyType, ValueType>
  • OptionalBinder:可以簡單理解為Type => Optional.ofNullable(GuiceMap.get(Type)).or(DefaultImpl),注入型別為Optional<Type>

Multibinder的使用例子:

public class GuiceMultiBinderDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                Multibinder<Processor> multiBinder = Multibinder.newSetBinder(binder(), Processor.class);
                multiBinder.permitDuplicates().addBinding().to(FirstProcessor.class).in(Scopes.SINGLETON);
                multiBinder.permitDuplicates().addBinding().to(SecondProcessor.class).in(Scopes.SINGLETON);
            }
        });
        injector.getInstance(Client.class).process();
    }

    @Singleton
    public static class Client {

        @Inject
        private Set<Processor> processors;

        public void process() {
            Optional.ofNullable(processors).ifPresent(ps -> ps.forEach(Processor::process));
        }
    }

    interface Processor {

        void process();
    }

    public static class FirstProcessor implements Processor {

        @Override
        public void process() {
            System.out.println("FirstProcessor process...");
        }
    }

    public static class SecondProcessor implements Processor {

        @Override
        public void process() {
            System.out.println("SecondProcessor process...");
        }
    }
}
// 輸出結果
FirstProcessor process...
SecondProcessor process...

MapBinder的使用例子:

public class GuiceMapBinderDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                MapBinder<Type, Processor> mapBinder = MapBinder.newMapBinder(binder(), Type.class, Processor.class);
                mapBinder.addBinding(Type.SMS).to(SmsProcessor.class).in(Scopes.SINGLETON);
                mapBinder.addBinding(Type.MESSAGE_TEMPLATE).to(MessageTemplateProcessor.class).in(Scopes.SINGLETON);
            }
        });
        injector.getInstance(Client.class).process();
    }

    @Singleton
    public static class Client {

        @Inject
        private Map<Type, Processor> processors;

        public void process() {
            Optional.ofNullable(processors).ifPresent(ps -> ps.forEach(((type, processor) -> processor.process())));
        }
    }

    public enum Type {

        /**
         * 簡訊
         */
        SMS,

        /**
         * 訊息模板
         */
        MESSAGE_TEMPLATE
    }

    interface Processor {

        void process();
    }

    public static class SmsProcessor implements Processor {

        @Override
        public void process() {
            System.out.println("SmsProcessor process...");
        }
    }

    public static class MessageTemplateProcessor implements Processor {

        @Override
        public void process() {
            System.out.println("MessageTemplateProcessor process...");
        }
    }
}
// 輸出結果
SmsProcessor process...
MessageTemplateProcessor process...

OptionalBinder的使用例子:

public class GuiceOptionalBinderDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
//                bind(Logger.class).to(LogbackLogger.class).in(Scopes.SINGLETON);
                OptionalBinder.newOptionalBinder(binder(), Logger.class)
                        .setDefault()
                        .to(StdLogger.class)
                        .in(Scopes.SINGLETON);
            }
        });
        injector.getInstance(Client.class).log("Hello World");
    }

    @Singleton
    public static class Client {

        @Inject
        private Optional<Logger> logger;

        public void log(String content) {
            logger.ifPresent(l -> l.log(content));
        }
    }


    interface Logger {

        void log(String content);
    }

    public static class StdLogger implements Logger {

        @Override
        public void log(String content) {
            System.out.println(content);
        }
    }
}

JIT Binding

JIT Binding也就是Just-In-Time Binding,也可以稱為隱式繫結(Implicit Binding)。隱式繫結需要滿足:

  • 建構函式必須無參,並且非private修飾
  • 沒有在Module實現中啟用Binder#requireAtInjectRequired()

呼叫Binder#requireAtInjectRequired()方法可以強制宣告Guice只使用帶有@Inject註解的構造器。呼叫Binder#requireExplicitBindings()方法可以宣告Module內必須顯式宣告所有繫結,也就是禁用隱式繫結,所有繫結必須在Module的實現中宣告。下面是一個隱式繫結的例子:

public class GuiceJustInTimeBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {

            }
        });
        Foo instance = injector.getInstance(Key.get(Foo.class));
    }

    public static class Foo {

        public Foo() {
            System.out.println("init Foo...");
        }
    }
}
// init Foo...

此外還有兩個執行時繫結註解:

  • @ImplementedBy:特化的Linked Binding,用於執行時繫結對應的目標型別
@ImplementedBy(MessageProcessor.class)
public interface Processor {
  
}
  • @ProvidedBy:特化的Provider Binding,用於執行時繫結對應的目標型別的Provider實現
@ProvidedBy(DruidDataSource.class)
public interface DataSource {
  
}

AOP特性

Guice提供了相對底層的AOP特性,使用者需要自行實現org.aopalliance.intercept.MethodInterceptor介面在方法執行點的前後插入自定義程式碼,並且通過Binder#bindInterceptor()註冊方法攔截器。這裡只通過一個簡單的例子進行演示,模擬的場景是方法執行前和方法執行完成後分別列印日誌,並且計算目標方法呼叫耗時:

public class GuiceAopDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bindInterceptor(Matchers.only(EchoService.class), Matchers.any(), new EchoMethodInterceptor());
            }
        });
        EchoService instance = injector.getInstance(Key.get(EchoService.class));
        instance.echo("throwable");
    }

    public static class EchoService {

        public void echo(String name) {
            System.out.println(name + " echo");
        }
    }

    public static class EchoMethodInterceptor implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            Method method = methodInvocation.getMethod();
            String methodName = method.getName();
            long start = System.nanoTime();
            System.out.printf("Before invoke method => [%s]\n", methodName);
            Object result = methodInvocation.proceed();
            long end = System.nanoTime();
            System.out.printf("After invoke method => [%s], cost => %d ns\n", methodName, (end - start));
            return result;
        }
    }
}

// 輸出結果
Before invoke method => [echo]
throwable echo
After invoke method => [echo], cost => 16013700 ns

自定義注入

通過TypeListenerMembersInjector可以實現目標型別例項的成員屬性自定義注入擴充套件。例如可以通過下面的方式實現目標例項的org.slf4j.Logger屬性的自動注入:

public class GuiceCustomInjectionDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure() {
                bindListener(Matchers.any(), new LoggingListener());
            }
        });
        injector.getInstance(LoggingClient.class).doLogging("Hello World");
    }

    public static class LoggingClient {

        @Logging
        private Logger logger;

        public void doLogging(String content) {
            Optional.ofNullable(logger).ifPresent(l -> l.info(content));
        }
    }

    @Qualifier
    @Retention(RUNTIME)
    @interface Logging {

    }

    public static class LoggingMembersInjector<T> implements MembersInjector<T> {

        private final Field field;
        private final Logger logger;

        public LoggingMembersInjector(Field field) {
            this.field = field;
            this.logger = LoggerFactory.getLogger(field.getDeclaringClass());
            field.setAccessible(true);
        }

        @Override
        public void injectMembers(T instance) {
            try {
                field.set(instance, logger);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(e);
            } finally {
                field.setAccessible(false);
            }
        }
    }

    public static class LoggingListener implements TypeListener {

        @Override
        public <I> void hear(TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) {
            Class<?> clazz = typeLiteral.getRawType();
            while (Objects.nonNull(clazz)) {
                for (Field field : clazz.getDeclaredFields()) {
                    if (field.getType() == Logger.class && field.isAnnotationPresent(Logging.class)) {
                        typeEncounter.register(new LoggingMembersInjector<>(field));
                    }
                }
                clazz = clazz.getSuperclass();
            }
        }
    }
}
// 輸出結果
[2022-02-22 00:51:33,516] [INFO] cn.vlts.guice.GuiceCustomInjectionDemo$LoggingClient [main] [] - Hello World

此例子需要引入logbackslf4j-api的依賴。

基於ClassGraph掃描和全自動註冊繫結

Guice本身不提供類路徑或者Jar檔案的類掃描功能,要實現類路徑下的所有Bean全自動註冊繫結,需要依賴第三方類掃描框架,這裡選用了一個效能比較高社群比較活躍的類庫io.github.classgraph:classgraph。引入ClassGraph的最新依賴:

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph</artifactId>
    <version>4.8.138</version>
</dependency>

編寫自動掃描Module

@RequiredArgsConstructor
public class GuiceAutoScanModule extends AbstractModule {

    private final Set<Class<?>> bindClasses = new HashSet<>();

    private final String[] acceptPackages;

    private final String[] rejectClasses;

    @Override
    public void configure() {
        ClassGraph classGraph = new ClassGraph();
        ScanResult scanResult = classGraph
                .enableClassInfo()
                .acceptPackages(acceptPackages)
                .rejectClasses(rejectClasses)
                .scan();
        ClassInfoList allInterfaces = scanResult.getAllInterfaces();
        for (ClassInfo i : allInterfaces) {
            ClassInfoList impl = scanResult.getClassesImplementing(i.getName());
            if (Objects.nonNull(impl)) {
                Class<?> ic = i.loadClass();
                int size = impl.size();
                if (size > 1) {
                    for (ClassInfo im : impl) {
                        Class<?> implClass = im.loadClass();
                        if (isSingleton(implClass)) {
                            String simpleName = im.getSimpleName();
                            String name = Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
                            bindNamedSingleInterface(ic, name, implClass);
                        }
                    }
                } else {
                    for (ClassInfo im : impl) {
                        Class<?> implClass = im.loadClass();
                        if (isProvider(implClass)) {
                            bindProvider(ic, implClass);
                        }
                        if (isSingleton(implClass)) {
                            bindSingleInterface(ic, implClass);
                        }
                    }
                }
            }
        }
        ClassInfoList standardClasses = scanResult.getAllStandardClasses();
        for (ClassInfo ci : standardClasses) {
            Class<?> implClass = ci.loadClass();
            if (!bindClasses.contains(implClass) && shouldBindSingleton(implClass)) {
                bindSingleton(implClass);
            }
        }
        bindClasses.clear();
        ScanResult.closeAll();
    }

    private boolean shouldBindSingleton(Class<?> implClass) {
        int modifiers = implClass.getModifiers();
        return isSingleton(implClass) && !Modifier.isAbstract(modifiers) && !implClass.isEnum();
    }

    private void bindSingleton(Class<?> implClass) {
        bindClasses.add(implClass);
        bind(implClass).in(Scopes.SINGLETON);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void bindSingleInterface(Class<?> ic, Class<?> implClass) {
        bindClasses.add(implClass);
        bind((Class) ic).to(implClass).in(Scopes.SINGLETON);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void bindNamedSingleInterface(Class<?> ic, String name, Class<?> implClass) {
        bindClasses.add(implClass);
        bind((Class) ic).annotatedWith(Names.named(name)).to(implClass).in(Scopes.SINGLETON);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private <T> void bindProvider(Class<?> ic, Class<?> provider) {
        bindClasses.add(provider);
        Type type = ic.getGenericInterfaces()[0];
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Class target = (Class) parameterizedType.getActualTypeArguments()[0];
        bind(target).toProvider(provider).in(Scopes.SINGLETON);
    }

    private boolean isSingleton(Class<?> implClass) {
        return Objects.nonNull(implClass) && implClass.isAnnotationPresent(Singleton.class);
    }

    private boolean isProvider(Class<?> implClass) {
        return isSingleton(implClass) && Provider.class.isAssignableFrom(implClass);
    }
}

使用方式:

GuiceAutoScanModule module = new GuiceAutoScanModule(new String[]{"cn.vlts"}, new String[]{"*Demo", "*Test"});
Injector injector = Guice.createInjector(module);

GuiceAutoScanModule目前只是一個並不完善的示例,用於掃描cn.vlts包下(排除類名以Demo或者Test結尾的類)所有的類並且按照不同情況進行繫結註冊,實際場景可能會更加複雜,可以基於類似的思路進行優化和調整。

小結

限於篇幅,本文只介紹了Guice的基本使用、設計理念和不同型別的繫結方式註冊,更深入的實踐方案後面有機會應用在專案中的時候再基於案例詳細聊聊Guice的應用。另外,Guice不是過時的元件,相對於SpringBoot一個最簡構建幾十MBFlat Jar,如果僅僅想要輕量級DI功能,Guice會是一個十分合適的選擇。

參考資料:

(本文完 c-4-d e-a-20220221)

相關文章