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

posted @ 2020-06-02 12:35  superChong  阅读(2783)  评论(0编辑  收藏  举报