在Guice中,注入器的工作是装配对象图,当请求某一类型实例时,注入器根据对象图来判断如何创建实例、解析依赖。要确定如何解析依赖就需要通过配置注入器的绑定方式。 要创建绑定(`Binding`)对象,可以继承自`AbstractModule`类,然后覆盖其`configure`方法,在方法调用`bind()`方法来指来定每一次绑定,这些方法带有类型检查,如果你使用了错误的类型编译器就会报告编译错误。如果你已经写好了`Module`类,则创建一个`Module`类对象作为参数传递给`Guice.createInjector()`方法用于创建一个注入器。 通过`Module`对象可以创建链接绑定(linked bindings)、实例绑定(instance bindings)、@Provides methods、提供者绑定(provider bindings)、构建方法绑定(constructor bindings)与无目标绑定(untargetted bindings)。这些绑定方式统称为内置绑定,相对应的还有种及时绑定,如果在解析一个依赖时如果在内置绑定中无法找到,那么Guice将会创建一个及时绑定。 ### 一、链接绑定(LinkdedBindings) 链接绑定即映射一类型到它的实现类,例如映射`TransactionLog`接口到实现类`DatabaseTransactionLog`: ```java public class BillingModule extends AbstractModule { @Override protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); } } ``` 这样,当你调用`injector.getInstance(TransactionLog.class)`方法,或者当注入器碰到`TransactionLog`依赖时,就会使用`DatabaseTransactionLog`对象。链接是从一类型到它任何的子类型,这包括接口实现类,类的子类;所以如下映射也是可以的:`bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);` 并且链接绑定支持链式写法: ```java public class BillingModule extends AbstractModule { @Override protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class); } } ``` 在这种情况下,当请求一个`TransactionLog`类型对象时,注入器将返回一个`MySqlDatabaseTransactionLog`对象。 ### 二、绑定注解 某些情况下你可能想为同一种类型设置多种绑定。这就可以通过绑定注解来实现,该注解与绑定的类型用于唯一结识一个绑定,合在一起称为Key。示例: ```java package example.pizza; import com.google.inject.BindingAnnotation; import java.lang.annotation.Target; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; @BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME) public @interface PayPal {} ``` 这里关键的是`@BindingAnnotation`元注解,当Guice描述到该注解时,就会把PayPal作为绑定注解。 然后在`Module`的`configure`方法中使用`annotatedWith`语句,如下: `bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);` 这样就把`CreditCardProcessor`映射到了`PayPalCreditCardProcessor`。 用法如下: ```java public class RealBillingService implements BillingService { @Inject public RealBillingService(@PayPal CreditCardProcessor processor, TransactionLog transactionLog) { ... } } ``` 还有一种情况,我们可以使用Guice已经定义好的`@Named`注解,例如: ```java public class RealBillingService implements BillingService { @Inject public RealBillingService(@Named("Checkout") CreditCardProcessor processor, TransactionLog transactionLog) { ... } } ``` 要绑定一具体名称,使用`Names.named()`来创建一个实现传给`annotatedWith`方法: ```java bind(CreditCardProcessor.class) .annotatedWith(Names.named("Checkout")) .to(CheckoutCreditCardProcessor.class); ``` 因为编译器不会对字符串进行检查,Guice建议我们少使用`@Named`注解,但是我个人认为,只要自己写代码时只要名称不要写错,通过这种方法式是最容易为同一类型映射多个绑定的,这很类似Spring中的实现方式,Spring的`@Service`, `@Controller`, `@Repository`不就可以指定名称吗? ### 三、实例绑定(Instance Bindings) 通过实例绑定我们可以为某类型绑定一个具体的实例,这仅仅适用于这些实例类型没有其它依赖的情况,例如值对象: ```java bind(String.class) .annotatedWith(Names.named("JDBC URL")) .toInstance("jdbc:mysql://localhost/pizza"); bind(Integer.class) .annotatedWith(Names.named("login timeout seconds")) .toInstance(10); ``` 使用方式如下 : ```java @Inject @Named("JDBC URL") private String url ``` Guice建议我们避免使用`.toInstance`来创建复杂的对象,因为这会延迟应用启动。类似地,可以使用`@Provides`方法实现。 ### 四、`@Provides`方法 当使用`@Provides`方法创建对象时,该方法必须定义在`Module`类中,并且它必须加以`@Provides`注解,该方法的返回值类型就是被绑定的对象。当注入器需要该类型的实例时,它就会来调用该方法。 ```java public class BillingModule extends AbstractModule { @Override protected void configure() { ... } @Provides TransactionLog provideTransactionLog() { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza"); transactionLog.setThreadPoolSize(30); return transactionLog; } } ``` 如果在`@Provides`方法上有`@PayPal`或`@Named("Checkout")`绑定注解,Guice以绑定注解优先。Guice在调用`@Provides`方法之前会先解析该方法的依赖: ```java @Provides @PayPal CreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) { PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor(); processor.setApiKey(apiKey); return processor; } ``` 关于异常:Guice不允许在`@Provides`方法中抛出异常。如果有异常抛出,那么异常将会被包装在`ProvisionException`对象中。 ### 五、提供者绑定(Provider Bindings) 如果`@Provides`方法越来越复杂,我们可能会想把它们移到一个单独的类中。一个提供者类实现了`Provider`接口,它是一个用于提供值的简单通用接口。 ```java public interface Provider { T get(); } ``` 如果提供者实现类有其自己的依赖时,可以通过在其构造方法上添加`@Inject`注解进行注入,以保证值安全返回。 ```java public class DatabaseTransactionLogProvider implements Provider { private final Connection connection; @Inject public DatabaseTransactionLogProvider(Connection connection) { this.connection = connection; } public TransactionLog get() { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setConnection(connection); return transactionLog; } } ``` 最后使用`.toProvider`语句来绑定到提供者: ```java public class BillingModule extends AbstractModule { @Override protected void configure() { bind(TransactionLog.class) .toProvider(DatabaseTransactionLogProvider.class); } } ``` ### 六、无目标绑定 Guice允许我们创建绑定时不指定目标类,也就是没有to语句,这对于具体类或者使用了`@ImplementedBy`或`@ProvidedBy`注解类型很有用。例如: 1. `bind(MyConcreteClass.class); ` 2. `bind(AnotherConcreteClass.class).in(Singleton.class); ` 然而,在使用绑定注解时,我们依赖必须指定绑定目标,即它是一个具体类,例如: ```java bind(MyConcreteClass.class) .annotatedWith(Names.named("foo")) .to(MyConcreteClass.class); bind(AnotherConcreteClass.class) .annotatedWith(Names.named("foo")) .to(AnotherConcreteClass.class) .in(Singleton.class); ``` ### 七、构造方法绑定 有些时候你可能需要将某一类型绑定到任一构建方法,例如在`@Inject`注解无法添加到目标类构造方法,其原因可能是这个类是第三方提供的,或者说该类有多个构建方法参与依赖注入。此时`@Provides`方法是解决这个问题的最好方案,因为它可以明确指定调用哪个构造方法,而且不需要使用反射机制。但是使用`@Provides`方法在某些地方有限制,例如:手动创建对象不能在AOP中使用。 正是因为这个原因,Guice使用了`toConstructor()`进行绑定,这需要我们使用反射来选择构造方法与处理异常。 ```java public class BillingModule extends AbstractModule { @Override protected void configure() { try { bind(TransactionLog.class).toConstructor( DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class)); } catch (NoSuchMethodException e) { addError(e); } } } ``` 上这个例子中`DatabaseTransactionLog`类必须有一个带`DatabaseConnection`参数的构造方法,该构造方法中不需要使用`@Inject`注解Guice会自动调用该构造方法。每一条`toConstructor()`语句创建的绑定,其作用域是独立的,如果你创建了多个单例绑定并且使用目标类的同一个构造方法,每一个绑定还是拥有各自的实例。 ### 八、及时绑定 当注入器需要某一类型实例的时候,它需要获取一个绑定。在`Module`类中的绑定叫做显示绑定,只要它们可用,注入器就可以使用它们。如果需要某一类型实例,但它又不是显示绑定,那么注入器将试图创建一个及时绑定(Just-In-Time bindings),它也被称为JIT绑定与隐式绑定。 可用于创建及时绑定的情况如下: 1. 有一个合适的构建方法,即非私有,不带参数或者标有`@Inject`注解的构造方法,例如: ```java public class PayPalCreditCardProcessor implements CreditCardProcessor { private final String apiKey; @Inject public PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) { this.apiKey = apiKey; } } ``` Guice不会创建内部类实例除非它有static修饰符,因为内部类含有一个指向外问类的隐式引用,而这个隐式引用无法注入。 2. `@ImplementedBy` `@ImplementedBy`注解于用告诉注入器某类型的缺省实现类型是什么,这与链接绑定很相似。为某一类型绑定一子类型如下: ```java @ImplementedBy(PayPalCreditCardProcessor.class) public interface CreditCardProcessor { ChargeResult charge(String amount, CreditCard creditCard) throws UnreachableException; } ``` `@ImplementedBy(PayPalCreditCardProcessor.class)`等效于下面的`bind()`语句: ```java bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class); ``` 如果某一类型即有`bind()`语句又有`@ImplementedBy`注解,则`bind()`语句优先。使用`@ImplementedBy`请小心,因为它为接口添加了编译时依赖。 3. `@ProvidedBy` `@ProvidedBy`注解用于告诉注入器,`Provider`类的实现是什么,例如: ```java @ProvidedBy(DatabaseTransactionLogProvider.class) public interface TransactionLog { void logConnectException(UnreachableException e); void logChargeResult(ChargeResult result); } ``` 这等价于`bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);` 类似`@ImplementedBy`注解,如果某个类型既使用了`bind()`语句,又使用了`@ProvidedBy`注解,那么`bind()`语句优先。 -------------------------------- END ------------------------------- 及时获取更多精彩文章,请关注公众号《Java精讲》。