Loading

轻量级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)

posted @ 2022-02-22 22:55  throwable  阅读(3016)  评论(2编辑  收藏  举报