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 }
该配置为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提供任务的依赖性调度,也可以自己通过注解实现,该知识正在深入研究中。。。。。