Springboot扩展开发-深入Spring Bean生命周期及核心扩展点

1.引言

Spring是一个开源的应用程序框架,主要在于简化企业级应用的开发。Spring Boot是Spring的一个子项目,它通过自动配置、起步依赖(starter dependencies)特性,进一步简化了Spring应用的开发和部署。在Spring应用中,Bean的生命周期管理至关重要。通过深入理解并利用各种扩展点,可以在Bean的不同生命周期阶段进行自定义的处理,从而实现更灵活、更强大的功能。

2.Spring Bean生命周期

源码解析见上一篇文章springboot bean生命周期和作用域

  • BeanDefinition 注册
    发生在bean实例化之前,Spring通过注解扫描或显式配置类发现需要管理的bean,并将被发现的bean生成一个BeanDefinition对象,将其注册到BeanDefinitionRegistry中,这个阶段可以通过可以通过实现ImportBeanDefinitionRegistrar接口来自定义,从而实现动态地向容器中添加新的BeanDefinition,源码详见springboot启动时自动装配过程 章节 2.源码解析
  • BeanFactoryPostProcessor 处理
    在所有BeanDefinition注册完成后,在bean实例化之前,Spring会调用所有实现了BeanFactoryPostProcessor接口的bean。这里可以在bean定义被加载到容器中后但在bean实例化之前对其进行修改
  • 实例化
    通过反射创建bean实例。在此阶段,可以使用FactoryBean来创建复杂对象,源码详见springboot bean生命周期和作用域
  • 属性填充
    设置bean的属性值(依赖注入)。可以使用BeanFactoryPostProcessor进行属性值处理。
  • 初始化前处理
    执行BeanPostProcessor的postProcessBeforeInitialization方法。可以在此阶段对bean进行预处理。
  • 初始化
    执行自定义的初始化方法(可以通过**@PostConstruct注解的方法、InitializingBean接口的afterPropertiesSet方法**、自定义的初始化方法)。SmartLifecycle接口提供了启动顺序控制。
  • 初始化后处理
    执行BeanPostProcessor的postProcessAfterInitialization方法。可以在此阶段对bean进行后处理。
  • 使用
    这里可以通过依赖注入(Dependency Injection, DI)“@Autowired” 或者 直接从应用上下文中获取Bean(通过ApplicationContext.getBean()方法 获取bean。
  • 销毁前处理
    执行DisposableBean接口的destroy方法或其他自定义的销毁方法(如@PreDestroy注解的方法)
    SmartLifecycle接口提供了关闭顺序控制。
  • 销毁
    bean被容器销毁并释放资源

3.bean生命周期演示

示例来自bean的生命周期。

  • 定义bean
//bean1
@Component
public class MyDependency {

    public MyDependency() {
        System.out.println("MyDependency created");
    }
}
//bean2

@Component
public class MyBean implements InitializingBean, DisposableBean, BeanNameAware, BeanFactoryAware {
    private MyDependency myDependency;
    private String someProperty = "TEST";

    private String beanName;
    private BeanFactory beanFactory;
    public String getSomeProperty() {
        return someProperty;
    }

    public void setSomeProperty(String someProperty) {
        this.someProperty = someProperty;
    }

    @Autowired
    public MyBean(MyDependency myDependency) {
        this.myDependency = myDependency;
        System.out.println("属性填充: " + myDependency);
    }

    @PostConstruct
    public void init() {
        System.out.println("PostConstruct 自定义初始化方法调用");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean.afterPropertiesSet() 调用");
    }
    //DisposableBean接口:如果实现了DisposableBean接口,则会调用其destroy()方法
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy() 调用");
    }

    //BeanNameAware:设置Bean名称。
    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("BeanNameAware Bean名称设置为: " + beanName);
    }

    //BeanFactoryAware:提供对创建该Bean的BeanFactory的引用
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        System.out.println("BeanFactoryAware BeanFactory设置为" );
    }
}
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("MyBeanPostProcessor#postProcessBeforeInitialization在初始化之前执行的逻辑");
        }
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("MyBeanPostProcessor#postProcessAfterInitialization在初始化之后执行的逻辑");
        }
        return bean;
    }
}
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        // 获取某个Bean的定义
        if (beanFactory.containsBeanDefinition("myBean")) {
            // 修改Bean定义的某个属性
            beanFactory.getBeanDefinition("myBean").getPropertyValues().add("someProperty", "modifiedValue");
            System.out.println("MyBeanFactoryPostProcessor: 在Bean实例化之前修改了myBean的someProperty属性");
        }
    }
}

在这里插入图片描述

4.最佳实践

4.1.创建Bean

在springboot中,如何创建一个bean呢?很多同学大概都知道吧。就是在上一篇文章中写到的如下:

  • @Controller通常用于标识Web控制器类;
  • @Service用于业务逻辑层;
  • @Repository用于数据访问层。
    上面三个注解都包含@Component注解,这意味着它们都可以被组件扫描自动检测并注册为Spring容器中的Bean。
  • @Component:这是一个通用的注解,用于标记任何Spring管理的组件。允许Spring通过组件扫描机制自动发现并注册到应用上下文中。
  • @Configuration:这个注解用于定义一个配置类,其中可以使用@Bean注解的方法来定义Spring管理的bean。配置类本身也是一个Spring bean。
  • @Bean:在配置类内部使用,用来显式声明一个或多个方法返回的对象应由Spring容器管理。通常与@Configuration一起使用。

但是假如我们的bean不符合Bean的定义,举个例子,我们的bean没有public构造方法,怎么处理,有些同学说,我可以在@configuration中添加@bean注入,这样也没问题。那假如我们的的bean只是个接口,使用上面的方式可以注入么?,接下来将要写的是,怎么将一个自定义对象,放入springboot。

4.1.1.FactoryBean的简单使用示例

通过工厂方法来创建和管理复杂的对象。FactoryBean 提供了一种灵活的方式来控制对象的创建过程,特别是当对象的构造函数不可访问或需要复杂的初始化逻辑时。上代码直接测试

//测试接口,使用FactoryBean创建
public class DemoClass {
    public void say() {
        System.out.println("DemoClass say Hello World");
    }
}
//测试类,注入DemoClass,并通过构造函数由springboot自动注入DemoClass 
@Component
public class DemoJava {
    private DemoClass demoClass;

    public DemoJava(DemoClass demoClass) {
        this.demoClass = demoClass;
        System.out.println("create DemoJava");
    }
    public void say() {
        System.out.println("DemoClass say Hello World");
        demoClass.say();
    }
}
//FactoryBean 测试类,创建DemoClass对象,注意,这里需要配置@Component注解,标注它是一个springboot的Bean
@Component
public class AntFactoryBean implements FactoryBean {
    @Override
    public DemoClass getObject() throws Exception {
        return new DemoClass();
    }

    @Override
    public Class<DemoClass> getObjectType() {
        return DemoClass.class;
    }
}
//注入Bean 并执行say方法
@Component
public class AntTestRunner implements CommandLineRunner {
    @Autowired
    private DemoJava demoJava;
    @Override
    public void run(String... args) throws Exception {
        demoJava.say();
    }
}

测试结果:
测试结果

从测试结果可以看出来,是不是DemoClass 已经在springboot容器中了?

  • 关键点解释:

getObject() 方法:
FactoryBean 的核心方法,用于返回实际的对象实例。在上面的例子中,我们通过 new 一个对象返回单例实例,当然我们可以通过其他任何方式,返回一个对象。
getObjectType() 方法:
返回由工厂创建的对象类型。这对于Spring容器来说非常重要,因为Spring要知道工厂生成的对象类型以便进行类型检查和依赖注入。
isSingleton() 方法:
指示由工厂创建的对象是否是单例的。如果返回 true,则表示该对象在整个应用程序上下文中只有一个实例;如果返回 false,则每次请求都会创建一个新的实例。

接下来,我们想一下,为什么mybatis可以将对象进行自动注入?很多同学是不是很迷茫?当然对java很熟悉的人是不是会想到一个方式,jdk动态代理。

4.1.2.FactoryBean通过动态代理将接口的对象注册到spring容器
//测试接口
public interface OrderMapper {

    void getOrder();
}
public class AntInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("AntMapperProxy invoke-"+method.getName());
        return "AntMapperProxy invoke Test……";
    }
}
// 使用代理类,返回OrderMapper的对象。
@Component
public class AntProxyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        return Proxy.newProxyInstance(OrderMapper.class.getClassLoader(), new Class[]{OrderMapper.class},new AntInvocationHandler());
    }

    @Override
    public Class getObjectType() {
        return OrderMapper.class;
    }
}
@Component
public class AntTestRunner implements CommandLineRunner {
    @Autowired
    private DemoJava demoJava;
    @Autowired
    private OrderMapper orderMapper;
    @Override
    public void run(String... args) throws Exception {
        String order = orderMapper.getOrder();
        System.out.println("AntTestRunner run-"+order);
    }
}

测试结果

从上面可以看出,是不是我们的接口也注册到springboot容器,并可以通过 @Autowired注解进行注入,调用方法。

那么,假如我们有很多个接口需要注册到springboot容器怎么处理呢?难道每个接口都写一个FactoryBean 的实现方法?
有没有一种方式可以批量注入,而不是每次都要写一个类?回顾上面的Bean的生命周期,是不是有很多地方可以修改Bean或者对Bean进行处理。但是我们现在要将Bean注册到Springboot容器,很容易想到的ImportBeanDefinitionRegistrar。

4.2.批量复杂的bean添加到容器

4.2.1.ImportBeanDefinitionRegistrar

动态向 Spring 容器启动时动态地注册 Bean 定义,也就是BeanDefinition。接口只有一个实现方法

void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

AnnotationMetadata:提供导入该注册器的类的元数据信息(如注解、类名等)。
BeanDefinitionRegistry:用于注册新的 Bean 定义

  • 测试例子:
public class DemoClass {
    public void say() {
        System.out.println("DemoClass say Hello World");
    }
}
//创建一个自定义的 ImportBeanDefinitionRegistrar 实现类,并在其中动态注册 DemoClass
public class AntImportBeanDefinitionResgistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(DemoClass.class);
        beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
        registry.registerBeanDefinition("demoClass", beanDefinition);
    }
}
@Component
public class DemoJava {
    private DemoClass demoClass;

    public DemoJava(DemoClass demoClass) {
        this.demoClass = demoClass;
        System.out.println("create DemoJava");
    }
    public void say() {
        System.out.println("DemoJava say Hello World");
        demoClass.say();
    }
}
//使用 @Import 注解导入 ImportBeanDefinitionRegistrar
@Configuration
@Import({AntImportBeanDefinitionResgistrar.class})
public class AntConfig {
}

@Component
public class AntTestRunner implements CommandLineRunner {
    @Autowired
    private DemoJava demoJava;
    @Autowired
    private OrderMapper orderMapper;
    @Override
    public void run(String... args) throws Exception {
        demoJava.say();
    }
}

输出:
测试结果
代码解释:

  • BeanDefinitionBuilder:
    BeanDefinitionBuilder 是一个辅助类,简化 BeanDefinition 的创建过程。
  • genericBeanDefinition(Class<?> beanClass) 方法用于创建一个新的 BeanDefinition
  • BeanDefinitionRegistry 提供了注册和移除 BeanDefinition 的方法。
  • registerBeanDefinition(String beanName, BeanDefinition beanDefinition) 方法用于将一个 BeanDefinition 注册到容器中。
  • @Import 注解用于导入配置类或实现 ImportBeanDefinitionRegistrar 接口的类。告诉 Spring 容器在启动时调用 AntImportBeanDefinitionResgistrar 的 registerBeanDefinitions 方法。

那么,怎么将接口注册到容器中呢?

4.2.2.将接口注册到容器中

主要思想是,BeanDefinition在生成是需要设置Class类型,因此需要对代理对做修改,修改为Class由构造函数传入。代码如下

//一个接口对象
public interface OrderMapper {
    String getOrder();
}
//InvocationHandler 实现
public class AntInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("AntMapperProxy invoke-"+method.getName());
        return "AntMapperProxy invoke Test……";
    }
}
//这里只是将单个接口OrderMapper注册到容器中,如果有多个,是不是可以通过for循环,依次注入,大家下来实验一下。
public class AntProxyImportBeanDefinitionResgistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(AntProxyFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
        beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
        registry.registerBeanDefinition("orderMapper", beanDefinition);
    }
}
@Component
public class AntTestRunner implements CommandLineRunner {
    @Autowired
    private DemoJava demoJava;
    @Autowired
    private OrderMapper orderMapper;
    @Override
    public void run(String... args) throws Exception {
        String order = orderMapper.getOrder();
        System.out.println("AntTestRunner run-"+order);
//        demoJava.say();
    }
}

测试结果:
测试结果
截止到这里,我们已经知道,怎么将接口注册到springboot 的容器中。看到这里大家想到了什么呢?是不是想到了mybatis的实现方式?接下来咱们模拟实现一个mybatis。

4.3.模拟实现mybatis将接口注册springboot Bean容器中。

整个代码逻辑为,创建一个ImportBeanDefinitionRegistrar,在ImportBeanDefinitionRegistrar类中,需要有以下步骤

  • 获取Mapper类
  • 定义BeanDefinition
  • 注册bean
4.3.1.定义FactoryBean
  • 从上面我们知道,将接口注册到springboot中,可以使用jdk动态代理+FactoryBean实现,为了实现查询功能,我们需要将DataSource 传入,
public class AntMapperFactoryBean<T> implements FactoryBean {
    private Class<T> clazz;
    private DataSource dataSource;

    public AntMapperFactoryBean(Class<T> clazz) {
        this.clazz = clazz;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public T getObject() throws Exception {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new AntMapperInvocationHandler(this.clazz, this.dataSource));
    }

    @Override
    public Class<?> getObjectType() {
        return this.clazz;
    }

    @Override
    public boolean isSingleton() {
        return FactoryBean.super.isSingleton();
    }
}
4.3.2.定义动态代理类,

主要逻辑是通过@Sql获取注解上的sql,并替换替换方法参数。映射成方法返回

public class AntMapperInvocationHandler<T> implements InvocationHandler {
    private Class<T> clazz;
    private DataSource dataSource;

    public AntMapperInvocationHandler(Class<T> clazz, DataSource dataSource) {
        this.clazz = clazz;
        this.dataSource = dataSource;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.isDefault()) {
            return invokeDefaultMethod(proxy, method, args);
        }

        // 假设方法上有 @Sql 注解
        Sql sqlAnnotation = method.getAnnotation(Sql.class);
        if (sqlAnnotation != null) {
            String sql = sqlAnnotation.value();
            Parameter[] parameters = method.getParameters();
            sql = replaceParameters(sql, parameters, args);
            try (Connection connection = DataSourceUtils.getConnection(dataSource);
                 PreparedStatement ps = connection.prepareStatement(sql)) {
                if (args != null) {
                    for (int i = 0; i < args.length; i++) {
                        ps.setObject(i + 1, args[i]);
                    }
                }
                ResultSet rs = ps.executeQuery();
                List<Map<String, Object>> results = new ArrayList<>();
                while (rs.next()) {
                    Map<String, Object> row = mapRowToMap(rs);
                    results.add(row);
                }
                    return results;

            } catch (SQLException e) {
                throw new RuntimeException("Error executing SQL", e);
            }
        }
        return null;
    }

    private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
        final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
                .getDeclaredConstructor(Class.class, int.class);
        if (!constructor.isAccessible()) {
            constructor.setAccessible(true);
        }
        final Class<?> declaringClass = method.getDeclaringClass();
        return constructor
                .newInstance(declaringClass,
                        MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
                .unreflectSpecial(method, declaringClass)
                .bindTo(proxy)
                .invokeWithArguments(args);
    }

    private String replaceParameters(String sql, Parameter[] parameters, Object[] args) {
        if (parameters == null || args == null || parameters.length != args.length) {
            return sql;
        }
        for (int i = 0; i < parameters.length; i++) {
            String placeholder = "#{" + parameters[i].getName() + "}";
            sql = sql.replace(placeholder, "?");
        }
        return sql;
    }

    private Map<String, Object> mapRowToMap(ResultSet rs) throws SQLException {
        Map<String, Object> row = new HashMap<>();
        ResultSetMetaData metaData = rs.getMetaData();
        int columnCount = metaData.getColumnCount();
        for (int i = 1; i <= columnCount; i++) {
            String columnName = metaData.getColumnName(i).toLowerCase();
            Object value = rs.getObject(i);
            row.put(columnName, value);
        }
        return row;
    }

}
4.3.3.定义ImportBeanDefinitionRegistrar,

主要逻辑,通过读取注解AntMapperScan,获取扫描包,使用Spring的ClassPathScanningCandidateComponentProvider包扫描接口(注意,ClassPathScanningCandidateComponentProvider包中需要自定义筛选逻辑,并添加包含过滤器,例子中只扫描带有 @AntMapper 注解的类。),处理完成后scanner.findCandidateComponents(basePackage)生成BeanDefinition的集合。这里代码是还可以优化的,大家自己优化一下。


public class AmtMapperImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 获取注解的属性值
        Map<String, Object> basePackages = importingClassMetadata.getAnnotationAttributes(AntMapperScan.class.getName(), true);

        String[] packagesToScan;
        if (basePackages == null || !basePackages.containsKey("basePackages") || ((String[]) basePackages.get("basePackages")).length == 0) {
            // 设置默认值为配置类所在的包及其子包
            String className = importingClassMetadata.getClassName();
            packagesToScan = new String[]{className.substring(0, className.lastIndexOf('.'))};
        } else {
            packagesToScan = (String[]) basePackages.get("basePackages");
        }

        Set<Class<?>> classes = findMapperInterfaces(packagesToScan);

        for (Class<?> clazz : classes) {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
            // 这里可以通过设置必要属性,如从注解中获取的属性值
            builder.setLazyInit(true);
            builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

            // 添加具体的属性值
            builder.addPropertyReference("dataSource", "dataSource"); // 引用Spring容器中的dataSource Bean

            AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
            beanDefinition.setBeanClass(AntMapperFactoryBean.class);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz);
            registry.registerBeanDefinition(clazz.getSimpleName(), beanDefinition);
        }
    }

    private Set<Class<?>> findMapperInterfaces(String[] basePackages) {
        // 创建自定义的 ClassPathScanningCandidateComponentProvider
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                // 自定义筛选逻辑:只选择接口且带有 @AntMapper 注解的类
                return beanDefinition.getMetadata().isInterface() &&
                        beanDefinition.getMetadata().hasAnnotation(AntMapper.class.getName());
            }
        };

        // 添加包含过滤器,指定只扫描带有 @AntMapper 注解的类
        scanner.addIncludeFilter(new AnnotationTypeFilter(AntMapper.class));

        Set<Class<?>> candidates = new HashSet<>();
        for (String basePackage : basePackages) {
            System.out.println("Scanning package: " + basePackage);
            Set<BeanDefinition> components = scanner.findCandidateComponents(basePackage);
            if (components.isEmpty()) {
                System.out.println("No candidate components found in package: " + basePackage);
            } else {
                for (BeanDefinition reader : components) {
                    try {
                        candidates.add(Class.forName(reader.getBeanClassName()));
                        System.out.println("Found candidate component: " + reader.getBeanClassName());
                    } catch (ClassNotFoundException e) {
                        System.out.println("Class not found: " + reader.getBeanClassName());
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return candidates;
    }
}
4.3.4. 接下来就是定义一些注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AntMapper {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface AntMapperScan {
    String[] basePackages() default {};
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Sql {
    String value() default "";
}
// 配置扫描包和Import加载
@Configuration
@Import(AmtMapperImportBeanDefinitionRegistrar.class)
@AntMapperScan(basePackages = "com.ant.spring.antbaties.mapper")
public class AntMapperConfiguration {
}
4.3.5.定义测试类
@AntMapper
public interface OrderMapper {
    @Sql("select * from orders where id = #{id}")
    List<Map> getOrder(int id);
}

@AntMapper
public interface UserMapper<T> {
    @Sql("select * from employees where name = #{name}")
    List<Map> getUser(String name);

    @Sql("select * from employees")
    List<Map> getUser();
}

@Component
public class AntTestRunner implements CommandLineRunner {
    @Autowired
    private UserMapper<List> userMapper;
    @Override
    public void run(String... args) throws Exception {
        List<Map> user = userMapper.getUser("小明");
        System.out.println("user-"+user);
        List<Map> allUser = userMapper.getUser();
        System.out.println("allUser-"+allUser);
    }
}

测试结果

5.总结

该 文章基于Spring Boot 扩展开发的关键点,详细描述了 Spring Bean 生命周期及其核心扩展机制。通过解析 Bean 生命周期的各个阶段,结合实际案例展示如何利用 BeanPostProcessor、BeanFactoryPostProcessor 和 ApplicationListener 等扩展点进行灵活的定制和增强功能,同时提供了最佳实践,希望能够在项目中能够用到,该文章主要适用于希望深入了解和应用 Spring Boot 扩展机制进行开发的人

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值