Google Guice - 依赖注入
— 焉知非鱼动机 #
将所有内容连接在一起是应用程序开发的一个单调乏味的部分。有几种方法可以将数据、服务和表示类相互连接起来。为了对比这些方法,我们将为披萨订购网站编写支付代码:
public interface BillingService {
/**
* Attempts to charge the order to the credit card. Both successful and
* failed transactions will be recorded.
*
* @return a receipt of the transaction. If the charge was successful, the
* receipt will be successful. Otherwise, the receipt will contain a
* decline note describing why the charge failed.
*/
Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}
伴随着实现,我们会给我们的代码写单元测试。在测试中,我们需要一个 FakeCreditCardProcessor
来避免支付到真实的信用卡。
直接构造函数调用 #
以下是我们刚刚刷新信用卡处理器和事务记录器时代码的样子:
public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog();
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
} catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
该代码给模块化和可测试性带来了问题。对真实信用卡处理器的直接编译时依赖意味着测试代码将收取信用卡费用!测试当收费被拒绝或服务不可用时会发生什么也很尴尬。
工厂 #
工厂类将客户端和实现类分离。一个简单的工厂使用静态方法来获取和设置接口的模拟实现。工厂使用一些样板代码实现:
public class CreditCardProcessorFactory {
private static CreditCardProcessor instance;
public static void setInstance(CreditCardProcessor processor) {
instance = processor;
}
public static CreditCardProcessor getInstance() {
if (instance == null) {
return new SquareCreditCardProcessor();
}
return instance;
}
}
在我们的客户端代码中,我们只是用工厂查找替换对 new
的调用:
public class RealBillingService implements BillingService {
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
TransactionLog transactionLog = TransactionLogFactory.getInstance();
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
} catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
工厂可以编写合适的单元测试:
public class RealBillingServiceTest extends TestCase {
private final PizzaOrder order = new PizzaOrder(100);
private final CreditCard creditCard = new CreditCard("1234", 11, 2010);
private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();
@Override public void setUp() {
TransactionLogFactory.setInstance(transactionLog);
CreditCardProcessorFactory.setInstance(processor);
}
@Override public void tearDown() {
TransactionLogFactory.setInstance(null);
CreditCardProcessorFactory.setInstance(null);
}
public void testSuccessfulCharge() {
RealBillingService billingService = new RealBillingService();
Receipt receipt = billingService.chargeOrder(order, creditCard);
assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}
这段代码很笨拙。全局变量保存模拟(mock)实现,因此我们需要小心设置它并将其删除。如果 tearDown
失败,全局变量将继续指向我们的测试实例。这可能会导致其他测试出现问题。它还阻止我们并行运行多个测试。
但最大的问题是依赖关系隐藏在代码中。如果我们在 CreditCardFraudTracker
上添加依赖项,我们必须重新运行测试以找出哪些会破坏。如果我们忘记为生产服务初始化工厂,我们在尝试收费之前不会发现。随着应用程序的增长,保姆工厂的生产力日益增加。
QA 或验收测试将捕获质量问题。这可能就足够了,但我们当然可以做得更好。
依赖注入 #
像工厂一样,依赖注入只是一种设计模式。核心原则是将行为与依赖性解析分开。在我们的示例中,RealBillingService
不负责查找 TransactionLog
和 CreditCardProcessor
。相反,它们作为构造函数参数传入:
public class RealBillingService implements BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
public RealBillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
} catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
我们不需要任何工厂,我们可以通过删除 setUp
和 tearDown
样板来简化测试用例:
public class RealBillingServiceTest extends TestCase {
private final PizzaOrder order = new PizzaOrder(100);
private final CreditCard creditCard = new CreditCard("1234", 11, 2010);
private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();
public void testSuccessfulCharge() {
RealBillingService billingService
= new RealBillingService(processor, transactionLog);
Receipt receipt = billingService.chargeOrder(order, creditCard);
assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertEquals(creditCard, processor.getCardOfOnlyCharge());
assertEquals(100, processor.getAmountOfOnlyCharge());
assertTrue(transactionLog.wasSuccessLogged());
}
}
现在,每当我们添加或删除依赖项时,编译器都会提醒我们需要修复哪些测试。依赖关系在 API 签名中公开。
不幸的是,现在 BillingService
的客户端需要查找其依赖项。我们可以通过再次应用模式来解决其中一些问题!依赖它的类可以在它们的构造函数中接受 BillingService
。对于顶级类,拥有一个框架很有用。否则,当需要使用服务时,您需要递归地构造依赖项:
public static void main(String[] args) {
CreditCardProcessor processor = new PaypalCreditCardProcessor();
TransactionLog transactionLog = new DatabaseTransactionLog();
BillingService billingService
= new RealBillingService(processor, transactionLog);
...
}
用 Guice 进行依赖注入 #
依赖注入模式导致代码模块化和可测试,而 Guice 使编写变得容易。要在我们的结算示例中使用 Guice,我们首先需要告诉它如何将接口映射到它们的实现。此配置在 Guice 模块中完成,该模块是实现 Module
接口的任何 Java 类:
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}
我们将 @Inject
添加到 RealBillingService
的构造函数中,该构造函数指示 Guice 使用它。 Guice 将检查带注释的构造函数,并查找每个参数的值。
public class RealBillingService implements BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
@Inject
public RealBillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
try {
ChargeResult result = processor.charge(creditCard, order.getAmount());
transactionLog.logChargeResult(result);
return result.wasSuccessful()
? Receipt.forSuccessfulCharge(order.getAmount())
: Receipt.forDeclinedCharge(result.getDeclineMessage());
} catch (UnreachableException e) {
transactionLog.logConnectException(e);
return Receipt.forSystemFailure(e.getMessage());
}
}
}
最后,我们可以把它们放在一起。 Injector
可用于获取任何绑定类的实例。
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
...
}
入门指南解释了这一切是如何工作的。
入门 #
如何开始使用 Guice 进行依赖注入。
开始 #
通过依赖注入,对象接受其构造函数中的依赖项。要构造对象,首先要构建其依赖项。但是要构建每个依赖项,您需要它的依赖项,依此类推。因此,在构建对象时,您确实需要构建对象图。
手动构建对象图是劳动密集型,容易出错,并且使测试变得困难。相反,Guice 可以为您构建对象图。但首先,Guice 需要配置为完全按照您的意愿构建图形。
为了说明,我们将启动 BillingService
类,该类在其构造函数中接受其依赖接口 CreditCardProcessor
和 TransactionLog
。为了明确说明 Guice 调用 BillingService
构造函数,我们添加 @Inject
注解:
class BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
@Inject
BillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
...
}
}
我们想使用 PaypalCreditCardProcessor
和 DatabaseTransactionLog
构建 BillingService
。 Guice 使用绑定将类型映射到它们的实现。模块是使用流畅的类似英语的方法调用指定的绑定集合:
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
/*
* This tells Guice that whenever it sees a dependency on a TransactionLog,
* it should satisfy the dependency using a DatabaseTransactionLog.
*/
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
/*
* Similarly, this binding tells Guice that when CreditCardProcessor is used in
* a dependency, that should be satisfied with a PaypalCreditCardProcessor.
*/
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
}
}
模块是注入器的构建块,它是 Guice 的对象图构建器。首先我们创建注入器,然后我们可以使用它来构建 BillingService
:
public static void main(String[] args) {
/*
* Guice.createInjector() takes your Modules, and returns a new Injector
* instance. Most applications will call this method exactly once, in their
* main() method.
*/
Injector injector = Guice.createInjector(new BillingModule());
/*
* Now that we've got the injector, we can build objects.
*/
BillingService billingService = injector.getInstance(BillingService.class);
...
}
通过构建 billingService
,我们使用 Guice 构建了一个小对象图。该图包含计费服务及其相关的信用卡处理器和事务日志。
https://github.com/google/guice/wiki/ 是谷歌出的依赖注入。
绑定 #
注入器(injector)的工作是组装对象图。你请求一个给定类型的实例,它会确定要构建的内容,解析依赖关系,并将所有内容连接在一起。要指定如何解析依赖关系,请使用绑定配置该注入器。
创建绑定 #
要创建绑定,请扩展 AbstractModule
并重写其 configure
方法。在方法体中,调用 bind()
来指定每个绑定。这些方法是带有类型检查的,因此如果使用错误的类型,编译器可以报告错误。创建模块后,将它们作为参数传递给 Guice.createInjector()
以构建注入器。
使用模块来创建 linked bindings, instance bindings, @Provides methods, provider bindings, constructor bindings 和 untargetted bindings.
更多绑定 #
除了您指定的绑定外,注入器还包含内置绑定。当请求的依赖项未找到时,它会尝试创建即时绑定。注入器还包括用于其他绑定的providers的绑定。
关联绑定 #
关联绑定(Linked Bindings)将类型映射到它的实现上。这个例子将接口 TransactionLog
映射到 DatabaseTransactionLog
实现上:
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
}
}
现在, 当你调用 injector.getInstance(TransactionLog.class)
时, 或者当注入器遇到对 TransactionLog
的依赖时, 它就会使用 DatabaseTransactionLog
。从一个类型关联到它的子类型中的任何一个, 例如实现类或扩展类。你甚至可以将具体的 DatabaseTransactionLog
类关联到子类上:
bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
关联绑定还可以链接到一块儿:
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
}
}
在这个情况下, 当要求 TransactionLog
时, 注入器会返回 MySqlDatabaseTransactionLog
。
绑定注解 #
有时您会想要同一类型的多个绑定。例如,您可能需要 PayPal 信用卡处理器和 Google Checkout 处理器。要启用此功能,绑定支持可选的绑定注释。注释和类型一起唯一标识绑定。这个对儿被称为钥匙(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 这是一个绑定注释。如果多个绑定注释应用于同一成员,Guice 将产生错误。@Target({FIELD, PARAMETER, METHOD})
是对用户的优惠。它可以防止@PayPal
意外地被应用于无用的地方。@Retention(RUNTIME)
使注释在运行时可用。
要依赖带注释的绑定,请将注释应用于注入的参数:
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
最后,我们创建一个使用注释的绑定。这使用 bind()
语句中的可选 annotatedWith
子句:
bind(CreditCardProcessor.class)
.annotatedWith(PayPal.class)
.to(PayPalCreditCardProcessor.class);
@Named #
Guice 带有一个内置的绑定注释 @Named
,它带有一个字符串:
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
要绑定特定名称,请使用 Names.named()
创建要传递给 annotatedWith
的实例:
bind(CreditCardProcessor.class)
.annotatedWith(Names.named("Checkout"))
.to(CheckoutCreditCardProcessor.class);
由于编译器无法检查字符串,我们建议谨慎使用 @Named
。定义您自己的专用注释可提供更好的类型安全性
绑定注释与属性 #
Guice 支持绑定具有属性值的注释(如 @Named
)。在极少数情况下,您需要这样的注释(并且不能使用 @Provides
方法),我们建议您使用 Auto/Value 项目中的 @AutoAnnotation,因为正确实现注释很容易出错。如果您决定手动创建自定义实现,请确保正确实现 Annotation Javadoc 中详述的 equals()
和 hashCode()
规范。将此类的实例传递给 annotatedWith()
绑定子句。
实例绑定 #
您可以将类型绑定到该类型的特定实例上。这通常仅适用于不具有自己的依赖关系的对象,例如值对象:
bind(String.class)
.annotatedWith(Names.named("JDBC URL"))
.toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class)
.annotatedWith(Names.named("login timeout seconds"))
.toInstance(10);
避免将 .toInstance
用于创建复杂的对象,因为它会减慢应用程序的启动速度。您可以改为使用@ Provides
方法。
@Provides 方法 #
当你需要代码来创建一个对象时,使用 @Provides
方法。该方法必须在模块中定义,并且必须具有 @Provides
注解。该方法的返回类型是绑定类型。只要注入器需要该类型的实例,它就会调用该方法。
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 绑定注释类型。依赖关系可以作为参数传递给方法。在调用该方法之前,注射器将为每个注入器执行绑定。
@Provides @PayPal
CreditCardProcessor providePayPalCreditCardProcessor(
@Named("PayPal API key") String apiKey) {
PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
processor.setApiKey(apiKey);
return processor;
}
抛出异常 #
Guice 不允许从 Providers 抛出异常。 @Provides
方法引发的异常将被包装在 ProvisionException
中。允许从 @Provides
方法抛出任何类型的异常(运行时或检查)是不好的做法。如果由于某种原因需要抛出异常,则可能需要使用 ThrowingProviders 扩展 的 @CheckedProvides
方法。
Provider 绑定 #
当你的 @Provides
方法开始变得复杂时, 你可以考虑将它们移动到自己的类中。provider 类实现了 Guice 的 Provider
接口,这是一个用于提供值的简单通用接口:
public interface Provider<T> {
T get();
}
我们的 provider 实现类具有自己的依赖关系,它通过 @Inject
-annotated 构造函数接收。它实现了 Provider
接口,用于定义具有完整类型安全性的返回内容:
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;
}
}
最后,我们使用 .toProvider
子句绑定到提供者(provider):
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class)
.toProvider(DatabaseTransactionLogProvider.class);
}
如果你的 providers 很复杂,请务必测试它们!
无目标绑定 #
创建没有目标的绑定。
你可以在不指定目标的情况下创建绑定。这对于由 @ImplementedBy
或 @ProvidedBy
注解的具体类和类型最有用。无目标绑定通知注入器有关类型的信息,因此它可能会急切地准备依赖关系。 无目标绑定没有子句,如下所示:
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
指定绑定注解时,你仍必须添加目标绑定,即使它是相同的具体类。例如:
bind(MyConcreteClass.class)
.annotatedWith(Names.named("foo"))
.to(MyConcreteClass.class);
bind(AnotherConcreteClass.class)
.annotatedWith(Names.named("foo"))
.to(AnotherConcreteClass.class)
.in(Singleton.class);
构造函数绑定 #
Guice 3.0中的新功能
偶尔有必要将类型绑定到任意构造函数。当 @Inject
注释无法应用于目标构造函数时会出现这种情况:要么是因为它是第三方类,要么是因为参与依赖注入的多个构造函数。 @Provides 方法为这个问题提供了最佳解决方案!通过显式调用目标构造函数,您不需要反射及其相关的陷阱。但是这种方法存在局限性:手动构造的实例不参与 AOP。
为了解决这个问题,Guice 必须使用 Constructor()
绑定。它们要求您反射性地选择目标构造函数并处理异常(如果找不到该构造函数):
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()
绑定都是独立的范围。如果创建多个以相同构造函数为目标的单例绑定,则每个绑定都会生成自己的实例。
内置绑定 #
您可以使用的更多绑定。
内置绑定 #
除了显式和即时绑定外,其他绑定也会自动包含在注入器中。只有注入器可以创建这些绑定并尝试自己绑定它们是一个错误。
Loggers #
Guice 有一个 java.util.logging.Logger
的内置绑定,用于保存一些样板。绑定自动将记录器的名称设置为注入 Logger
的类的名称。
@Singleton
public class ConsoleTransactionLog implements TransactionLog {
private final Logger logger;
@Inject
public ConsoleTransactionLog(Logger logger) {
this.logger = logger;
}
public void logConnectException(UnreachableException e) {
/* the message is logged to the "ConsoleTransacitonLog" logger */
logger.warning("Connect exception failed, " + e.getMessage());
}
注射器 #
在框架代码中,有时您在运行时之前不知道所需的类型。在这种罕见的情况下,您应该注射注射器。注入注入器的代码不会自我记录其依赖关系,因此这种方法应该谨慎进行。
Providers #
对于 Guice 所知道的每种类型,它也可以注入该类型的 Provider。Injecting Providers详细描述了这一点。
TypeLiterals #
Guice 为其注入的所有内容提供完整的类型信息。如果您正在注入参数化类型,则可以注入 TypeLiteral<T>
以反射性地告诉您元素类型。
The Stage #
Guice 支持阶段(stage)枚举,以区分开发和生产运行。
MembersInjectors #
绑定到提供程序或编写扩展时,您可能希望Guice将依赖项注入您自己构造的对象中。为此,在 MembersInjector<T>
(其中T是您的对象的类型)上添加依赖项,然后调用 membersInjector.injectMembers(myNewObject)
。
即时绑定 #
由 Guice 自动创建的绑定。
即时捆绑 #
当注入器需要类型的实例时,它需要绑定。模块中的绑定称为显式绑定,只要可用,注入器就会使用它们。如果需要类型但没有显式绑定,则注入器将尝试创建实时绑定。这些也称为 JIT 绑定和隐式绑定。
合格的构造函数 #
Guice 可以使用类型的 injectable 构造函数为具体类型创建绑定。这可以是非私有的,无参数的构造函数,也可以是带有 @Inject
批注的构造函数:
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private final String apiKey;
@Inject
public PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
Guice 不会构造嵌套类,除非它们具有 static
修饰符。内部类具有对其无法注入的封闭类的隐式引用。
@ImplementedBy #
注释类型告诉注入器它们的默认实现类型是什么。 @ImplementedBy 注释的作用类似于链接绑定,指定构建类型时要使用的子类型。
@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {
ChargeResult charge(String amount, CreditCard creditCard)
throws UnreachableException;
}
上面的注释等效于以下 bind()
语句:
bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);
如果类型同时包含 bind()
语句(作为第一个参数)并且具有 @ImplementedBy
注释,则使用 bind()
语句。注释建议可以使用绑定覆盖默认实现。小心使用 @ImplementedBy
; 它从接口向其实现添加了编译时依赖项。
@ProvidedBy #
@ProvidedBy
告诉注入器有关生成实例的 Provider
类:
@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {
void logConnectException(UnreachableException e);
void logChargeResult(ChargeResult result);
}
注释等效于 toProvider()
绑定:
bind(TransactionLog.class)
.toProvider(DatabaseTransactionLogProvider.class);
与 @ImplementedBy
一样,如果类型在 bind()
语句中注释并使用,则将使用 bind()
语句。
作用域 #
作用域 #
默认情况下,Guice 每次提供一个值时都会返回一个新实例。此行为可通过作用域进行配置。范围允许您重用实例:应用程序的生命周期(@Singleton
),会话(@SessionScoped
)或请求(@RequestScoped
)。Guice包含一个servlet扩展,用于定义Web应用程序的范围。可以为其他类型的应用程序编写自定义作用域。
应用范围 #
Guice 使用注释来标识范围。通过将范围注释应用于实现类来指定类型的范围。除了功能性之外,此注释还可用作文档。例如,@Singleton
表示该类旨在是线程安全的。
@Singleton
public class InMemoryTransactionLog implements TransactionLog {
/* everything here should be threadsafe! */
}
范围也可以在bind
语句中配置:
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
并通过注释@Provides
方法:
@Provides @Singleton
TransactionLog provideTransactionLog() {
...
}
如果类型和语句中的作用域存在冲突bind()
,则将使用bind()
语句的作用域。如果使用您不想要的范围注释类型,请将其绑定到Scopes.NO_SCOPE
。
在链接的绑定中,范围适用于绑定源,而不是绑定目标。假设我们有一个Applebees
实现两者Bar
和Grill
接口的类。这些绑定允许该类型的两个实例,一个用于Bar
s,另一个用于Grill
s:
bind(Bar.class).to(Applebees.class).in(Singleton.class);
bind(Grill.class).to(Applebees.class).in(Singleton.class);
这是因为范围适用于绑定类型(Bar
,Grill
),而不是满足该绑定(Applebees
)的类型。要仅允许创建单个实例,请在@Singleton
该类的声明上使用注释。或者添加另一个绑定:
bind(Applebees.class).in(Singleton.class);
这种绑定使得.in(Singleton.class)
上面的其他两个条款变得不必要。
该in()
子句既可以接受范围注释RequestScoped.class
,也可以接受以下Scope
实例ServletScopes.REQUEST
:
bind(UserPreferences.class)
.toProvider(UserPreferencesProvider.class)
.in(ServletScopes.REQUEST);
注释是首选,因为它允许模块在不同类型的应用程序中重用。例如,@RequestScoped
对象可以作用于Web应用程序中的HTTP请求,也可以作用于API服务器中的RPC。
Eager Singletons #
Guice有特殊的语法来定义可以热切构造的单例:
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();
Eager singletons 可以更快地揭示初始化问题,并确保最终用户获得一致,快捷的体验。懒惰的单例可以实现更快的编辑 - 编译 - 运行开发周期。使用Stage
枚举指定应使用的策略。
生产 | 发展 | |
---|---|---|
.asEagerSingleton() | eager | eager |
.in(Singleton.class) | eager | lazy |
.in(Scopes.SINGLETON) | eager | lazy |
@Singleton | eager* | lazy |
* Guice只会热切地为他们所知道的类型建立单例。这些是模块中提到的类型,以及这些类型的传递依赖性。
选择范围 #
如果对象是有状态的,则范围应该是显而易见的。每个应用程序是@Singleton
,每个请求是@RequestScoped
,等等。如果对象是无状态且创建成本低,则不需要确定范围。保留未绑定的绑定,Guice将根据需要创建新实例。
单例在Java应用程序中很流行,但它们没有提供太多价值,特别是在涉及依赖注入时。虽然单例保存对象创建(以及后来的垃圾收集),但单例的初始化需要同步; 获取单个初始化实例的句柄只需要读取volatile。单身人士最适合:
- 有状态对象,例如配置或计数器
- 构造或查找昂贵的对象
- 占用资源的对象,例如数据库连接池。
范围和并发 #
注释的类@Singleton
,@SessionScoped
必须是线程安全的。注入这些类的所有东西也必须是线程安全的。最小化可变性以限制需要并发保护的状态量。
@RequestScoped
对象不需要是线程安全的。对象@Singleton
或@SessionScoped
对象依赖于一个通常是错误的@RequestScoped
。如果您需要较窄范围内的对象,请注入Provider
该对象。
Guice如何初始化您的对象
注射 #
依赖注入模式将行为与依赖性解析分开。该模式不是直接查找依赖项或从工厂查找依赖项,而是建议传入依赖项。将依赖项设置为对象的过程称为注入。
构造函数注入 #
构造函数注入将实例化与注入相结合。要使用它,请使用注释注释构造函数@Inject
。此构造函数应接受类依赖项作为参数。然后,大多数构造函数将参数分配给最终字段。
public class RealBillingService implements BillingService {
private final CreditCardProcessor processorProvider;
private final TransactionLog transactionLogProvider;
@Inject
public RealBillingService(CreditCardProcessor processorProvider,
TransactionLog transactionLogProvider) {
this.processorProvider = processorProvider;
this.transactionLogProvider = transactionLogProvider;
}
如果您的类没有@Inject
注释构造函数,Guice将使用public,no-arguments构造函数(如果存在)。首选注释,该类型参与依赖注入的文档。
构造函数注入与单元测试很好地协同工作。如果您的类在单个构造函数中接受其所有依赖项,则不会意外忘记设置依赖项。当引入新的依赖项时,所有的调用代码都会方便地中断!修复编译错误,您可以确信所有内容都已正确连接。
方法注入 #
Guice可以注入具有@Inject
注释的方法。依赖关系采用参数的形式,在调用方法之前,注入器会解析这些参数。注入的方法可以具有任意数量的参数,并且方法名称不会影响注入。
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private static final String DEFAULT_API_KEY = "development-use-only";
private String apiKey = DEFAULT_API_KEY;
@Inject
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
字段注射 #
Guice使用@Inject
注释注入字段。这是最简洁的注射剂,但是最不可测试的。
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
@Inject Connection connection;
public TransactionLog get() {
return new DatabaseTransactionLog(connection);
}
}
避免使用final
具有弱语义的字段的字段注入。
可选注射 #
有时候,当它存在时使用依赖是很方便的,当它没有依赖时它会回退到默认值。方法和字段注入可以是可选的,这会导致Guice在依赖项不可用时以静默方式忽略它们。要使用可选注入,请应用@Inject(optional=true)
注释:
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private static final String SANDBOX_API_KEY = "development-use-only";
private String apiKey = SANDBOX_API_KEY;
@Inject(optional=true)
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
混合可选注射和即时结合可能会产生令人惊讶的结果。例如,即使Date
未明确绑定,也始终注入以下字段。这是因为Date
有一个公共的无参数构造函数,它有资格进行实时绑定。
@Inject(optional=true) Date launchDate;
按需注射 #
方法和字段注入可用于初始化现有实例。您可以使用Injector.injectMembers
API:
public static void main(String[] args) {
Injector injector = Guice.createInjector(...);
CreditCardProcessor creditCardProcessor = new PayPalCreditCardProcessor();
injector.injectMembers(creditCardProcessor);
静态注射 #
当迁移应用程序从静态工厂吉斯,可以逐步改变。静电注射是一个有用的拐杖。通过获取对注入类型的访问而不自己注入,它使对象可以部分地参与依赖注入。requestStaticInjection()
在模块中使用以指定在注入器创建时注入的类:
@Override public void configure() {
requestStaticInjection(ProcessorFactory.class);
...
}
Guice将注入具有@Inject
注释的类的静态成员:
class ProcessorFactory {
@Inject static Provider<Processor> processorProvider;
/**
* @deprecated prefer to inject your processor instead.
*/
@Deprecated
public static Processor getInstance() {
return processorProvider.get();
}
}
静态成员不会在实例注入时注入。建议不要将此API用于一般用途,因为它遇到许多与静态工厂相同的问题:测试时笨拙,依赖性不透明,依赖于全局状态。
自动注射 #
Guice自动注入以下所有内容:
- 实例传递给
toInstance()
绑定语句 toProvider()
在绑定语句中传递给的实例将在创建注入器本身时注入对象。如果他们需要满足其他初始注射,Guice会在使用前注射它们。
AOP #
用Guice拦截方法
面向方面编程 #
为了补充依赖注入,Guice支持方法拦截。此功能使您可以编写每次调用匹配方法时执行的代码。它适用于交叉问题(“方面”),例如交易,安全性和日志记录。因为拦截器将问题分为方面而不是对象,所以它们的使用称为面向方面编程(AOP)。
大多数开发人员不会直接编写方法拦截器; 但他们可能会看到它们在Warp Persist等集成库中的使用。那些需要选择匹配方法,创建拦截器,并在模块中配置它们。
Matcher是一个接受或拒绝值的简单接口。对于Guice AOP,您需要两个匹配器:一个用于定义哪些类参与,另一个用于这些类的方法。为了简化这一过程,有工厂类可以满足常见的情况。
只要调用匹配方法,就会执行MethodInterceptors。他们有机会检查调用:方法,参数和接收实例。他们可以执行他们的交叉逻辑,然后委托给底层方法。最后,他们可以检查返回值或异常并返回。由于拦截器可以应用于许多方法并且将接收许多调用,因此它们的实现应该是有效且不引人注目的。
示例:周末禁止方法调用 #
为了说明方法拦截器如何与Guice协同工作,我们将在周末禁止拨打我们的披萨计费系统。送货员只在星期一到星期五工作,所以我们会防止披萨无法送达!此示例在结构上类似于使用AOP进行授权。
要将选择方法标记为仅工作日,我们定义注释:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface NotOnWeekends {}
…并将其应用于需要截获的方法:
public class RealBillingService implements BillingService {
@NotOnWeekends
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
...
}
}
接下来,我们通过实现org.aopalliance.intercept.MethodInterceptor
接口来定义拦截器。当我们需要调用底层方法时,我们通过调用invocation.proceed()
:
public class WeekendBlocker implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Calendar today = new GregorianCalendar();
if (today.getDisplayName(DAY_OF_WEEK, LONG, ENGLISH).startsWith("S")) {
throw new IllegalStateException(
invocation.getMethod().getName() + " not allowed on weekends!");
}
return invocation.proceed();
}
}
最后,我们配置一切。这是我们为要拦截的类和方法创建匹配器的地方。在这种情况下,我们匹配任何类,但只匹配带有我们@NotOnWeekends
注释的方法:
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class),
new WeekendBlocker());
}
}
把它们放在一起,(并等到星期六),我们看到该方法被截获,我们的订单被拒绝:
Exception in thread "main" java.lang.IllegalStateException: chargeOrder not allowed on weekends!
at com.publicobject.pizza.WeekendBlocker.invoke(WeekendBlocker.java:65)
at com.google.inject.internal.InterceptorStackCallback.intercept(...)
at com.publicobject.pizza.RealBillingService$$EnhancerByGuice$$49ed77ce.chargeOrder(<generated>)
at com.publicobject.pizza.WeekendExample.main(WeekendExample.java:47)
限制 #
在幕后,通过在运行时生成字节码来实现方法拦截。Guice动态创建一个子类,通过重写方法来应用拦截器。如果您使用的是不支持字节码生成的平台(例如Android),则应使用Guice而不支持AOP。
这种方法对可拦截的类和方法施加了限制:
- 类必须是公共的或包私有的。
- 课程必须是非最终的
- 方法必须是公共的,包私有的或受保护的
- 方法必须是非最终的
- 实例必须由Guice通过
@Inject
-annotated或no-argument构造函数创建。不可能对非Guice构造的实例使用方法拦截。
注入拦截器 #
如果需要将依赖项注入拦截器,请使用requestInjection
API。
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
WeekendBlocker weekendBlocker = new WeekendBlocker();
requestInjection(weekendBlocker);
bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class),
weekendBlocker);
}
}
另一个选择是使用Binder.getProvider并在拦截器的构造函数中传递依赖项。
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
bindInterceptor(any(),
annotatedWith(NotOnWeekends.class),
new WeekendBlocker(getProvider(Calendar.class)));
}
}
注入拦截器时要小心。如果您的拦截器调用它本身正在拦截的方法,您可能会收到StackOverflowException
由于无休止的递归。
AOP联盟 #
Guice实现的方法拦截器API是名为AOP Alliance的公共规范的一部分。这使得可以在各种框架中使用相同的拦截器。
最佳实践 #
尽量减少可变性 #
尽可能使用构造函数注入来创建不可变对象。不可变对象简单,可共享,并且可以组合。按照此模式定义您的注射类型:
public class RealPaymentService implements PaymentService {
private final PaymentQueue paymentQueue;
private final Notifier notifier;
@Inject
RealPaymentRequestService(
PaymentQueue paymentQueue,
Notifier notifier) {
this.paymentQueue = paymentQueue;
this.notifier = notifier;
}
...
此类的所有字段都是final,并由单个@Inject
注释的构造函数初始化。Effective Java讨论了不可变性的其他好处。
注入方法和字段 #
构造函数注入有一些限制:
- 注入的构造函数可能不是可选的。
- 除非Guice创建对象,否则不能使用它。这是某些框架的破解者。
- 子类必须调用
super()
所有依赖项。这使构造函数注入变得麻烦,尤其是当注入的基类发生变化时。
当您需要初始化非Guice构造的实例时,方法注入最有用。像AssistedInject和Multibinder 这样的扩展使用方法注入来初始化绑定对象。
场注入具有最紧凑的语法,因此它经常出现在幻灯片和示例中。它既不是封装也不是可测试的。决不注入最终场 ; JVM不保证注入的值对所有线程都可见。
仅注入直接依赖项 #
避免仅将对象注入作为获取另一个对象的手段。例如,不要注入Customer
一个方法来获取Account
:
public class ShowBudgets {
private final Account account;
@Inject
ShowBudgets(Customer customer) {
account = customer.getPurchasingAccount();
}
相反,直接注入依赖项。这使测试更容易; 测试用例不需要关心客户。使用@Provides
您的方法Module
创建绑定Account
,使用绑定Customer
:
public class CustomersModule extends AbstractModule {
@Override public void configure() {
...
}
@Provides
Account providePurchasingAccount(Customer customer) {
return customer.getPurchasingAccount();
}
通过直接注入依赖项,我们的代码更简单。
public class ShowBudgets {
private final Account account;
@Inject
ShowBudgets(Account account) {
this.account = account;
}
解决循环依赖关系 #
假设您的应用程序有几个类,包括Store,Boss和Clerk。
public class Store {
private final Boss boss;
//...
@Inject public Store(Boss boss) {
this.boss = boss;
//...
}
public void incomingCustomer(Customer customer) {...}
public Customer getNextCustomer() {...}
}
public class Boss {
private final Clerk Clerk;
@Inject public Boss(Clerk Clerk) {
this.Clerk = Clerk;
}
}
public class Clerk {
// Nothing interesting here
}
现在,依赖链一切都很好:构建一个Store导致构建一个Boss,这导致构建一个Clerk。但是,为了让秘书让客户进行销售,他需要参考商店来获取这些客户
public class Store {
private final Boss boss;
//...
@Inject public Store(Boss boss) {
this.boss = boss;
//...
}
public void incomingCustomer(Customer customer) {...}
public Customer getNextCustomer() {...}
}
public class Boss {
private final Clerk clerk;
@Inject public Boss(Clerk clerk) {
this.clerk = clerk;
}
}
public class Clerk {
private final Store shop;
@Inject Clerk(Store shop) {
this.shop = shop;
}
void doSale() {
Customer sucker = shop.getNextCustomer();
//...
}
}
这导致了一个周期:职员 - >商店 - >老板 - >职员。在尝试建造一个职员时,将建造一个商店,这需要一个Boss,这需要一个职员!
有几种方法可以解决此循环:
消除循环(推荐) #
循环通常反映不充分的颗粒分解。要消除此类周期,请将Dependency Case提取到单独的类中。
在这个例子中,管理传入客户的工作可以被提取到另一个类中,例如CustomerLine
,可以注入到文员和商店。
public class Store {
private final Boss boss;
private final CustomerLine line;
//...
@Inject public Store(Boss boss, CustomerLine line) {
this.boss = boss;
this.line = line;
//...
}
public void incomingCustomer(Customer customer) { line.add(customer); }
}
public class Clerk {
private final CustomerLine line;
@Inject Clerk(CustomerLine line) {
this.line = line;
}
void doSale() {
Customer sucker = line.getNextCustomer();
//...
}
}
虽然Store和Clerk都依赖于CustomerLine,但依赖关系图中没有循环(尽管您可能希望确保Store和Clerk都使用相同的CustomerLine实例)。这也意味着当您的商店有大型帐篷销售时,您的文员将能够销售汽车:只需注入不同的CustomerLine。
与提供商打破周期 #
注入Guice提供程序将允许您在依赖关系图中添加一个接缝。店员仍然会依赖商店,但是在他需要商店之前,店员不会看商店。
public class Clerk {
private final Provider<Store> shopProvider;
@Inject Clerk(Provider<Store> shopProvider) {
this.shopProvider = shopProvider;
}
void doSale() {
Customer sucker = shopProvider.get().getNextCustomer();
//...
}
}
请注意,除非将Store绑定为Singleton或在其他范围内重复使用,shopProvider.get()
否则调用将最终构建一个新的Store,它将构建一个新的Boss,它将再次构建一个新的Clerk!
工厂方法将两个对象绑在一起 #
当你的依赖关系紧密联系在一起时,用上述方法解开它们是行不通的。当使用类似View / Presenter范例的东西时,会出现这样的情况:
public class FooPresenter {
@Inject public FooPresenter(FooView view) {
//...
}
public void doSomething() {
view.doSomethingCool();
}
}
public class FooView {
@Inject public FooView(FooPresenter presenter) {
//...
}
public void userDidSomething() {
presenter.theyDidSomething();
}
//...
}
每个对象都需要另一个对象。在这里,您可以使用AssistedInject来解决它:
public class FooPresenter {
private final FooView view;
@Inject public FooPresenter(FooView.Factory viewMaker) {
view = viewMaker.create(this);
}
public void doSomething() {
//...
view.doSomethingCool();
}
}
public class FooView {
@Inject public FooView(@Assisted FooPresenter presenter) {...}
public void userDidSomething() {
presenter.theyDidSomething();
}
public static interface Factory {
FooView create(FooPresenter presenter)
}
}
当尝试使用Guice来表示业务对象模型时,也会出现这种情况,业务对象模型可能具有反映不同类型关系的周期。 AssistedInject对于这种情况也相当不错。
避免静态状态 #
静态和可测试性是敌人。您的测试应该快速且无副作用。但静态字段所持有的非常量值是一种难以管理的问题。可靠地拆除由测试模拟的静态单体是很棘手的,这会干扰其他测试。
requestStaticInjection()
是一个拐杖。Guice包含此API,以便于从静态配置的应用程序迁移到依赖注入的应用程序。使用Guice开发的新应用程序不应使用此API。
虽然静态状态不好,但static 关键字没有任何问题。静态类是可以的(首选甚至!)和纯函数(排序,数学等),静态就好了。
使用@Nullable #
要NullPointerExceptions
在代码库中消除,必须遵守空引用的规定。我们通过遵循并执行一个简单的规则来成功: 除非明确指定,否则每个参数都是非空的。 该番石榴:谷歌核心库的Java和JSR-305拥有简单的API得到控制一空。Preconditions.checkNotNull
如果找到空引用,则可用于快速失败,@Nullable
并可用于注释允许该null
值的参数:
import static com.google.common.base.Preconditions.checkNotNull;
import javax.annotation.Nullable;
public class Person {
...
public Person(String firstName, String lastName, @Nullable Phone phone) {
this.firstName = checkNotNull(firstName, "firstName");
this.lastName = checkNotNull(lastName, "lastName");
this.phone = phone;
}
*Guice 默认禁止 null。*它会拒绝注入 null
,ProvisionException
而是拒绝注入。如果null
您的班级允许,您可以使用注释字段或参数@Nullable
。Guice识别任何@Nullable
注释,例如edu.umd.cs.findbugs.annotations.Nullable或javax.annotation.Nullable。
模块应该快速且无副作用 #
Guice模块不是使用外部XML文件进行配置,而是使用常规Java代码编写。Java很熟悉,可以与IDE一起使用,并且可以在重构后继续使用。
但是Java语言的全部功能都需要付出代价:在模块中很容易做得太多。很容易连接到数据库连接或在Guice模块中启动HTTP服务器。不要这样做!在模块中进行繁重的工作会产生问题:
- **模块启动,但它们不会关闭。**如果您在模块中打开数据库连接,则不会有任何挂钩来关闭它。
- **应该测试模块。**如果模块打开数据库作为执行过程,则很难为其编写单元测试。
- **模块可以被覆盖。**Guice模块支持覆盖,允许生产服务替换为轻量级或测试服务。当生产服务作为模块执行的一部分创建时,此类覆盖无效。
而不是在模块本身中工作,定义一个可以在适当的抽象级别上完成工作的接口。在我们的应用中,我们使用此接口:
public interface Service {
/**
* Starts the service. This method blocks until the service has completely started.
*/
void start() throws Exception;
/**
* Stops the service. This method blocks until the service has completely shut down.
*/
void stop();
}
在创建Injector之后,我们通过启动其服务来完成引导我们的应用程序。我们还添加了关闭挂钩,以便在应用程序停止时干净地释放资源。
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(
new DatabaseModule(),
new WebserverModule(),
...
);
Service databaseConnectionPool = injector.getInstance(
Key.get(Service.class, DatabaseService.class));
databaseConnectionPool.start();
addShutdownHook(databaseConnectionPool);
Service webserver = injector.getInstance(
Key.get(Service.class, WebserverService.class));
webserver.start();
addShutdownHook(webserver);
}
注意 Provider 的 I/O. #
该Provider
接口方便调用者,但缺少语义:
- **提供者不声明已检查的例外。**如果您正在编写需要从特定类型的故障中恢复的代码,则无法捕获
TransactionRolledbackException
。ProvisionException
允许您从常规配置故障中恢复,并且您可以迭代其原因,但您无法指定这些原因可能是什么。 - 提供商不支持超时。
- **提供商没有定义重试策略。**当值不可用时,
get()
多次调用可能会导致多个失败的条款。
ThrowingProviders是一个Guice扩展,它实现了一个抛出异常的提供者。它允许失败作为范围,因此每个请求或会话只发生一次失败的查找。
https://blog.csdn.net/xtayfjpk/article/details/40657781
避免模块中的条件逻辑 #
创建具有移动部件的模块很有诱惑力,并且可以配置为针对不同环境以不同方式运行:
public class FooModule extends AbstractModule {
private final String fooServer;
public FooModule() {
this(null);
}
public FooModule(@Nullable String fooServer) {
this.fooServer = fooServer;
}
@Override protected void configure() {
if (fooServer != null) {
bind(String.class).annotatedWith(named("fooServer")).toInstance(fooServer);
bind(FooService.class).to(RemoteFooService.class);
} else {
bind(FooService.class).to(InMemoryFooService.class);
}
}
}
条件逻辑本身并不算太糟糕。但是配置未经测试时会出现问题。在此示例中,InMemoryFooService
它用于开发并RemoteFooService
用于生产。但是,如果不对此特定情况进行测试,则无法确定它是否RemoteFooService
适用于集成应用程序。
要解决此问题,请尽量减少应用程序中不同配置的数量。如果将生产和开发拆分为不同的模块,则更容易确保测试整个生产代码路径。在这种情况下,我们分成FooModule
了RemoteFooModule
和InMemoryFooModule
。这也会阻止生产类对测试代码具有编译时依赖性。
保持Guice实例化类的构造函数尽可能隐藏。 #
考虑这个简单的界面:
public interface DataReader {
Data readData(DataSource dataSource);
}
使用公共类实现此接口是一种常见的反射:
public class DatabaseDataReader implements DataReader {
private final ConnectionManager connectionManager;
@Inject
public DatabaseDataReader(
ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}
@Override
public Data readData(DataSource dataSource) {
// ... read data from the database
return Data.of(readInData, someMetaData);
}
}
快速检查此代码可以发现此实现没有任何错误。不幸的是,这种检查排除了时间维度和无人看守的代码库随时间变得更紧密耦合的必然性。
类似于旧的公理,午夜之后没有任何好处发生,我们也知道在构造函数公开后没有任何好处发生:公共构造函数将在代码库中引入非法用法。这些用途必然会:
- 使重构更加困难。
- 打破接口实现抽象障碍。
- 在代码库中引入更紧密的耦合。
也许最糟糕的是,任何直接使用构造函数都会绕过Guice的对象实例化。
作为更正,只需限制实现类及其构造函数的可见性。通常,包私有是两者的首选,因为这有利于:
- 将类绑定
Module
在同一个包中 - 通过直接实例化对单元进行单元测试
作为一个简单的,记忆记得public
和@Inject
像精灵和矮人:他们可以一起工作,但在一个理想的世界,他们将独立并存。
避免注入可关闭的资源
如果Closable
通过依赖注入提供资源,则可能难以有效地管理Closable的生命周期:
class MyModule extends AbstractModule() {
@Provides
FileOutputStream provideFileStream() {
return new FileOutputStream("/tmp/outfile");
}
}
...
class Client {
private final FileOutputStream fos;
@Inject Client(FileOutputStream fos, OtherDependency other, ...) {
this.fos = fos;
}
void doSomething() throws IOException {
fos.write("hello!");
}
}
这种方法存在许多问题,因为它与资源管理有关:
-
如果构造了多个Client类,则会针对同一文件打开多个输出流,并且对该文件的写入可能会相互冲突。
-
目前尚不清楚哪个班级有责任关闭
FileOutputStream
资源:
- 如果资源是为唯一所有权创建的
Client
,那么客户端关闭它是有意义的。 - 但是,如果资源是作用域的(例如:您添加
@Singleton
到@Provides
方法中),那么突然没有Client
关闭资源的责任。如果Client
关闭流,则该流的所有其他用户将处理已关闭的资源。需要有一些其他“资源管理器”对象也可以获取该流,并且其关闭功能在正确的位置调用。这样做可能很棘手。
- 如果资源是为唯一所有权创建的
-
例如,如果其他依赖关系的构造
Client
失败(导致aProvisionException
),则FileOutputStream
可能已构造泄漏并且未正确关闭,即使Client
通常正确地关闭其资源。
首选的解决方案是不注入可关闭的资源,而是注入可以暴露必要时使用的短期可关闭资源的对象。以下示例使用Guava的CharSource作为资源管理器对象:
class MyModule extends AbstractModule() {
@Provides
CharSink provideCharSink() {
return Files.asCharSink(new File("/tmp/outfile"), StandardCharsets.UTF_8);
}
}
...
class Client {
private final CharSink sink;
@Inject Client(CharSink sink, OtherDependency other, ...) {
this.sink = sink;
}
void doSomething() throws IOException {
sink.write("hello!"); // Opens the file at this point, and closes once its done.
}
}
如果没有类似的非可关闭资源,您可以编写一个简单的包装器:
class ResourceManager {
@Inject ResourceManager(@Config String configs, ...) {}
/**
* Returns a new thing for you to use and dispose of
*/
OutputStream provideInstance() { return new...(); }
}
...
class Client {
private final ResourceManager resource;
@Inject Client(ResourceManager resource, OtherDependency other, ...) {
this.resource = resource;
}
void doSomething() {
try (OutputStream actualStream = resource.provideInstance()) {
// write to actualStream, closing with try-with-resources
}
}
}
此模式可以扩展到其他资源:与直接注入数据库连接句柄相反,注入连接池对象,这些对象要求您的对象在需要时请求这些连接对象并安全地关闭它们。
FAQ #
经常问的问题 #
如何注入配置参数? #
您需要一个绑定注释来标识您的参数。创建一个定义参数的注释类:
/**
* Annotates the URL of the foo server.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface FooServerAddress {}
将注释绑定到模块中的值:
public class FooModule extends AbstractModule {
private final String fooServerAddress;
/**
* @param fooServerAddress the URL of the foo server.
*/
public FooModule(String fooServerAddress) {
this.fooServerAddress = fooServerAddress;
}
@Override public void configure() {
bindConstant().annotatedWith(FooServerAddress.class).to(fooServerAddress);
...
}
}
最后,将它注入你的类:
public class FooClient {
@Inject
FooClient(@FooServerAddress String fooServerAddress) {
...
}
您可以使用Guice的内置@Named
绑定注释来保存一些击键,而不是创建自己的击键。
如何加载配置属性? #
使用Names.bindProperties()为配置文件中的每个属性创建绑定。
如何通过Guice创建对象时传递参数? #
您不能直接将参数传递给注入值。但是你可以使用Guice创建一个Factory
,并使用该工厂来创建你的对象。
public class Thing {
// note: no @Inject annotation here
private Thing(A a, B b) {
...
}
public static class Factory {
@Inject
public Factory(A a) { ... }
public Thing make(B b) { ... }
}
}
public class Example {
@Inject
public Example(Thing.Factory factory) { ... }
}
请参阅AssistedInject,它可用于删除工厂样板。
如何构建两个相似但略有不同的对象树? #
这通常被称为“机器人腿”问题:如何创建一个具有两个Leg
对象的机器人,左边一个注入一个LeftFoot
,右边一个注入一个RightFoot
。但只有一个Leg
类在两个上下文中都被重用。
有一个PrivateModules解决方案。它使用两个独立的私有模块,@Left
一个和@Right
一个。每个人都有对未注释的绑定Foot.class
和Leg.class
,并公开了注解绑定Leg.class
:
class LegModule extends PrivateModule {
private final Class<? extends Annotation> annotation;
LegModule(Class<? extends Annotation> annotation) {
this.annotation = annotation;
}
@Override protected void configure() {
bind(Leg.class).annotatedWith(annotation).to(Leg.class);
expose(Leg.class).annotatedWith(annotation);
bindFoot();
}
abstract void bindFoot();
}
public static void main(String[] args) {
Injector injector = Guice.createInjector(
new LegModule(Left.class) {
@Override void bindFoot() {
bind(Foot.class).toInstance(new Foot("leftie"));
}
},
new LegModule(Right.class) {
@Override void bindFoot() {
bind(Foot.class).toInstance(new Foot("righty"));
}
});
}
我怎样才能注入一个内部类? #
Guice不支持这一点。但是,您可以注入嵌套类(有时称为“静态内部类”):
class Outer {
static class Nested {
...
}
}
如何使用泛型类型注入类? #
您可能需要注入一个参数化类型的类,如List<String>
:
class Example {
@Inject
void setList(List<String> list) {
...
}
}
您可以使用TypeLiteral创建绑定。TypeLiteral
是一个特殊的类,允许您指定完整的参数化类型。
@Override public void configure() {
bind(new TypeLiteral<List<String>>() {}).toInstance(new ArrayList<String>());
}
或者,您可以使用@Provides方法。
@Provides List<String> providesListOfString() {
return new ArrayList<String>();
}
如何将可选参数注入构造函数? #
构造函数和@Provides
方法都不支持可选注入。要解决此问题,您可以创建一个包含可选值的内部类:
class Car {
private final Engine engine;
private final AirConditioner airConditioner;
@Inject
public Car(Engine engine, AirConditionerHolder airConditionerHolder) {
this.engine = engine;
this.airConditioner = airConditionerHolder.value;
}
static class AirConditionerHolder {
@Inject(optional=true) AirConditioner value = new NoOpAirconditioner();
}
}
这也允许可选参数的默认值。
如何注入方法拦截器? #
为了在AOP中注入依赖项MethodInterceptor
,请requestInjection()
与标准bindInterceptor()
调用一起使用。
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
MethodInterceptor interceptor = new WeekendBlocker();
requestInjection(interceptor);
bindInterceptor(any(), annotatedWith(NotOnWeekends.class), interceptor);
}
}
另一个选择是使用Binder.getProvider并在拦截器的构造函数中传递依赖项。
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
bindInterceptor(any(),
annotatedWith(NotOnWeekends.class),
new WeekendBlocker(getProvider(Calendar.class)));
}
}
我怎样才能回答其他问题? #
请发布到google-guice讨论组。
学习 Guice(一):第一个 Guice 应用
原文在:http://dyingbleed.com/guice-1/, http://dyingbleed.com/guice-2/, http://dyingbleed.com/guice-3/, 这儿照搬下来, 放在一块
Guice 是 Google 开发并开源的轻量级 DI (依赖注入)框架
GitHub 地址:https://github.com/google/guice
依赖 #
编辑 pom.xml 文件, 添加依赖:
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.1.0</version>
</dependency>
绑定 #
定义模块类, 继承 com.google.inject.AbstractModule
抽象类, 实现 configure
方法。在 configure 方法内部, 绑定(调用 bind 方法)接口到实现类(调用 to 方法)。例子:
public class ApplicationModule extends AbstractModule {
@override
protected void configure() {
bind(UserService.class).to(UserServiceImpl.class);
}
}
从名字可以看出, Impl 就是 Implement, bind(UserService.class).to(UserServiceImpl.class);
这句的意思就是把接口和实现绑定到一块。
注入 #
使用 👆 配置的 ApplicationModule 模块创建 Injector 注入器, 之后, 即可通过注入器获取实例:
Injector injector = Guice.createInjector(new ApplicationModule());
Application app = injector.getInstance(Application.class);
通过 @Inject
注解, 注入绑定到 UserService 接口的实现类 UserServiceImpl
@Inject
private UserService userService;
完整的例子:
public class Application {
@inject
private UserService userService;
public static void main(String[] args) {
Injector injector = Guice.createInjector(new ApplicationModule());
Application app = injector.getInstance(Application.class);
app.run;
}
public void run() {
// use userService do something
}
}
学习 Guice(二):Spark 依赖注入
绑定 #
class ApplicationModule(spark: SparkSession, date: LocalDate) extends AbstractModule {
override def configure(): Unit = {
bind(classOf[SparkSession]).toInstance(spark) // ①
bind(classOf[Source]).to(classOf[SourceImpl]) // ②
bind(classOf[Sink]).to(classOf[SinkImpl]) // ③
}
}
① 绑定 SparkSession 实例到 SparkSession 类。
② 绑定 SourceImpl 实现类到 Source 接口。
③ 绑定 SinkImpl 实现类到 Sink 接口。
> '②'.uniname # CIRCLED DIGIT TWO
> say "\c[CIRCLED DIGIT THREE]" # ③
定义接口与实现 #
接口定义举例:
trait Source {
def userDF: DataFrame
}
实现类举例:
class SourceImpl @Inject()(spark: SparkSession) extends Source { // 注入 SparkSession 实例
override def userDF: DataFrame = {
spark.table("dw.user")
}
}
应用入口 #
val injector = Guice.createInjector(new ApplicationModule(spark)) // ①
injector.getInstance(classOf[Application]).run() // ②
① 创建 Injector 实例 ② 运行
👇 是应用程序的骨架:
class Application @Inject() (spark: SparkSession) {
@Inject
var source: Source = _
@Inject
var sink: Sink = _
def run(): Unit = {
// 处理逻辑
}
}
学习 Guice(三):Spark 切面编程实践
定义注解 #
用于标注需要启用测量 Spark 指标的方法。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
pubic @interface EnableMeasure {}
定义方法拦截器 #
class MeasureInterceptor extends MethodInterceptor {
@Inject
private var spark: SparkSession = _
override def invoke(invocation: MethodInvocation): AnyRef = {
val listener = new MeasureSparkListener
spark.sparkContext.addSparkListener(listener)
val ret = invocation.proceed()
ret
}
}
绑定 #
在 Module 的 configure 方法中, 将拦截器与注解进行绑定:
val measureInterceptor = new MeasureInterceptor
requestInjection(measureInterceptor)
bindInterceptor(Matchers.any, Matchers.annotatedWith(classOf[EnableMeasure]), measureInterceptor ) // 绑定注解
使用 #
注入依赖
val injector = Guice.createInjector(module, new ApplicationModule) // ① 创建注入器
injector.getInstance(classOf[Application]).run() // ② 获取程序实例并运行
启用:
class Application {
@EnableMeasure
def run(): Unit = {
// TODO
}
}