Google Guice 4:Bindings(2)

4 Scopes (实例的作用域)

4.1 默认规则:unreuse instance

  • 到目前为止,通过bind().to()@Provides定义的binding,每次需要注入实例对象时,Guice都会创建一个新的实例

    // 修改DatabaseTransactionLog,使其打印自身的hash code
    @Override
    public void log(String msg) {
          
          
        System.out.printf("%s@%s: %s\n", this.getClass().getSimpleName(), 
        		Integer.toHexString(hashCode()), msg);
    }
    // 创建多个MyDatabase实例,需要多次注入TransactionLog
    MyDatabase database1 = injector.getInstance(MyDatabase.class);
    database1.createTable("olap.users");
    
    MyDatabase database2 = injector.getInstance(MyDatabase.class);
    database2.createTable("presto.queries");
    
  • 执行上述代码,发现Guice为MyDatabase注入了不同的DatabaseTransactionLog实例

  • 这是Guice的默认规则,Guice returns a new instance each time it supplies a value.

4.2 build-in scopes

  • 对象的生命周期可以是应用程序级别的、session级别的、request级别,通过**作用域(scope)**可以配置对象的生命周期,从而实现对象的复用

单例@Singleton

  • 在实际应用场景中,需要将类定义为单例模式

  • 例如,在PrestoDB中,与属性配置有关的类(SystemSessionProperties)、监控各种事件的类(GcMonitor)、任务调度有关的类(NodeScheduler)等都被设置为单例

  • Guice支持两种@Singletonjavax.inject.Singletoncom.google.inject.Singleton,官方建议使用javax.inject.Singleton,因为注入框架也支持它

  • 使用@Singleton修饰DatabaseTransactionLog,将其定义为单例

    @Singleton
    public class DatabaseTransactionLog implements TransactionLog {
          
          
    	 /* everything here should be threadsafe! */
    }
    
  • 重新执行上述代码,发现Guice为MyDatabase注入的是一个实例

@RequestScoped

  • 除了@Singleton这个内置的scope,Guice官方文档还提到了@RequestScoped,servlet extension所包含的、用于web应用程序的scope

  • Guice并未说明@RequestScoped的详细信息,推测是com.google.inject.servlet.RequestScoped,可以通过如下maven依赖进行引入

    <dependency>
        <groupId>com.google.inject.extensions</groupId>
        <artifactId>guice-servlet</artifactId>
        <version>5.0.1</version>
    </dependency>
    

4.3 如何使用scope

  • 方法一: 在实现类上直接使用@Singleton

    @Singleton
    public class DatabaseTransactionLog implements TransactionLog {
          
          
    	 /* everything here should be threadsafe! */
    }
    
  • 方法二: 在bind()语句中定义单例

    // 1. 使用注解定义单例
    bind(TransactionLog.class).annotatedWith(Names.named("database")).to(DatabaseTransactionLog.class).in(Singleton.class);
    // 对应的in()方法
    void in(Class<? extends Annotation> scopeAnnotation);
            
    // 2. 实用Scopes.SINGLETON定义单例 
    bind(TransactionLog.class).annotatedWith(Names.named("database")).to(DatabaseTransactionLog.class).in(Scopes.SINGLETON);
    // 对应的in()方法
    void in(Scope scope);
    
  • 方法三:@Provides方法上定义单例

    @Provides
    @Named("database")
    @Singleton
    public TransactionLog providerDatabaseTransactionLog(DatabaseTransactionLog log) {
          
          
        return log;
    }
    
    @Provides
    @Named("database")
    @Singleton
    public TransactionLog providerDatabaseTransactionLog() {
          
          
        return new DatabaseTransactionLog();
    }
    
  • 上述三种定义单例的方式,最终使得Guice只会创建一个DatabaseTransactionLog对象

4.4 scopes apply to the binding source

  • 在linked binding中,scope是应用在binding source上,而非binding target

    In linked bindings, scopes apply to the binding source, not the binding target.

  • 与其说作用在binding source上,更易理解的是:作用在整个binding上

以上面@Provides定义的linked binding为例:

  • binding source:TransactionLog,binding target:DatabaseTransactionLog
  • 感觉@Singleton不会起作用,因为TransactionLog —> DatabaseTransactionLog,最终是需要得到DatabaseTransactionLog对象
  • 而无论是通过方法入参传入,还是通过new DatabaseTransactionLog()创建,而@Singleton标记不是的DatabaseTransactionLog,Guice应该会创建多个DatabaseTransactionLog对象
  • 实际上,Guice只会创建一个DatabaseTransactionLog对象,这是由于Singleton作用在TransactionLog上,使得TransactionLog只对应一个DatabaseTransactionLog对象
  • 这个DatabaseTransactionLog对象一旦创建好,Guice就像是直接从缓存中获取一样,而无需再创建新的DatabaseTransactionLog对象

示例2:一个类实现了两个接口,最终创建几个对象?

  • 小学生Pupil类,同时实现了Person和Student接口

    public interface Person {
          
          
        void eat();
    }
    
    public interface Student {
          
          
        void study();
    }
    
    public class Pupil implements Person, Student {
          
          
        @Override
        public void eat() {
          
          
            System.out.println("A pupil is eating potato chips");
        }
    
        @Override
        public void study() {
          
          
            System.out.println("A pupil is studying math");
        }
    
        @Override
        public String toString() {
          
          
            return this.getClass().getSimpleName() + "@" + Integer.toHexString(this.hashCode());
        }
    }	
    
  • 在Module中定义binding关系

    bind(Person.class).to(Pupil.class).in(Scopes.SINGLETON);
    bind(Student.class).to(Pupil.class).in(Singleton.class);
    
  • 从Guice获取实例:

    Student student = injector.getInstance(Student.class);
    Person person = injector.getInstance(Person.class);
    System.out.printf("student: %s, person: %s", student, person);
    
  • 执行代码,发现Student对应的Pupil对象和Person对应的Pupil对象,并非同一个

  • 如果Singleton作用在binding target上,则最终将从Guice获得同一个Pupil对象;但是,Singleton作用在binding source上,使得Student和Person将对应不同的Pupil对象

如何解决上述问题?

  • 若我们希望Pupil是单例,可以在定义Pupil类时使用@Singleton

    @Singleton
    public class Pupil implements Person, Student {
          
          }
    
  • 或者添加新的binding,将Pupil定义为单例

    bind(Pupil.class).in(Singleton.class);
    

4.5 单例的懒汉模式 vs 恶汉模式

  • 恶汉模式的单例(Eager singleton)会在类加载后完成初始化,以保证用户获得一致且快速的体验

  • 懒汉模式的单例(Lazy singleton,第一次使用时才进行初始化,可以加快程序的编译、运行周期

  • Guice中定义单例的方式很多,具体为哪种模式,可以参考表格

    定义方式 PRODUCTION DEVELOPMENT
    .asEagerSingleton() eager eager
    .in(Singleton.class) eager lazy
    .in(Scopes.SINGLETON) eager lazy
    @Singleton eager lazy

Guice comes with a built-in @Singleton scope that reuses the same instance during the lifetime of an application within a single injector. Both javax.inject.Singleton and com.google.inject.Singleton are supported by Guice, but prefer the standard javax.inject.Singleton since it is also supported by other injection frameworks like Dagger.
在这里插入图片描述

5. Instance Bindings

  • 到目前为止,我们都是将type绑定到具体的implemention(实现)。

  • 在某些场景下,我们需要将type绑定到该type的具体实例

  • 例如,在整个应用中jdbc串是固定的,我们可以通过Guice为String类型的jdbcUrl传入固定值

  • MyDatabase的构造函数中,使用@Named("mysql jdbc url")标记MySQL JDBC串,然后通过instance binding为其赋值"jdbc:mysql://localhost/user"

    @Inject
    public MyDatabase(@Named("database")TransactionLog log, @Named("mysql jdbc url")String jdbcUrl) {
          
          
        this.log = log;
        System.out.println("Initialize MyDatabase with jdbcUrl: " + jdbcUrl);
    }
    
    // 通过instance binding配置jdbc url
    bind(String.class).annotatedWith(Names.named("mysql jdbc url")).toInstance("jdbc:mysql://localhost/users");
    
  • 针对基本数据类型、常见类型(如String、enum、Class),还可以使用bindConstant()直接绑定具体实例

    // 创建binding Annotation
    @Target({
          
          ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface ForDatabase {
          
          
    }
    
    // 使用binding Annotation
    @Inject
    public MyDatabase(@Named("database") TransactionLog log, @ForDatabase String jdbcUrl) {
          
          
        this.log = log;
        System.out.println("Initialize MyDatabase with jdbcUrl: " + jdbcUrl);
    }
    
    // instance binding,注入具体的String对象
    bindConstant().annotatedWith(ForDatabase.class).to("jdbc:mysql://localhost/users");
    
  • 最终,创建MyDatabase时将打印传入的jdbcUrl

6. Provide Bindings

6.1 @Provides Methods

  • 最简单的方法,使用@Provides method实现对象的创建
  • @Provides method也不陌生了,之前在很多地方都有使用
    • 可以直接定义@Provides method
      @Provides
      public TransactionLog provideTransactionLog() {
              
              
          DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
          transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
          transactionLog.setThreadPoolSize(30);
          return transactionLog;
      }
      
    • 还可以定义带注解的@Provides method
      // instance binding,定义jdbcUrl的值
      bind(String.class).annotatedWith(Names.named("mysql jdbc url")).toInstance("jdbc:mysql://localhost/users");
      
      // Guice自动传入jdbcUrl
      @Provides
      @Database
      public TransactionLog provideTransactionLog(@Named("mysql jdbc url") String jdbcUrl) {
              
              
          DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
          transactionLog.setJdbcUrl(jdbcUrl);
          transactionLog.setThreadPoolSize(30);
          return transactionLog;
      }
      
      // 匹配到使用@Database标识的@Provides method
      @Inject
      public MyDatabase(@Database TransactionLog log) {
              
               
          this.log = log;
      }
      
      @Override
      public void log(String msg) {
              
              
          System.out.printf("jdbcUrl: %s, threadPoolSize: %d\n", jdbcUrl, threadPoolSize);
      }
      
    • 最终,为MyDatabase注入的TransactionLog信息为:jdbcUrl: jdbc:mysql://localhost/users, threadPoolSize: 30

6.2 Provider Bindings

  • @Provides method中,创建对象的代码可能变得越来越复杂,也可能由于Module中存在多个@Provides method,导致Module变得越来越臃肿

  • 这时,可以考虑将@Provides method中代码转移到一个独立的类中,这个类实现了Guice的Provider接口

    public interface Provider<T> extends javax.inject.Provider<T> {
          
          
    	  T get();
    }
    
  • 下面的代码,在Provider的实现类中定义DatabaseTransactionLog的创建逻辑。其中,Connection需要Guice自动注入

    public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
          
          
        private final Connection connection;
    
        @Inject
        public DatabaseTransactionLogProvider(Connection connection) {
          
          
            this.connection = connection;
        }
    
        public TransactionLog get() {
          
          
            DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
            transactionLog.setConnection(connection);
            return transactionLog;
        }
    }
    
  • 在Module中使用toProvider()定义binding

    bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
    

7. Constructor Bindings

  • 之前,我们通过@Inject标识某个构造函数,告诉Guice这既是依赖注入的入口,又是创建实例对象的入口
  • 但是以下情况,@Inject将变得不再适用:
    • 使用的是第三方类
    • 类的多个构造函数参与了依赖注入

如何理解这个类的多个构造函数参与了依赖注入

  • 不同的dependent class,需要注入使用不同的构造函数创建的依赖
  • 例如,有的dependent class只需要注入一个通过默认构造函数创建的依赖,有的dependent class需要注入一个通过有参构造函数创建的依赖

  • 面对上述情况,可以通过Provide binding自己决定使用哪个构造函数创建对象

  • 但是,在AOP这种不能手动构造对象的情况,Provide binding也变得不再适用

  • 这时,可以考虑使用toConstructor()定义constructor binding

  • 定义有多个构造函数的MyTransactionLog

    public class MyTransactionLog implements TransactionLog {
          
          
        private String jdbcUrl;
        private int threadPoolSize;
    
        public MyTransactionLog() {
          
          
        }
    
        public MyTransactionLog(@Named("mysql") String jdbcUrl) {
          
          
            this.jdbcUrl = jdbcUrl;
        }
    
        @Override
        public void log(String msg) {
          
          
            // 打印自身信息
            System.out.printf("jdbcUrl: %s, threadPoolSize: %d\n", jdbcUrl, threadPoolSize);
        }
    }
    
  • 向MyDatabase和MyWarehouse注入TransactionLog

    @Inject
    public MyDatabase(@Database TransactionLog log) {
          
           
        this.log = log;
    }
    
    @Inject
    public MyWarehouse(@Named("warehouse") TransactionLog log) {
          
          
        this.log = log;
    }
    
  • 使用toConstructor()定义constructor binding,使得MyDatabase和MyWarehouse注入的

    try {
          
          
        // MyDatabase使用的构造函数
        bind(TransactionLog.class).annotatedWith(Database.class)
                .toConstructor(MyTransactionLog.class.getConstructor(String.class));
        // MyWarehouse使用的构造函数
        bind(TransactionLog.class).annotatedWith(Names.named("warehouse"))
                .toConstructor(MyTransactionLog.class.getConstructor());
    } catch (NoSuchMethodException e) {
          
          
        throw new RuntimeException(e);
    }
    
  • 最终,MyDatabase和MyWarehouse将被注入具有不同属性的TransactionLog
    在这里插入图片描述

8. Just-in-time Bindings

  • 以上binding,最终都落在了继承AbstractModule自定义Module中,被叫做显式绑定(explicit binding
  • 如果一个type被需要,却又没有显示绑定,这是Guice会去寻找隐式binding( implicit binding
  • 隐式绑定,又叫Just-In-Time binding,简称 JIT binding

8.1 @Inject的隐式绑定

  • Guice创建一个类的对象时,需要找到该类可以依赖注入的构造函数(injectable constructor

以下两种情况,Guice认为构造函数是可注入的:

  1. 使用@Inject标识的构造函数(推荐的方式)
  2. 无参构造函数:
    • 且定义在非private的类中的、非private构造函数、(实际上,Guice支持private类中private构造函数,但是不推荐,因为反射会导致程序运行变慢)
    • 且未要求必须使用显式绑定
      binder().requireAtInjectRequired();
      
      在这里插入图片描述

以下情况,Guice认为构造函数不是可注入的

  • 未使用@Inject标识的、有一个或多个参数的构造函数
  • 类中不止存在一个使用@Inject标识的构造函数
  • 非静态的内部类中的构造函数

8.2 其他隐式绑定

8.2.1 @ImplementedBy

  • 使用@ImplementedBy定义linked binding

    @ImplementedBy(DatabaseTransactionLog.class)
    public interface TransactionLog {
          
          
        void log(String msg);
    }
    
  • 等价于下面的bind()语句

    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    
  • 如果既使用了bind(),又使用了@ImplementedBy,则bind()的优先级更高,会覆盖@ImplementedBy的定义

8.2.2 @ProvidedBy

  • 定义好的Provider类,可以通过@ProvidedBy告知Guice

    @ProvidedBy(DatabaseTransactionLogProvider.class)
    public interface TransactionLog {
          
          
        void log(String msg);
    }
    
  • 等价于下面的语句

    bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
    

8.3 关闭隐式绑定

  • 从Guice 3.0开始,在configure()方法中使用如下语句,可以关闭隐式绑定

    binder().requireExplicitBindings();
    

9. 后记

  • Guice还提供了很多其他的binding,例如支持binding多个实现的multi binding

    Multibinder<UriSummarizer> uriBinder = Multibinder.newSetBinder(binder(), UriSummarizer.class);
    // 可以Set的形式注入
    Set<UriSummarizer> summarizers
    
    MapBinder<String, EncoderFactory> encoderFactories = MapBinder.newMapBinder(binder, String.class, EncoderFactory.class);
    // 可以Map的形式注入
    Map<String, EncoderFactory>
    
  • 需要的小伙伴可以继续深入学习,这里就不再记录总结了

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/129172019