Guice vs Spring-依赖注入

2023/05/13

1. 概述

Google Guice和Spring是用于依赖注入的两个强大框架。 这两个框架都涵盖了依赖注入的所有概念,但它们都有自己的实现方式。

在本教程中,我们将讨论Guice和Spring框架在配置和实现方面的差异。

2. Maven依赖

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>5.0.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.13</version>
</dependency>

3. 依赖注入配置

依赖注入是一种编程技术,通过依赖注入使我们的类独立于它们的依赖关系。

在本节中,我们介绍Spring和Guice在配置依赖注入的方式上不同的几个核心特性。

3.1 Spring注入

Spring在一个特殊的配置类中声明了依赖注入配置,此类必须由@Configuration注解进行标注。 Spring容器使用这个类作为bean定义的来源。

Spring管理的类称为Spring bean

Spring使用@Autowired注解自动注入依赖bean。 @Autowired是Spring内置核心注解的一部分,我们可以在成员变量、setter方法和构造函数上使用@Autowired。

Spring还支持@Inject,@Inject是Java CDI(Contexts and Dependency Injection)的一部分,它定义了依赖注入的标准。

假设我们要自动将依赖bean注入到成员变量,可以简单地使用@Autowired对其进行标注:


@Component
public class UserService {

    @Autowired
    private AccountService accountService;
}

@Component
public class AccountServiceImpl implements AccountService {

}

其次,我们创建一个配置类,在加载应用程序上下文时用作bean定义的来源:


@Configuration
@ComponentScan("cn.tuyucheng.taketoday.di.spring")
public class SpringMainConfig {

}

请注意,我们使用@Component标注了UserService和AccountServiceImpl以将它们注册为bean。 SpringMainConfig类上的@ComponentScan注解告诉Spring在哪里扫描带注解的组件

尽管我们的@Component注解标注的是AccountServiceImpl,但Spring可以将其映射到AccountService,因为它实现了AccountService。

然后,我们需要定义一个应用程序上下文来访问bean。请注意,我们会在所有Spring单元测试中引用此上下文:


@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {SpringMainConfig.class})
class SpringUnitTest {

    @Autowired
    ApplicationContext context;
}

在运行时,我们可以从UserService bean中检索AccountService实例:

class SpringUnitTest {

    @Test
    void givenAccountServiceAutowiredToUserService_whenGetAccountServiceInvoked_thenReturnValueIsNotNull() {
        UserService userService = context.getBean(UserService.class);
        assertNotNull(userService.getAccountService());
    }
}

3.2 Guice绑定

Guice在一个称为Module的特殊类中管理其依赖关系,Module必须继承AbstractModule类并重写其configure()方法。

Guice中使用的绑定等同于Spring中的注入。 简单地说,绑定允许我们定义如何将依赖项注入到一个类中,Guice绑定在Module的configure()方法中声明

Guice使用@Inject注解来注入依赖项,而不是@Autowired

让我们编写一个与上述Spring等效的Guice示例:

public class GuiceUserService {

    @Inject
    private AccountService accountService;
}

其次,我们编写一个Module类,它是绑定定义的来源:

public class GuiceModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(AccountService.class).to(AccountServiceImpl.class);
    }
}

通常,如果在configure()方法中没有显式定义任何绑定,我们希望Guice从它们的默认构造函数中实例化每个依赖对象。 但是由于接口不能直接实例化,我们需要定义绑定来告诉Guice哪个接口将与哪个实现配对。

然后,我们需要使用GuiceModule定义一个Injector来获取类的实例。 请注意,我们所有的Guice测试都将使用这个Injector:

class GuiceUnitTest {

    private final Injector injector = Guice.createInjector(new GuiceModule());
}

最后,在运行时,我们检索一个具有非空accountService属性的GuiceUserService实例:

class GuiceUnitTest {

    @Test
    void givenAccountServiceAutowiredToUserService_whenGetAccountServiceInvoked_thenReturnValueIsNotNull() {
        GuiceUserService guiceUserService = injector.getInstance(GuiceUserService.class);
        assertNotNull(guiceUserService.getAccountService());
    }
}

3.3 Spring @Bean注解

Spring还提供了一个方法级别的@Bean注解来注册bean,作为其类级别注解(如@Component)的替代方案。 @Bean注解方法的返回值在容器中注册为bean。

假设我们有一个BookServiceImpl实例,我们希望它可以用于注入,就可以使用@Bean来注册我们的实例:

public class SpringMainConfig {

    @Bean
    public BookService bookServiceGenerator() {
        return new BookServiceImpl();
    }
}

现在我们可以得到一个BookService bean:

class SpringUnitTest {

    @Test
    void givenBookServiceIsRegisteredAsBeanInContext_WhenBookServiceIsRetrievedFromContext_ThenReturnValueIsNotNull() {
        BookService bookService = context.getBean(BookService.class);
        assertNotNull(bookService);
    }
}

3.4 Guice @Provides注解

作为Spring中的@Bean注解的等价物,Guice有一个内置的@Provides注解来实现同样功能。与@Bean一样,@Provides只能应用于方法。

现在让我们用Guice实现前面的Spring bean示例,我们需要做的就是将以下代码添加到我们的Module类中:

public class GuiceModule extends AbstractModule {

    @Provides
    public BookService bookServiceGenerator() {
        return new BookServiceImpl();
    }
}

现在,我们可以检索BookService的一个实例:

class GuiceUnitTest {

    @Test
    void givenBookServiceIsProvideByGuiceModuleWhenBookServiceIsRetrievedFromGuiceThenReturnValueIsNotNull() {
        BookService bookService = injector.getInstance(BookService.class);
        assertNotNull(bookService);
    }
}

3.5 Spring中的类路径组件扫描

Spring提供了一个@ComponentScan注解,通过扫描预定义的包来自动检测和实例化带注解的组件。

@ComponentScan注解告诉Spring将扫描哪些包以查找带注解的组件,它与@Configuration注解一起使用。

3.6 Guice中的类路径组件扫描

与Spring不同,Guice没有这样的组件扫描功能。 但实现它并不难,有一些像Governator这样的插件可以把这个特性引入到Guice中。

3.7 Spring中的对象识别

Spring通过名称识别对象。Spring将对象保存在一个大致类似于Map<String, Object>的结构中,这意味着我们不能有两个同名的对象。

由于具有多个同名bean而导致的bean冲突是Spring开发人员遇到的一个常见问题,例如,让我们考虑以下bean声明:


@Configuration
@Import({SpringBeansConfig.class})
@ComponentScan("cn.tuyucheng.taketoday.di.spring")
public class SpringMainConfig {

    @Bean
    public BookService bookServiceGenerator() {
        return new BookServiceImpl();
    }
}

@Configuration
public class SpringBeansConfig {

    @Bean
    public AudioBookService bookServiceGenerator() {
        return new AudioBookServiceImpl();
    }
}

我们已经在SpringMainConfig类中为BookService定义了一个bean,其名称为bookServiceGenerator()的方法名。

要演示出bean的冲突问题,我们需要声明具有相同名称的bean方法,但是我们不能在一个类中有两个同名的不同方法。 出于这个原因,我们在另一个配置类中声明了AudioBookService bean,它的bean名称也为bookServiceGenerator()的方法名。

现在,让我们在单元测试中获取这些bean:

BookService bookService = context.getBean(BookService.class);
assertNotNull(bookService); 
AudioBookService audioBookService = context.getBean(AudioBookService.class);
assertNotNull(audioBookService);

运行后,单元测试将失败:

org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'AudioBookService' available

首先,Spring在保存bean的map中注册了名为“bookServiceGenerator”的AudioBookService bean。 然后,由于HashMap数据结构的“不允许重复key”性质,它必须通过BookService的bean定义覆盖它。

最后,我们可以通过使bean方法名称唯一或将name属性设置为每个@Bean的唯一名称来解决这个问题

3.8 Guice中的对象识别

与Spring不同,Guice的结构大致为Map<Class<?>, Object>,这意味着在不使用额外元数据的情况下,我们就不能将多个绑定绑定到同一类型。

Guice提供了绑定注解来为同一类型定义多个绑定,让我们看看如果在Guice中为同一类型使用两个不同的绑定会发生什么。

public class Person {
}

现在,我们为Person类声明两个不同的绑定:

public class GuiceModule extends AbstractModule {

    @SneakyThrows
    @Override
    protected void configure() {
        bind(Person.class).toConstructor(Person.class.getConstructor());
        bind(Person.class).toProvider(Person::new);
    }
}

下面我们获取Person类的实例:

Person person = injector.getInstance(Person.class);
assertNotNull(person);

当我们运行该测试时,这将失败:

com.google.inject.CreationException: Unable to create injector, see the following errors:

1) [Guice/BindingAlreadySet]: Person was bound multiple times.

我们可以通过简单地去掉Person类的一个绑定来解决这个问题。

3.9 Spring中的可选依赖

可选依赖是自动装配或注入bean时不需要的依赖bean。

对于已经被@Autowired注解标注的字段,如果在上下文中没有找到匹配数据类型的bean,Spring会抛出NoSuchBeanDefinitionException。

但是,有时我们可能希望跳过某些依赖bean的自动装配并将它们保留为null,而不是引发异常

让我们看看下面的例子:


@Component
public class BookServiceImpl implements BookService {

    @Autowired
    private AuthorService authorService;
}
public class AuthorServiceImpl implements AuthorService {
}

从上面的代码中我们可以看到,AuthorServiceImpl类没有使用@Component注解标注,并且假设在配置类中也没有针对它声明@Bean方法。

现在,让我们运行以下测试,看看会发生什么:

class SpringUnitTest {

    @Test
    void givenBookServiceIsRegisteredAsBeanInContext_WhenBookServiceIsRetrievedFromContext_ThenReturnValueIsNotNull() {
        BookService bookService = context.getBean(BookService.class);
        assertNotNull(bookService);
    }
}

毫不奇怪,它将失败:

org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type 'AuthorService' available

我们可以通过使用Java 8的Optional类型使authorService依赖成为可选的,以避免此异常

public class BookServiceImpl implements BookService {

    @Autowired
    private Optional<AuthorService> authorService;
}

现在,我们的authorService依赖更像是一个容器,可能包含也可能不包含AuthorService类型的bean。 即使在我们的应用程序上下文中没有AuthorService的bean,我们的authorService字段仍然是非空的空容器。 因此,Spring不会抛出NoSuchBeanDefinitionException。

作为Optional的替代方案,我们可以通过将@Autowired注解的required属性设置为false(默认设置为true)来使依赖成为可选的。

因此,如果其数据类型的bean在上下文中不可用,Spring会跳过注入依赖,依赖项将保持设置为null:


@Component
public class BookServiceImpl implements BookService {

    @Autowired(required = false)
    private AuthorService authorService;
}

有时将依赖标记为可选可能是有用的,因为并非所有依赖总是必需的。

考虑到这一点,我们应该记住在开发过程中使用额外null检查,以避免由于null依赖导致的任何NullPointerException。

3.10 Guice中的可选依赖

就像Spring一样,Guice也可以使用Java 8的Optional类型来使依赖成为可选的。

假设我们要创建一个类并具有Foo依赖:

public class FooProcessor {
    @Inject
    private Foo foo;
}

现在,让我们为Foo类定义一个绑定:

public class GuiceModule extends AbstractModule {

    @SneakyThrows
    @Override
    protected void configure() {
        bind(Foo.class).toProvider(() -> null);
    }
}

现在我们在单元测试中获取FooProcessor的实例:

com.google.inject.ProvisionException:
null returned by binding at GuiceModule.configure(..)
but the 1st parameter of FooProcessor.[...] is not @Nullable

为了跳过这个异常,我们可以将Foo的类型声明为Optional:

public class FooProcessor {
    @Inject
    private Optional<Foo> foo;
}

@Inject没有将依赖标记为可选的required属性,在Guice中使依赖变为可选的另一种方法是使用@Nullable注解

Guice允许在使用@Nullable的情况下注入空值,如上面的异常消息中所述:

public class FooProcessor {
    @Inject
    @Nullable
    private Foo foo;
}

4. 依赖注入类型的实现

在本节中,我们通过几个示例来了解依赖注入类型并比较Spring和Guice提供的实现。

4.1 Spring中的构造注入

在基于构造函数的依赖注入中,我们在实例化时将所需的依赖传递给一个类

假设我们想要一个Spring组件,并且想要通过它的构造函数添加依赖,我们可以使用@Autowired标注该构造函数:


@Component
public class SpringPersonService {

    private PersonDao personDao;

    @Autowired
    public SpringPersonService(PersonDao personDao) {
        this.personDao = personDao;
    }
}

从Spring 4开始,如果类只有一个构造函数,则这种类型的注入不需要显示指定@Autowired。

class SpringUnitTest {

    @Test
    void givenSpringPersonServiceConstructorAnnotatedByAutowired_WhenSpringPersonServiceIsRetrievedFromContext_ThenInstanceWillBeCreatedFromTheConstructor() {
        SpringPersonService personService = context.getBean(SpringPersonService.class);
        assertNotNull(personService);
    }
}

4.2 Guice中的构造注入

我们可以重新编写之前的示例以在Guice中实现构造函数注入。 请注意,Guice使用@Inject而不是@Autowired。

public class GuicePersonService {

    private PersonDao personDao;

    @Inject
    public GuicePersonService(PersonDao personDao) {
        this.personDao = personDao;
    }
}

以下在测试中从Injector获取GuicePersonService类的实例:

class GuiceUnitTest {

    @Test
    void givenGuicePersonServiceConstructorAnnotatedByInject_WhenGuicePersonServiceIsRetrievedFromModule_ThenInstanceWillBeCreatedFromTheConstructor() {
        GuicePersonService guicePersonService = injector.getInstance(GuicePersonService.class);
        assertNotNull(guicePersonService);
    }
}

4.3 Spring中的Setter注入

在基于setter的依赖注入中,容器在调用构造函数实例化组件后,会调用类的setter方法

假设我们希望Spring使用setter方法自动注入依赖,我们可以使用@Autowired标注该setter方法:


@Component
public class SpringPersonService {

    private PersonDao personDao;

    @Autowired
    public void setPersonDao(PersonDao personDao) {
        this.personDao = personDao;
    }
}

每当我们需要SpringPersonService类的实例时,Spring将通过调用setPersonDao()方法自动注入personDao字段。

我们可以在测试中获取SpringPersonService bean并访问它的personDao字段,如下所示:

class SpringUnitTest {

    @Test
    void givenPersonDaoAutowiredToSpringPersonServiceBySetterInjection_WhenSpringPersonServiceRetrievedFromContext_ThenPersonDaoInitializedByTheSetter() {
        SpringPersonService personService = context.getBean(SpringPersonService.class);
        assertNotNull(personService);
        assertNotNull(personService.getPersonDao());
    }
}

4.4 Guice中的Setter注入

我们可以简单地更改前面小节中的例子,使用setter注入:

public class GuicePersonService {

    private PersonDao personDao;

    @Inject
    public void setPersonDao(PersonDao personDao) {
        this.personDao = personDao;
    }
}

每次我们从Injector获取GuicePersonService类的实例时,我们都会将personDao字段传递给上面的setter方法。

我们可以在测试中获取SpringPersonService实例并访问它的personDao字段,如下所示:

class GuiceUnitTest {

    @Test
    void givenGuicePersonServiceConstructorAnnotatedByInject_WhenGuicePersonServiceIsRetrievedFromModule_ThenInstanceWillBeCreatedFromTheConstructor() {
        GuicePersonService guicePersonService = injector.getInstance(GuicePersonService.class);
        assertNotNull(guicePersonService);
        assertNotNull(guicePersonService.getPersonDao());
    }
}

4.5 Spring和Guice中的字段注入

在基于字段依赖注入的情况下,我们通过使用@Autowired或@Inject标记字段来注入依赖

5. 总结

在本教程中,我们探讨了Guice和Spring框架在实现依赖注入方面的几个核心差异。 与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章