quartz定時任務中日志切面踩坑實錄


一,背景介紹

  系統較為復雜,現拆解日志切面部分,表述如下

  1,A定時任務執行之前,記錄開始日志

  2,執行成功,記錄成功日志,同時獲取執行方法的結果

  3,執行失敗,記錄失敗日志。

二,代碼結構 

  直接點,say nothing without codes,

1         <dependency>
2             <groupId>org.quartz-scheduler</groupId>
3             <artifactId>quartz</artifactId>
4             <version>2.2.1</version>
5         </dependency>    

其他類似,slf4j,guava,springboot自己引入即可。

1,代碼結構

 

 

 

接着我們逐一介紹組件

2,配置類

 1 @Configuration
 2 public class JobConfig {
 3 
 4     @Bean(name = "LoadABCDJob")
 5     public JobDetailFactoryBean LoadABCDJob() {
 6         JobDetailFactoryBean jobDetail = new JobDetailFactoryBean();
 7         jobDetail.setJobClass(LoadABCDJob.class);
 8         jobDetail.setDurability(true);
 9         jobDetail.setName("LoadABCDJob");
10         jobDetail.setGroup("LoadABCDJob");
11         return jobDetail;
12     }
13 }
View Code

該配置為job配置,是quartz提供,另外還需tri配置,作用是配置執行頻次

@Configuration
@ConditionalOnProperty(name = "org.quartz.existing.jobs", havingValue = "true")
public class TrigConfig {
    @Value("${job.ABCDJob.cron}")
    private String abcdJobCron;

    @Bean
    public CronTriggerFactoryBean LoadABCDJobCron(@Qualifier("LoadABCDJob") JobDetailFactoryBean jobDetailFactoryBean) {
        CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
        trigger.setJobDetail(jobDetailFactoryBean.getObject());
        //MISFIRE_INSTRUCTION_DO_NOTHING
        trigger.setMisfireInstruction(2);
        trigger.setCronExpression(abcdJobCron);
        trigger.setName("LoadABCDJobTri");
        trigger.setGroup("LoadABCDJobTri");
        return trigger;
    }
}

其中@ConditionalOnPorperty是可選配置,當開關關閉時,不加載tri。

3,job代碼

@Slf4j
@DisallowConcurrentExecution
public class LoadABCDJob extends QuartzJobBean {

    @Autowired
    ABCDService abcdService;

    @Override
    protected void executeInternal(JobExecutionContext context) {
        log.info("開始定時任務");
        abcdService.synData(new ArrayList<TdABCDLog>());
        log.info("定時任務結束");
    }

}

其中@DisalllowConcurrentExecution作用是解決多邊部署情況下、多線程情況下重復執行的問題。@Slf4j是guava提供的日志包

4,service層

@Service
@Slf4j
public class ABCDService {

    @Transactional
    public TdABCDLog synData(List<TdABCDLog> abcdLogs) {
        return null;
    }
}

其中業務邏輯跟本文無關,我已經刪除了。方法的參數是日志記錄bean,自己定義即可,問題不大

5,日志service層

@Service
public class ABCDLogService {

    /**
     * 新增
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void start(String taskDef) {
        //新增日志

    }

    /**
     * 成功
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public TdABCDLog success(String taskDef) {
        //更新日志
        return null;
    }

    /**
     * 失敗
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public TdABCDLog fail(String taskDef) {
        //失敗日志
        return null;
    }
}

沒啥好解釋的,主要是dao層操作數據庫用。底層代碼就不放了,大家都懂得。

6,切面層

@Component
@Aspect
@Slf4j
public class LogAspect {

    @Autowired
    ABCDLogService abcdLogService;

    @Pointcut("execution(public * aspectDemo.service.ABCDService.syn*(..))")
    public void abcdPointCut() {
    }

    @Around("abcdPointCut()")
    public void processABCD(ProceedingJoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        String taskName = null;
        Object[] args = joinPoint.getArgs();
        ArrayList<TdABCDLog> abcdLogs = new ArrayList<>();
        abcdLogService.start(taskName);
        try {
            TdABCDLog tdABCDLog = (TdABCDLog) joinPoint.proceed();
            tdABCDLog = abcdLogService.success(taskName);
            abcdLogs.add(tdABCDLog);
        } catch (Throwable throwable) {
            TdABCDLog tdABCDLog = abcdLogService.fail(taskName);
            abcdLogs.add(tdABCDLog);
            log.error("taskName", throwable);
        } finally {
            for (Object arg : args) {
                if (arg instanceof List) {
                    ((List<TdABCDLog>) arg).addAll(abcdLogs);
                    break;
                }
            }
        }
    }
}

幾點說明:

  @PointCut配置執行以syn開頭的方法

  joinPoint.getSignature().getName();獲取方法名稱

  joinPoint.getArgs();獲取方法參數,該步為了代理方法和切面之前傳遞參數,比如執行結果啊,啥的,后續在業務邏輯里可以通過kafak等中間件或者郵件中心將結果通知到相關人。

三,遇到的問題

  以上是正確代碼,說下踩坑過程吧

  1,切面切service方法,本來想切job的executeInternal方法的,但是quartzjobBean無法代理,就是指定Proxy = true,spring也不會給quartz生成代理,可以通過Context.getCurrentProy()方法查看,代理對象為空。沒有辦法,所以才代理service層的方法的。

  2,事務問題,通過@Transactional實現,發現spring生成service代理的時候,把日志切面也給包事務里了,這樣就帶來了問題。

  日志-》定時任務-》日志結束,由於數據庫隔離級別我看不到開始日志,只有整個事務結束,我才看得到結果,這樣日志切面顯得毫無意義,我就想通過日志查看定時任務的狀態。

  解決辦法:通過事務傳播級別@Transactional(propagation = Propagation.REQUIRES_NEW)解決,原理:事務嵌套事務,讓日志獨立於任務。

四,總結

  通過日志切面和定時任務進行解耦合,可以實現兩塊代碼的相對獨立,代碼閱讀性較好,同時滿足代碼專一性原則。

  本次實踐較為簡單,quartz還提供一系列接口可以對任務的tri更新,查詢任務執行狀態等等,后續可以單獨講講。

  另外quartz提供任務的依賴性調度,也可以自己通過注解實現,該知識正在深入研究中。。。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM