Urs Beeli
Urs Beeli

Reputation: 815

Dynamically creating beans using application properties

In my default SpringBoot application that has the spring-boot-starter-actuator dependency, a call to /actuator/health (with show-details: true) returns the following information

{
  "status": "UP",
  "components": { ... },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 2000396742656,
        "free": 581706977280,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": { "status": "UP" }
  }
}

However, I have two drives on my machine, so I would like to see the disk space details for both of them. So I added a configuration file:

@Configuration
public class DiskSpaceConfig {
  @Bean
  public AbstractHealthIndicator driveC() {
    return new DiskSpaceHealthIndicator(new File("C:/"), DataSize.ofMegabytes(100));
  }

  @Bean
  public AbstractHealthIndicator driveD() {
    return new DiskSpaceHealthIndicator(new File("D:/"), DataSize.ofMegabytes(100));
  }
}

Now, my health endpoint returns the following data

{
  "status": "UP",
  "components": { ... },
    "diskSpace": { "status": "UP", "details": { ... } },
    "driveC": { "status": "UP", "details": { ... } },
    "driveD": { "status": "UP", "details": { ... } },
    "ping": { "status": "UP" }
  }
}

(I'll think about how to suppress the default "diskSpace" entry later)

However, as I might want to run my application on different machines, I would like to configure the disk drives to check dynamically in my application.yml file.

media:
  health:
    drives:
      - name: driveC
        path: "C:/"
      - name: driveD
        path: "D:/"

Also adding

@Slf4j
@Component
@ConfigurationProperties(prefix = "media.health")
@Data
public class MediaHealthConfigLoader {
  private List<DriveConfig> drives;

  @PostConstruct
  public void postProcessBeanFactory() {
    log.info("Found {} configured drives", drives.size());
  }
}

@Data
public class DriveConfig {
  private String name;
  private String path;
}

@Configuration
@EnableConfigurationProperties
public class DiskSpaceConfig {
}

The configuration is loaded during startup and logged to the console.

However, my question is, how do I dynamically create those beans and add them to the bean context? I assume I would need access to the BeanFactory. I've tried the following:

@Slf4j
@Configuration
@EnableConfigurationProperties
@RequiredArgsConstructor
public class DiskSpaceConfig {
  private final MediaHealthConfigLoader mediaHealthConfigLoader;
  private final BeanFactory beanFactory;

  @PostConstruct
  public void createDiskSpaceHealthIndicators() {
    ArrayList<AbstractHealthIndicator> beans = new ArrayList<>();
    if (mediaHealthConfigLoader.getDrives() != null) {
      log.info("Creating {} dynamic DiskSpaceHealthIndicators", mediaHealthConfigLoader.getDrives().size());
      mediaHealthConfigLoader.getDrives().forEach(drive -> {
        DiskSpaceHealthIndicator indicator = new DiskSpaceHealthIndicator(new File(drive.getPath()), DataSize.ofMegabytes(100));
        log.info("Created indicator with name={} for path={}: bean={}", drive.getName(), drive.getPath(), indicator);
        
        // -> I would like to do something like: beanFactory.addBean(drive.getName(), indicator)
      });
    }
  }
}

However, my beanFactory only has getters, so there is no way to add my newly created beans.

I've also tried to hook into bean creation using a BeanFactoryPostProcessor, however, it seems that the configuration properties (or the class loading them) are not ready when that callback is executed.

@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicHealthIndicatorFactory implements BeanFactoryPostProcessor {
  private final MediaHealthConfigLoader mediaHealthConfigLoader;

  @Override
  public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {
    // mediaHealthConfigLoader is null here :-(

    mediaHealthConfigLoader.getDrives().forEach(drive -> {
        DiskSpaceHealthIndicator newBean = new DiskSpaceHealthIndicator(new File(drive.getPath()), DataSize.ofMegabytes(100));
        beanFactory.initializeBean(newBean, drive.getName());
        beanFactory.registerSingleton(drive.getName(), newBean);
    });
  }
}

alternatively

@Slf4j
@Component
@ConfigurationProperties(prefix = "media.health") // commenting out the same annotation on MediaHealthConfigLoader
public class DynamicHealthIndicatorFactory implements BeanFactoryPostProcessor {
  private List<DriveConfig> drives;

  @Override
  public void postProcessBeanFactory(final ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    // drives is null here :-(
    drives.forEach(drive -> {
        DiskSpaceHealthIndicator newBean = new DiskSpaceHealthIndicator(new File(drive.getPath()), DataSize.ofMegabytes(100));
        beanFactory.initializeBean(newBean, drive.getName());
        beanFactory.registerSingleton(drive.getName(), newBean);
    });
  }
}

However, both of these approaches cause NullPointerExceptions because neithe the properties nor the autowired bean are ready at this point of the lifecycle.

I'm sure I'm not the first person trying to do something like this, but it seems I fail to find the correct search terms to find a solution.

Upvotes: 2

Views: 2408

Answers (1)

Dirk Deyne
Dirk Deyne

Reputation: 6936

This should work ref: gist DemoApplication

@Configuration
@EnableConfigurationProperties
@AllArgsConstructor
class DiskSpaceConfig {
    private final MediaHealthConfigLoader mediaHealthConfigLoader;
    private final ConfigurableListableBeanFactory beanFactory;

    @PostConstruct
    public void registerBeans(){
        mediaHealthConfigLoader.getDrives().forEach(drive -> {
            DiskSpaceHealthIndicator newBean = new DiskSpaceHealthIndicator(new File(drive.getPath()), DataSize.ofMegabytes(100));
            beanFactory.initializeBean(newBean, drive.getName());
            beanFactory.registerSingleton(drive.getName(), newBean);
        });
    }
}

You can disable the default diskspace if you want

management:
  health:
    diskspace:
      enabled: false

Upvotes: 2

Related Questions