Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null

five-five 2024-08-03 13:05:03 阅读 67

Spring踩坑:抽象类作为父类,使用子类@Autowired属性进行填充,属性值为null

Spring Boot中抽象类和依赖注入的最佳实践引言在抽象类中使用@Autowired注解protected vs private修饰符低版本Spring Boot的注意事项

构造器中的依赖注入陷阱为什么不能在构造器中使用注入的属性?子类构造的问题

@PostConstruct的使用正确使用@PostConstruct的例子子类中的@PostConstruct

避免在构造器中使用ApplicationContext.getBean错误示例正确做法

最佳实践示例常见问题和解决方案1. 循环依赖2. 依赖注入在单元测试中的问题3. 属性注入vs构造器注入4. 抽象类中的 @Autowired 方法5. 运行时依赖注入

最佳实践总结结论

Spring Boot中抽象类和依赖注入的最佳实践

引言

在Spring Boot应用程序中,抽象类经常被用作一种强大的设计模式,用于封装共同的行为和属性。然而,当涉及到依赖注入时,特别是在抽象类中,我们需要格外小心。本文将深入探讨在Spring Boot 2.0及以上版本中使用抽象类作为父类时的最佳实践,特别关注依赖注入的正确使用方式。

在抽象类中使用@Autowired注解

在Spring Boot 2.0及以上版本中,我们可以直接在抽象类的属性上使用@Autowired注解进行依赖注入。这为我们提供了一种方便的方式来在父类中定义共同的依赖,供子类使用。

protected vs private修饰符

当在抽象类中使用@Autowired注解时,我们通常有两种选择来修饰这些属性:protected或private。

使用protected修饰符:

<code>public abstract class AbstractService {

@Autowired

protected SomeRepository repository;

}

优点:

允许子类直接访问注入的依赖提供了更大的灵活性,子类可以根据需要重写或扩展这些依赖的使用

缺点:

可能会破坏封装性,因为子类可以直接修改这些依赖

使用private修饰符:

public abstract class AbstractService {

@Autowired

private SomeRepository repository;

protected SomeRepository getRepository() {

return repository;

}

}

优点:

保持了良好的封装性父类可以控制子类如何访问这些依赖

缺点:

需要额外的getter方法来允许子类访问这些依赖

在Spring Boot 2.0中,这两种方式都是可行的。选择哪种方式主要取决于你的设计需求和偏好。如果你希望严格控制依赖的访问,使用private加getter方法可能是更好的选择。如果你希望提供最大的灵活性给子类,使用protected可能更合适。

低版本Spring Boot的注意事项

在低于2.0的Spring Boot版本中,使用protected修饰符通常是更安全的选择。这是因为在一些早期版本中,private字段的自动注入可能会遇到问题。如果你正在使用较旧的Spring Boot版本,建议使用protected修饰符来确保依赖能够正确注入。

构造器中的依赖注入陷阱

在抽象类中,我们经常需要在构造器中执行一些初始化逻辑。然而,这里有一个重要的陷阱需要注意:不应该在构造器中引用通过@Autowired注入的属性。

为什么不能在构造器中使用注入的属性?

原因在于Spring的bean生命周期和依赖注入的时机。当Spring创建一个bean时,它遵循以下步骤:

实例化bean(调用构造器)注入依赖(设置@Autowired字段)调用初始化方法(如@PostConstruct注解的方法)

这意味着在构造器执行时,@Autowired注解的属性还没有被注入,它们的值为null。如果你在构造器中尝试使用这些属性,很可能会遇到NullPointerException。

让我们看一个错误的例子:

public abstract class AbstractService {

@Autowired

private SomeRepository repository;

public AbstractService() {

// 错误:此时repository还是null

repository.doSomething();

}

}

这段代码会在运行时抛出NullPointerException,因为在构造器执行时,repository还没有被注入。

子类构造的问题

这个问题在子类中更加复杂。当你创建一个抽象类的子类时,子类的构造器会首先调用父类的构造器。这意味着即使是在子类的构造器中,父类中@Autowired注解的属性仍然是null。

public class ConcreteService extends AbstractService {

public ConcreteService() {

super(); // 调用AbstractService的构造器

// 错误:此时父类中的repository仍然是null

getRepository().doSomething();

}

}

这段代码同样会抛出NullPointerException,因为在调用子类构造器时,父类中的依赖还没有被注入。

@PostConstruct的使用

为了解决构造器中无法使用注入依赖的问题,Spring提供了@PostConstruct注解。被@PostConstruct注解的方法会在依赖注入完成后被自动调用,这使得它成为执行初始化逻辑的理想位置。

正确使用@PostConstruct的例子

public abstract class AbstractService {

@Autowired

private SomeRepository repository;

@PostConstruct

public void init() {

// 正确:此时repository已经被注入

repository.doSomething();

}

}

在这个例子中,init()方法会在所有依赖注入完成后被调用,因此可以安全地使用repository。

子类中的@PostConstruct

子类也可以定义自己的@PostConstruct方法,这些方法会在父类的@PostConstruct方法之后被调用:

public class ConcreteService extends AbstractService {

@Autowired

private AnotherDependency anotherDependency;

@PostConstruct

public void initChild() {

// 父类的init()方法已经被调用

// 可以安全地使用父类和子类的所有依赖

getRepository().doSomething();

anotherDependency.doSomethingElse();

}

}

这种方式确保了所有的初始化逻辑都在依赖注入完成后执行,避免了NullPointerException的风险。

避免在构造器中使用ApplicationContext.getBean

另一个常见的陷阱是在构造器中使用ApplicationContext.getBean()方法来获取bean。这种做法应该被避免,原因如下:

在构造器执行时,ApplicationContextAware接口可能还没有被调用,这意味着ApplicationContext可能还不可用。即使ApplicationContext可用,其他bean可能还没有被完全初始化,调用getBean()可能会返回未完全初始化的bean或触发意外的初始化。使用ApplicationContext.getBean()会使你的代码与Spring框架紧密耦合,降低了可测试性和可维护性。

错误示例

public abstract class AbstractService implements ApplicationContextAware {

private ApplicationContext context;

public AbstractService() {

// 错误:此时context还是null

SomeBean someBean = context.getBean(SomeBean.class);

}

@Override

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

this.context = applicationContext;

}

}

这段代码会抛出NullPointerException,因为在构造器执行时,setApplicationContext()方法还没有被调用。

正确做法

正确的做法是使用依赖注入,让Spring容器管理对象的创建和依赖关系:

public abstract class AbstractService {

@Autowired

private SomeBean someBean;

@PostConstruct

public void init() {

// 正确:此时someBean已经被注入

someBean.doSomething();

}

}

这种方式不仅避免了NullPointerException,还降低了与Spring框架的耦合度,使代码更易于测试和维护。

最佳实践示例

让我们通过一个完整的例子来展示这些最佳实践:

@Service

public abstract class AbstractUserService {

@Autowired

private UserRepository userRepository;

@Autowired

private EmailService emailService;

protected AbstractUserService() {

// 构造器中不做任何依赖相关的操作

}

@PostConstruct

protected void init() {

// 初始化逻辑

System.out.println("AbstractUserService initialized with " + userRepository.getClass().getSimpleName());

}

public User findUserById(Long id) {

return userRepository.findById(id).orElse(null);

}

protected void sendEmail(User user, String message) {

emailService.sendEmail(user.getEmail(), message);

}

// 抽象方法,由子类实现

public abstract void processUser(User user);

}

@Service

public class ConcreteUserService extends AbstractUserService {

@Autowired

private SpecialProcessor specialProcessor;

@PostConstruct

protected void initChild() {

System.out.println("ConcreteUserService initialized with " + specialProcessor.getClass().getSimpleName());

}

@Override

public void processUser(User user) {

User processedUser = specialProcessor.process(user);

sendEmail(processedUser, "Your account has been processed.");

}

}

// 使用示例

@RestController

@RequestMapping("/users")

public class UserController {

@Autowired

private ConcreteUserService userService;

@GetMapping("/{id}")

public ResponseEntity<User> getUser(@PathVariable Long id) {

User user = userService.findUserById(id);

if (user != null) {

userService.processUser(user);

return ResponseEntity.ok(user);

} else {

return ResponseEntity.notFound().build();

}

}

}

在这个例子中:

AbstractUserService​ 是一个抽象类,它定义了一些通用的用户服务逻辑。依赖(UserRepository​ 和 EmailService​)通过 @Autowired​ 注入到抽象类中。初始化逻辑放在 @PostConstruct​ 注解的 init()​ 方法中,确保在所有依赖注入完成后执行。​ConcreteUserService​ 继承自 AbstractUserService​,并实现了抽象方法。​ConcreteUserService​ 有自己的依赖(SpecialProcessor​)和初始化逻辑。在 UserController​ 中,我们注入并使用 ConcreteUserService​。

这个设计遵循了我们讨论的所有最佳实践:

在抽象类中使用 @Autowired​ 注入依赖避免在构造器中使用注入的依赖使用 @PostConstruct​ 进行初始化不使用 ApplicationContext.getBean()

常见问题和解决方案

在使用抽象类和依赖注入时,开发者可能会遇到一些常见问题。以下是一些问题及其解决方案:

1. 循环依赖

问题:当两个类相互依赖时,可能会导致循环依赖问题。

解决方案:

重新设计以消除循环依赖使用 @Lazy​ 注解来延迟其中一个依赖的初始化使用 setter 注入而不是构造器注入

@Service

public class ServiceA {

private ServiceB serviceB;

@Autowired

public void setServiceB(@Lazy ServiceB serviceB) {

this.serviceB = serviceB;

}

}

@Service

public class ServiceB {

@Autowired

private ServiceA serviceA;

}

2. 依赖注入在单元测试中的问题

问题:在单元测试中,可能难以模拟复杂的依赖注入场景。

解决方案:

使用 Spring 的测试支持,如 @SpringBootTest​为测试创建一个简化的配置类使用模拟框架如 Mockito 来模拟依赖

@SpringBootTest

class ConcreteUserServiceTest {

@MockBean

private UserRepository userRepository;

@Autowired

private ConcreteUserService userService;

@Test

void testFindUserById() {

when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Test User")));

User user = userService.findUserById(1L);

assertNotNull(user);

assertEquals("Test User", user.getName());

}

}

3. 属性注入vs构造器注入

问题:虽然属性注入(使用 @Autowired​ on fields)很方便,但它可能使得依赖关系不那么明显。

解决方案:考虑使用构造器注入,特别是对于必需的依赖。这使得依赖关系更加明确,并有助于创建不可变的服务。

@Service

public abstract class AbstractUserService {

private final UserRepository userRepository;

private final EmailService emailService;

@Autowired

protected AbstractUserService(UserRepository userRepository, EmailService emailService) {

this.userRepository = userRepository;

this.emailService = emailService;

}

// ... 其他方法

}

@Service

public class ConcreteUserService extends AbstractUserService {

private final SpecialProcessor specialProcessor;

@Autowired

public ConcreteUserService(UserRepository userRepository,

EmailService emailService,

SpecialProcessor specialProcessor) {

super(userRepository, emailService);

this.specialProcessor = specialProcessor;

}

// ... 其他方法

}

这种方法的优点是:

依赖关系更加明确有助于创建不可变的服务更易于单元测试

4. 抽象类中的 @Autowired 方法

问题:有时我们可能想在抽象类中有一个被 @Autowired 注解的方法,但这个方法在子类中被重写了。

解决方案:使用 @Autowired 注解抽象方法,并在子类中实现它。

public abstract class AbstractService {

@Autowired

protected abstract Dependencies getDependencies();

@PostConstruct

public void init() {

getDependencies().doSomething();

}

}

@Service

public class ConcreteService extends AbstractService {

@Autowired

private Dependencies dependencies;

@Override

protected Dependencies getDependencies() {

return dependencies;

}

}

这种方法允许子类控制依赖的具体实现,同时保持父类的通用逻辑。

5. 运行时依赖注入

问题:有时我们可能需要在运行时动态注入依赖,而不是在启动时。

解决方案:使用 ObjectProvider<T>​ 来延迟依赖的解析。

@Service

public abstract class AbstractDynamicService {

@Autowired

private ObjectProvider<DynamicDependency> dependencyProvider;

protected DynamicDependency getDependency() {

return dependencyProvider.getIfAvailable();

}

// ... 其他方法

}

这种方法允许我们在需要时才解析依赖,这在某些场景下可能很有用,比如条件性的bean创建。

最佳实践总结

基于我们的讨论,以下是在Spring Boot中使用抽象类和依赖注入的最佳实践总结:

在抽象类中使用 @Autowired: 可以直接在抽象类的字段上使用 @Autowired 注解。使用 protected 修饰符可以让子类直接访问这些依赖,而使用 private 加 getter 方法可以提供更好的封装。避免在构造器中使用注入的依赖: 构造器执行时,依赖还没有被注入,因此不应该在构造器中使用它们。使用 @PostConstruct 进行初始化: 将需要依赖的初始化逻辑放在 @PostConstruct 注解的方法中,确保所有依赖都已注入。不要在构造器中使用 ApplicationContext.getBean: 这可能导致意外的行为,因为在构造器执行时,ApplicationContext 可能还未完全准备好。考虑使用构造器注入: 对于必需的依赖,构造器注入可以使依赖关系更加明确,并有助于创建不可变的服务。处理循环依赖: 使用 @Lazy 注解或 setter 注入来解决循环依赖问题。合理使用抽象方法: 在抽象类中定义抽象方法可以让子类控制某些依赖的具体实现。使用 ObjectProvider 进行动态依赖注入: 当需要在运行时动态解析依赖时,考虑使用 ObjectProvider。注意测试: 在单元测试中,使用 Spring 的测试支持和模拟框架来处理复杂的依赖注入场景。遵循 SOLID 原则: 特别是单一责任原则和依赖倒置原则,这有助于创建更易维护和测试的代码。

结论

在Spring Boot中使用抽象类和依赖注入是一种强大的技术,可以帮助我们创建灵活、可维护的代码。然而,它也带来了一些挑战,特别是在处理依赖注入的时机和方式上。

通过遵循本文讨论的最佳实践,我们可以避免常见的陷阱,充分利用Spring Boot提供的依赖注入功能。记住,关键是要理解Spring Bean的生命周期,合理使用 @PostConstruct 注解,避免在不适当的时候访问依赖,并选择适合你的项目的依赖注入方式。

最后,虽然这些是普遍认可的最佳实践,但每个项目都有其独特的需求。因此,始终要根据你的具体情况来调整这些实践。持续学习和实践是掌握Spring Boot中抽象类和依赖注入的关键。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。