Spring的注入方式
Spring支持哪些注入方式
字段注入
@Autowired
private Bean bean;
构造器注入
@Component
class Test {
private final Bean bean;
@Autowired
public Test(Bean bean) {
this.bean = bean;
}
}
setter注入
@Component
class Test {
private Bean bean;
@Autowired
public void setBean(Bean bean) {
this.bean = bean;
}
}
为什么Spring不建议使用基于字段的依赖注入
@Autowired
private Bean bean;
编写代码时,IDEA会给予警告:Field injection is not recommended,不推荐使用字段注入。
官方文档建议强制依赖使用构造器注入,而可选依赖使用setter注入。
强制依赖
强制依赖意味着一个对象或组件在运行时必须依赖于另一个对象或组件才能正常工作。如果没有提供强制依赖的对象或组件,系统将无法正常运行或功能将受到限制。在Spring中,通过基于构造器注入实现强制依赖,即在对象创建时必须提供相应的依赖对象。
例如,一个订单管理系统中,订单服务类需要依赖于数据库访问类来实现数据持久化操作。如果没有提供数据库访问类的实例,订单服务类将无法正常工作。
// 订单服务类,强制依赖于数据库访问类
@Component
public class OrderService {
private final DatabaseAccess databaseAccess;
@Autowired
public OrderService(DatabaseAccess databaseAccess) {
this.databaseAccess = databaseAccess;
}
public void createOrder(Order order) {
// 使用依赖的数据库访问类进行数据持久化操作
databaseAccess.save(order);
// 其他业务逻辑
}
}
// 数据库访问类
@Component
public class DatabaseAccess {
public void save(Order order) {
// 实现数据持久化逻辑
}
}
在上述代码中,订单服务类 OrderService 在构造函数中强制依赖于数据库访问类 DatabaseAccess。如果没有提供 DatabaseAccess 的实例,OrderService 将无法正常工作。
可选依赖
可选依赖意味着一个对象或组件在运行时可以选择性地依赖于另一个对象或组件。即使没有提供可选依赖的对象或组件,系统仍然可以正常运行,只是某些功能可能会受到限制或不可用。在Spring中,通过基于setter注入实现可选依赖,即在对象创建后可以动态地注入依赖对象。
例如,一个邮件发送服务类可以选择性地依赖于邮件模板类来生成邮件内容。如果没有提供邮件模板类的实例,邮件发送服务仍然可以发送邮件,但邮件内容将不会包含自定义的模板信息。
// 邮件发送服务类,可选依赖于邮件模板类
@Component
public class EmailService {
private EmailTemplate emailTemplate;
@Autowired(required = false)
public void setEmailTemplate(EmailTemplate emailTemplate) {
this.emailTemplate = emailTemplate;
}
public void sendEmail(String recipient, String content) {
if (emailTemplate != null) {
// 使用依赖的邮件模板类生成邮件内容
String formattedContent = emailTemplate.format(content);
// 发送邮件逻辑
// ...
} else {
// 发送邮件逻辑(没有模板的情况)
// ...
}
}
}
// 邮件模板类
@Component
public class EmailTemplate {
public String format(String content) {
// 实现邮件模板格式化逻辑
// ...
return formattedContent;
}
}
在上述代码中,邮件发送服务类 EmailService 可选依赖于邮件模板类 EmailTemplate。通过 setEmailTemplate 方法进行注入,如果没有提供 EmailTemplate 的实例,EmailService 仍然可以发送邮件,只是邮件内容不包含自定义的模板信息。
使用字段注入的问题
单一职责问题
根据SOLID设计原则来讲,一个类的设计应该符合单一职责原则,就是一个类只能做一件功能,当我们使用基于字段注入的时候,随着业务的暴增,字段越来越多,我们是很难发现我们已经默默中违背了单一职责原则的。
但是如果我们使用基于构造器注入的方式,因为构造器注入的写法比较臃肿,所以它就在间接提醒我们,违背了单一职责原则,该做重构了。
- 基于字段注入
@Component
class Test {
@Autowired
private Bean bean1;
@Autowired
private Bean bean2;
@Autowired
private Bean bean3;
@Autowired
private Bean bean4;
}
- 基于构造器注入
@Component
class Test {
private final Bean bean1;
private final Bean bean2;
private final Bean bean3;
private final Bean bean4;
@Autowired
public Test(Bean bean1, Bean bean2,
Bean bean3, Bean bean4) {
this.bean1 = bean1;
this.bean2 = bean1;
this.bean3 = bean1;
this.bean4 = bean1;
}
}
可能产生NullPointerException
- Hollis
对于一个Bean来说,它的初始化顺序:
静态变量或静态语句块 -> 实例变量或初始化语句块 -> 构造方法 -> @Autowired
// 在静态语句块,初始化语句块,构造方法中使用Autowired表明的字段,都会引起NPE问题
@Component
class Test {
@Autowired
private Bean bean;
private final String beanName;
public Test() {
// 此时bean尚未被初始化,会抛出NPE
this.beanName = bean.getName();
}
}
相反,用构造器的DI,就会实例化对象在使用的过程中,字段一定不为空。
- ChatGPT
在Spring框架中,基于字段的注入是通过自动装配(autowiring)实现的,而基于构造器的注入是通过构造函数注入实现的。
基于字段的注入可能会产生NullPointerException的原因是,当使用字段注入时,Spring容器会在实例化bean后直接通过反射将依赖注入到字段上。如果依赖的bean在注入时还未被实例化或者注入的依赖为null,就会导致NullPointerException。
@Component
public class UserService {
@Autowired
private UserRepository userRepository;
public void saveUser(User user) {
userRepository.save(user);
}
}
@Repository
public class UserRepository {
public void save(User user) {
// 保存用户逻辑
}
}
在上面的例子中,UserService类中的userRepository字段使用了@Autowired注解进行了自动装配。如果在实例化UserService对象时,userRepository还未被实例化或者注入的userRepository为null,那么在调用saveUser方法时就会产生NullPointerException。
相比之下,基于构造器的注入则可以避免这个问题。在构造器注入中,依赖会在实例化bean时通过构造函数的参数传入。如果依赖的bean还未被实例化或者注入的依赖为null,就会在实例化bean时抛出异常,而不是在使用时产生NullPointerException。
@Component
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void saveUser(User user) {
userRepository.save(user);
}
}
@Repository
public class UserRepository {
public void save(User user) {
// 保存用户逻辑
}
}
在上面的例子中,UserService类的构造函数接收一个UserRepository参数,并将其赋值给userRepository字段。如果在实例化UserService对象时,userRepository还未被实例化或者注入的userRepository为null,就会在实例化UserService时抛出异常,而不是在调用saveUser方法时产生NullPointerException。
隐藏依赖
对于一个正常的使用依赖注入的Bean来说,它应该“显示”的通知容器,自己需要哪些Bean,可以通过构造器通知,public的setter方法通知,这些设计都是没问题的。
但是对于private的filed来说,从设计的角度角度来讲,外部的容器是不应该感知到bean内部的private内容的,所以理论上,private的filed是没办法通知到容器的(不考虑反射,但从设计角度理解),所以从这个角度来看,我们不能通过字段注入
不利于测试
使用了Autowired注解,说明这个类依赖了Spring容器,这让我们在进行UT的时候必须要启动一个Spring容器才可以测试这个类,显然太麻烦,这种测试方式非常重,对于大型项目来说,往往启动一个容器就要好几分钟,这样非常耽误时间。
不过,如果使用构造器的依赖注入就不会有这种问题,或者,我们可以使用Resource注解也可以解决上述问题。
使用构造器注入可能有哪些问题
如果我们两个bean循环依赖的话,构造器注入就会抛出异常:
@Component
public class BeanTwo implements Bean{
Bean beanOne;
public BeanTwo(Bean beanOne) {
this.beanOne = beanOne;
}
}
@Component
public class BeanOne implements Bean{
Bean beanTwo;
public BeanOne(Bean beanTwo) {
this.beanTwo = beanTwo;
}
}
如果两个类彼此循环引用,那说明代码的设计一定是有问题的。如果临时解决不了,我们可以在某一个构造器中加入@Lazy注解,让一个类延迟初始化即可。
@Component
public class BeanOne implements Bean{
Bean beanTwo;
@Lazy
public BeanOne(Bean beanTwo) {
this.beanTwo = beanTwo;
}
}