Spring Boot「50」扩展:Google Guice,另一个 DI 框架

562 阅读10分钟

我正在参加「掘金·启航计划」

Google Guice(发音同“juice”,果汁)是 Google 公司开源的一个 DI(Dependency Injection,依赖注入)框架。 DI 是一种设计模式或编程范式,它鼓励用户尽量地去声明它的依赖,然后通过某种方式(构造器、函数等)注入,而不是自己去创建依赖对象。 这种设计模式使得类的行为(或功能)与依赖的解析、处理分离,也使得应用职责更单一,封闭性更好。 Guice 官方文档中提供了一个示例,来说明使用 DI 模式与不使用 DI 的差异。

// 未使用 DI 模式
class Foo {
  private Database database;  // We need a Database to do some work
  Foo() {
    // 需要显式地创建依赖
    this.database = new Database("/path/to/my/data");
  }
}
// 使用 DI 模式
class Foo {
  private Database database;  // We need a Database to do some work
  // 不再需要手动创建,而由框架或其他模块传入
  /**
  * 这里即所谓的依赖注入,将显示创建依赖的逻辑从当前类中分离出去
  * 让当前类更关注于自身逻辑的实现,而不必过多关心依赖对象的创建
  * 更够达到降低耦合度的目的
  */
  Foo(Database database) {
    this.database = database;
  }
}

通过前面的介绍,我们可以了解到,DI 模式的核心就是将类的功能或行为与依赖对象的解析、创建等逻辑分离开来。 当提及 DI 时,经常会听到如下的概念,它们所描述的设计思想,与 DI 是相同的:

  • Inversion of control,控制反转。
  • Hollywood principle,好莱坞原则。
  • Injection,注入。

01-理解 Guice 中的核心概念

从某种程度上,可以把 Guice 理解为或认为是一个 Map(映射表),key -> value。 实际上的 Guice 实现要更为复杂。

  • Guice key,映射表中的键;一般由两部分组成,一个类型(必须的)和一个绑定注解(binding annotation,作用是确定依赖,可选的)。 key 一般通过如下接口获得:
Key.get(String.class, English.class);
// 或当不需要 binding annotation 就能确定时
Key.get(String.class);  // map 中不存在同类型的对象,例如两个 String 类对象
  • Provider,映射表中的值;它仅有一个方法 Provider#get 方法,能够返回特定类型的对象实例。 一般来说,应用并不需要直接实现 Provider 接口。 通过继承 AbstractModule,Guice 能够根据配置自动创建相应的 Provider。 例如:
class DemoModule extends AbstractModule {
  @Provides
  static Integer provideCount() {
    return 3;
  }

  @Provides
  static String provideMessage() {
    return "hello world";
  }
}

Guice 根据上述配置,会自动创建 Provider<Integer>Provider<String>。 其中,前者会在调用 Provider#get 时返回 3,后者返回 "hello world"。

有了前面对 Guice 的基本理解,接下来我们就能讨论如何使用 Guice 进行依赖注入了。 Guice 中的核心接口,或者说应用与 Guice 交互的核心接口是 Injector。 Injector 的创建一般通过如下的方式:

 Injector injector = Guice.createInjector(new SomeModule());

应用中各个类之间的依赖关系,形成了一个有向图。 这些依赖关系描述在配置文件中,即继承了 AbstractModule 的类中。 Injector 了解这个依赖关系有向图中的所有信息。 当你向 Injector 请求某个实例,例如:

injector.getInstance(String.class);

或者要求 Injector 为某个对象注入它需要的依赖时,例如:

injector.injectMembers(someObj);

Injector 会以深度优先的方式遍历有向图,尝试为它注入所有的依赖项。

01.1-Binding

正如前面介绍的,Guice 中的配置是通过 Module 来完成的。 Module 是 Guice 中的配置单元,定义了要加入到 map 中的 key -> value 关系(在 Guice 中称为 binding)。 Guice 支持两种方式来定义、或声明 binding:

  1. @Provides 等注解的方式,在 *Module 中的方法上使用。
  2. 使用 Guice DSL,在 *Module#configure 方法中,通过 bind(..).to(..) 形式来定义。
Guice DSLMental Model
bind(key).toInstance(value)map.put(key, () -> value)
bind(key).toProvider(provider)map.put(key, provider)
bind(key).to(anotherKey)map.put(key, map.get(anotherKey))
@Provides Foo provideFoo() {...}map.put(Key.get(Foo.class), module::provideFoo)

前面提到的 DemoModule 中就定义了两个 binding:

  • bind(Key.get(Integer.class)).toInstance(3)
  • bind(Key.get(String.class)).toInstance("hello world")

Guice 中的 binding 分为多种类型:

  • Linked binding,将某个类型映射到它的实现上,例如 bind(BillingService.class).to(BillingServiceImpl.class)。 或者,使用 @Provides 方式:

    class DemoModule extends AbstractModule {
        // 等价与上面的 bind(BillingService.class).to(BillingServiceImpl.class)
        @Provides BillingService billingService(BillingServiceImple impl) {
            return impl;
        }
    }
    
  • Instance Binding,将某个类型映射到它的某个具体实例上,例如bind(String.class).toInstance("hello world")

  • Provider Binding,将某个类型映射到能够提供该类型实例的 Provider 上,例如 bind(String.class).toProvider(() -> "hello world")

  • Constructor Binding,将某个类型映射到某个 Constructor 上,主要用在第三方库的类或者类中有多个 Constructor 时,用来指定使用某个特定的 Constructor。 例如:

    bind(TransactionLog.class).toConstructor(
            DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
    
  • Built-in Binding,Guice 内置了一些开箱即用的 binding,应用可以直接使用。 如何理解这些内置 binding 呢? 借助之前的 map 类比,这些内置的 binding 就是 Guice 初始化 map 时会添加进来的 kv 对。 这些内置 binding 中常用的一些包括 Logger、Injector、已知类型的 Provider 等;

  • Just-in-time Binding,在 *Module 中定义的的 binding 被称为是 explicit bindings; 如果 Injector 中不存在某个类型的 explicit binding,而应用又请求这个类型的实例时,Guice 会尝试为它创建 jit binding 或 implicit binding。 Guice 创建 implicit binding 时,需要用到目标类型的 injectable constructor。 injectable constructor 指:

    1. @Inject 标注的构造器
    2. 或无参构造器,且是非私有类的非私有构造器、私有类的私有构造器(可用,但不推荐)

    not injectable constructor 指:

    1. 有一个或多个参数,且没有 @Inject 注解
    2. @Inject 标注的构造器有多个
    3. 非静态嵌套类中定义的构造器。原因是内部类对外部类对象有隐含的依赖,Guice 无法完成注入。

    injectable constructor 举例:

    public final class Foo {
        // @Inject 标注的构造器
        @Inject
        Foo(Bar bar) { }
    }
    public final class Bar {
        // 非私有类的非私有构造器
        Bar() {}
        private static class Baz {
            // 私有类的私有构造器,也可用,但不推荐
            private Baz() {}
        }
    }
    

    not injectable constructor 举例:

    public final class Foo {
        // 构造器包含一个或多个参数,且没有 @Inject 注解
        Foo(Bar bar) {  }
    }
    public final class Bar {
        // 非私有类中的私有构造器,not injectable 
        private Bar() {}
    
        class Baz {
            // 非静态内部嵌套类的构造器
            Baz() {}
        }
    }
    

01.2-Injection

Injection(注入,或依赖注入)指把依赖设置到对象中的过程(例如通过构造器、setter 方法等)。 注入有如下几种类型:

  1. Constructor injection,在构造器上注解 @Inject,Guice 会自动注入所需依赖。
  2. Method injection,Guice 也支持在类方法上注解 @Inject
  3. Field injection,最简洁的方式,直接注解在某个 field 上。

Injection point(注入点)指 Guice 支持的、会自动注入依赖的位置,它包括:

  • injectable constructor
  • @Provides 注解的方法
  • @Inject 注解的类方法
  • @Inject 注解的 filed

Guice 进行依赖注入的实例:

class Foo {
  private Database database;

  /*** database = map.get(Key.get(Database.class)).get() */
  @Inject
  Foo(Database database) {  // We need a database, from somewhere
    this.database = database;
  }
}

// 或 Module 中
@Provides
Database provideDatabase(
    /*** databasePath = map.get(Key.get(String.class, DatabasePath.class)).get()*/
    @DatabasePath String databasePath) {
  return new Database(databasePath);
}

在前面,我有提到 Guice 会为所有已知类型创建对应的 Provider。 所以,在注入的时候,可以选择注入特定类型的实例,也可以选择注入该类型的 Provider。 例如,上述 @Inject Foo(Database database){..} 也可以换成另一种方式 @Inejct Foo(Provider<Database> pDatabase) { this.database = pDatabase.get(); }

02-Guice 对 AOP 的支持

在之前的一篇文章里 Spring Boot「49」扩展:Spring AOP,我介绍了 Spring 中对 AOP 的支持。 Guice 同样支持 AOP。 简单来说,Guice 中实现 AOP 的方式是通过方法拦截。 Guice 中与 AOP 相关的接口包括 com.google.inject.matcher.Matcherorg.aopalliance.intercept.MethodInterceptor。 其中,前者 Matcher 一般需要两个,一个用来去定哪些类需要拦截,另一个用来确定这些类中的哪些方法需要拦截。 后者 MethodInterceptor 来自于 aopalliance 定义的 API,它会在拦截的方法比调用时调用,换句话说,它就是 advice(对这个概念比较陌生的同学,可以翻阅我之前的文章进行了解)。

Guice 官网提供了一个示例,在一个披萨订购系统中,使用 AOP 实现周末禁止下单的功能。 接下来,我将与大家一块学习下。 首先,披萨订单系统中,有一个相关的订单服务。

/**
 * Pizza 订购系统
 */
public class PizzaBillingService {

    public void chargeOrder() {
        System.out.println("charge order...");
    }
}

它只有一个方法,就是下订单。 如果有一个需求,周末披萨店休息,不营业,我们就要修改订单系统,在休息时不接受订单。 根据这个需求,先定义一个注解,表示周末不允许的行为:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NotOnWeekends {
}

之后,在 chargeOrder 上增加注解 @NotOnWeekends

然后,定义一个拦截器:

/**
 * 周末不支持下单
 */
public class WeekendBlocker implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        final Calendar today = new GregorianCalendar();
        final String displayName = today.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.ENGLISH);
        System.out.println(displayName);
        // 如果是周末
        if (displayName.startsWith("S")) {
            throw new IllegalStateException(invocation.getMethod().getName() + " not allowed on weekends!");
        }
        return invocation.proceed();
    }
}

准备就绪后,通过 NotOnWeekendsModule 将这些组装起来。

public class NotOnWeekendsModule extends AbstractModule {
    @Override
    protected void configure() {
        // any() 表示任何类都要进行拦截
        // annotatedWith(NotOnWeekends.class) 表示带有 @NotOnWeekends 的方法需要拦截
        // 表示要执行 new WeekendBlocker() 中的逻辑
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class), new WeekendBlocker());
    }
}

最后,编写测试程序测试下:

public class PizzaBillingApplication {
    public static void main(String[] args) {
        final Injector injector = Guice.createInjector(new NotOnWeekendsModule());
        final PizzaBillingService service = injector.getInstance(PizzaBillingService.class);

        service.chargeOrder();
    }
}

周末的时候运行上述程序,会提示:

Exception in thread "main" java.lang.IllegalStateException: chargeOrder not allowed on weekends!
	at self.samson.example.guice.pizza.WeekendBlocker.invoke(WeekendBlocker.java:21)
	at self.samson.example.guice.pizza.PizzaBillingApplication.main(PizzaBillingApplication.java

非周末时,则能正常运行。

03-Guice 与 Spring IoC 的对比

Google Guice 和 Spring IoC 都提供了 DI 实现。 baeldung.com 中有一篇文章对比了两者在概念、使用方法的不同。 感兴趣的读者可以自行去了解下,限于篇幅问题,这里不再展开介绍。

另外,关于多个 DI 框架的对比,在 stackoverflow.com 上有一个高质量的帖子,感兴趣的也可以翻阅下。 Why use/develop Guice, when You have Spring and Dagger?

这里,我大致总结下这个回答中的内容。 首先,这篇帖子是关于 Dagger、Guice、Spring 三个 DI 框架的对比。 作者列出了这三个框架出现的时间线:

  • 2004年,Spring 1.0 发布
  • 2007年,Guice 发布
  • 2009年,JSR-330 发布,定义了 javax.inject.*
  • 2013年,Dagger 1 发布(2016年废弃)
  • 2015年,Dagger 2 发布

Spring、Guice、Dagger 三个框架,就 DI 实现上,存在如下的差异:

  1. Spring 是一个相对重量级的框架。如果你的应用采用 Spring 开发,那么使用它提供的 DI 功能不需要做额外的工作。
  2. Guice 是一个相对轻量的框架。它集成的功能比 Spring 要少。
  3. Dagger 是三者中最轻量的。它与前两者最大的不同是发生在编译时,所以运行时性能比较好,更适合于手机端(android)这种移动设备。
  4. 三者都支持 JSR-330。

另外再补充一个 Guice 官网提供的对 "how does Guice compare to Spring?" 问题的回答。

Guice is anything but a rehash of Spring.

Guice 出现在 Java 5 推出之后,所以能够更充分的利用 Java 提供的注解、泛型等新特性(Spring vs. Guice)。

04-总结

在今天的文章中,我介绍了 Google 开源的 Guice 框架。 它是一个类似于 Spring IoC 的框架,提供了基于依赖注入的编程范式实现。 到目前为止,我介绍了 Guice 中的核心概念,以及使用方式。 然后,我又介绍了 Guice 对 AOP 的支持,并通过一个披萨订购服务演示了如何实现的切面增强。 最后,给出了多个 DI 实现框架的对比及一些参考资料。

希望今天的内容能对你有所帮助。如果认为对你有用,请点赞、收藏支持,谢谢!

我正在参加「掘金·启航计划」