0
点赞
收藏
分享

微信扫一扫

MySql中表的复合查询

小时候是个乖乖 2024-11-06 阅读 9

1. IOC(控制反转)的基本概念

1.1.什么是控制反转(Inversion of Control,IOC)?

控制反转(Inversion of Control,简称IOC)是一种设计原则,用于实现对象之间的解耦。在传统编程中,对象通常会自行创建和管理其依赖对象,导致组件之间的耦合度较高。而IOC则将这种依赖的管理权从对象本身“反转”到外部容器(例如Spring容器)中,通过依赖注入(DI,Dependency Injection)机制来提供所需的依赖对象。

在IOC中,应用程序不再控制依赖对象的创建和管理,而是依赖于IOC容器来创建和管理这些对象。容器负责实例化、配置和注入对象的依赖关系。这种控制的“反转”使得对象不再需要知道如何获取其依赖,从而减少了模块之间的耦合。

1.2.与传统编程中控制流程的区别

在传统编程中,控制流程是由应用程序开发者直接管理的,典型的流程如下:

传统编程:手动管理依赖

在应用程序中,开发者需要在代码中手动创建和初始化对象,并明确它们之间的依赖关系。

例如,类A依赖于类B,开发者需要在A的代码中显式地创建B的实例,如 A a = new A(new B())。这种方式的问题是类A和类B紧密耦合,当类B的实现发生变化时,类A也可能需要修改,难以维护和扩展。

控制反转(IOC):容器管理依赖

在使用IOC的编程中,对象不再自己管理其依赖,而是通过容器(如Spring的IOC容器)来提供这些依赖。开发者不再手动创建依赖对象,而是通过声明的方式告知容器需要什么,容器会在运行时注入这些依赖。

例如,类A只需要声明它依赖于类B,容器会在类A被创建时自动将类B注入,无需在A的代码中显式创建B。这实现了类A与类B的解耦,类A只需知道它需要类B的接口(或抽象类),而不关心类B的具体实现。

1.3.IOC如何实现解耦?

通过IOC实现解耦的关键在于将对象之间的依赖关系交由容器管理,从而使得对象之间的耦合度降低。具体来说:

依赖注入(Dependency Injection)

IOC通过依赖注入来实现解耦。依赖注入的基本思想是将对象的依赖项通过构造函数、Setter方法或注解的方式注入,而不是由对象自己创建依赖项。

例如,类A依赖于类B,类B的实例不再由类A创建,而是由IOC容器管理,类A仅声明它需要类B,容器负责在运行时将类B的实例注入给类A。

通过接口编程解耦

在IOC中,依赖注入通常结合接口编程实现。类A依赖于类B的接口(而不是具体实现),容器可以在运行时根据配置注入不同的类B实现。

这使得类A和类B的实现解耦,类B的实现可以随时更换而不影响类A。

生命周期管理的解耦

容器不仅管理依赖,还管理对象的生命周期。对象的创建、初始化、销毁等都由容器统一控制,减少了应用程序中的复杂度。

例如,Spring容器在启动时会创建所需的Bean实例,并在适当的时候进行销毁,开发者不需要关注对象的生命周期管理。

1.4.IOC带来的优势

解耦合

通过IOC,模块之间的依赖性降低,各个模块可以独立开发和维护。特别是通过接口和抽象类的方式,类与类之间依赖于契约(接口)而非具体实现。这种松耦合设计使得代码更加灵活,可以更容易地替换和扩展依赖的实现。

代码更具可测试性

使用IOC后,依赖关系可以通过注入的方式进行控制,这使得在单元测试中可以轻松地对依赖进行Mock或替换,提升了测试的方便性和覆盖率。

更好的扩展性和可维护性

因为模块之间是解耦的,因此在更改实现、添加功能或重构代码时,改动的影响范围更小,系统更加容易维护和扩展。

降低重复代码

通过容器管理Bean的创建、依赖注入和生命周期,开发者可以集中精力于业务逻辑,而不是处理对象的创建、管理和销毁,减少了重复代码。

2. 依赖注入(Dependency Injection)

2.1.依赖注入的概念

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现对象之间的松耦合。其核心思想是将对象所依赖的其他对象(依赖项)从外部传递给它,而不是让对象自己去创建这些依赖项。这一过程通常由IOC容器(如Spring容器)来管理。通过依赖注入,类不再负责自己获取依赖,而是依赖于容器来提供和注入所需的对象,从而实现控制反转(IOC)。

在依赖注入中,类的依赖项在创建时被注入进来,这使得类与它所依赖的具体实现解耦,增强了代码的可扩展性、可维护性和测试性。

2.2.依赖注入的类型

依赖注入有三种主要方式,每种方式的应用场景和优缺点略有不同:

  1. 构造函数注入
  2. Setter方法注入
  3. 接口注入(较少使用)

2.2.1构造器注入(推荐方式)

使用构造器注入能确保依赖在类实例化时就被正确注入,并且方便进行单元测试。

@RestController
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}

2.2.2.字段注入 

使用@Autowired直接注入到类的成员变量中。这是最常见但不推荐的方式,因为它使得依赖关系不那么显式,并且在单元测试中可能不太灵活。

@RestController
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}

2.2.3.Setter注入 

通过提供一个setter方法来注入依赖,尽管使用频率较低,但它可以在某些需要动态设置依赖的场景中使用。

@RestController
public class UserController {

private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}

@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}

2.2.4.getBean() 方法

getBean() 是Spring容器提供的用于手动获取Bean实例的方法。通常,Spring框架推荐通过依赖注入(构造函数注入、字段注入、Setter方法注入)来管理Bean的实例和依赖关系。但是,在某些特定的场景下,手动调用getBean()方法来获取Bean实例更加灵活和适用。

getBean()的使用场景

1.动态获取Bean实例

在应用程序中,有时需要根据运行时的条件、用户输入或某种业务逻辑动态选择不同的Bean实例。例如,当你有多种不同的实现类或策略模式时,可以在运行时根据用户的选择动态获取某个实现类,而不需要在启动时固定某一个Bean。

示例代码:

public class PaymentProcessor {
private ApplicationContext context;

public PaymentProcessor(ApplicationContext context) {
this.context = context;
}

public void processPayment(String paymentMethod) {
PaymentService paymentService;
if ("paypal".equals(paymentMethod)) {
paymentService = context.getBean("paypalService", PaymentService.class);
} else if ("creditcard".equals(paymentMethod)) {
paymentService = context.getBean("creditCardService", PaymentService.class);
} else {
paymentService = context.getBean("defaultPaymentService", PaymentService.class);
}
paymentService.process();
}
}

场景描述:在此示例中,PaymentProcessor根据用户选择的支付方式,动态从Spring容器中获取对应的支付服务Bean(paypalServicecreditCardService等)。这是getBean()的典型使用场景之一。

  • 不同支付方式的处理类,如支付网关在运行时根据用户选择决定使用支付宝、微信支付或信用卡支付的不同实现。
  • 在多种算法实现中,动态获取某个具体的实现类。

2.在非Spring管理的类中使用Spring Bean

通常,Spring会自动管理和注入依赖,但在某些场景下,我们的类并不由Spring容器直接管理,比如:

  • 工具类或静态类中需要使用Spring容器的Bean。
  • 在启动类或第三方库中需要使用Spring的Bean时,这些类通常不在Spring的管理之下。

3.在测试中灵活获取和替换Bean

在单元测试或集成测试中,有时需要测试不同的Bean实现,或动态获取某个Bean进行测试。依赖注入虽然可以简化大部分测试,但在某些情况下,手动调用getBean()可以让测试更灵活。

示例场景:

  • 动态替换或模拟某个Bean,进行测试。
  • 测试不同环境下Bean的行为,如通过不同配置加载不同的Bean实现。

4.在应用程序的启动流程中使用

在Spring Boot应用的启动过程中,开发者可以在启动类中手动获取某些Bean,执行一些初始化操作。虽然可以使用依赖注入,但在某些情况下,getBean()方法可以更灵活地获取特定的Bean。

示例场景:

  • 在Spring Boot应用启动后,获取某些Bean进行初始化操作,如在应用启动后进行数据加载、定时任务启动等。
getBean()的使用步骤

1.获取Spring容器实例: 首先,需要确保你有一个Spring容器实例,通常可以通过ApplicationContext来获取。Spring Boot会自动管理这个容器,所以你可以通过@Autowired注入Spring上下文,或手动初始化上下文:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

2.调用getBean()获取Bean: 使用context.getBean()方法来获取所需的Bean。你可以通过名字、类型,或名字和类型结合的方式来获取Bean。

MyService myService = context.getBean(MyService.class);

3.执行Bean的逻辑: 获取Bean实例后,可以正常调用其方法,执行相应的业务逻辑。

 2.2.5.不需要@Autowired注解的情况

1. 构造函数注入

从Spring 4.3开始,如果一个Bean只有一个构造函数,Spring会自动使用该构造函数进行依赖注入,无需@Autowired注解。

@Service
public class UserService {
private final UserRepository userRepository;

// 不需要@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}

2. 单一实现的接口

如果一个接口只有一个实现类,Spring会自动将这个实现类注入到需要该接口的地方,无需额外配置。

public interface MessageService {
String getMessage();
}

@Service
public class EmailService implements MessageService {
public String getMessage() {
return "Email message";
}
}

@Controller
public class MessageController {
private final MessageService messageService;

// 自动注入EmailService,无需@Autowired
public MessageController(MessageService messageService) {
this.messageService = messageService;
}
}

 3. @Configuration类中的@Bean方法

在@Configuration类中定义的@Bean方法可以直接使用其他Bean作为参数,Spring会自动注入。

@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
// 创建并返回DataSource
}

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
// Spring自动注入上面定义的dataSource
return new JdbcTemplate(dataSource);
}
}

2.3.依赖注入的优点

1.解耦合

依赖注入使得对象不再需要知道如何获取其依赖项,这降低了类之间的耦合度。类只依赖于接口或抽象类型,而具体实现由容器在运行时提供。这使得模块之间可以独立开发和维护,增强了代码的灵活性。

2.增强可测试性

依赖注入有助于单元测试。通过将依赖项注入类中,开发者可以轻松地替换实际依赖为模拟对象(Mock对象)来进行测试,而无需依赖外部环境。这使得测试更加独立、快速和准确。

3.提高可维护性

由于依赖关系由容器管理,开发者不需要手动管理依赖对象的创建与销毁,减少了重复代码和复杂性。需要修改依赖对象时,只需更改配置或注入的对象类型即可,不必修改业务逻辑代码。

4.增强扩展性

通过依赖注入,可以轻松替换类的具体实现。例如,可以根据环境或需求的变化注入不同的实现类,从而扩展系统功能而无需修改现有代码。这非常适合多态性和灵活配置的场景。

5.生命周期管理

容器负责管理Bean的整个生命周期,包括创建、初始化、依赖注入和销毁。开发者无需关注对象的生命周期管理,从而简化了开发过程,并提高了系统的可靠性和一致性。


2.4.依赖注入的应用场景

1.服务与模块解耦

在大型应用程序中,多个服务或模块通常互相依赖。通过依赖注入,这些模块可以依赖接口而不是具体实现,从而减少模块之间的耦合,方便模块的独立开发和维护。

2.配置与依赖的灵活管理

依赖注入使得配置更加灵活。在不同的环境中(如开发环境、测试环境和生产环境),可以通过依赖注入不同的实现类来实现环境间的无缝切换。应用程序可以轻松更改依赖对象,而不需要修改其核心代码。

3.单元测试

在单元测试中,开发者可以使用Mock对象替换实际依赖,从而隔离测试。通过依赖注入,测试可以更加灵活,因为注入的对象可以根据测试场景动态切换,简化了测试用例的编写。

4.复杂对象的创建

当对象依赖多个其他对象时,依赖注入通过容器统一管理这些依赖的创建与注入,避免了繁琐的手动初始化代码。特别是在涉及多个服务或工具类的场景下,依赖注入减少了对象的创建难度。

5.灵活的插件式架构

在某些需要插件式架构的场景下,依赖注入非常适合。通过将不同的实现类注入系统,用户可以根据不同需求动态加载不同的功能模块或服务,实现更灵活的插件化设计。

2.5.通过IOC容器管理Bean的生命周期和依赖关系

2.5.1. Bean 定义

Spring 容器会在启动时加载所有 Bean 的配置,这些配置可以通过 XML 文件、注解(如 @Component@Service 等)或者 Java 配置类(使用 @Configuration@Bean)来定义。当你通过这些方式定义一个 Bean 时,Spring 容器并不会立刻创建该对象,而是等待容器启动或者需要使用时才创建。

2.5.2. Bean 的实例化

当程序启动时,Spring 容器会根据定义创建 Bean 的实例。如果你的类是通过注解或配置类标注为 Spring Bean 的,Spring 会负责创建这些对象。这个阶段相当于用 new 来创建一个对象。

2.5.3. 属性注入

属性注入阶段是 Spring Bean 生命周期中的一个重要环节。在这个阶段,Spring 的重点是为每个 Bean 注入它所需要的依赖。可以将这个阶段理解为 Bean 自身的准备工作,Spring 会确保 Bean 的内部所有依赖(即它需要的其他 Bean)都已经正确注入并可用。

关键点:

  • 只在 Bean 内部进行:这一阶段的操作只是在当前 Bean 的内部完成,Spring 会分析 Bean 需要的依赖,并为其注入相应的其他 Bean。这是为了让 Bean 自己的属性或方法具备必要的依赖,从而能够正常工作。

  • 不涉及外部类的使用:虽然此时 Bean 的内部依赖已经准备就绪,但这个阶段并不涉及外部类如何使用这个 Bean。换句话说,Bean 的依赖已经满足,但 Bean 还没有真正参与到应用程序的实际业务逻辑中。

  • 例如:一个 OrderService 需要 PaymentService 的支持,Spring 会在这个阶段将 PaymentService 注入到 OrderService 中,但此时 OrderService 还没有被外部类调用或使用,它只是做好了内部依赖的准备。

总结来说,属性注入阶段 的核心是让 Bean 自身的依赖注入完成,保证其内部所需要的其他 Bean 都已经正确注入,但这个 Bean 还没有被应用程序中的其他类使用。

2.5.4. Bean的初始化

在依赖注入完成后,如果Bean实现了InitializingBean接口,Spring会调用其afterPropertiesSet()方法,或通过@PostConstruct注解指定的方法进行进一步初始化。这是Bean完成依赖注入后的额外配置阶段。

2.5.5 Bean的使用

使用阶段是 Spring Bean 生命周期中的下一个重要环节。在这个阶段,应用程序的其他类(如控制器、服务层等)开始通过 @Autowired 等方式使用那些已经完成依赖注入的 Bean。这时,Bean 已经完成了初始化并准备好参与应用程序的实际业务逻辑。

关键点

  • Bean 可以被外部类使用:与属性注入阶段不同,使用阶段的重点是外部类如何使用这些已经准备好的 Bean。Spring 容器中的其他类可以通过 @Autowired 注入这些 Bean,并在需要时调用它们的功能。

  • 参与实际业务逻辑:在使用阶段,Bean 开始真正参与到应用程序的核心业务逻辑中。例如,控制器类可能会通过注入的服务类来处理用户请求,执行数据库操作,或者完成复杂的业务逻辑处理。

  • 例如:在属性注入阶段,OrderService 已经获得了 PaymentService 的支持;而在使用阶段,OrderController 通过 @Autowired 注入了 OrderService,并调用它的 processOrder() 方法,开始执行订单处理的实际业务逻辑。 

2.5.6. Bean的销毁

在应用程序关闭或Spring容器销毁时,Spring会销毁所有的单例(singleton)作用域的Bean。如果Bean实现了DisposableBean接口,Spring会调用其destroy()方法,或者通过@PreDestroy注解指定的方法进行清理操作。

3. Spring IOC 容器(结合Spring Boot框架)

3.1.什么是Spring IOC容器

在Spring Boot中,IOC(控制反转)容器是核心部分。Spring IOC容器负责创建、管理和配置Spring应用中的所有Bean对象。它通过依赖注入(DI)机制,将对象的依赖从外部注入,使得类与类之间松耦合。Spring Boot在启动时自动配置和初始化IOC容器,因此开发者不需要像传统Spring应用那样手动配置大量的XML或Java类来管理Bean。

Spring Boot的自动配置机制使得大部分Bean的配置和依赖注入通过自动装配(Autowiring)来完成,简化了开发过程。

3.2.Spring容器的基本工作原理

自动配置与启动

在Spring Boot中,IOC容器通过SpringApplication.run()方法启动,容器会自动扫描项目中的所有类,创建并管理所有需要的Bean对象。

通过Spring Boot的@SpringBootApplication注解(其中包含@ComponentScan@EnableAutoConfiguration),容器会自动扫描类路径下的所有组件,并将它们注册到容器中,完成Bean的实例化和依赖注入。

Bean的管理与生命周期

Spring Boot中,容器负责管理Bean的生命周期,包括Bean的创建、初始化、依赖注入、销毁等。开发者可以使用@Component@Service@Repository等注解将类注册为Bean。

使用@Autowired进行自动装配时,容器会根据依赖关系自动注入相应的Bean。

依赖注入

Spring Boot通过构造函数、Setter方法、字段注入等多种方式实现依赖注入。在大多数场景中,Spring Boot会自动处理依赖的注入,开发者只需专注于业务逻辑。

使用Spring Boot时,依赖注入的简便性大幅提升,容器会通过自动配置机制为常用组件自动注入所需依赖。

3.3.容器的类型

在Spring中,IOC容器有两个主要接口:BeanFactoryApplicationContext。Spring Boot默认使用的是ApplicationContext,并提供了大量扩展功能。

3.3.1. BeanFactory(轻量级容器)

概念

BeanFactory 是Spring最基本的IOC容器接口,提供了核心的Bean管理功能,但它在Spring Boot中使用较少。BeanFactory的主要特点是惰性初始化,即只有在第一次请求某个Bean时,才会创建该Bean的实例。这种特性使得BeanFactory适合轻量级应用或启动时需要较快性能的场景。

在Spring Boot中的应用

  • 在Spring Boot中,BeanFactory通常作为底层实现,用于支持ApplicationContext。在绝大多数场景中,开发者不直接使用BeanFactory,因为它功能有限,不支持Spring Boot中常用的自动配置、事件机制等功能。
  • 在一些性能敏感的场景中,Spring Boot仍然可能使用BeanFactory来延迟加载某些Bean,提升启动性能。

使用场景

  • 资源受限的环境,如嵌入式系统或需要极快启动时间的应用中,可以通过自定义Spring Boot配置来使用BeanFactory

3.3.2. ApplicationContext(更高级的容器)

概念

ApplicationContext 是Spring中功能更丰富的容器接口,Spring Boot默认使用的正是ApplicationContext。它不仅支持BeanFactory的所有功能,还包括了一些企业级应用必需的扩展功能,如事件传播国际化自动配置等。Spring Boot通过ApplicationContext容器提供了强大的自动配置机制,使得应用可以在最少的配置下运行。

在Spring Boot中的应用

1.自动配置

Spring Boot通过@EnableAutoConfiguration注解和spring-boot-autoconfigure模块,自动扫描并注册应用程序所需的Bean。Spring Boot通过ApplicationContext提供的自动装配功能,使得常见的组件(如数据源、事务管理器、消息队列等)无需显式配置,容器会根据类路径中的依赖自动加载和注入相应的Bean。

2.事件传播

ApplicationContext支持Spring事件机制,允许Bean发布和监听应用事件。Spring Boot通过事件机制让开发者可以方便地响应容器启动、关闭等生命周期事件。例如,可以使用ApplicationListener监听ApplicationReadyEvent事件,以在应用程序启动后执行特定操作。

@Component
public class StartupListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
System.out.println("Application started!");
}
}

3.环境与配置管理

ApplicationContext支持环境配置管理,Spring Boot通过application.propertiesapplication.yml文件管理配置,容器可以自动读取这些配置并将其注入到Bean中。例如,使用@Value注解读取配置项,或通过Environment接口访问配置。

@Value("${app.name}")
private String appName;

4.国际化支持

ApplicationContext提供了国际化(I18N)功能,通过配置MessageSource,可以加载多语言资源文件,支持多语言应用的开发。

5.Bean生命周期自动管理

ApplicationContext自动管理Bean的整个生命周期,开发者可以通过@PostConstruct@PreDestroy等注解自定义初始化和销毁方法,而Spring Boot通过自动配置让这一切更加透明化和自动化。

6.Profile管理

ApplicationContext支持不同环境下的配置管理(如开发、测试、生产环境),Spring Boot通过@Profile注解和application-{profile}.yml文件来实现多环境的配置切换。

@Profile("dev")
@Bean
public DataSource devDataSource() {
return new HikariDataSource();
}

优点

  • 功能丰富:Spring Boot依赖于ApplicationContext提供的自动配置、事件机制、国际化、环境管理等功能,简化了开发过程。
  • 自动管理Bean的生命周期:Spring Boot通过ApplicationContext自动管理Bean的初始化、依赖注入和销毁,开发者无需手动控制Bean的生命周期。
  • 启动效率高ApplicationContext在Spring Boot应用启动时会提前初始化所有Bean,确保依赖关系清晰,避免运行时的依赖问题。

使用场景

  • 企业级应用:Spring Boot中,ApplicationContext广泛用于需要管理多个Bean、使用事件机制、配置管理和国际化支持的企业级应用。
  • 自动配置场景:Spring Boot默认使用ApplicationContext来管理自动配置的所有Bean,特别适合快速开发和配置简化的场景。

4. Bean 的定义和管理

4.1.什么是Bean:Spring容器中的核心组件

在Spring框架中,Bean是由Spring IOC容器管理的对象,代表应用程序中的组件。Bean可以是任何Java对象,比如服务类、数据访问层类或其他业务逻辑类。Spring容器通过读取配置或注解,管理这些Bean的生命周期,包括创建、配置、依赖注入和销毁。Bean是Spring应用程序的基本构建块,Spring容器通过依赖注入来管理这些对象的依赖关系,从而实现松耦合设计。


4.2.配置Bean的多种方式

Spring提供了三种主要方式来定义和配置Bean:基于XML配置、基于注解配置、基于Java配置类。这三种方式可以根据项目需求选择或结合使用。

4.2.1. 基于XML配置

概念

在Spring的早期版本中,XML文件是最主要的Bean配置方式。开发者通过在XML文件中显式定义Bean及其依赖关系,Spring容器在启动时会读取这些配置文件,并根据配置创建相应的Bean实例和管理它们的依赖。

示例:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

<!-- 定义一个Bean -->
<bean id="myService" class="com.example.MyService">
<property name="repository" ref="myRepository"/>
</bean>

<!-- 定义另一个Bean -->
<bean id="myRepository" class="com.example.MyRepository"/>
</beans>

说明

  • <bean>:定义一个Bean,包含Bean的ID和类名。ID用于标识该Bean,类名是该Bean对应的Java类。

  • <property>:用来注入依赖。ref属性指定引用的其他Bean,如上面myService依赖了myRepository

特点

  • 集中配置:所有Bean定义在XML文件中,便于整体管理,但可能导致配置文件冗长,尤其在大型项目中。

  • 易于修改:可以在不修改代码的情况下修改依赖关系,但由于配置与代码分离,调试和理解难度较大。


4.2.2. 基于注解配置

概念

为了简化XML配置,Spring引入了基于注解的配置方式。通过在Java类上使用特定的注解(如@Component@Service@Repository等),Spring容器可以自动扫描类路径,并将这些类注册为Bean。Spring Boot尤其依赖注解配置,自动装配大量Bean,减少手动配置的工作量。

常用注解

  • @Component:通用组件Bean。

  • @Service:用于标注业务逻辑层的Bean。

  • @Repository:用于标注数据访问层的Bean。

  • @Controller:用于标注控制层的Bean(通常用于Spring MVC)。

示例:

import org.springframework.stereotype.Component;

@Component
public class MyService {

private final MyRepository myRepository;

// 通过构造函数注入依赖
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}

public void performAction() {
myRepository.doSomething();
}
}

@Component
public class MyRepository {
public void doSomething() {
System.out.println("Doing something in the repository");
}
}

说明

  • @Component:将MyServiceMyRepository注册为Bean,Spring容器会自动扫描这些类,并将它们注入到IOC容器中。

  • 构造函数注入:MyService通过构造函数接收并依赖MyRepository,容器会自动处理依赖注入。

特点

  • 简化配置:消除了XML配置文件,类本身通过注解直接声明为Bean,代码更加简洁。

  • 自动扫描:Spring容器通过类路径扫描自动注册Bean,不需要显式定义每个Bean,适合快速开发。


4.2.3. 基于Java配置类

概念

Spring 3.0引入了基于Java配置类的方式,使用@Configuration注解类,并通过@Bean注解方法来定义Bean。这种方式更加灵活,允许开发者在配置中添加逻辑控制,还可以结合Spring的其他功能(如条件化加载Bean、配置Profile等)来实现更复杂的场景。

示例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

@Bean
public MyService myService() {
return new MyService(myRepository());
}

@Bean
public MyRepository myRepository() {
return new MyRepository();
}
}

public class MyService {

private final MyRepository myRepository;

public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}

public void performAction() {
myRepository.doSomething();
}
}

public class MyRepository {
public void doSomething() {
System.out.println("Doing something in the repository");
}
}

说明

  • @Configuration:标记该类为配置类,Spring容器会将该类中的@Bean方法返回的对象注册为Bean。

  • @Bean:标记方法,容器会将该方法的返回值作为Bean放入IOC容器中。

特点

  • 灵活性高:Java配置类允许使用逻辑来配置Bean,例如条件化加载Bean、动态生成Bean。

  • 易于调试:相比XML配置,Java配置类可以使用IDE的调试功能,更加便于开发和测试。


4.2.4.总结表格

配置方式概念示例方式特点
基于XML配置通过XML文件定义Bean和依赖<bean>标签集中配置,适合分离代码和配置,文件可能冗长
基于注解配置使用注解自动扫描类路径并注册Bean@Component等注解简化配置,自动扫描,代码更加简洁
基于Java配置类通过Java类和方法定义Bean,使用逻辑控制配置@Configuration@Bean高灵活性,支持逻辑控制和动态Bean注册

5. Bean的作用域(Scope)

作用域决定了Spring容器如何管理Bean的实例。Spring默认会为每个@Service Bean分配一个单例作用域(singleton),即整个应用程序中只有一个实例。但如果需要,Spring允许我们为@Service Bean设置其他作用域。

常见的作用域: 

5.1 singleton(单例,默认作用域)

  • 默认作用域:当你使用@Service时,如果不指定作用域,Spring会默认使用singleton作用域。
  • 单例模式:表示Spring容器中每个Bean在整个应用中只有一个实例。无论你在应用的哪个部分引用这个Bean,Spring都会返回同一个实例。
  • 优点:单例模式节省内存和提高性能,因为在整个应用生命周期中只有一个实例。 

示例:

@Service
public class UserService {
// 默认是 singleton
}

singleton作用域下,Spring在启动时创建UserService实例,并在整个应用程序中共享同一个实例。 

5.2 prototype(原型作用域)

  • 多实例模式:每次需要这个Bean时,Spring都会创建一个新的实例。
  • 使用场景:当你希望每次访问时都能获得一个新的@Service对象实例,而不是共享一个单例。
  • 注意:Spring仅负责创建新实例,不管理Bean的生命周期(如销毁)。因此,使用prototype时,Bean的销毁需要手动处理。

示例:

@Service
@Scope("prototype")
public class ReportService {
// 每次注入时都会创建一个新实例
}

prototype作用域下,每次请求ReportService时,Spring都会创建一个新的实例。

5.3 request(仅适用于Web应用)

  • 请求作用域:表示每次HTTP请求都会创建一个新的Bean实例。这个作用域仅在Web应用中使用,常用于处理与单个HTTP请求相关的业务逻辑。
  • 使用场景:当你希望每个HTTP请求都有一个独立的@Service实例时使用。

示例:

@Service
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class RequestScopedService {
// 每次HTTP请求都会创建一个新的实例
}

5.4 session(仅适用于Web应用)

  • 会话作用域:在一个HTTP会话(HttpSession)内共享同一个Bean实例。当会话结束时,Bean也会销毁。
  • 使用场景:当你希望一个用户的会话中始终共享同一个@Service实例时使用。

示例:

@Service
@Scope(value = WebApplicationContext.SCOPE_SESSION)
public class SessionScopedService {
// 在一个会话中,实例是共享的
}

5.5 总结 

作用域类型适用范围实例化频率生命周期适用场景
singleton所有应用容器启动时创建一个实例全局共享默认作用域,适用于大部分情况
prototype所有应用每次请求时创建新实例由容器外管理适合需要每次调用时生成新对象的场景
requestWeb应用每次HTTP请求时创建新实例HTTP请求范围内适用于每个HTTP请求需要独立状态的场景
sessionWeb应用每个HTTP会话创建新实例HTTP会话范围内适用于用户登录会话、购物车等需要在会话内共享的场景

6. Bean的生命周期管理

Spring容器不仅负责创建和管理Bean实例,还负责管理Bean的整个生命周期,包括Bean的创建、初始化、销毁等多个阶段。理解Spring中Bean的生命周期管理有助于开发者更好地控制Bean的行为,确保Bean在应用程序的不同阶段按预期工作。

6.1.生命周期的各个阶段

6.1.1. Bean 定义

Spring 容器会在启动时加载所有 Bean 的配置,这些配置可以通过 XML 文件、注解(如 @Component@Service 等)或者 Java 配置类(使用 @Configuration@Bean)来定义。当你通过这些方式定义一个 Bean 时,Spring 容器并不会立刻创建该对象,而是等待容器启动或者需要使用时才创建。

6.1.2. Bean 的实例化

当程序启动时,Spring 容器会根据定义创建 Bean 的实例。如果你的类是通过注解或配置类标注为 Spring Bean 的,Spring 会负责创建这些对象。这个阶段相当于用 new 来创建一个对象。

6.1.3. 属性注入

属性注入阶段是 Spring Bean 生命周期中的一个重要环节。在这个阶段,Spring 的重点是为每个 Bean 注入它所需要的依赖。可以将这个阶段理解为 Bean 自身的准备工作,Spring 会确保 Bean 的内部所有依赖(即它需要的其他 Bean)都已经正确注入并可用。

关键点:

  • 只在 Bean 内部进行:这一阶段的操作只是在当前 Bean 的内部完成,Spring 会分析 Bean 需要的依赖,并为其注入相应的其他 Bean。这是为了让 Bean 自己的属性或方法具备必要的依赖,从而能够正常工作。

  • 不涉及外部类的使用:虽然此时 Bean 的内部依赖已经准备就绪,但这个阶段并不涉及外部类如何使用这个 Bean。换句话说,Bean 的依赖已经满足,但 Bean 还没有真正参与到应用程序的实际业务逻辑中。

  • 例如:一个 OrderService 需要 PaymentService 的支持,Spring 会在这个阶段将 PaymentService 注入到 OrderService 中,但此时 OrderService 还没有被外部类调用或使用,它只是做好了内部依赖的准备。

总结来说,属性注入阶段 的核心是让 Bean 自身的依赖注入完成,保证其内部所需要的其他 Bean 都已经正确注入,但这个 Bean 还没有被应用程序中的其他类使用。

6.1.4. Bean的初始化

在依赖注入完成后,如果Bean实现了InitializingBean接口,Spring会调用其afterPropertiesSet()方法,或通过@PostConstruct注解指定的方法进行进一步初始化。这是Bean完成依赖注入后的额外配置阶段。

6.1.5 Bean的使用

使用阶段是 Spring Bean 生命周期中的下一个重要环节。在这个阶段,应用程序的其他类(如控制器、服务层等)开始通过 @Autowired 等方式使用那些已经完成依赖注入的 Bean。这时,Bean 已经完成了初始化并准备好参与应用程序的实际业务逻辑。

关键点

  • Bean 可以被外部类使用:与属性注入阶段不同,使用阶段的重点是外部类如何使用这些已经准备好的 Bean。Spring 容器中的其他类可以通过 @Autowired 注入这些 Bean,并在需要时调用它们的功能。

  • 参与实际业务逻辑:在使用阶段,Bean 开始真正参与到应用程序的核心业务逻辑中。例如,控制器类可能会通过注入的服务类来处理用户请求,执行数据库操作,或者完成复杂的业务逻辑处理。

  • 例如:在属性注入阶段,OrderService 已经获得了 PaymentService 的支持;而在使用阶段,OrderController 通过 @Autowired 注入了 OrderService,并调用它的 processOrder() 方法,开始执行订单处理的实际业务逻辑。 

6.1.6. Bean的销毁

在应用程序关闭或Spring容器销毁时,Spring会销毁所有的单例(singleton)作用域的Bean。如果Bean实现了DisposableBean接口,Spring会调用其destroy()方法,或者通过@PreDestroy注解指定的方法进行清理操作。

6.2.自定义Bean的初始化和销毁

自定义初始化

Spring容器在Bean创建完毕并完成依赖注入后,允许开发者在Bean初始化阶段执行一些额外的操作。Spring提供了三种常用方式让开发者自定义Bean的初始化行为:

  • @PostConstruct 注解
  • InitializingBean 接口
  • init-method 配置

自定义销毁

在Spring容器关闭时,单例作用域的Bean会被销毁,Spring允许开发者在销毁阶段执行自定义的清理操作。Spring也提供了三种常用方式来定义Bean销毁时的逻辑:

  • @PreDestroy 注解
  • DisposableBean 接口
  • destroy-method 配置

6.2.1. 使用 @PostConstruct@PreDestroy 注解

@PostConstruct@PreDestroy 是JDK提供的标准注解,Spring支持使用这两个注解来定义Bean的初始化和销毁方法。@PostConstruct会在Bean完成依赖注入之后被调用,@PreDestroy则会在Spring容器销毁Bean之前执行。

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class MyService {

// 初始化方法
@PostConstruct
public void init() {
System.out.println("MyService: Bean is going through init (@PostConstruct)");
}

public void performAction() {
System.out.println("MyService: Performing some action");
}

// 销毁方法
@PreDestroy
public void cleanup() {
System.out.println("MyService: Bean is going to be destroyed (@PreDestroy)");
}
}

说明

  • @PostConstruct 注解的方法会在Spring完成依赖注入后立即调用。此时,所有依赖Bean都已经准备就绪。
  • @PreDestroy 注解的方法会在Spring容器关闭或Bean销毁前执行,可以用来释放资源或执行清理任务。

6.2.2. 使用 InitializingBeanDisposableBean 接口

Spring提供的InitializingBean接口和DisposableBean接口也可以用来定义Bean的初始化和销毁逻辑。实现这些接口后,开发者可以分别通过afterPropertiesSet()方法和destroy()方法来进行初始化和销毁操作。

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class MyAnotherService implements InitializingBean, DisposableBean {

// 初始化方法
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("MyAnotherService: Bean is going through init (afterPropertiesSet)");
}

public void performAction() {
System.out.println("MyAnotherService: Performing some action");
}

// 销毁方法
@Override
public void destroy() throws Exception {
System.out.println("MyAnotherService: Bean is being destroyed (destroy)");
}
}

说明

  • afterPropertiesSet() 方法是 InitializingBean 接口的方法,在Bean的依赖注入完成后自动调用。
  • destroy() 方法是 DisposableBean 接口的方法,在Bean被销毁时执行。

6.2.3. 使用 init-methoddestroy-method 配置

如果不想在代码中直接使用注解或实现接口,还可以在Spring的配置文件中(XML或Java配置类)指定初始化和销毁方法。此方式通常用于配置外部类的初始化和销毁逻辑。

XML配置

<bean id="myExternalService" class="com.example.MyExternalService" init-method="customInit" destroy-method="customDestroy"/>

Java配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public MyExternalService myExternalService() {
return new MyExternalService();
}
}

public class MyExternalService {

public void customInit() {
System.out.println("MyExternalService: Custom init-method called");
}

public void customDestroy() {
System.out.println("MyExternalService: Custom destroy-method called");
}
}

说明

  • init-methoddestroy-method 是通过Spring配置文件(XML或Java配置类)指定的,适合不能修改源代码的第三方类。

6.3.BeanPostProcessor 的作用

BeanPostProcessor 是Spring框架中的一个扩展接口,允许在Spring容器管理的每个Bean的初始化前后进行自定义的处理。它提供了两个关键的回调方法:postProcessBeforeInitializationpostProcessAfterInitialization,用于在Bean的生命周期中插入特定的逻辑。

BeanPostProcessor 默认对容器中的所有Bean进行处理,不过开发者可以在实现中选择性地对某些Bean进行操作,例如根据Bean的类型或名称进行过滤。常见的场景中,BeanPostProcessor可以用于对某些特定类型的Bean进行增强、代理或者修改属性。

6.3.1.BeanPostProcessor 的两个关键方法

postProcessBeforeInitialization(Object bean, String beanName)

  • 该方法在Bean的属性注入完成之后,初始化方法(如@PostConstructafterPropertiesSet)执行之前调用。
  • 适用于在Bean初始化之前进行某些处理,比如修改Bean的某些属性,或做一些预处理。
  • 返回的Bean可以是原始Bean或修改后的Bean。

postProcessAfterInitialization(Object bean, String beanName)

  • 该方法在Bean的初始化方法执行完毕后调用。
  • 适用于增强Bean的功能,或者对Bean进行动态代理操作。
  • 返回的Bean可以是原始Bean或代理后的Bean。

BeanPostProcessor的完整代码示例

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

// 在Bean初始化之前调用
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("Before Initialization: Bean Name - " + beanName);

// 仅对特定的Bean类型进行处理
if (bean instanceof MyService) {
System.out.println("Modifying MyService before initialization.");
((MyService) bean).setServiceName("ModifiedServiceName");
}

// 返回原始Bean或修改后的Bean
return bean;
}

// 在Bean初始化之后调用
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("After Initialization: Bean Name - " + beanName);

// 可以进一步增强Bean或返回代理对象
if (bean instanceof MyService) {
System.out.println("Modifying MyService after initialization.");
// 可以在此返回Bean的代理类
}

// 返回原始Bean或增强后的Bean
return bean;
}
}

说明:

  • postProcessBeforeInitialization 中,我们可以修改Bean的属性。例如,如果发现某个Bean的属性未按预期值设置,可以在这个阶段进行调整。
  • postProcessAfterInitialization 中,我们可以对已经初始化的Bean进行增强或代理操作,特别是在AOP场景中,用于创建Bean的代理对象。

6.3.2.常见的使用场景

1. 日志记录与监控

BeanPostProcessor 可以用于对每个Bean的初始化过程进行日志记录和监控。在初始化前后插入日志,能够清楚地知道每个Bean的生命周期状态,特别是在大型应用中,可以帮助开发者排查问题。

System.out.println("Before Initialization: " + beanName);
System.out.println("After Initialization: " + beanName);
2. 动态代理与AOP增强

BeanPostProcessor 是Spring AOP实现的基础之一,Spring在创建Bean的过程中使用postProcessAfterInitialization来对目标Bean进行代理,将切面逻辑织入Bean中。通过这种方式,开发者可以在Bean的某些方法执行前后插入自定义逻辑。

if (bean instanceof SomeService) {
// 返回该Bean的代理对象,增加AOP逻辑
return Proxy.newProxyInstance(bean.getClass().getClassLoader(),
bean.getClass().getInterfaces(), new CustomInvocationHandler(bean));
}
3. 条件化处理特定的Bean

有时,开发者只想处理某些特定类型的Bean,或者根据Bean的名称进行筛选。通过在BeanPostProcessor中对Bean的类型或名称进行判断,可以有选择地对某些Bean进行处理,而非处理所有Bean。

if (bean instanceof MyService) {
// 只处理 MyService 类型的Bean
((MyService) bean).setServiceName("AutoModifiedServiceName");
}
4. 与第三方框架集成

BeanPostProcessor 可以用来在Bean初始化时,与第三方框架进行集成。比如,在某些情况下,你可以在Bean的初始化过程中,自动配置某些资源(如数据库连接、缓存配置等),将这些配置注入到Bean中。

5. 修改Bean的属性

有些时候,我们希望在Bean初始化之前或之后修改Bean的某些属性,比如根据应用程序的上下文动态设置Bean的属性。BeanPostProcessor 可以帮助我们在不修改Bean定义代码的前提下,实现这些属性的动态配置。

if (bean instanceof MyService) {
// 在初始化之前修改Bean的属性
((MyService) bean).setServiceName("ModifiedService");
}

6.3.3.执行顺序

  1. postProcessBeforeInitialization:此方法在Bean的依赖注入完成之后,初始化方法执行之前被调用。适用于在Bean初始化之前对Bean做一些前置处理或属性修改。

  2. Bean的初始化方法:如@PostConstruct注解标注的方法、InitializingBean.afterPropertiesSet()方法等。

  3. postProcessAfterInitialization:此方法在Bean初始化方法完成后调用,适用于对Bean进行增强、代理或其他后处理操作。

7. 自动装配(Autowiring)

7.1.自动装配的概念

自动装配(Autowiring) 是Spring框架中一种用于自动管理依赖注入的机制,允许Spring容器根据类型、名称等规则自动将所需的Bean注入到目标Bean中,而无需显式地进行配置。这种方式极大地简化了代码和配置的编写,使得依赖管理更加简洁和高效。

通过自动装配,Spring会在容器中查找符合条件的Bean,并将其注入到需要的地方,开发者不需要手动编写代码来获取这些依赖。常见的自动装配方式包括:基于类型、基于名称和基于优先级的装配


7.2.自动装配的类型

7.2.1. @Autowired 注解:根据类型自动装配

@Autowired 是Spring框架中最常用的自动装配注解,用于根据类型自动将依赖注入到目标Bean中。Spring会在容器中查找类型匹配的Bean,并将其注入到标注了@Autowired的字段、构造函数或Setter方法中。

  • 字段注入@Autowired可以标注在字段上,Spring会自动注入符合类型的Bean。
  • 构造函数注入:通过构造函数注入依赖时,@Autowired可以标注在构造函数上。
  • Setter方法注入:使用@Autowired标注在Setter方法上进行依赖注入。

示例代码:字段注入

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

@Autowired // 根据类型自动装配
private MyRepository myRepository;

public void performAction() {
myRepository.doSomething();
}
}

示例代码:构造函数注入

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

private final MyRepository myRepository;

@Autowired
public MyService(MyRepository myRepository) { // 构造函数注入
this.myRepository = myRepository;
}

public void performAction() {
myRepository.doSomething();
}
}

示例代码:Setter方法注入

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyService {

private MyRepository myRepository;

@Autowired
public void setMyRepository(MyRepository myRepository) { // Setter方法注入
this.myRepository = myRepository;
}

public void performAction() {
myRepository.doSomething();
}
}

7.2.2. @Qualifier 注解:与 @Autowired 结合使用,根据名字自动装配

在某些情况下,Spring容器中可能存在多个相同类型的Bean,此时Spring会遇到装配的歧义,无法确定应该注入哪个Bean。为了避免冲突,Spring提供了@Qualifier注解,允许开发者通过Bean的名字来指定具体注入的Bean。@Qualifier通常与@Autowired一起使用。

典型场景

  • 接口有多个实现类时。
  • Spring容器中有多个相同类型的Bean时。

示例代码:多个相同类型的Bean

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class MyService {

@Autowired
@Qualifier("specificRepository") // 指定注入的Bean名称
private MyRepository myRepository;

public void performAction() {
myRepository.doSomething();
}
}

@Component("specificRepository")
public class SpecificRepository implements MyRepository {
@Override
public void doSomething() {
System.out.println("SpecificRepository doing something");
}
}

@Component("defaultRepository")
public class DefaultRepository implements MyRepository {
@Override
public void doSomething() {
System.out.println("DefaultRepository doing something");
}
}

在这个示例中,Spring容器中有两个MyRepository类型的Bean,分别是SpecificRepositoryDefaultRepository。通过@Qualifier,我们明确指定注入的是名为specificRepository的Bean,解决了多个相同类型Bean的冲突问题。


7.2.3. @Primary 注解:优先选择特定的Bean进行装配

当容器中有多个相同类型的Bean时,Spring允许开发者使用@Primary注解来指定默认的Bean。当没有明确指定使用哪个Bean时,Spring会优先选择标注了@Primary的Bean进行注入。它提供了一种默认的优先级机制,避免了每次都使用@Qualifier来显式指定Bean。

示例代码:使用 @Primary

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary // 默认优先选择该Bean进行注入
public class DefaultRepository implements MyRepository {
@Override
public void doSomething() {
System.out.println("DefaultRepository doing something");
}
}

@Component
public class SpecificRepository implements MyRepository {
@Override
public void doSomething() {
System.out.println("SpecificRepository doing something");
}
}

在该示例中,Spring容器中有两个类型为MyRepository的Bean。由于DefaultRepository被标注了@Primary,因此当没有使用@Qualifier明确指定时,Spring会优先注入DefaultRepository


7.3.自动装配的默认行为及其解决冲突的方法

7.3.1. 自动装配的默认行为

根据类型自动装配@Autowired 默认根据类型进行自动装配,Spring会在容器中查找类型匹配的Bean并注入到目标对象中。

必须匹配:默认情况下,@Autowired 要求Spring必须找到一个匹配的Bean,否则会抛出NoSuchBeanDefinitionException异常。

解决方法:如果不希望强制注入,可以通过required = false属性来标记依赖为可选。

@Autowired(required = false)
private MyRepository myRepository; // 可选的注入,找不到不会抛异常

7.3.2. 自动装配中的冲突与解决

多个相同类型的Bean:在某些情况下,可能会出现多个符合条件的Bean,导致Spring无法确定应该注入哪个Bean。常见的冲突和解决方法包括:

解决方法:

  • 使用@Qualifier:明确指定要注入的具体Bean的名称。
  • 使用@Primary:为某个Bean标注@Primary,将其设为默认优先注入的Bean。

示例:解决多个相同类型Bean的冲突

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class MyService {

@Autowired
@Qualifier("specificRepository") // 指定注入的Bean
private MyRepository myRepository;

public void performAction() {
myRepository.doSomething();
}
}

@Component("specificRepository")
public class SpecificRepository implements MyRepository {
@Override
public void doSomething() {
System.out.println("SpecificRepository doing something");
}
}

@Component("defaultRepository")
@Primary // 标记为默认注入的Bean
public class DefaultRepository implements MyRepository {
@Override
public void doSomething() {
System.out.println("DefaultRepository doing something");
}
}

在这个示例中,虽然DefaultRepository被标记为@Primary,但由于在MyService中通过@Qualifier明确指定了SpecificRepository,因此Spring会注入specificRepository,覆盖@Primary的默认行为。

8. Bean的依赖关系与循环依赖

8.1. 什么是循环依赖问题

循环依赖是指两个或多个Bean在相互之间形成依赖关系,导致Spring容器在创建这些Bean时陷入相互等待的死循环。例如,BeanA依赖于BeanB,而BeanB又依赖于BeanA,这样的依赖关系会导致Spring容器无法完成Bean的创建和依赖注入。

循环依赖的典型例子

@Component
public class BeanA {
@Autowired
private BeanB beanB;
}

@Component
public class BeanB {
@Autowired
private BeanA beanA;
}

在这个例子中,BeanA依赖BeanB,而BeanB又依赖BeanA,因此形成了循环依赖。如果没有特别处理,Spring在尝试创建这两个Bean时将无法完成依赖注入,导致创建失败。


8.2. Spring如何解决构造函数注入与Setter注入中的循环依赖

Spring在面对构造函数注入Setter方法注入/字段注入时,处理循环依赖的方式是不同的。

8.2.1.构造函数注入中的循环依赖

  • 无法解决:Spring无法解决构造函数注入中的循环依赖问题。构造函数注入意味着在实例化Bean的过程中,必须通过构造函数传递所有依赖项。如果两个Bean相互依赖,那么Spring在实例化Bean时会陷入死循环,无法完成依赖的注入。

示例:构造函数注入中的循环依赖

@Component
public class BeanA {

private final BeanB beanB;

@Autowired
public BeanA(BeanB beanB) {
this.beanB = beanB;
}
}

@Component
public class BeanB {

private final BeanA beanA;

@Autowired
public BeanB(BeanA beanA) {
this.beanA = beanA;
}
}

在这个例子中,BeanABeanB通过构造函数注入相互依赖。Spring在尝试实例化BeanA时,需要先创建BeanB,但BeanB的构造函数又依赖BeanA,导致循环依赖无法解决。最终Spring会抛出BeanCurrentlyInCreationException异常。

为什么构造函数注入无法解决循环依赖?

在构造函数注入中,Spring在创建Bean时必须通过构造函数传递所有的依赖对象,因此无法跳过依赖的创建。而且,当一个Bean依赖另一个未创建完成的Bean时,Spring无法提前引用这个未初始化的对象。由于构造函数参数必须是已经完全准备好的依赖对象,Spring不能通过延迟初始化或三级缓存来解决这个问题。


8.2.2.Setter方法注入或字段注入中的循环依赖

  • 可以解决:Spring可以通过三级缓存机制解决Setter方法注入字段注入中的循环依赖问题。这是因为在Setter方法注入或字段注入的情况下,Bean的实例化和依赖注入是分离的。Spring可以先创建Bean的实例对象,而不急于注入依赖,这使得Spring可以将这个“半成品Bean”存入缓存中,允许其他Bean在初始化时引用这个尚未完全初始化的Bean。
三级缓存机制的工作原理:
  • 一级缓存singletonObjects):存储完全初始化的单例Bean。
  • 二级缓存earlySingletonObjects):存储部分初始化的早期Bean引用,主要用于代理对象。
  • 三级缓存singletonFactories):存储创建Bean实例的工厂对象,在Bean尚未完成初始化时,通过工厂获取Bean的“半成品”。

工作流程

  1. Spring首先调用构造函数实例化Bean,但不进行依赖注入。这时,Spring将这个“未完成初始化”的Bean放入三级缓存中。
  2. 当另一个Bean需要依赖这个“半成品Bean”时,Spring从三级缓存中提前引用它,即使它尚未完全初始化。
  3. 最后,等所有Bean实例化完成后,Spring再回过头来进行依赖注入和初始化。

示例:通过Setter方法注入解决循环依赖

@Component
public class BeanA {

private BeanB beanB;

@Autowired
public void setBeanB(BeanB beanB) {
this.beanB = beanB;
}
}

@Component
public class BeanB {

private BeanA beanA;

@Autowired
public void setBeanA(BeanA beanA) {
this.beanA = beanA;
}
}

在这个示例中,BeanABeanB通过Setter方法进行依赖注入。Spring可以先通过构造函数创建BeanABeanB的实例对象,并将它们的“半成品”放入缓存,允许相互引用。最后,Spring再为它们完成依赖注入,解决循环依赖问题。

为什么三级缓存可以解决Setter方法或字段注入的循环依赖?
  • 三级缓存机制允许Spring将“半成品Bean”暂时存储,先创建对象实例,再进行依赖注入。
  • 构造函数注入无法跳过依赖注入这一步,因为构造函数要求所有依赖在对象实例化时就必须传入。而Setter方法注入和字段注入则可以分阶段处理:首先通过构造函数创建对象实例,随后再进行依赖注入。

8.3. 循环依赖可能导致的问题及其解决方案

8.3.1. 构造函数注入中的循环依赖问题

  • 问题:构造函数注入在创建Bean时,要求所有依赖对象必须在构造函数中传入。如果这些Bean互相依赖,Spring将无法创建它们,导致循环依赖无法解决。
  • 结果:Spring会抛出BeanCurrentlyInCreationException异常,提示当前Bean正在创建中,无法完成依赖注入。

解决方案

1.使用Setter方法或字段注入

示例

@Component
public class BeanA {
private BeanB beanB;

@Autowired
public void setBeanB(BeanB beanB) {
this.beanB = beanB;
}
}

@Component
public class BeanB {
private BeanA beanA;

@Autowired
public void setBeanA(BeanA beanA) {
this.beanA = beanA;
}
}

将构造函数注入改为Setter方法注入或字段注入,允许Spring通过三级缓存机制解决循环依赖问题。

2.使用@Lazy注解延迟加载

示例

@Component
public class BeanA {

private final BeanB beanB;

@Autowired
public BeanA(@Lazy BeanB beanB) {
this.beanB = beanB;
}
}

@Component
public class BeanB {

private final BeanA beanA;

@Autowired
public BeanB(@Lazy BeanA beanA) {
this.beanA = beanA;
}
}

如果循环依赖无法避免,可以使用@Lazy注解来推迟Bean的初始化。@Lazy会告诉Spring在需要时再初始化依赖的Bean,而不是在容器启动时立即加载。

8.3.2. 堆栈溢出(StackOverflowError)

  • 问题:如果没有正确处理循环依赖,Spring可能陷入无限递归,导致堆栈溢出(StackOverflowError)。这通常发生在递归调用的场景中,尤其是没有通过合适的依赖注入策略打破循环依赖时。

解决方案

1.通过重构减少依赖

示例:引入Service来协调BeanABeanB的依赖关系。

@Component
public class Service {

private final BeanA beanA;
private final BeanB beanB;

@Autowired
public Service(BeanA beanA, BeanB beanB) {
this.beanA = beanA;
this.beanB = beanB;
}

public void doSomething() {
beanA.doSomething();
beanB.doSomething();
}
}

尽量减少Bean之间的直接依赖。如果Bean之间有复杂的依赖关系,可以通过引入中间层或服务来协调这些依赖,避免直接的循环依赖。

9. 工厂Bean(FactoryBean)

9.1. 什么是 FactoryBean

FactoryBean 是Spring框架中的一个特殊接口,提供了一种自定义创建Bean对象的机制。与普通Bean不同,FactoryBean允许开发者定制Bean的创建过程,而不是通过Spring容器的默认机制来实例化Bean。

当Spring容器遇到实现了FactoryBean接口的Bean时,Spring会调用getObject()方法来获取实际的Bean,而不是直接使用FactoryBean实例本身。这种机制非常适合创建复杂的Bean,或者需要动态生成Bean的场景。

FactoryBean的三个主要方法:

  • T getObject():返回实际的Bean对象,这个方法定义了如何创建该Bean。
  • boolean isSingleton():决定该Bean是单例模式还是多例模式(默认为单例)。
  • Class<?> getObjectType():返回这个FactoryBean所管理的Bean的类型。

通过这些方法,开发者可以灵活地控制Bean的生成和生命周期。

9.2.FactoryBean 的核心作用总结:

  • 自定义创建逻辑:通过实现FactoryBean接口中的getObject()方法,你可以完全掌控Bean的生成方式。Spring容器在需要这个Bean时,会调用getObject()方法来获取它,而不是直接使用默认的创建方式。
  • 处理复杂的Bean创建过程:当一个对象的创建过程涉及多个步骤、外部资源初始化、或复杂的依赖时,FactoryBean允许你在创建过程中插入自定义逻辑。例如,加载配置文件、动态选择实现类、连接外部服务等。
  • 动态和条件化创建BeanFactoryBean允许在运行时动态决定返回哪个Bean对象。这对于根据不同的条件或状态来选择具体实现的场景非常有用。
  • 创建代理对象:在某些高级场景下,特别是AOP中,FactoryBean可以用于创建动态代理对象,增强原有Bean的功能,如添加事务、日志等功能。

示例:使用FactoryBean创建复杂对象 

import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;

@Component
public class MyFactoryBean implements FactoryBean<MyService> {

@Override
public MyService getObject() throws Exception {
// 在这里加入创建MyService实例的复杂逻辑
return new MyService();
}

@Override
public Class<?> getObjectType() {
return MyService.class;
}

@Override
public boolean isSingleton() {
return true; // 是否为单例
}
}

在这个例子中,MyFactoryBean通过实现FactoryBean<MyService>接口,提供了自定义的getObject()方法来生成MyService实例。Spring容器会通过getObject()方法返回的实例来注入,而不是直接使用MyFactoryBean本身。

8.3. FactoryBean 与普通Bean的区别

FactoryBean与普通Bean的区别主要体现在创建方式返回对象灵活性上:

1. 创建方式的不同

  • 普通Bean:Spring通过反射等机制自动创建普通Bean,开发者无需干预。Spring容器根据Bean的定义(如构造函数、Setter方法)自动实例化对象,完成依赖注入。
  • FactoryBean:通过实现FactoryBean接口,开发者可以自定义Bean的创建过程。FactoryBean提供了对对象创建的精细化控制,允许插入自定义的逻辑,如复杂的初始化、条件判断、外部资源加载等。这使得FactoryBean适合处理创建过程复杂的Bean。

2. 返回对象的不同

  • 普通Bean:Spring容器管理的普通Bean,返回的是该类的实例对象本身。
  • FactoryBean:Spring容器通过调用FactoryBean.getObject()方法返回由FactoryBean创建的对象,而不是返回FactoryBean本身。如果需要获取FactoryBean本身,而非它创建的Bean,需要在Bean名称前加上&符号(如&myFactoryBean)。

3. 灵活性

  • 普通Bean:普通Bean的创建和管理过程是固定的,适合大多数普通对象的实例化。Spring通过配置(如注解、XML或Java配置)自动创建并管理这些Bean。
  • FactoryBean:允许高度自定义Bean的创建过程。开发者可以在FactoryBean中加入复杂的逻辑,比如代理对象的生成、运行时动态决定返回哪个对象、或者将外部资源转化为Spring管理的Bean对象。

10. 环境和属性管理

  • 通过 @Value 注解注入外部属性
  • 使用 PropertySource 加载外部配置文件
  • 使用 Environment 接口管理环境变量和配置

在Spring应用程序中,环境和属性管理是确保应用程序能够灵活响应不同环境(如开发、测试、生产)的配置需求的重要机制。Spring提供了多种方式来处理外部配置,包括注入外部属性、加载自定义配置文件、以及管理环境变量。

application.properties文件是Spring Boot的默认配置文件格式之一。你可以通过@Value或@ConfigurationProperties注解将这些配置值导入到配置类或其他组件中。

10.1. 通过 @Value 注解注入外部属性

@Value注解用于从application.properties文件中读取单个配置项。

导入过程:

  1. Spring Boot在启动时会自动读取application.properties中的内容。
  2. 当Spring容器创建带有@Value注解的bean时,会将配置文件中的对应值注入到bean的字段中。

示例:

application.properties:

my.custom.url=http://example.com
my.custom.timeout=5000

AppConfig.java

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

@Value("${my.custom.url}")
private String url;

@Value("${my.custom.timeout}")
private int timeout;

@Bean
public MyService myService() {
return new MyServiceImpl(url, timeout);
}
}
  • @Value("${my.custom.url}")会将application.properties中my.custom.url的值(http://example.com)注入到url字段中。
  • @Value("${my.custom.timeout}")会将my.custom.timeout的值(5000)注入到timeout字段中。

10.2 使用@ConfigurationProperties读取配置

@ConfigurationProperties注解适用于将一组相关的配置映射到Java对象中,通常用来处理较为复杂的配置。

导入过程:

  1. 定义POJO类并标注@ConfigurationProperties,指定要映射的配置前缀。
  2. Spring Boot启动时,将配置文件中符合前缀的属性值注入到这个类的对应字段中。
  3. 通过在配置类中注入该POJO类,你可以直接使用这些配置值。

示例:

application.properties:

my.service.url=http://example.com
my.service.timeout=5000

MyServiceProperties.java:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "my.service")
public class MyServiceProperties {

private String url;
private int timeout;

// getters and setters
public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public int getTimeout() {
return timeout;
}

public void setTimeout(int timeout) {
this.timeout = timeout;
}
}

AppConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

private final MyServiceProperties myServiceProperties;

public AppConfig(MyServiceProperties myServiceProperties) {
this.myServiceProperties = myServiceProperties;
}

@Bean
public MyService myService() {
return new MyServiceImpl(myServiceProperties.getUrl(), myServiceProperties.getTimeout());
}
}
  • MyServiceProperties类通过@ConfigurationProperties(prefix = "my.service")将application.properties中的my.service.url和my.service.timeout映射到类的url和timeout字段中。
  • 通过在AppConfig类中注入MyServiceProperties,你可以直接使用这些配置值来创建MyService的bean。

10.3 使用 @PropertySource 加载外部配置文件

@PropertySource 注解允许我们从特定的外部配置文件(如.properties文件)中加载属性,并将这些属性注入到Spring容器中。它常用于加载非默认的配置文件,比如自定义的配置文件。

用法和特点

1.加载自定义配置文件:如果你有一个额外的配置文件,比如 custom.properties,可以通过 @PropertySource 将其加载到Spring环境中。

示例

# custom.properties
custom.property1=value1
custom.property2=value2

2.在Spring配置类中加载该文件

@Configuration
@PropertySource("classpath:custom.properties")
public class CustomConfig {
@Value("${custom.property1}")
private String property1;

@Value("${custom.property2}")
private String property2;
}

通过这种方式,Spring会将custom.properties中的属性加载到环境中,供应用程序使用。

3.可加载多个文件@PropertySource 支持同时加载多个配置文件。

示例

@PropertySource({"classpath:custom1.properties", "classpath:custom2.properties"})
public class MultiConfig {
// 加载多个配置文件
}

4.文件位置的灵活性@PropertySource 支持从 classpath 或文件系统中加载配置文件。

10.4. 使用 Environment 接口管理环境变量和配置

Environment 是Spring中的一个接口,用于访问应用程序运行时的环境变量和属性配置。通过Environment接口,开发者可以方便地获取系统环境变量、外部配置文件中的属性值,以及命令行参数等。

10.4.1.Environment 的常用方法:

  • getProperty(String key):获取指定键的属性值。
  • getRequiredProperty(String key):获取属性值,如果属性不存在则抛出异常。
  • getActiveProfiles():获取当前激活的Profile配置。
  • getDefaultProfiles():获取默认的Profile配置。

示例:获取环境中的属性

你可以通过注入Environment对象来访问环境变量或配置文件中的属性:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class EnvironmentConfig {

@Autowired
private Environment environment;

public void printEnvironmentProperties() {
String appName = environment.getProperty("app.name");
String appVersion = environment.getProperty("app.version", "1.0.0"); // 如果属性不存在,设置默认值
String javaHome = environment.getProperty("JAVA_HOME"); // 获取系统环境变量

System.out.println("App Name: " + appName);
System.out.println("App Version: " + appVersion);
System.out.println("JAVA_HOME: " + javaHome);
}
}

在这个示例中,我们通过Environment对象获取了application.properties文件中的app.nameapp.version属性,同时还获取了系统环境变量JAVA_HOMEEnvironment接口可以轻松访问系统的环境变量、配置文件以及命令行参数。

10.4.2.使用 Environment 接口的意义

1.统一管理外部配置和环境变量

Environment 接口可以访问各种配置源,包括:

  • 配置文件中的属性(如 application.propertiesapplication.yml)。
  • 系统环境变量。
  • JVM系统属性(如 -D 参数)。
  • 命令行参数。
  • @PropertySource 加载的自定义配置文件。

2.灵活的Profile管理

Spring的 Profile 是一种机制,可以根据不同的环境激活不同的配置(如开发环境、生产环境等)。通过Environment接口,你可以轻松地获取当前激活的Profile并根据不同的Profile来调整应用行为。这对于支持多环境部署的应用程序非常重要。

示例:根据不同的Profile加载不同的配置

@Autowired
private Environment environment;

public void printEnvironmentDetails() {
String appName = environment.getProperty("app.name");
String profile = environment.getActiveProfiles()[0]; // 获取当前激活的Profile

System.out.println("App Name: " + appName);
System.out.println("Active Profile: " + profile);
}

这种统一性使得你可以通过Environment接口从多个配置源中获取值,而不需要依赖多个不同的机制。这对于现代的云原生应用来说尤其有用,应用可能在不同环境中运行(开发、测试、生产等),而且配置可能来自不同的地方(配置文件、环境变量、云服务等)。

11. Spring IOC 容器中的事件机制

Spring的事件机制是一种基于观察者模式的实现,允许在Spring应用中进行松耦合的组件交互。通过这种机制,Spring容器中的对象可以发布和监听事件,事件机制常用于解耦组件间的交互,尤其是在异步处理任务或不同模块间的通信中。

11.1. Spring事件机制的原理和用法

Spring事件机制的核心原理可以用观察者模式来解释:

  • 发布者(Publisher):事件发布者负责在应用中生成事件并将其发送到Spring的事件系统。通常使用ApplicationEventPublisher来发布事件。
  • 监听器(Listener):事件监听器负责监听特定类型的事件,并在事件发生时触发相应的处理逻辑。监听器通常使用注解@EventListener方式定义。
  • Spring容器:作为事件传递的中介,Spring容器负责维护事件监听器的注册,并在有事件发布时通知所有相关的监听器进行响应。

当发布者生成事件时,Spring容器会根据事件的类型,将事件发送给所有注册了相应事件类型的监听器。通过这种方式,发布者和监听器之间无需直接交互,解耦了两者的关系。

11.2. 自定义事件类

首先,我们需要创建一个自定义事件类,继承自 ApplicationEvent,这是Spring中所有事件的基类。自定义事件类用于封装事件数据和事件源。

import org.springframework.context.ApplicationEvent;

// 自定义事件类,继承 ApplicationEvent
public class UserRegistrationEvent extends ApplicationEvent {
private String username;

public UserRegistrationEvent(Object source, String username) {
super(source); // 调用父类构造方法,设置事件源
this.username = username;
}

public String getUsername() {
return username;
}
}

说明:

  • 继承 ApplicationEvent:所有Spring事件都必须继承自 ApplicationEvent,这是Spring事件机制的基础类。自定义事件类 UserRegistrationEvent 继承 ApplicationEvent,因此可以被Spring容器识别为事件。

  • super(source) 的作用:在 UserRegistrationEvent 构造函数中,调用 super(source) 将事件源(source)传递给 ApplicationEvent 父类。source 表示事件的触发者(通常是事件发布者),Spring会将这个对象存储起来,以便在事件被处理时,监听器能够知道事件是由哪个对象发布的。

  • 事件携带的信息UserRegistrationEvent 类还包含了一个 username 字段,用于传递用户注册的用户名。这种自定义信息可以帮助监听器在处理事件时获取事件相关的具体数据。


11.3. 事件发布者

事件发布者负责在事件发生时发布事件。Spring通过 ApplicationEventPublisher 提供事件发布功能。事件发布者可以通过依赖注入或者实现 ApplicationEventPublisherAware 接口来获取 ApplicationEventPublisher 的实例。

方式一:通过依赖注入的方式注入 ApplicationEventPublisher

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class UserRegistrationPublisher {

// 通过 @Autowired 注入 ApplicationEventPublisher
@Autowired
private ApplicationEventPublisher eventPublisher;

public void publishEvent(String username) {
// 创建事件对象
UserRegistrationEvent event = new UserRegistrationEvent(this, username);
// 发布事件
eventPublisher.publishEvent(event);
}
}

方式二:通过实现 ApplicationEventPublisherAware 接口

另一种方式是实现 ApplicationEventPublisherAware 接口,以手动注入 ApplicationEventPublisher

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;

@Component
public class UserRegistrationPublisher implements ApplicationEventPublisherAware {

private ApplicationEventPublisher eventPublisher;

// 实现 ApplicationEventPublisherAware 接口的 setApplicationEventPublisher 方法
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.eventPublisher = publisher;
}

public void publishEvent(String username) {
// 创建并发布事件
UserRegistrationEvent event = new UserRegistrationEvent(this, username);
eventPublisher.publishEvent(event);
}
}

说明:

  • 注入 ApplicationEventPublisher:事件发布者可以通过两种方式获得 ApplicationEventPublisher

    • 通过 @Autowired 注入。
    • 通过实现 ApplicationEventPublisherAware 接口,重写 setApplicationEventPublisher 方法。这种方式要求必须实现并重写该方法,否则 ApplicationEventPublisher 无法被注入,事件发布者也就无法发布事件。
  • publishEvent() 方法:事件发布者通过 eventPublisher.publishEvent(event) 将事件发布到Spring容器中。Spring容器会将事件传递给所有监听该事件的监听器。发布者不需要直接与监听器交互,Spring负责事件的分发。


11.4. 事件监听器

事件监听器用于监听并处理Spring容器中的事件。通过 @EventListener 注解来实现事件监听:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class UserRegistrationListener {

// 监听 UserRegistrationEvent
@EventListener
public void handleUserRegistrationEvent(UserRegistrationEvent event) {
System.out.println("处理用户注册事件,发送欢迎邮件给:" + event.getUsername());
// 执行处理逻辑,例如发送邮件等
}
}

说明:

  • @EventListener 注解:通过 @EventListener 注解,任何Spring Bean中的方法都可以被注册为事件监听器。只要方法的参数类型与事件类型匹配,Spring就会在事件发布时自动调用该方法。

  • 事件类型匹配:监听器方法的参数类型决定了它监听的事件类型。在上面的示例中,handleUserRegistrationEvent 方法的参数类型为 UserRegistrationEvent,因此该方法会监听并处理 UserRegistrationEvent 类型的事件。

  • 事件的传递与处理:当 UserRegistrationEvent 事件发布时,Spring容器会自动将事件对象传递给 handleUserRegistrationEvent 方法中的 event 参数,监听器无需手动接收事件。监听器可以在方法体内根据事件携带的数据执行相应的业务逻辑。

12. AOP 与 IOC 的结合

在Spring框架中,AOP(面向切面编程)和IOC(控制反转)结合,能够在不改变核心业务逻辑的情况下动态地将横切关注点(如日志、事务等)应用于目标对象。Spring通过IOC容器管理切面和目标对象的生命周期,自动生成动态代理对象来执行切面逻辑。

12.1. 介绍Spring的AOP(面向切面编程)

AOP(Aspect-Oriented Programming)是一种编程范式,主要用于将横切关注点(Cross-Cutting Concerns)从核心业务逻辑中分离出来。横切关注点包括日志记录、事务管理、安全检查等,它们往往与多处业务逻辑相关。AOP可以让开发者在不修改业务逻辑的情况下,将这些功能灵活地应用到目标对象上。

AOP的核心概念

  • 切面(Aspect):封装横切关注点的模块,如日志切面、事务管理切面等。
  • 通知(Advice):在切面中定义的具体操作,决定切面在目标方法执行的哪个阶段(如前置、后置)执行。
  • 切点(Pointcut):定义了通知应该应用到哪些连接点,通常通过表达式指定。
  • 连接点(Join Point):方法执行等可以插入切面逻辑的特定位置。
  • 目标对象(Target Object):被切面逻辑增强的业务类。
  • 代理(Proxy):Spring AOP会在运行时为目标对象生成一个代理对象,并在代理对象中注入切面逻辑。

12.2. AOP如何与IOC结合,管理切面逻辑

在Spring框架中,AOP和IOC结合在一起,实现了对目标对象的动态代理注入与切面逻辑的无缝管理。Spring通过IOC容器管理切面和目标对象的生命周期,将生成的代理对象替换原始的目标对象,实现切面逻辑的动态注入和管理。

12.2.1.AOP与IOC结合的核心机制

1.切面注册与管理

  • 切面类在Spring中通常被注册为一个Bean,并使用 @Aspect 标注。
  • Spring IOC容器启动时,会扫描@Aspect注解的类,将其注册为Spring Bean,解析切点表达式并识别需要增强的目标对象。

示例:定义一个日志记录切面

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethod() {
System.out.println("方法执行前记录日志...");
}
}

在此示例中,LoggingAspect类定义了一个日志切面,Spring会识别出它是一个切面类,并会在符合切点条件的目标对象上应用该切面逻辑。

2.代理对象的生成与注入

  • Spring AOP会根据切点条件自动为符合条件的目标对象生成代理对象。代理对象会封装切面逻辑,当调用目标方法时先执行切面逻辑,再执行实际的业务方法。
  • 代理对象由Spring容器生成并管理,取代原始目标对象。这样在注入时,Spring会将代理对象注入到依赖该目标对象的其他Bean中。
  • 这意味着,无论方法调用是否涉及切面逻辑,所有对目标对象的调用实际上都是通过代理对象进行的

3.代理对象的拦截与切面执行

  • 代理对象会拦截所有对目标方法的调用,并检查该方法是否符合切点条件。如果符合条件,则按照通知类型(如前置通知、后置通知等)执行切面逻辑。
  • 如果方法不符合切点条件,代理对象会直接调用目标方法,不执行任何切面逻辑。这种机制确保切面逻辑可以灵活应用,而不会影响不相关的方法执行。

12.2.2.IOC容器在AOP中的作用

  • 自动管理切面与目标对象的生命周期:Spring容器负责创建、初始化、管理切面和目标对象。当切面或目标对象被注册到容器中时,容器会自动为目标对象生成代理对象,并在容器内管理代理对象的生命周期。

  • 切面与目标对象的解耦:Spring容器通过AOP代理对象将切面逻辑注入到目标对象,而无需直接修改目标对象的代码,从而实现业务逻辑与切面逻辑的解耦。这种解耦使得切面逻辑可以独立于目标对象动态添加或移除,便于系统的扩展和维护。

  • 将代理对象注入到其他Bean中:Spring会将生成的代理对象注入到依赖该目标对象的其他Bean中,保证所有对目标对象的调用都通过代理对象进行。如果方法符合切面条件,代理对象会执行切面逻辑。


12.3. 动态代理与Spring的AOP机制

在Spring AOP中,动态代理是实现切面逻辑的关键机制。Spring框架会自动为符合切点条件的目标对象生成动态代理,并且通过IOC容器将生成的代理对象而不是原始目标对象注入到其他Bean中。这意味着,无论是否涉及切面逻辑,所有对目标对象的调用都会通过代理对象完成。

12.3.1.Spring AOP如何自动生成代理对象

1.IOC容器扫描和识别切面

  • Spring在容器启动时会扫描所有Bean,识别出标注了@Aspect的切面类。
  • 通过解析切点表达式,Spring可以确定哪些目标对象符合切面逻辑的应用条件。

2.自动创建代理对象并替换目标对象

  • 当Spring容器发现某个目标对象符合切点条件时,会为该对象生成代理对象。代理对象封装了目标对象及其切面逻辑。
  • Spring会自动选择代理方式:如果目标对象实现了接口,使用JDK动态代理;如果没有实现接口,则使用CGLIB动态代理

3.将代理对象注册到IOC容器中

  • Spring将生成的代理对象替换原始目标对象并注册到IOC容器中。所有需要该目标对象的Bean都会获得代理对象,而非原始目标对象。
  • 这意味着,不论调用的目标方法是否触发切面逻辑,所有对目标对象的调用实际上都通过代理对象完成。

12.3.2.Spring代理对象的工作流程

1.拦截方法调用:当其他Bean调用注入的代理对象时,代理对象会拦截方法调用。

2.切面逻辑判断:代理对象会根据切点表达式判断该方法调用是否符合切面条件。

  • 如果符合条件,则在方法执行前后执行切面逻辑(例如前置通知、后置通知等)。
  • 如果不符合条件,则直接调用目标对象的方法,不执行任何切面逻辑。

12.3.3.示例:Spring AOP动态代理的自动注入

假设有一个事务管理的切面TransactionAspect,在com.example.service包下的所有方法前执行事务启动操作。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TransactionAspect {
@Before("execution(* com.example.service.*.*(..))")
public void startTransaction() {
System.out.println("事务开始...");
}
}
  • 定义切面和切点TransactionAspect通过切点表达式execution(* com.example.service.*.*(..)),在com.example.service包下的所有方法前启动事务。
  • 代理对象生成与注册:Spring容器启动时检测到切面,自动为com.example.service包下符合切点条件的目标对象生成代理对象。代理对象替代原始目标对象,代理对象会被注入到其他依赖这些服务的Bean中。
  • 方法调用拦截与切面执行:当其他Bean调用这些服务的方法时,调用的实际上是代理对象的方法。代理对象会在符合切点条件时先执行startTransaction()方法,然后执行目标方法;如果方法不符合切点条件,则直接调用目标方法。

13. Bean的条件创建

13.1 使用 @Conditional 实现条件化的Bean创建

在Spring中,@Conditional注解允许开发者在特定条件满足时才注册某个Bean,这种机制增强了应用的灵活性,使得应用在不同的环境或配置下可以动态调整Bean的加载行为。通过@Conditional,可以让Spring根据运行时的环境、系统属性或其他配置有选择地加载特定的组件。

@Conditional 的基本原理

  • @Conditional注解与条件类配合使用,条件类实现Condition接口,并通过其matches方法定义条件。
  • 当Spring容器加载Bean时,会调用条件类的matches方法判断是否满足条件。如果返回true,则注册该Bean;如果返回false,则跳过Bean的注册。

使用@Conditional的结构

  1. 定义条件类:条件类需要实现Condition接口,并重写matches方法以定义条件逻辑。
  2. 在Bean方法上使用@Conditional:将条件类作为参数传递给@Conditional注解,以便Spring在注册Bean时依据该条件类的逻辑判断是否创建Bean。

示例:条件化地加载不同数据库配置的Bean

假设我们有一个数据库配置类 DatabaseConfig,其中根据数据库类型的不同加载相应的数据源Bean,例如在database.typeMYSQL时加载MySQL数据源,而在database.typePOSTGRESQL时加载PostgreSQL数据源。

DatabaseConfig类代码

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DatabaseConfig {

@Bean
@Conditional(MySQLDatabaseCondition.class)
public DataSource mysqlDataSource() {
// MySQL数据源配置
}

@Bean
@Conditional(PostgreSQLDatabaseCondition.class)
public DataSource postgresDataSource() {
// PostgreSQL数据源配置
}
}

在上述代码中:

  • mysqlDataSource 方法仅在 MySQLDatabaseCondition 条件类的matches方法返回true时注册。
  • postgresDataSource 方法仅在 PostgreSQLDatabaseCondition 条件类的matches方法返回true时注册。

条件类的实现

为实现这一逻辑,我们定义两个条件类:MySQLDatabaseConditionPostgreSQLDatabaseCondition。它们分别判断database.type环境变量是否为MYSQLPOSTGRESQL

MySQLDatabaseCondition类

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class MySQLDatabaseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String dbType = context.getEnvironment().getProperty("database.type");
return "MYSQL".equalsIgnoreCase(dbType);
}
}

PostgreSQLDatabaseCondition类

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class PostgreSQLDatabaseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String dbType = context.getEnvironment().getProperty("database.type");
return "POSTGRESQL".equalsIgnoreCase(dbType);
}
}

matches方法的详解

matches方法是条件判断的核心,返回值决定Bean是否注册:

  • ConditionContext参数:提供应用上下文信息,比如环境配置、Bean定义等。在此例中,我们使用context.getEnvironment()来获取环境信息。
  • AnnotatedTypeMetadata参数:包含注解的元数据信息,虽然本示例中未使用,但在复杂的条件判断中可以利用它获取注解信息。

MySQLDatabaseConditionmatches方法中:

  • 我们使用context.getEnvironment().getProperty("database.type")获取环境变量database.type的值。
  • 如果该值为MYSQL,则返回true,Spring会注册mysqlDataSource Bean;否则返回false,跳过该Bean。

PostgreSQLDatabaseCondition中,类似地,我们判断database.type是否为POSTGRESQL,从而决定是否注册postgresDataSource Bean。

如何设置环境变量

通过以下两种方式设置database.type,以便应用启动时根据条件动态加载数据源:

  • 在application.properties文件中设置

    database.type=MYSQL或database.type=POSTGRESQL
    
  • 通过启动参数设置

    java -jar myapp.jar --database.type=MYSQL
    

应用场景

  • 基于配置的选择性加载:通过配置文件中某个值的不同,可以灵活地加载不同实现,如数据库类型、缓存类型等。
  • 基于环境或系统属性的加载:通过系统属性或环境变量,动态地选择需要加载的组件,确保应用能适配不同的运行环境。
  • 自定义条件:对于复杂的业务逻辑,可以通过自定义条件来控制Bean的注册。

13.2. 使用 @Profile 注解实现不同环境下的Bean配置

在Spring应用中,@Profile注解用于根据不同的环境(Profile)来选择性地注册Bean。通过这种方式,Spring可以在开发、测试、生产等环境中加载适合当前环境的Bean配置,确保应用行为的灵活性和环境适配性。


@Profile 的基本原理

  • @Profile注解可以标注在@Bean方法上或整个@Configuration类上,指定Bean或配置类的环境。
  • 通过设置Spring的spring.profiles.active属性来激活特定的Profile,Spring会在启动时根据激活的Profile来决定加载哪些Bean。
  • @Profile常用于区分环境的特定配置,例如不同环境下的数据库连接、缓存配置等。

使用 @Profile 的方式

可以将@Profile用于不同级别的Bean和配置类,以根据当前环境灵活加载适当的配置。

1.在 @Bean 方法上使用 @Profile

当需要为不同环境定义不同的Bean实现时,可以直接在@Bean方法上标注@Profile

示例:不同环境下的数据库配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

@Bean
@Profile("dev")
public DataSource devDataSource() {
return new DevDataSource(); // 开发环境的数据源配置
}

@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new ProdDataSource(); // 生产环境的数据源配置
}
}

在上面的代码中:

devDataSource方法使用@Profile("dev")注解,表示它仅在dev环境激活时注册。

prodDataSource方法使用@Profile("prod")注解,仅在prod环境激活时注册。

2.在类级别使用 @Profile

如果整个配置类都应该在特定环境下生效,可以将@Profile注解加在@Configuration类上。这种方式适用于整个配置类中的Bean都需要受环境控制的情况。

@Configuration
@Profile("dev")
public class DevConfig {
// 开发环境的特定配置
}

@Configuration
@Profile("prod")
public class ProdConfig {
// 生产环境的特定配置
}

在这种情况下,DevConfig类仅在dev环境激活时加载,ProdConfig类仅在prod环境激活时加载。


如何激活 @Profile

在Spring应用中,@Profilespring.profiles.active属性配合使用,可以在配置文件或启动参数中指定当前的Profile。

application.properties中设置

  • 通过在application.properties文件中设置spring.profiles.active属性来激活某个Profile。例如:
spring.profiles.active=dev
  • 这样Spring会加载标注为@Profile("dev")的所有Bean。

通过启动参数指定

  • 通过命令行参数--spring.profiles.active来指定Profile。例如:
java -jar myapp.jar --spring.profiles.active=prod
  • 这会激活prod Profile,加载生产环境的配置。

使用 @Profile("default") 指定默认配置

Spring允许开发者使用@Profile("default")来指定默认环境下的Bean配置。当没有明确指定Profile时,Spring会加载@Profile("default")标注的Bean。

示例:设置默认数据源

@Configuration
public class DataSourceConfig {

@Bean
@Profile("dev")
public DataSource devDataSource() {
return new DevDataSource();
}

@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new ProdDataSource();
}

@Bean
@Profile("default")
public DataSource defaultDataSource() {
return new DefaultDataSource();
}
}

在这个示例中:

  • spring.profiles.active没有被指定时,Spring会加载@Profile("default")标注的defaultDataSource,提供一个通用的默认配置。
  • spring.profiles.active=devprod时,Spring只会加载对应的配置,而忽略默认配置。

@Profile的实际应用场景

  1. 环境特定配置@Profile可以帮助开发者为不同的环境(如开发、测试、生产)提供合适的Bean配置。例如,开发环境使用调试配置,生产环境使用优化后的配置。

  2. 功能模块的按需加载:通过@Profile,可以在特定环境中启用或禁用某些功能模块。例如,在开发环境启用调试功能,而在生产环境禁用。

  3. 第三方服务的模拟和替换:在测试环境中使用模拟的服务Bean,在生产环境中使用真实的服务配置。例如,使用MockDatabase来模拟数据库连接,而在生产环境中加载真实的数据库配置。

14. 注解与元数据

14.1 如何创建自定义注解来管理Bean

在Spring框架中,自定义注解让Bean管理更灵活。通过自定义注解,可以在应用中轻松地标记和管理Bean,从而简化配置和开发流程。Spring通过自动扫描带有特定注解的类,将它们纳入IOC容器,管理Bean的生命周期。


14.1.1.创建自定义注解的步骤

  1. 定义注解类

    • 使用@interface关键字定义注解。
    • 可以为注解添加属性,属性可以指定默认值,提供给开发者灵活的配置方式。
  2. 应用元注解

    • 使用@Component等元注解使Spring识别并管理自定义注解。
    • 使用@Retention@Target元注解来控制自定义注解的保留策略和适用范围。

示例:定义一个自定义注解来管理Bean

我们将创建一个自定义注解@CustomService,用于标记某个类是Spring管理的服务类,类似于@Service。此注解包含@Component元注解,使Spring自动识别它为一个组件。此外,还添加了一些属性供配置使用。

import org.springframework.stereotype.Component;
import java.lang.annotation.*;

@Target(ElementType.TYPE) // 指定该注解应用于类、接口等类型
@Retention(RetentionPolicy.RUNTIME) // 使注解在运行时可见
@Component // 将注解标记为Spring Bean,使得Spring容器能管理
public @interface CustomService {
String value() default ""; // 可选属性,用于指定Bean名称
String logLevel() default "INFO"; // 日志级别
int order() default 0; // 加载顺序
}

解释

  • @Target(ElementType.TYPE):限制@CustomService只能用于类、接口或枚举。
  • @Retention(RetentionPolicy.RUNTIME):在运行时保留此注解,使Spring能在启动时读取它。
  • @Component:Spring将带有@CustomService注解的类自动识别为Bean,并注入IOC容器。
  • 当自定义注解中包含@Component元注解时,Spring会尝试将注解的value属性值作为Bean的名称使用。如果value为空,则使用默认名称(类名首字母小写)。

14.1.2.@Target 和 @Retention 元注解的所有可能情况

@Target控制注解的作用范围,可以指定注解适用于类、方法、字段等。常用的取值包括:

  • ElementType.TYPE:表示注解可以应用于类、接口、枚举等类型上。
  • ElementType.METHOD:表示注解可以应用于方法。
  • ElementType.FIELD:表示注解可以应用于类的字段。
  • ElementType.PARAMETER:表示注解可以应用于方法的参数。
  • ElementType.CONSTRUCTOR:表示注解可以应用于构造函数。
  • ElementType.ANNOTATION_TYPE:表示注解可以应用于其他注解(即元注解)。
  • ElementType.LOCAL_VARIABLE:表示注解可以应用于局部变量。
  • ElementType.PACKAGE:表示注解可以应用于包。

@Retention控制注解的生命周期,决定注解在哪个阶段可见。常用的取值包括:

  • RetentionPolicy.SOURCE:注解只在源码中存在,编译后被丢弃。
  • RetentionPolicy.CLASS:注解在编译期存在,编译后的字节码文件中包含注解,但在运行时不可见。
  • RetentionPolicy.RUNTIME:注解在运行时可见,可以通过反射读取。

14.1.3.通过反射读取自定义注解的属性值

在运行时,Spring或开发者可以通过反射机制读取注解的属性值。反射是一种Java机制,允许在运行时动态地访问类和对象的信息(如注解、方法、属性等),即使这些信息在编译时未知。

代码 1:读取注解的属性值

import java.lang.reflect.Method;

// 自定义注解读取器
public class AnnotationReader {
public static void main(String[] args) {
try {
// 获取MyService类的Class对象
Class<MyService> clazz = MyService.class;

// 获取MyService类上的CustomService注解实例
CustomService customServiceAnnotation = clazz.getAnnotation(CustomService.class);

// 检查注解是否存在,并读取其属性值
if (customServiceAnnotation != null) {
String value = customServiceAnnotation.value(); // 获取Bean名称
String logLevel = customServiceAnnotation.logLevel(); // 获取日志级别
int order = customServiceAnnotation.order(); // 获取加载顺序

// 输出注解的属性值
System.out.println("Bean名称: " + value);
System.out.println("日志级别: " + logLevel);
System.out.println("加载顺序: " + order);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

代码解释:

  • 获取Class对象Class<MyService> clazz = MyService.class;

    • 通过Class对象,使用反射操作MyService类。反射的入口就是Class对象,通过它可以获取类的注解、属性、方法等信息。
  • 获取注解实例CustomService customServiceAnnotation = clazz.getAnnotation(CustomService.class);

    • 通过getAnnotation()方法检查类上是否存在CustomService注解。如果存在,返回注解实例;如果不存在,返回null
  • 检查并读取注解属性值

    • customServiceAnnotation.value():获取value属性值(Bean名称)。
    • customServiceAnnotation.logLevel():获取logLevel属性值(日志级别)。
    • customServiceAnnotation.order():获取order属性值(加载顺序)。

代码 2:在业务类中通过注解属性控制逻辑

@CustomService(value = "myCustomService", logLevel = "DEBUG", order = 1)
public class MyService {

// 存储注解属性的日志级别
private final String logLevel;

// 构造方法中通过反射获取注解的logLevel属性
public MyService() {
this.logLevel = this.getClass().getAnnotation(CustomService.class).logLevel();
}

// 根据logLevel的值控制日志输出
public void performAction() {
if ("DEBUG".equals(logLevel)) {
System.out.println("DEBUG: 正在执行服务逻辑...");
} else {
System.out.println("INFO: 服务已启动。");
}
}
}

代码解释:

  • 注解属性的存储:定义了一个logLevel变量,用于存储注解logLevel属性值。

  • 构造方法中读取注解属性

    • 通过this.getClass().getAnnotation(CustomService.class).logLevel()在构造方法中获取CustomService注解的logLevel属性,并将其赋值给logLevel变量。这样,logLevel就可用于控制业务方法的行为。
  • 根据logLevel控制行为

    • performAction方法中,根据logLevel的值选择不同的日志输出。若logLevelDEBUG,则输出调试日志,否则输出简要日志。

14.2. 元注解(如 @Component)如何与IOC结合

在Spring框架中,元注解是一种用于描述其他注解的注解。通过在自定义注解中使用元注解,Spring可以在启动时自动识别和管理带有这些自定义注解的类,将其作为Bean注册到IOC容器中。最常见的元注解包括@Component@Configuration@Service@Repository等,它们都是@Component的衍生注解,用于实现Spring的自动扫描和注入机制。


14.2.1. 什么是元注解

元注解是用于标注其他注解的注解。Spring中的元注解主要用于控制自定义注解的行为,使Spring在扫描时可以根据元注解的类型将自定义注解标记的类纳入IOC容器。Spring的常见元注解包括:

  • @Component:标记为一个通用Bean,自动注册到Spring IOC容器。
  • @Service:特定于业务逻辑的Bean,一般用于服务层,实质上等效于@Component
  • @Repository:用于数据访问层(DAO)的Bean,Spring会为其自动提供数据访问异常转换功能。
  • @Configuration:用于定义配置类,类中包含Bean定义和依赖注入配置。

14.2.2. @Component元注解的作用

@Component是Spring中最通用的元注解,用于标记一个类为Spring管理的Bean。通过@Component标记的类会被Spring扫描到,并自动注册到IOC容器中。自定义注解如果包含@Component元注解,Spring也会将该注解标记的类自动识别为Bean。

@Component在Spring中的功能

  • 使得类自动成为Spring Bean,无需在XML或Java配置类中显式定义。
  • 在Spring的自动扫描机制下,Spring会找到所有标记了@Component的类并实例化为Bean。
  • 配合@Autowired@Qualifier等注解实现自动注入。

14.2.3. 自定义注解中的 @Component

在自定义注解中使用@Component元注解,可以将自定义注解标记的类纳入IOC容器,使Spring自动管理该类。例如,我们可以创建一个自定义注解@CustomService,并包含@Component元注解,使Spring在扫描时将其识别为Bean。

示例:自定义注解 @CustomService

import org.springframework.stereotype.Component;
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component // 将自定义注解标记为Spring组件
public @interface CustomService {
String value() default ""; // 可选属性,用于指定Bean名称
}

在这里,@CustomService注解包含@Component元注解,因此任何带有@CustomService的类都会被Spring容器自动识别并注册为Bean。

使用自定义注解

@CustomService("myCustomService")
public class MyService {
public void performAction() {
System.out.println("执行自定义服务操作...");
}
}
  • 解释:Spring容器会自动扫描@CustomService注解的类MyService,并将其注册为Bean,名称为myCustomService

14.2.4. Spring如何结合 @Component 实现自动扫描

Spring在启动时会根据配置的包路径进行自动扫描,扫描范围内的所有标记了@Component或其衍生注解的类都会被Spring识别为Bean,自动注册到IOC容器中。

自动扫描的配置

在Spring Boot项目中,@SpringBootApplication注解会自动启动Spring的组件扫描,扫描当前包及其子包中的所有组件:

@SpringBootApplication  // 启动Spring Boot应用,并自动扫描当前包及子包的组件
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

如果需要手动指定扫描路径,可以使用@ComponentScan注解:

@SpringBootApplication
@ComponentScan(basePackages = "com.example.service")
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

14.3. 注解的继承性

在Java中,注解的继承性与类和接口的继承不同。注解本身不支持直接继承关系,即一个注解不能通过extends关键字继承另一个注解。不过,Java通过@Inherited元注解提供了一种方式,使得某些注解可以被类的子类“继承”下来。这种继承性在框架开发和组件设计中有着独特的应用价值。


14.3.1. Java中注解的继承限制

Java的注解系统对继承有严格的限制。默认情况下:

  • 注解不具备继承性,不能像类和接口一样通过extends实现注解继承。
  • @Inherited仅对类有效,无法在方法或字段上生效。这意味着只能将带@Inherited注解的父类注解“继承”到子类上。

14.3.2. 使用@Inherited实现注解的“继承”

@Inherited是一个元注解,用于标注某个注解具有继承性。当一个类使用了带@Inherited的注解时,它的子类会自动拥有该注解。这种“继承”仅在子类继承父类时有效,但不适用于方法、字段等其他成员。

示例:@Inherited的基本用法

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) // 仅用于类或接口
public @interface InheritedService {
String value() default "defaultService";
}

在上面的例子中,InheritedService注解使用了@Inherited,表示它可以被继承。注意@Inherited注解只适用于ElementType.TYPE(类或接口)。

示例:在父类和子类中的继承效果

@InheritedService(value = "baseService")
public class BaseService {
}

public class ChildService extends BaseService {
}

在这个例子中,BaseService类使用了@InheritedService注解,而ChildService类继承自BaseService。通过反射,可以看到ChildService类自动“继承”了@InheritedService注解。


14.3.3. @Inherited的局限性与使用场景

@Inherited为注解带来了一定的继承性,但它的局限性较为明显:

  • 仅对类有效@Inherited只在类的继承结构中生效,对方法和字段无效。例如,如果一个方法标记了@InheritedService,则子类中覆盖的方法不会继承此注解。
  • 无法覆盖父类注解的属性:当子类继承父类的注解时,无法更改继承来的注解属性值,子类只能继承父类注解的原始属性。

示例:@Inherited的局限性

public class AnotherChildService extends BaseService {
// 虽然继承了@InheritedService注解,但无法更改它的属性值
}

在这个例子中,AnotherChildService继承了BaseService的注解,但无法为value属性设置新的值,依然保持了父类的属性baseService


什么时候使用@Inherited

尽管@Inherited有一些限制,但它在一些特定场景下仍然有用:

  • 标识性注解:可以用来标识一些通用的、在类层级中需要一致性的属性。例如,标记一个服务类或数据访问类。
  • 框架或组件设计:在设计框架时可以使用@Inherited为基础类定义一些共同的功能,这样子类不需要重复定义。

例如,在某些业务系统中,可以用@Inherited标记基础服务类,表示该类及其所有子类都具备特定的功能或属性。

举报

相关推荐

0 条评论