概述

相信很多人都用过spring,这也是大部分java从业人员最长使用到的框架,在之前的文章中我也有介绍spring相关的知识。spring的核心是IOC,那么 除了spring之外还有没有其他的IOC的框架呢?答案是有的!这就是我们今天要提的guice,一款由google开发的小众、轻量级的IOC框架。

详解

guice详解

在详细介绍guice之前,我们还是先来了解一下guice中的一些概念并由浅入深的介绍一下使用中的一些小技巧,最后会尝试使用guice和 jetty构建一个mini的web工程。

依赖注入

在guice中,是使用一个Map<Key、Provider>的结构来存放java bean的(在spring中是用Map<String, Class>来存放的), 因此guice使用key来代表一个可以用来完成注入的对象,如下:

1
Key<String> databaseKey = Key.get(String.class);

上面这种方式就可以用来获取一个String类型的bean,不过正常的应用中可能会包含多个类型相同的对象,如下:

1
2
3
4
5
6
7
8
9
final class MultilingualGreeter {
private String englishGreeting;
private String spanishGreeting;

MultilingualGreeter(String englishGreeting, String spanishGreeting) {
this.englishGreeting = englishGreeting;
this.spanishGreeting = spanishGreeting;
}
}

在上面的代码中我们看到了MultilingualGreeter依赖于两个String类型的bean,那么我们如何区分这两个string呢?guice 给我们提供了注解的方式来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
final class MultilingualGreeter {
private String englishGreeting;
private String spanishGreeting;

@Inject
MultilingualGreeter(
@English String englishGreeting, @Spanish String spanishGreeting) {
this.englishGreeting = englishGreeting;
this.spanishGreeting = spanishGreeting;
}
}

我们在使用的时候,通过下面的方式就可以获取到相应的对象了:

1
2
Key<String> englishGreetingKey = Key.get(String.class, English.class);
Key<String> spannishGreetingKey = Key.get(String.class, Spanish.class);

这个时候,当我们想要创建一个MultilingualGreeter对象的时候,就等价于下面这种方式来创建:

1
2
3
String english = injector.getInstance(Key.get(String.class, English.class));
String spanish = injector.getInstance(Key.get(String.class, Spanish.class));
MultilingualGreeter greeter = new MultilingualGreeter(english, spanish);

上面我们简单的演示了一下依赖注入的过程,不过这种方式太硬了,硬的让人没办法接受,谁会在工程实践中这么使用呢? 下面用一个例子来说明一下。我们知道依赖注入包含了field注入,setter注入、constructor注入,接下来我们先来看一下 guice给我们提供的这些个注入的方式:

field注入:

1
2
3
4
public class A {
@Inject
private B b;
}

相信用过spring的人马上就会想到@Autowired这个注解了,这样做本身并没有什么问题,不过 这样的话A对象的创建强依赖于框架了(只能通过guice框架完成对象A对象的创建了)。那么很自然我们 可能就会想到使用下面这种方式:

构造器注入

1
2
3
4
5
6
7
public class A {
private B b;
@Inject
public A(B b) {
this.b = b;
}
}

上面这个样子多了一个构造函数,这样我们既可以通过框架来生成A对象,也可以绕开框架通过构造函数 来创建A对象。不过考虑一下我现在如果不用spring框架了,要用其他的IOC框架,是不是要把打开所有 的注解都替换一遍?WTF!?大家在Spring的开发中应该从来都没有想过这个问题吧。看一下guice给我们 提供了什么样的骚操作吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
public class MyModule implements Module {
@Singleton
@Provides
A provideA(B b) {
return new A(b);
}
}

上面我们可以看到又引入了两个新的概念:@Provides和Module,guice中使用Provider来代表一个用于生成特定对象的工厂类, 下面是Provider接口的定义:

1
2
3
interface Provider<T> {
T get();
}

我们可以通过实现该接口来创建T类型的对象,不过更多的时候,我们看到的是会在Module中通过方法注解的方式来描述对象的创建, 这样做的好处是将创建对象的任务交给了guice的injector,这里modules的作用更多的是像spring中的beans标签或者springboot中的 @confituration注解,而@provider的作用则更多的像是bean标签。上面A对象的创建是不是会发现代码完全是clean的了, 对象的装配都整合在一个Module中了,之前的功能bean零注解了!而且仔细想一下通过Provider这种方式来创建对象也更自由了,我们可以 创建一个更复杂的bean。

这里我们还是简单的说一下module吧。Guice的Module可以用来指定接口和其对应的实现的关系, 这里我们通常可以重写configure方法,在其中使用bind方法来指定接口和实现。另外,我们也可以使用方法级别的@Provide来指定 创建一个对象的方法。

绑定关系

guice给我们提供了多种绑定接口和实现之间关系的方法,最常见的就是bind操作了,该操作的主要的目的是构建一个映射关系,如上面所说的那样, 我们一般是在module中构建这种关系,一个module有点类似于一个有向图,我们在创建对象的时候是采用深度遍历的策略去寻找依赖关系,而我们的map 则保存了图中每一个节点的信息。不过bind也包含了很多种情况,接下来我们就一点一点的来看一下:

guice语法 等价语法
bind(key).toInstance(value)(instance binding) map.put(key, () -> value)
bind(key).toProvider(provider)(provider binding) map.put(key, provider)
bind(key).to(anotherKey)(linked binding) map.put(key, map.get(anotherKey))
@Provides Foo provideFoo() {…}(provider method binding) map.put(Key.get(Foo.class), module::provideFoo)

simpleBind

我们最常见的就是这种类型的bind了,上面使用到的也是这种类型的bind操作。

  • linked bind:将类型和实现直接进行绑定,形式如bind(TransactionLogInterface.class).to(DatabaseTransactionLogImpl.class);
  • bind annotation:使用注解+Type来确定一个实现类,注解是可以使用我们自定义的,也可以使用guice给我们提供的@Name,具体用法bind(CreditCardProcessor.class) .annotatedWith(注解.class).to(PayPalCreditCardProcessor.class);bind(CreditCardProcessor.class).annotatedWith(Names.named("名称")) .to(CheckoutCreditCardProcessor.class);
  • instance bind:可以绑定中类型和该类型的一个实例,具体用法为bind(key).toInstance(value),再加上bind annotation,我们可以更灵活一点:bind(CreditCardProcessor.class).annotatedWith(Names.named("名称")).toInstance('instance')
  • @Provides Methods: 这种方式我们在前面也已经见到过了,也是声明关系的一种方式,个人觉得这种是最灵活的实现方式
  • Provider:同@Provides类似,当使用@Provides实现过于复杂的时候,我们可以实现Provider接口的方式来完成这种绑定的机制,不过此时要使用bind(key).toProvider(provider)来绑定
  • Untargeted Bindings:有时候对于一个具体的实现我们也希望将其注入到容器,这种情况下并没有接口可以绑定,guice给我们提供了方便的实现:bind(MyConcreteClass.class); bind(AnotherConcreteClass.class).in(Singleton.class);,当然我们也可以用上面那种最简单的bind接口到实现的方式,不过这里的接口和实现是同一个类型而已。
  • Constructor Bindings:这里的Construct bind并不是前面的构造器注入,而是当我们引用了第三方的库,第三方库的某个类的构造函数没办法使用@Inject来将其注入到容器中,这种情况下我们要 将其注入进来应该怎么办呢?就要使用这种方式了:bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));,另外 多说一句,我们要将mybatis整合进来的话就可以使用这种方式。

mapBind

上面我们看到的是实现类(被依赖的类)只有一个的情况,那么当一个被依赖的接口有多个实现的时候, 我们想要按需来装配一个对象的时候应该怎么搞呢?

1
2
3
4
5
6
public interface Command {
String exec();
}
public interface CommandExecutor {
String exec(String commandName);
}

如上,我们希望按照CommandExecutor传入的参数commandName来决定究竟调用哪一个Commond, 按照上面的写法我们可以写成下面这种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CommandA implements Command {
public String exec() {
return "a_result";
}
}
public class CommandB implements Command {
public String exec() {
return "b_result";
}
}
public class MyModule implements Module {
@Provides
@Singleton
CommandExecutor provideCommandExecutor(
CommandA ca,
CommandB cb) {
Map<String, Command> commands = new HashMap<>();
commands.put("a", ca);
commands.put("b", cb);

return commandName -> commands.get(commandName).exec();
}
}

看样子功能是开发完了。现在需求有了变更,我们新增了一种commond,也想要加入进来,那么应该怎么办呢? 只可以修改MyModule#provideCommandExecutor方法了,这违反了开闭原则(对扩展开放,对修改关闭)。 看来我们要完成这种形式的装配还得另求途径了。索性guice给我们提供了一种比较好的方案MapBinder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ReusableModule implements Module {
public static MapBinder<String, Command> contributeCommands(
Binder binder) {
return MapBinder.newMapBinder(
binder, String.class, Command.class);
}
@Override
public void configure(Binder binder) {
contributeCommands(binder);
}
@Provides
@Singleton
CommandExecutor provideCommandExecutor(
Map<String, Command> commandMap) {
return name -> commandMap.get(name).exec();
}
}
public static class AppModule1 implements Module {
@Override
public void configure(Binder binder) {
ReusableModule.contributeCommands(binder)
.addBinding("a")
.to(CommandA.class);
}
}
public static class AppModule2 implements Module {
@Override
public void configure(Binder binder) {
ReusableModule.contributeCommands(binder)
.addBinding("b")
.to(CommandB.class);
}
}

上面这里我们就可以在不改变原有代码功能之上,随意扩展自己的逻辑了!

multiBind

在上面的代码中我们看到了使用MapperBinder来存放map结构的注入对象,接下来我们可以看一下集合式的, 可以分为两种情况:

  • 注入的对象是常量
1
2
3
4
5
public void configure(Binder binder) {
Multibinder.newSetBinder(binder, String.class)
.addBinding()
.toInstance("value");
}
  • 注入的对象是变量:
1
2
3
4
5
6
@Override
public void configure(Binder binder) {
Multibinder.newSetBinder(binder, MyType.class)
.addBinding()
.to(MySubType.class);
}

针对上面两种情况,当我们需要获取对象的时候,可以采用provider的方式来获取:

1
2
3
4
5
@Provides
@Singleton
MySubType provideMySubtype() {
return new MySubType("provided_value");
}

genericBind

讨论到这里,其实还有一种情况没有涉及,那就是范型,我们知道java在运行的时候会擦除范型, 那么当我们想要获取范型信息的时候该怎么获取呢?

1
2
3
4
5
6
7
8
9
public class MyGenericType<T> {
private T value;
public MyGenericType(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}

如上面的范型类,我们可以通过上面的方式获取到MyGenericType这种类型的对象,但是我们如何 获取范型上面的类型是什么呢?最常使用的方式就是创建一个匿名内部类来反应这个范型究竟是 那种类型,我们在使用的时候可以将该匿名类作为一个key来绑定到擦除范型之后的类型之上, guice给我们提供了TypeLiteral来实现这个功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void configure(Binder binder) {
TypeLiteral<MyGenericType<String>> stringType =
new TypeLiteral<MyGenericType<String>>() {};
Multibinder.newSetBinder(binder, MyGenericType.class)
.addBinding()
.to(stringType);
TypeLiteral<MyGenericType<Integer>> intType =
new TypeLiteral<MyGenericType<Integer>>() {};
Multibinder.newSetBinder(binder, MyGenericType.class)
.addBinding()
.to(intType);
}
@Singleton
@Provides
MyGenericType<Integer> provideIntType() {
return new MyGenericType<>(5);
}
@Singleton
@Provides
MyGenericType<String> provideStringType() {
return new MyGenericType<>("string");
}

实例化对象

上面我们看到了guice中绑定依赖的方式,接下来我们来看一下,在绑定依赖之后,我们如果想要获取一个对象应该怎么办吧。 Guice的injector更多的像是一个hashMap,该hashMap是Map<Key、Provider>这种结构,或者我们在spring中常见的ApplicationContext, 我们想要获取某一个对象的话,可以直接通过injector.getInstance(class)的方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.google.inject.*;

public class DemoModule extends AbstractModule {

@Override
protected void configure() {
// 在重写的方法中指定绑定关系,这里是指定一个字符串bean
bind(Key.get(String.class)).toInstance("wang er shuai");
}

@Provides
public Integer age() {
// @provide有点类似于spring中的@Bean,用于在方法上注解来获取一个java bean,这里的bean是一个简单的数字9
return 9;
}

/**
* 业务bean
*/
static class Person {

private String name;

private Integer age;

@Inject
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}

public void SayHello() {
System.out.println("hello " + name + ", your age is " + age);
}
}

public static void main(String[] args) {

// 这一步用于获取Module中的映射关系,
Injector injector = Guice.createInjector(new DemoModule());
// 通过Guice的DI生成我们所需要的对象,并执行方法进行测试
injector.getInstance(Person.class).SayHello();
}
}

最后我们来看一下Scope的概念及用法吧: 在上面的代码中我们并没有看到Scope相关的代码,不过不要紧,这个本身也不难,我们就简单的分析一下吧。 我们知道在spring 的 web应用中,我们经常使用的就是单例Singleton,在最上面的代码中我们获取Person类型的对象,如果我们再次调用该方法, 尝试获取person对象的话,那么两个对象是否一致呢?答案是否定的!也就是说上面的代码中我们多次获取对象返回的是多个对象,那么有没有什么办法让我们 能够复用对象呢?答案就是Scope!这个和spring中的scope也是对应起来的,默认的情况下有5中scope

常见的指定scope的方式如下:

  • 标注在实现类上面:

    1
    2
    3
    4
    @Singleton
    public class InMemoryTransactionLog implements TransactionLog {
    /* everything here should be threadsafe! */
    }
  • 通过bind代码实现

1
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
  • 通过注解@Provides声明方法
1
2
3
4
@Provides @Singleton
TransactionLog provideTransactionLog() {
...
}

除了上面的Scope之外还有lazy或者eager的策略供我们使用:

1
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();

和Spring对比

spring声明配置信息的时候,是在一个被@Configuration注解的类中,spring容器将这个类当作一个资源池。被@Configuration注解的类 相较于基于xml文件的的spring的话,更多的像是一个beans标签,注意并不是bean标签!在装备具备依赖关系的对象的时候,spring同样也提供了 @Autowired标签,该标签支持构造器注入、setter方法注入、field注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明javabean
@Component
public class UserService {
// 使用注解装配依赖关系
    @Autowired
    private AccountService accountService;
}

@Component
public class AccountServiceImpl implements AccountService {
}

// 将指定包路径下的被@Component注解了的类扫描到容器中
@Configuration
@ComponentScan("com.baeldung.di.spring")
public class SpringMainConfig {
}

// 获取context并从容器中获取响应的对象
ApplicationContext context = new AnnotationConfigApplicationContext(SpringMainConfig.class);
UserService userService = context.getBean(UserService.class);
assertNotNull(userService.getAccountService());

小结

Guice的injector更多的像是一个hashMap,该hashMap是Map<Key、Provider>这种结构,或者我们在spring中常见的ApplicationContext。 在Module中配置接口和实现之间的关系,@Inject有点类似于@Autowired,不过功能更加强大,因为可以在构造函数上,也可以在字段上指定。 Injector则类似于spring中的ApplicationContext,也即是整个容器。还有一点值得注意,我们通常会将bean Module化,这样更方便管理。 另外,guice使用Key来表示一个可以被发现的依赖,上面Person类的构造函数中注入了两个依赖, 这些依赖在Guice中是被当作Key的类型,也就是String 本质上是Key,Integer本质上是Key。 至于guice和spring的对比,详细建议参考这篇文章: spring和guice对比guice stories 另外,我自己也基于jetty和guice完成了一款迷你的web框架,具体参考:guice-with-jetty