【问题标题】:How is dependency injection helpful when we have multiple implementing classes?当我们有多个实现类时,依赖注入有什么帮助?
【发布时间】:2019-12-27 09:16:14
【问题描述】:

我一直在 Spring boot 中使用 @Autowired 进行依赖注入。从我读过的所有关于依赖注入的文章中,他们提到当我们(如果)决定将来更改实现类时,依赖注入非常有用。

例如,让我们处理一个Car 类和一个Wheel 接口。 Car 类需要实现Wheel 接口才能工作。所以,我们继续在这个场景中使用依赖注入

// Wheel interface
public interface Wheel{
   public int wheelCount();
   public void wheelName();
...
}


// Wheel interface implementation
public class MRF impements Wheel{
   @Override
   public int wheelCount(){
   ......
}...
}



// Car class
public class Car {

    @Autowired
    Wheel wheel;
}


现在在上面的场景中,ApplicationContext 会发现有一个 Wheel 接口的实现,从而将它绑定到 Car 类。将来,如果我们将实现更改为 XYZWheel 实现类并删除 MRF 实现,那么同样应该工作。

但是,如果我们决定在我们的应用程序中保留Wheel 接口的两个实现,那么我们需要在自动装配时特别提及我们感兴趣的依赖项。因此,更改将如下 -

// Wheel interface
public interface Wheel{
   public int wheelCount();
   public void wheelName();
...
}

@Qualifier("MRF")
// Wheel interface implementation
public class MRF impements Wheel{
   @Override
   public int wheelCount(){
   ......
}...
}

// Wheel interface implementation
@Qualifier("XYZWheel")
public class XYZWheel impements Wheel{
   @Override
   public int wheelCount(){
   ......
}...
}


// Car class
public class Car {

    @Autowired
    @Qualifier("XYZWheel")
    Wheel wheel;
}


所以,现在我必须手动定义我想要自动装配的具体实现。那么,依赖注入在这里有什么帮助呢?我可以很好地使用 new 运算符来实际实例化我需要的实现类,而不是依赖 Spring 为我自动装配它。

所以我的问题是,当我有多个实现类并因此需要手动指定我感兴趣的类型时,自动装配/依赖注入有什么好处?

【问题讨论】:

  • 你创造了界限。 Car 不关心更改。您可以以某种方式实现它,这样您就不必更改 Car 中的任何内容(在 XYZWheel 中使用 @Primary)。如果有任何机会,在新的XYZWheel 中出现问题,该错误包含在XYZWheel 的单元中。一些备注:你应该使用@Inject 而不是@Autowired --- 你应该更喜欢构造函数注入而不是字段注入。
  • @Turing85 using @Primary 只是将关注点转移到实现类,不是吗?我们现在不使用限定符,而是在实现类中指定我们选择的 bean。此外,如果XYZWheel 中出现问题,自动装配如何解决问题?如果我使用 XYZ bean,无论我使用 Autowired 还是使用 new 手动实例化它的 bean,我最终都会遇到错误
  • 这正是用例。如果您有多个实现,您通常希望在运行时选择实现。如果你可以在编译时选择,你甚至不需要接口。
  • 更准确地说:你不想主动选择。在大多数情况下,决定是通过在场(OP 描述的第一个场景)完成的:通过将一个(并且只有一个!)数据库驱动程序放在类路径上来选择数据库驱动程序。如果没有或有多个,我们很可能会看到来自 DI 容器的错误。

标签: java spring spring-boot dependency-injection autowired


【解决方案1】:

如果您有选择地使用 @Primary@Conditional 的限定符来设置您的 bean,则不必硬连线实现。

一个真实的例子适用于身份验证的实现。对于我们的应用程序,我们有一个集成到另一个系统的真实身份验证服务,以及一个模拟服务,用于我们想要在不依赖该系统的情况下进行本地测试。

这是 auth 的基本用户详细信息服务。我们没有为它指定任何限定符,即使它可能有两个 @Service 目标,Mock 和 Real。

@Autowired
BaseUserDetailsService userDetailsService;

这个基础服务是抽象的,并且具有在 mock 和 real auth 之间共享的所有方法的实现,以及两个与 mock 相关的默认抛出异常的方法,因此我们的 Real auth 服务不会被意外地用于模拟.

public abstract class BaseUserDetailsService implements UserDetailsService {
    public void mockUser(AuthorizedUserPrincipal authorizedUserPrincipal) {
        throw new AuthException("Default service cannot mock users!");
    }

    public UserDetails getMockedUser() {
        throw new AuthException("Default service cannot fetch mock users!");
    }

    //... other methods related to user details
}

从那里,我们有了扩展这个基类的真正的身份验证服务,并且是@Primary

@Service
@Primary
@ConditionalOnProperty(
        value="app.mockAuthenticationEnabled",
        havingValue = "false",
        matchIfMissing = true)
public class RealUserDetailsService extends BaseUserDetailsService {

}

这个类可能看起来很稀疏,因为它是。这个实现的基础服务最初是唯一的身份验证服务,我们对其进行了扩展以支持模拟身份验证,并有一个扩展类成为“真正的”身份验证。 真实身份验证是主要身份验证,除非启用模拟身份验证,否则始终启用。


我们还有模拟的身份验证服务,它有一些实际模拟的覆盖,以及一个警告:

@Slf4j
@Service
@ConditionalOnProperty(value = "app.mockAuthenticationEnabled")
public class MockUserDetailsService extends BaseUserDetailsService {

    private User mockedUser;

    @PostConstruct
    public void sendMessage() {
        log.warn("!!! Mock user authentication is enabled !!!");
    }

    @Override
    public void mockUser(AuthorizedUserPrincipal authorizedUserPrincipal) {
        log.warn("Mocked user is being created: " + authorizedUserPrincipal.toString());
        user = authorizedUserPrincipal;
    }

    @Override
    public UserDetails getMockedUser() {
        log.warn("Mocked user is being fetched from the system! ");
        return mockedUser;
    }
}

我们在专用于模拟的端点中使用这些方法,这也是有条件的:

@RestController
@RequestMapping("/api/mockUser")
@ConditionalOnProperty(value = "app.mockAuthenticationEnabled")
public class MockAuthController {
    //...
}

在我们的应用程序设置中,我们可以使用一个简单的属性来切换模拟身份验证。

app:
  mockAuthenticationEnabled: true

使用条件属性,我们不应该准备多个身份验证服务,但即使我们这样做了,我们也不会发生任何冲突。

  1. 出现严重错误:没有 Real,没有 Mock - 应用程序无法启动,没有 bean。
  2. mockAuthEnabled = true: no Real, Mock - 应用程序使用 Mock。
  3. mockAuthEnabled = false:真实,无 Mock - 应用程序使用真实。
  4. 出现严重错误:Real AND Mock both - 应用程序使用 Real bean。

【讨论】:

  • 感谢您的解释。我不知道使用 Base 类的引用自动装配扩展类。您使用了@Autowired BaseUserDetailsService userDetailsService,然后您有 2 个扩展 BaseUserDetailsS​​ervice 的类。有条件地将 2 个扩展类/bean 中的任何一个注入到上述自动装配的实例中。因此,如果我们要自动装配接口(而不是类),然后有条件地提供实现类的 bean,则此行为完全相同。我的理解正确吗?
  • 是的,其实它应该是一个抽象类,只是还没来得及重构它XD。但是你可以使用这个类,只要你不在基类上使用@Service注解。
【解决方案2】:

理解Dependency Injection (DI) 的最佳方式(我认为)是这样的:

DI 是一种允许您动态替换您的 @autowired 接口由您在运行时实现。这是 您的DI 框架(Spring、Guice 等)的角色来执行此操作 行动。

在您的Car 示例中,您创建了Wheel 的一个实例作为接口,但在执行期间,Spring 会创建您的实现实例,例如MRFXYZWheel。 回答你的问题:

我认为这取决于您要实现的逻辑。这不是 你的DI 框架的角色来选择你想要的Wheel 你的Car。不知何故,你将不得不定义你想要的接口 作为依赖注入。

请提供任何其他答案,因为DI 有时会引起混淆。提前致谢。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-04-13
    • 2023-03-14
    • 1970-01-01
    • 2018-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-08
    相关资源
    最近更新 更多