guice小结
概述
相信很多人都用过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 | final class MultilingualGreeter { |
在上面的代码中我们看到了MultilingualGreeter依赖于两个String类型的bean,那么我们如何区分这两个string呢?guice 给我们提供了注解的方式来解决这个问题:
1 | final class MultilingualGreeter { |
我们在使用的时候,通过下面的方式就可以获取到相应的对象了:
1 | Key<String> englishGreetingKey = Key.get(String.class, English.class); |
这个时候,当我们想要创建一个MultilingualGreeter对象的时候,就等价于下面这种方式来创建:
1 | String english = injector.getInstance(Key.get(String.class, English.class)); |
上面我们简单的演示了一下依赖注入的过程,不过这种方式太硬了,硬的让人没办法接受,谁会在工程实践中这么使用呢? 下面用一个例子来说明一下。我们知道依赖注入包含了field注入,setter注入、constructor注入,接下来我们先来看一下 guice给我们提供的这些个注入的方式:
field注入:
1 | public class A { |
相信用过spring的人马上就会想到@Autowired这个注解了,这样做本身并没有什么问题,不过 这样的话A对象的创建强依赖于框架了(只能通过guice框架完成对象A对象的创建了)。那么很自然我们 可能就会想到使用下面这种方式:
构造器注入
1 | public class A { |
上面这个样子多了一个构造函数,这样我们既可以通过框架来生成A对象,也可以绕开框架通过构造函数 来创建A对象。不过考虑一下我现在如果不用spring框架了,要用其他的IOC框架,是不是要把打开所有 的注解都替换一遍?WTF!?大家在Spring的开发中应该从来都没有想过这个问题吧。看一下guice给我们 提供了什么样的骚操作吧:
1 | public class A { |
上面我们可以看到又引入了两个新的概念:@Provides和Module,guice中使用Provider来代表一个用于生成特定对象的工厂类, 下面是Provider接口的定义:
1 | interface Provider<T> { |
我们可以通过实现该接口来创建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 | public interface Command { |
如上,我们希望按照CommandExecutor传入的参数commandName来决定究竟调用哪一个Commond, 按照上面的写法我们可以写成下面这种形式:
1 | public class CommandA implements Command { |
看样子功能是开发完了。现在需求有了变更,我们新增了一种commond,也想要加入进来,那么应该怎么办呢? 只可以修改MyModule#provideCommandExecutor方法了,这违反了开闭原则(对扩展开放,对修改关闭)。 看来我们要完成这种形式的装配还得另求途径了。索性guice给我们提供了一种比较好的方案MapBinder:
1 | public class ReusableModule implements Module { |
上面这里我们就可以在不改变原有代码功能之上,随意扩展自己的逻辑了!
multiBind
在上面的代码中我们看到了使用MapperBinder来存放map结构的注入对象,接下来我们可以看一下集合式的, 可以分为两种情况:
- 注入的对象是常量
1 | public void configure(Binder binder) { |
- 注入的对象是变量:
1 |
|
针对上面两种情况,当我们需要获取对象的时候,可以采用provider的方式来获取:
1 |
|
genericBind
讨论到这里,其实还有一种情况没有涉及,那就是范型,我们知道java在运行的时候会擦除范型, 那么当我们想要获取范型信息的时候该怎么获取呢?
1 | public class MyGenericType<T> { |
如上面的范型类,我们可以通过上面的方式获取到MyGenericType这种类型的对象,但是我们如何 获取范型上面的类型是什么呢?最常使用的方式就是创建一个匿名内部类来反应这个范型究竟是 那种类型,我们在使用的时候可以将该匿名类作为一个key来绑定到擦除范型之后的类型之上, guice给我们提供了TypeLiteral来实现这个功能,代码如下:
1 |
|
实例化对象
上面我们看到了guice中绑定依赖的方式,接下来我们来看一下,在绑定依赖之后,我们如果想要获取一个对象应该怎么办吧。
Guice的injector更多的像是一个hashMap,该hashMap是Map<Key>、Provider>>这种结构,或者我们在spring中常见的ApplicationContext,
我们想要获取某一个对象的话,可以直接通过injector.getInstance(class)的方式,代码如下:
1 | import com.google.inject.*; |
最后我们来看一下Scope的概念及用法吧:
在上面的代码中我们并没有看到Scope相关的代码,不过不要紧,这个本身也不难,我们就简单的分析一下吧。
我们知道在spring 的 web应用中,我们经常使用的就是单例Singleton,在最上面的代码中我们获取Person类型的对象,如果我们再次调用该方法,
尝试获取person对象的话,那么两个对象是否一致呢?答案是否定的!也就是说上面的代码中我们多次获取对象返回的是多个对象,那么有没有什么办法让我们
能够复用对象呢?答案就是Scope!这个和spring中的scope也是对应起来的,默认的情况下有5中scope
常见的指定scope的方式如下:
标注在实现类上面:
1
2
3
4
public class InMemoryTransactionLog implements TransactionLog {
/* everything here should be threadsafe! */
}通过bind代码实现
1 | bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class); |
- 通过注解@Provides声明方法
1 |
|
除了上面的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 | // 声明javabean |
小结
Guice的injector更多的像是一个hashMap,该hashMap是Map<Key>、Provider>>这种结构,或者我们在spring中常见的ApplicationContext。
在Module中配置接口和实现之间的关系,@Inject有点类似于@Autowired,不过功能更加强大,因为可以在构造函数上,也可以在字段上指定。
Injector则类似于spring中的ApplicationContext,也即是整个容器。还有一点值得注意,我们通常会将bean Module化,这样更方便管理。
另外,guice使用Key来表示一个可以被发现的依赖,上面Person类的构造函数中注入了两个依赖,
这些依赖在Guice中是被当作Key的类型,也就是String 本质上是Key