英文:
Spring Dependency Injection - Private fields - Anti Pattern? Why does it even work?
问题
我一般是一个C#开发者,但现在在使用Java,我注意到很多使用Spring进行依赖注入的代码会将注入值直接注入到私有属性中,没有公开的方法来设置这个值。我对这实际上能够正常工作感到惊讶,不过我猜这可能是通过反射实现的?
这肯定是糟糕的做法,对吗?我无法理解如何进行单元测试或检查类时,任何人都能知道需要从某个外部框架设置私有成员。
在单元测试时,你如何设置属性?或者仅在使用该类时呢?
我猜在单元测试中必须使用Spring,这似乎太过繁琐。你应该能够在没有IOC容器的情况下进行单元测试,这个类完全依赖于Spring...
我有遗漏什么吗?
依赖注入难道不总是需要某种公开的设置方法,最好是尽可能使用构造函数吗?或者我在Java方面有所遗漏...?
谢谢
英文:
I am generally a c# developer but working on Java now and then I see a lot of dependency injection using Spring on private properties, with no public way of setting the value. I was surprised this actually works, but I guess it’s possible via reflection?
Surely this is terrible practice?! I can't see how anyone unit testing or inspecting the class would possibly know that a private member needs to be set from some external framework.
How would you even set the property when you are unit testing? Or just using the class stand alone?
I guess you have to use spring in your unit tests which seems really overkill. Surely you should be able to unit test without your IOC container? The class becomes completely dependent on spring...
Have I missed anything here?
Should dependency injection not always involve a public setter of some kind, and preferably use the constructor if possible? Or is there something about Java I am missing...?
Thanks
答案1
得分: 2
你可以始终模拟注入的Bean,即使你有私有字段。你应该查看Spring文档中的 @MockBean
。基本上,你可以这样做:
@ExtendWith({SpringExtension.class})
class MyServiceTest{
@MockBean
private RepositoryInterface repository;
@Autowired
private MyService service;
}
假设 RepositoryInterface
是一个接口(而不是具体类),它被注入到 MyService
中。发生的情况是,如果你从Spring Initialzr创建了你的pom.xml
,那么JUnit5的SpringExtension
应该已经在你的依赖中,它将使用另一个称为Mockito的框架为该接口构建一个模拟。然后,Spring IoC 将在服务中注入创建的模拟对象。这适用于字段注入:
@Service
public class MyService{
@Autowired
private RepositoryInterface repositoryInterface
}
setter注入:
@Service
public class MyService{
private RepositoryInterface repositoryInterface
@Autowired
public void setRepository(RepositoryInterface repositoryInterface){
this.repositoryInterface = repositoryInterface;
}
}
或构造函数注入:
@Service
public class MyService{
private RepositoryInterface repositoryInterface
public MyService(RepositoryInterface repositoryInterface){
this.repositoryInterface = repositoryInterface;
}
}
基本上,最后一种方法是推荐的,因为这种方式可以使服务的依赖关系更加明确,这更符合代码风格。不推荐使用字段注入,因为它隐藏了类的依赖关系。因此,使用构造函数注入构建测试的推荐方式如下:
@ExtendWith({SpringExtension.class})
class MyServiceTest{
@MockBean
private RepositoryInterface repository;
private MyService service;
@BeforeEach
void setup(){
service = new MyService(repository);
}
}
希望这有助于你的理解。
英文:
You can always mock injected beans even if you have private fields. You should have a look on @MockBean
from Spring documentation. Essentially, you could do the following:
@ExtendWith({SpringExtension.class})
class MyServiceTest{
@MockBean
private RepositoryInterface repository;
@Autowired
private MyService service;
}
Supposing that RepositoryInterface
is an interface (and not a concrete class) that is injected in MyService
. What happens is that the SpringExtension
for JUnit5, which should be already in your dependencies if you created your pom.xml from Spring Initialzr, will build a mock for that interface using another framework that is called Mockito (maybe have a look to it). Then Spring IoC will inject the created mock in the service. This works for field injection:
@Service
public class MyService{
@Autowired
private RepositoryInterface repositoryInterface
}
setter injection:
@Service
public class MyService{
private RepositoryInterface repositoryInterface
@Autowired
public void setRepository(RepositoryInterface repositoryInterface){
this.repositoryInterface = repositoryInterface;
}
}
or constructor injection:
@Service
public class MyService{
private RepositoryInterface repositoryInterface
public MyService(RepositoryInterface repositoryInterface){
this.repositoryInterface = repositoryInterface;
}
}
Essentially, the last one is the recommended one because in this way your service's dependencies will be explicit. It's more on code style. Field injection is not recommended because iy hides your class dependencies. So, the recommended way of building a test using the constructor injection would be the following:
@ExtendWith({SpringExtension.class})
class MyServiceTest{
@MockBean
private RepositoryInterface repository;
private MyService service;
@BeforeEach
void setup(){
service = new MyService(repository);
}
}
Hope this helps your understanding.
答案2
得分: 2
是的,它起作用。一些测试框架允许注入私有字段。
而且,是的,这是反模式,会增加技术债务 - 虽然容易编写,但很难维护这样的代码,与其在编译时出现错误,还不如在运行时出现错误。不要这样做。使用构造函数注入。
英文:
Yes, it works. Some testing frameworks allow inject private fields.
And yes, it's antipattern, adds technical debt - easy to write, but hard to maintain such code, instead of compile-time errors you'll have runtime errors. Don't do that. Use constructor injection.
答案3
得分: 1
有基于字段的注入,基于setter的注入,基于注解的注入和基于构造函数的注入。基于构造函数的注入在测试时是最好的,因为您可以轻松地模拟所需的依赖关系。在可能的情况下,使用最终字段来定义服务总是很好的。
class MyService {
private final MyDependency dependency;
@Autowired // 不是必需的,只是为了这个示例而显式添加的
public MyService(MyDependency dependency) {
this.dependency = dependency;
}
}
英文:
There is field based injection, setter based injection, annotation based injection, and constructor based injection. Constructor based injection is best for testing simply because you can easily mock the dependencies needed. It's always nice to define services with final fields where possible.
class MyService {
private final MyDependency dependency;
@Autowired // not needed, explicit just for this example
public MyService(MyDependency dependency) {
this.dependency = dependency;
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论