Spring Application的@PostConstruct方法

时间:2018-01-25 11:55:58

标签: java spring spring-data-jpa future taskscheduler

我正在使用Spring TaskScheduler在应用程序启动时安排任务(显然......)。

TaskScheduler是在我的SpringConfig中创建的:

@Configuration
@EnableTransactionManagement
public class SpringConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        return new ThreadPoolTaskScheduler();
    }

}

Spring Boot应用程序在我的Main.class中启动,并安排任务@PostConstruct

@SpringBootApplication
@ComponentScan("...")
@EntityScan("...")
@EnableJpaRepositories("... .repositories")
@EnableAutoConfiguration
@PropertySources(value = {@PropertySource("classpath:application.properties")})
public class Main {

    private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static SpringApplication application = new SpringApplication(Main.class);

    private TaskScheduler taskScheduler;

    private AnalysisCleaningThread cleaningThread;

    @Inject
    public void setCleaningThread(AnalysisCleaningThread cleaningThread) {
        this.cleaningThread = cleaningThread;
    }

    @Inject
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public static void main(String[] args)
            throws Exception {

        try {

            //Do some setup

            application.run(args);

        } catch (Exception e) {

            LOGGER.error(e.getMessage(), e);
        }
    }


    @PostConstruct
    public void init()
            throws Exception {

        //Do some setup as well

        ScheduledFuture scheduledFuture = null;
        LOGGER.info("********** Scheduling one time Cleaning Thread. Starting in 5 seconds **********");
        Date nowPlus5Seconds = Date.from(LocalTime.now().plus(5, ChronoUnit.SECONDS).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant());
        scheduledFuture = this.taskScheduler.schedule(this.cleaningThread, nowPlus5Seconds);




        while (true) {
           //Somehow blocks thread from running
           if (scheduledFuture.isDone()) {
               break;
           }
           Thread.sleep(2000);
        }



        //schedule next periodic thread

}

应用程序必须等待线程完成,因为它的任务是在意外的应用程序关闭后清理脏数据库条目。接下来的任务是获取已清理的条目并再次处理它们。 清理线程实现如下:

@Named
@Singleton
public class AnalysisCleaningThread implements Runnable {

    private static Logger LOGGER = LoggerFactory.getLogger(AnalysisCleaningThread.class);

    private AnalysisService analysisService;

    @Inject
    public void setAnalysisService(AnalysisService analysisService) {
        this.analysisService = analysisService;
    }

    @Override
    public void run() {
        List<Analysis> dirtyAnalyses = analysisService.findAllDirtyAnalyses();
        if(dirtyAnalyses != null && dirtyAnalyses.size() > 0) {
            LOGGER.info("Found " + dirtyAnalyses.size() + " dirty analyses. Cleaning... ");
            for (Analysis currentAnalysis : dirtyAnalyses) {
                //Reset AnalysisState so it is picked up by ProcessingThread on next run
                currentAnalysis.setAnalysisState(AnalysisState.CREATED);
            }
            analysisService.saveAll(dirtyAnalyses);
        } else {
            LOGGER.info("No dirty analyses found.");
        }
    }

}

我在第一行run方法和第二行上设置了一个断点。如果我使用ScheduledFuture.get(),则调用第一行,然后调用JPA存储库方法,但它永远不会返回...它不会在控制台中生成查询...

如果我使用ScheduledFuture.isDone(),则根本不会调用run方法......

编辑:

所以我进一步研究了这个问题,这就是我发现它停止工作的地方:

  1. 我使用了scheduledFuture.get()等待任务完成
  2. 调用AnalysisCleaningThread的run()方法中的第一行代码,该代码应调用服务以检索分析列表
  3. 调用CglibAopProxy来拦截方法
  4. ReflectiveMethodInvocation - &gt; TransactionInterceptor - &gt; TransactionAspectSupport - &gt; DefaultListableBeanFactory - &gt;调用AbstractBeanFactory以按类型
  5. 搜索和匹配PlatformTransactionManager bean
  6. 使用beanName “main”调用DefaultSingletonBeanRegistry.getSingleton,并在第187行 synchronized(this.singletonObjects)应用程序暂停且永不继续
  7. 从我的观点来看,似乎this.singletonObjects目前正在使用,因此线程无法以某种方式继续......

1 个答案:

答案 0 :(得分:2)

所以我已经做了很多研究,因为发生了这个问题,终于找到了解决我的罕见病例的方法。

我首先注意到的是,如果没有future.get(),AnalysisCleaningThread确实运行没有任何问题,但运行方法花费了2秒钟来执行第一行,所以我认为必须有一些东西在最终进行数据库调用之前在后台进行。

我在原始问题编辑中通过调试发现的是,应用程序在第93行synchronized(this.singletonObjects)方法中的同步块DefaultSingletonBeanRegistry.getSingleton处停止,因此必须拿着那个锁物。一旦调用DefaultSingletonBeanRegistry.getSingleton的迭代方法将“main”作为参数“beanName”传递到getSingleton,它就会在该行停止。

BTW调用该方法(或更好的方法链)以获取PlatformTransactionManager bean的实例以进行该服务(数据库)调用。

我的第一个想法是,它必定是一个僵局。

最后的想法

根据我的理解,bean在其生命周期内仍未准备就绪(仍然在其@PostConstruct init()方法中)。当spring尝试获取平台事务管理器的实例以便进行数据库查询时,应用程序会死锁。它实际上是死锁,因为在迭代所有bean名称以找到PlatformTansactionManager时,它还会尝试解析当前正在等待的“main”bean,因为@PostConstruct方法中的future.get()。因此它无法获取实例并且永远等待释放锁。

解决方案

由于我不想将该代码放在另一个类中,因为Main.class是我的入口点,所以我开始寻找一个钩子,它在应用程序完全启动后启动任务。

我在@EventListener上讨论了ApplicationReadyEvent.class,在我的情况下,我会听取@SpringBootApplication @ComponentScan("de. ... .analysis") @EntityScan("de. ... .persistence") @EnableJpaRepositories("de. ... .persistence.repositories") @EnableAutoConfiguration @PropertySources(value = {@PropertySource("classpath:application.properties")}) public class Main { private final static Logger LOGGER = LoggerFactory.getLogger(Main.class); private static SpringApplication application = new SpringApplication(Main.class); private TaskScheduler taskScheduler; private AnalysisProcessingThread processingThread; private AnalysisCleaningThread cleaningThread; @Inject public void setProcessingThread(AnalysisProcessingThread processingThread) { this.processingThread = processingThread; } @Inject public void setCleaningThread(AnalysisCleaningThread cleaningThread) { this.cleaningThread = cleaningThread; } @Inject public void setTaskScheduler(TaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; } public static void main(String[] args) throws Exception { try { //Do some setup application.run(args); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } @PostConstruct public void init() throws Exception { //Do some other setup } @EventListener(ApplicationReadyEvent.class) public void startAndScheduleTasks() { ScheduledFuture scheduledFuture = null; LOGGER.info("********** Scheduling one time Cleaning Thread. Starting in 5 seconds **********"); Date nowPlus5Seconds = Date.from(LocalTime.now().plus(5, ChronoUnit.SECONDS).atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant()); scheduledFuture = this.taskScheduler.schedule(this.cleaningThread, nowPlus5Seconds); try { scheduledFuture.get(); } catch (InterruptedException | ExecutionException e) { LOGGER.error("********** Cleaning Thread did not finish as expected! Stopping thread. Dirty analyses may still remain in database **********", e); scheduledFuture.cancel(true); } } } 并且有效。这是我的代码解决方案。

@PostConstruct

摘要

@PostConstruct方法执行spring数据存储库调用,在极少数情况下,如果使用PlatformTransactionManager注释的方法没有结束,则会发生死锁 在spring之前可以获取DefaultSingletonBeanRegistry.getSingleton bean来执行spring数据存储库查询。无论是无限循环还是future.get()方法都无关紧要....只有当迭代所有已注册的beanNames并最终调用@PostConstruct以找到PlatformTransactionManager bean的方法调用带有当前位于PlatformTransactionManager方法中的bean名称的getSingleton时,才会发生这种情况。如果它在此之前找到sbt doc,则不会发生。

我希望它可以理解,而且我的英语很好解释它。