輕量級DI框架Guice使用詳解


背景

在日常寫一些小工具或者小項目的時候,有依賴管理和依賴注入的需求,但是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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM