【Springboot 入门培训 】#19 Spring Boot 组件扫描与bean生命周期


  首先,我想先为你介绍一下“Spring”,这是一个开放源代码的设计模式解决方案和轻量级的反转控制(IoC)和面向切面(AOP)的容器框架。在这个框架中,有一个重要的概念叫做“组件扫描”。

1 什么是组件扫描

  为了进行依赖注入,Spring 创建了一个所谓的应用程序上下文。在启动期间,Spring 实例化对象并将它们添加到应用程序上下文中。应用程序上下文中的对象称为“Spring beans”或“组件”。Spring 解决了 Spring bean 之间的依赖关系,并将 Spring bean 注入到其他 Spring bean 的字段或构造函数中。

  那么,什么是组件扫描呢?组件扫描就是Spring框架用来发现并自动注册你的应用程序中的bean的方式。这些bean可以是任何用@Component,@Controller,@Service,@Repository或者其他注解标记的类。Spring框架会扫描这些标记,并在应用程序的上下文中创建和管理这些bean。

1. 启动 Spring 应用
   |
2. 查找所有的 `@Configuration` 类
   |
3. 找到 `ExplicitScan` 类
   |
4. 发现 `@ComponentScan` 注解,读取 `basePackages` 属性
   |
5. 从 `io.reflectoring.vehicles` 包开始,扫描这个包和它的所有子包
   |
6. 对每个找到的类,检查是否存在 `@Component`、`@Service`、`@Repository`、`@Controller`等 Spring 注解
   |
7. 对于带有这些注解的类,将它们注册为 Spring Beans

在类路径中搜索应该对应用程序上下文有贡献的类的过程称为组件扫描。

  • @Component

这是一个通用的构造型注释,用于指示该类是 Spring 管理的组件。其他刻板印象是@Component.

  • @Controller

这表明带注释的类是一个 Spring 管理的控制器,它提供带注释的方法@RequestMapping来响应 Web 请求。

Spring 4.0 引入了@RestController结合了@Controller和 的注释@ResponseBody,使得创建返回 JSON 对象-的 RESTful 服务变得容易。

  • @Service

我们可以将@Service构造型用于包含业务逻辑的类或来自服务层的类。

  • @Repository

我们可以使用@Repository负责提供对数据库实体的访问的 DAO 类的构造型。

  如果我们使用 Spring Data 来管理数据库操作,那么我们应该使用 Spring Data Repository 接口而不是构建我们自己的@Repository-annotated 类。

2 何时使用组件扫描

  组件扫描的实现是通过@ComponentScan注解来实现的。如果你的应用是一个Spring Boot应用,那么,包含Spring Boot应用类的包以及其下的所有包都会被一个隐式的组件扫描所覆盖。这是因为Spring Boot的@SpringBootApplication注解包含了@Configuration, @ComponentScan, 和@EnableAutoConfiguration三个注解。

  默认情况下,@ComponentScan注解会扫描当前包以及其所有子包的组件。所以,如果你的应用程序的包结构没有变化,你不需要显式地进行组件扫描。注意,如果你在默认的包中指定了带有@Configuration注解的类,Spring会扫描类路径中所有JAR文件的所有类。这种情况不是很推荐的,如果项目jar包的数量很巨大那么注解会扫描的时间会很长,导致启动的时间会很慢。

  我们利用@ComponentScan@Configuration注解引导 Spring 扫描带有各种构造型注解的类。注解@ComponentScan有多个属性,这些属性可以根据我们的需求进行调整以获得预期的扫描行为。

我们将使用 的ApplicationContext方法getBeanDefinitionNames()来检查已成功扫描并添加到应用程序上下文的 beans 列表。

@Component
class BeanViewer {
  private final Logger LOG = LoggerFactory.getLogger(getClass());
  @EventListener
  public void showBeansRegistered(ApplicationReadyEvent event) {
    String[] beanNames = event.getApplicationContext()
      .getBeanDefinitionNames();
      for(String beanName: beanNames) {
        LOG.info("{}", beanName);
      }
  }
}

  以上BeanViewer将打印在应用程序上下文中注册的所有 beans。这将帮助我们检查我们的组件是否正确加载。

例子1 同一个包下:

让我们用一个实例来解释这个过程。假设我们有一个应用程序,其结构如下:

|- com.example.demo (main package)
   |- DemoApplication.java (Spring Boot Application class)
   |- UserService.java (@Service annotated class)
   |- BeanViewer.java

  当我们启动这个 Spring Boot 应用时,UserService 类会被自动扫描并作为一个 Spring bean 注册到应用程序上下文中,因为它位于 DemoApplication.java 同一个包或其子包下。

例子2 不在同包下:

现在,假设我们有另一个包,它并不在 DemoApplication.java 的包或子包下:

|- com.example.demo
   |- DemoApplication.java
   |- UserService.java
|- com.example.utils
   |- UtilityService.java (@Service annotated class)

  在这种情况下,UtilityService 类不会被默认的组件扫描机制捕捉到。

  为了让 Spring 扫描并注册这个类,我们需要在 DemoApplication.java 类或任何其他的配置类上添加 @ComponentScan 注解,并指定 com.example.utils 作为需要扫描的包。

@SpringBootApplication
@ComponentScan(basePackages = "com.example.utils")
public class DemoApplication {
    // ...
}

  当然,@ComponentScan 注解也支持更多的定制,@ComponentScan注解提供了更多的定制化选项让我们看一下它可以用来修改其行为的注释的属性:

  • basePackages:获取应扫描组件的包名称列表。
  • basePackageClasses:获取应扫描其包的类的列表。
  • includeFilters:使我们能够指定应扫描哪些类型的组件。
  • excludeFilters: 这是相反的includeFilters。我们可以在扫描时根据条件指定条件来忽略某些组件。
  • useDefaultFilters:如果为真,它会启用自动检测带有任何构造型注释的类。如果为 false,则将包含属于includeFilters和定义的过滤条件的组件。excludeFilters

在控制组件扫描时,需要对此类配置项有深入的理解,这样可以帮助我们创建出优雅和高效的 Spring 应用。

3 扫描整个包basePackages与 includeFilters

  在了解了如何使用Spring的@ComponentScan注解,我们可以开始深入探索其高级选项,例如定制包扫描以及包含和排除过滤器。
  首先,我们创建一个ExplicitScan类,并将其放在主应用程序包io.reflectoring.componentscan中。这样,该类就会被Spring的默认组件扫描捕获。然后,我们在该类中使用@ComponentScan注解,并通过basePackages属性指定另一个包io.reflectoring.vehicles作为扫描目标:

package io.reflectoring.componentscan;
@Configuration
@ComponentScan(basePackages= "io.reflectoring.vehicles")
public class ExplicitScan {
}

  运行这个应用程序,我们可以看到vehicles包中的所有组件都被成功地注册到了应用程序的上下文中。其中,BeanViewer类的日志输出就是这个结果的直接证明。

includeFilters

  那么,如果我们只希望包含某个特定类型的组件呢?比如,我们只希望包含扩展了Zht类的组件。这时,我们就可以使用includeFilters属性并配合FilterType.ASSIGNABLE_TYPE过滤器来实现这个需求:

@Configuration
@ComponentScan(basePackages= "io.reflectoring.vehicles",
  includeFilters=
    @ComponentScan.Filter(
      type=FilterType.ASSIGNABLE_TYPE,
      classes=Zht.class),
    useDefaultFilters=false)
public class ExplicitScan {
}

  在这个例子中,我们修改了ExplicitScan类以包含扩展了Zht类的组件,同时设置useDefaultFiltersfalse以禁用默认的过滤器。运行这个应用程序,只有继承Zht类的组件会被扫描到,因为它们都扩展了Zht类。

其他可用的过滤器类型是:

  • ANNOTATION:仅匹配具有特定构造型注释的类。
  • ASPECTJ: 使用 AspectJ 类型模式表达式匹配类
  • ASSIGNABLE_TYPE:匹配扩展或实现此类或接口的类。
  • REGEX:使用包名称的正则表达式匹配类。

在上面的例子中,我们修改了我们的ExplicitScan类以includeFilters包含扩展的组件Car.class,我们正在改变useDefaultFilters = false以便只应用我们的特定过滤器。

excludeFilters

  另一方面,我们也可以使用excludeFilters属性来排除一些我们不希望被组件扫描捕获的类。同样,我们需要配合FilterType.ASSIGNABLE_TYPE过滤器来实现这个需求:

@Configuration
@ComponentScan(basePackages= "io.reflectoring.vehicles",
  excludeFilters=
    @ComponentScan.Filter(
      type=FilterType.ASSIGNABLE_TYPE,
      classes=Car.class))
public class ExplicitScan {
}

  在这个例子中,我们没有设置useDefaultFiltersfalse,所以默认的过滤器仍然会被应用。运行这个应用程序,继承Zht类的组件会被排除在了组件扫描之外。
  最后,我想强调一下,@ComponentScan注解是一个强大的工具,但也需要谨慎使用。否则,它可能会让你的应用程序的组成规则变得混乱。为了保持清晰和明确的规则,一个好的做法是在一个显式导入的@Configuration类中使用@ComponentScan注解,并且仅仅自动扫描这个类所在的包。这样,我们就可以更好地管理和控制我们的应用程序上下文了

4 Spring boot 的 Bean 生命周期

  现在开始我们称,在创建、编排和销毁ApplicationContext方面受 Spring 控制的每个对象都称为 Spring Bean。
  通过上面的介绍我们知道定义 Spring bean 的最常见方法是使用注解@Component

@Component
class MySpringBean {
  ...
}

  如果启用了 Spring boot的组件扫描,则会将一个对象MySpringBean添加到应用程序上下文中。
  另一种方法创建Spring bean的方法式在@Configuration注解类中创建一个@Bean注解方法来返回MySpringBean类的对象,让这个类对象变为spring bean。

@Configuration
class MySpringConfiguration {
  @Bean
  public MySpringBean mySpringBean() {
    return new MySpringBean();
  }
}

4.1 生命周期

  当我们看到 Spring bean 的生命周期时,生命周期时分为对象实例化到销毁的许多阶段。为了方便大家理解,我们将它们分为创建和销毁阶段

Bean 创建阶段

  • **实例化:**这是 bean 的一切开始的地方。Spring 实例化 bean 对象就像我们手动创建 Java 对象实例一样。
  • **填充属性:**实例化对象后,Spring 扫描实现接口的 beanAware并开始设置相关属性。
  • 预初始化: SpringBeanPostProcessor在此阶段开始运行。这些postProcessBeforeInitialization()方法完成了他们的工作。此外,@PostConstruct带注释的方法会在它们之后运行。
  • AfterPropertiesSet: Spring 执行afterPropertiesSet()实现InitializingBean.
  • 自定义初始化:initMethod Spring 触发我们在注释属性中定义的初始化方法@Bean
  • 初始化后: Spring 的BeanPostProcessors 第二次起作用。此阶段触发postProcessAfterInitialization()方法。

Bean 销毁阶段

  • Pre-Destroy: Spring@PreDestroy在这个阶段触发带注解的方法。
  • 销毁: Spring 执行实现destroy()的方法DisposableBean
  • 自定义销毁:destroyMethod我们可以使用注释中的属性定义自定义销毁挂钩@Bean,Spring 在最后阶段运行它们。
Spring Bean生命周期
1. 实例化 Bean
   |
2. 填充属性
   |
3. 预初始化 - `postProcessBeforeInitialization()` 方法
   |
4. 执行 `@PostConstruct` 注解的方法
   |
5. 调用 `afterPropertiesSet()` 方法(如果Bean实现了`InitializingBean`接口)
   |
6. 执行自定义初始化方法(如果在 `@Bean` 注解中定义了 `initMethod` 属性)
   |
7. 初始化后 - `postProcessAfterInitialization()` 方法
   |
   V
(在这里, Bean处于完全初始化和可用的状态, 直到它被销毁)
   |
8. 调用 `@PreDestroy` 注解的方法
   |
9. 执行 `destroy()` 方法(如果Bean实现了`DisposableBean`接口)
   |
10. 执行自定义销毁方法(如果在 `@Bean` 注解中定义了 `destroyMethod` 属性)
   |
   V
(在这里, Bean被完全销毁, 并从Spring应用上下文中移除)

1 启动接口
  我们可以实现 Spring 的InitializingBean接口中的afterPropertiesSet()方法启动Spring bean 生命周期。

@Component
class MySpringBean implements InitializingBean {
  @Override
  public void afterPropertiesSet() {
  }
}

2 销毁接口
  同样的,我们可以实现DisposableBean接口的destroy()方法销毁阶段的方法。

@Component
class MySpringBean implements DisposableBean {
  @Override
  public void destroy() {
    //...
  }
}

3 JSR-250 注释启动与销毁
  我们可以使用Spring JSR-250标准中的@PostConstruct@PreDestroy方法,将它们挂钩到预初始化和销毁阶段。

@Component
class MySpringBean {
  @PostConstruct
  public void postConstruct() {
  }
  @PreDestroy
  public void preDestroy() {
  }
}

4 使用@Bean注释的属性
  此外,我们在创建 Spring bean 时,可以在 @Bean 配置中设置注解的initMethod属性和destroyMethod属性来设置初始化启动和销毁。

@Configuration
class MySpringConfiguration {
  @Bean(initMethod = "onInitialize", destroyMethod = "onDestroy")
  public MySpringBean mySpringBean() {
    return new MySpringBean();
  }
}

  我们应该注意,如果我们的 bean 中有一个名为close()or的公共方法shutdown(),那么默认情况下它会自动触发销毁回调。

@Component
class MySpringBean {
  public void close() {
  }
}

  如果我们不希望这种默认销毁行为,我们可以通过设置禁用destroyMethod=""来关闭它。

@Configuration
class MySpringConfiguration {
  @Bean(destroyMethod = "")
  public MySpringBean mySpringBean() {
    return new MySpringBean();
  }
}

5 BeanPostProcessor 设置
  我们可以利用该BeanPostProcessor接口在 Spring bean 初始化之前或之后运行任何自定义操作,来实现启动和销毁bean。

class MyBeanPostProcessor implements BeanPostProcessor {
  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName)
    throws BeansException {
    return bean;
  }
  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName)
    throws BeansException {
    return bean;
  }
}

6 使用Aware接口
  进入生命周期的另一种方法是使用接口Aware来实现启动与销毁。

@Component
class MySpringBean implements BeanNameAware, ApplicationContextAware {

  @Override
  public void setBeanName(String name) {
    //...
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext)
    throws BeansException {
    //...
  }

}

4.2 Bean 生命周期

  当我们需要调整我们的软件以满足新的需求时,寻找最佳实践以维持我们代码库的长期可维护性是至关重要的。在 Spring 框架中,大多数情况下,挂钩到 bean 生命周期是扩展我们应用程序的好方法。让我们深入了解一下这个过程。

1 BeanNameAware 接口
  一种常见的情况是在运行时获取 bean 的属性,如 bean 的名称。例如,当我们需要在日志中记录 bean 的创建:

@Component
class NamedSpringBean implements BeanNameAware {
  Logger logger = LoggerFactory.getLogger(NamedSpringBean.class);
  public void setBeanName(String name) {
    System.out.println(name + "获得bean名字");
  }
}

  在这个例子中,我们通过实现 BeanNameAware 接口,能够在 Spring 创建 bean 时获取其名称,从而可以在日志中记录它。
2 动态改变bean实例
  我们可能需要以编程方式定义 Spring bean。这在我们需要动态地创建和更改 bean 实例的情况下特别有用。例如,我们可以创建一个 IpToLocationService,这个服务可以动态地更新 IpDatabaseRepository 以便它始终使用最新版本的数据库:

@Service
class IpToLocationService implements BeanFactoryAware {

  DefaultListableBeanFactory listableBeanFactory;
  IpDatabaseRepository ipDatabaseRepository;

  @Override
  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    listableBeanFactory = (DefaultListableBeanFactory) beanFactory;
    updateIpDatabase();
  }
    
  public void updateIpDatabase(){
      
  }
    
}

  在这个例子中,我们通过实现 BeanFactoryAware 接口来获取 BeanFactory 实例,此外我们在方法中updateIpDatabase()获取实例后立即调用我们的方法。因此,我们可以在 Spring 上下文启动时创建 bean 的第一个实例。BeanFactory``setBeanFactory()``IpDatabaseRepository
  另一种情况是从 Spring 上下文之外访问ApplicationContextor实例。BeanFactory
例如,我们可能希望将 注入BeanFactory到非 Spring 类中,以便能够访问该类中的 Spring bean 或配置。

class AutowireCapableJobFactory
   extends SpringBeanJobFactory implements ApplicationContextAware {
  private AutowireCapableBeanFactory beanFactory;
  @Override
  public void setApplicationContext(final ApplicationContext context) {
    beanFactory = context.getAutowireCapableBeanFactory();
  }
  @Override
  protected Object createJobInstance(final TriggerFiredBundle bundle)
      throws Exception {
    final Object job = super.createJobInstance(bundle);
    beanFactory.autowireBean(job);
    return job;
  }
}

  在此示例中,我们使用ApplicationContextAware接口来访问 bean 工厂,并使用 bean 工厂自动装配Job最初不受 Spring 管理的 bean 中的依赖项。

此外,也可以用 Spring - Jersey集成,Jersey也是常用的一种方法。

@Configuration
class JerseyConfig extends ResourceConfig {
  @Autowired
  private ApplicationContext applicationContext;
  @PostConstruct
  public void registerResources() {
    applicationContext.getBeansWithAnnotation(Path.class).values()
      .forEach(this::register);
  }
}

  通过将 Jersey 标记ResourceConfig为 Spring @Configuration,我们注入ApplicationContext并查找所有由 Jersey 注释的 bean @Path,以便在应用程序启动时轻松注册它们。

4.3 周期各个阶段

  下面大家可以看到Spring bean生命周期中各个阶段的执行顺序。

class MySpringBean implements BeanNameAware, ApplicationContextAware,
    InitializingBean, DisposableBean {

  private String message;

  public void sendMessage(String message) {
    this.message = message;
  }

  public String getMessage() {
    return this.message;
  }

  @Override
  public void setBeanName(String name) {
    System.out.println("--- setBeanName executed ---");
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext)
      throws BeansException {
    System.out.println("--- setApplicationContext executed ---");
  }

  @PostConstruct
  public void postConstruct() {
    System.out.println("--- @PostConstruct executed ---");
  }

  @Override
  public void afterPropertiesSet() {
    System.out.println("--- afterPropertiesSet executed ---");
  }

  public void initMethod() {
    System.out.println("--- init-method executed ---");
  }

  @PreDestroy
  public void preDestroy() {
    System.out.println("--- @PreDestroy executed ---");
  }

  @Override
  public void destroy() throws Exception {
    System.out.println("--- destroy executed ---");
  }

  public void destroyMethod() {
    System.out.println("--- destroy-method executed ---");
  }

}

此外,我们创建了一个BeanPostProcessor挂钩到初始化之前和之后的阶段:

class MyBeanPostProcessor implements BeanPostProcessor {

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName)
      throws BeansException {
    if (bean instanceof MySpringBean) {
      System.out.println("--- postProcessBeforeInitialization executed ---");
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName)
      throws BeansException {
    if (bean instanceof MySpringBean) {
      System.out.println("--- postProcessAfterInitialization executed ---");
    }
    return bean;
  }

}

接下来,我们编写一个 Spring 配置来定义我们的 bean:

@Configuration
class MySpringConfiguration {

  @Bean
  public MyBeanPostProcessor myBeanPostProcessor(){
    return new MyBeanPostProcessor();
  }

  @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
  public MySpringBean mySpringBean(){
    return new MySpringBean();
  }

}

最后,我们编写一个@SpringBootTest来运行我们的 Spring 上下文:

@SpringBootTest
class BeanLifecycleApplicationTests {

  @Autowired
  public MySpringBean mySpringBean;

  @Test
  public void testMySpringBeanLifecycle() {
    String message = "Hello World";
    mySpringBean.sendMessage(message);
    assertThat(mySpringBean.getMessage()).isEqualTo(message);
  }

}

因此,我们的测试方法记录了生命周期阶段之间的执行顺序:

--- setBeanName executed ---
--- setApplicationContext executed ---
--- postProcessBeforeInitialization executed ---
--- @PostConstruct executed ---
--- afterPropertiesSet executed ---
--- init-method executed ---
--- postProcessAfterInitialization executed ---
...
--- @PreDestroy executed ---
--- destroy executed ---
--- destroy-method executed ---

  在本文中,我们了解了 bean 生命周期阶段是什么、为什么以及我们如何挂接到 Spring 中的生命周期阶段。Spring 在 bean 生命周期中有许多阶段以及许多接收回调的方法。我们可以像在BeanPostProcessor。尽管每个方法都有其用途,但我们应该注意使用 Spring 接口将我们的代码耦合到 Spring Framework。
  另一方面,@PostConstruct注释@PreDestroy是 Java API 的一部分。因此,我们认为它们是接收生命周期回调的更好替代方案,因为它们甚至可以将我们的组件与 Spring 分离
系列文章目录

【Springboot 入门培训 】#1 MyBatis项目运行环境配置
【Springboot 入门培训 】#2 MyBatis 增改删除与查询 in like foreach操作
【Springboot 入门培训 】#3 MyBatis 多数据源与缓存和数据连接池设置
【Springboot 入门培训 】#4 WEB+JSP MVC项目搭建
【Springboot 入门培训 】#5 WEB+Thymeleaf MVC项目搭建与测试
【Springboot 入门培训 】#6 (Framework7 移动 webapp) WEB APP 项目搭建
【Springboot 入门培训 】#7 (Framework7 移动webapp) 页面路由跳转
【Springboot 入门培训 】#8 (Framework7 移动webapp) Component 模板MVVM与AJAX
【Springboot 入门培训 】# 9 Security(一) 登录验证初始化
【Springboot 入门培训 】#10 Security(二) 数据库DB 登录验证
【Springboot 入门培训 】#11 Security(三) json 前后端分离跨域登录
【Springboot 入门培训 】#12 Security(四) Jwt 前后端分离跨域登录
【Springboot 入门培训 】#13 Security(五) oauth2 基础应用
【Springboot 入门培训 】#14 WebJars 样式包BootStrap 5架构
【Springboot 入门培训 】#15 MyBatis-Thymeleaf 插件在项目中的应用
【Springboot 入门培训 】#16 Spring boot 日志 Slf4j + Logback
【Springboot 入门培训 】#17 WebJars + BootStrap5 常用JS组件应用
【Springboot 入门培训 】#18 SpringBoot Cache 缓存实现
Spring boot 中Thymeleaf 模板 html 标签使用

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zht_bs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值