一,背景介紹
系統較為復雜,現拆解日志切面部分,表述如下
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 }
該配置為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提供任務的依賴性調度,也可以自己通過注解實現,該知識正在深入研究中。。。。。
