一个普通的Java项目,如果想对某些类织入额外的代码,一个比较好的选择是Aspectj,它对项目的侵入最小,只需要写一个Aspectj的切面文件,然后使用构建工具引入Aspectj的插件(gradle、maven),它就能在编译时织入你想要的代码。
我们项目中有一个使用quartz定时任务的工程,有很多的job,现在想要将这些job监控起来,job执行否?job执行成功否?基本思想是在每个job执行开始记录,执行结束记录,抛异常记录。有两种方案。一种是写一个抽象类,所有的job继承于此类,此类中在job真正执行方法之前之后和抛异常的时候进行处理。
/** * @author xiongshiyan at 2018/3/2 */ public class JobExecute extends Model<JobExecute> { public static final String ID = "id"; public static final String DAY = "day"; public static final String START = "start"; public static final String END = "end"; public static final String JOB_NAME = "jobName"; public static final String IS_SUCCESS_FINISHED = "isSuccessFinished"; public static final int SUCCESS = 1; public static final int FAIL = 0; public static final JobExecute dao = new JobExecute(); /** * job开始的时候记录,说明开始执行了 * @param day 哪一天 * @param start 开始时间 * @param jobName jobName * @return JobExecute 后面更新此model的某些字段 */ public JobExecute insertStart(Date day, Date start, String jobName){ JobExecute execute = new JobExecute(); execute.set(JobExecute.DAY,day); execute.set(JobExecute.START,start); execute.set(JobExecute.JOB_NAME,jobName); execute.set(JobExecute.IS_SUCCESS_FINISHED,FAIL); execute.save(); return execute; } /** * 正常结束的时候更新字段 */ public void updateEnd(){ this.set(END,new Date()); this.set(IS_SUCCESS_FINISHED,SUCCESS); this.update(); } }
/** * @author xiongshiyan at 2018/3/2 * 在开始和结束的时候增加日志记录 * id day start_time end_time is_success_finished job_name */ public abstract class AbstractLoggingJob implements Job{ private static final Logger logger = LoggerFactory.getLogger(AbstractLoggingJob.class); @Override public void execute(JobExecutionContext context) throws JobExecutionException { //1.开始的时候记录 Date start = new Date(); JobExecute execute = JobExecute.dao.insertStart(start, start, jobName()); //2.开始Job try { exe(context); } catch (Exception e) { logger.error(jobName() + " 发生异常:" + e.getMessage() , e); //发生了异常就往外抛,3就不会执行,end时间就会为空 throw new JobExecutionException(e); } //3.结束的时候更新这条记录 execute.updateEnd(); } /** * 真正执行的Job方法,子类复写 */ protected abstract void exe(JobExecutionContext context) throws Exception; /** * 子类返回JobName,默认就是类名 * @return job's name */ protected String jobName(){ return this.getClass().getName(); } }
这种方案比较简单,但是需要改每个job,容易出错。实际我们采用了第二种方案--采用Aspectj进行织入。
首先我们需要满足jdk和gradle的条件,1.8.0_121以上的JDK版本,gradle4.1以上。最开始我的jdk版本是1.8.0_65都编译不通过,老在下载依赖的时候报错。
其次,我们需要在gradle的编译文件中引入gradle-aspectj插件和aspectj的依赖。
buildscript { repositories { maven { url "https://maven.eveoh.nl/content/repositories/releases" } } dependencies { classpath "nl.eveoh:gradle-aspectj:2.0" } } ext.aspectjVersion = '1.8.13' apply plugin: 'aspectj' compileAspect { additionalAjcArgs = ['encoding': 'UTF-8', 'source': '1.8', 'target': '1.8'] }
compile 'org.aspectj:aspectjrt:1.8.13'
非常注意:设置文件编码,否则出现乱码。
然后,编写aspectj的切面文件,引入切面代码,这其中最重要的是表达式的编写,参见
http://blog.csdn.net/sunlihuo/article/details/52701548。
/** * @author xiongshiyan at 2018/3/6 * 切面文件 */ public aspect LoggingAspectJ { private static final Logger logger = LoggerFactory.getLogger(LoggingAspectJ.class); public pointcut jobs(JobExecutionContext context) : execution(public void cn.esstx.dzg.runner.tinyrunner.job..*.execute(org.quartz.JobExecutionContext)) && args(context); private JobExecute execute = null; before(JobExecutionContext context): jobs(context){ logger.info("[before]" + thisJoinPoint.getTarget().getClass() .getCanonicalName() + "." + thisJoinPoint.getSignature().getName()); Date start = new Date(); String jobName = thisJoinPoint.getTarget().getClass().getName(); execute = JobExecute.dao.insertStart(start, start, jobName); } //有成功返回,说明执行成功了,如果异常就不会执行 after(JobExecutionContext context) returning() : jobs(context) { logger.info("[after returning]" + thisJoinPoint.getTarget().getClass().getCanonicalName() + "." + thisJoinPoint.getSignature().getName()); execute.updateEnd(); } //抛不抛出异常都会执行 after(JobExecutionContext context) : jobs(context){ logger.info("[after]" + thisJoinPoint.getTarget().getClass() .getCanonicalName() + "." + thisJoinPoint.getSignature().getName()); } //抛异常的时候 after(JobExecutionContext context) throwing(java.lang.Exception e) : jobs(context){ logger.error("[after throwing]" + thisJoinPoint.getTarget().getClass().getCanonicalName() + "." + thisJoinPoint.getSignature().getName() + " throwing=" + e.getMessage()); } }
最后,执行gradle clean build 得到植入后的class文件,测试类为EveryMinuteTestJob,执行两次之后抛异常,反编译之后的代码如下。
public class EveryMinuteTestJob implements Job { private static int xx; private static final Logger logger; static { ajc$preClinit(); xx = 0; logger = LoggerFactory.getLogger(EveryMinuteTestJob.class); } public EveryMinuteTestJob() { } public void execute(JobExecutionContext context) throws JobExecutionException { JobExecutionContext var2 = context; JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, context); try { try { LoggingAspectJ.aspectOf().ajc$before$cn_esstx_dzg_runner_tinyrunner_LoggingAspectJ$1$ddb27cec(var2, var3); logger.info("execute method invoked-------"); ++xx; if (xx == 2) { throw new RuntimeException("simulation throw a exception"); } LoggingAspectJ.aspectOf().ajc$afterReturning$cn_esstx_dzg_runner_tinyrunner_LoggingAspectJ$2$ddb27cec(var2, var3); } catch (Throwable var6) { LoggingAspectJ.aspectOf().ajc$after$cn_esstx_dzg_runner_tinyrunner_LoggingAspectJ$3$ddb27cec(var2, var3); throw var6; } LoggingAspectJ.aspectOf().ajc$after$cn_esstx_dzg_runner_tinyrunner_LoggingAspectJ$3$ddb27cec(var2, var3); } catch (Exception var7) { LoggingAspectJ.aspectOf().ajc$afterThrowing$cn_esstx_dzg_runner_tinyrunner_LoggingAspectJ$4$ddb27cec(context, var7, var3); throw var7; } } }从反编译文件中,我们能够看出,每种通知的执行位置。before是目标方法之前,afterReturning是有返回值的时候,说明方法正常执行了可以得到执行的结果,after是目标方法执行之后,不管抛不抛出异常,afterThrowing是抛异常之后执行,around可以完全控制。
我们根据日志打印,可以清楚地看见这个执行顺序。
dzg-server-runner-prod: 2018-03-06 20:41:00.009 [DefaultQuartzScheduler_Worker-1] INFO (LoggingAspectJ.aj:19) - [before]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:41:00.047 [DefaultQuartzScheduler_Worker-1] INFO (EveryMinuteTestJob.java:17) - execute method invoked------- dzg-server-runner-prod: 2018-03-06 20:41:00.049 [DefaultQuartzScheduler_Worker-1] INFO (LoggingAspectJ.aj:30) - [after returning]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:41:00.057 [DefaultQuartzScheduler_Worker-1] INFO (LoggingAspectJ.aj:38) - [after]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:42:00.001 [DefaultQuartzScheduler_Worker-2] INFO (LoggingAspectJ.aj:19) - [before]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:42:00.010 [DefaultQuartzScheduler_Worker-2] INFO (EveryMinuteTestJob.java:17) - execute method invoked------- dzg-server-runner-prod: 2018-03-06 20:42:00.011 [DefaultQuartzScheduler_Worker-2] INFO (LoggingAspectJ.aj:38) - [after]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:42:00.011 [DefaultQuartzScheduler_Worker-2] ERROR (LoggingAspectJ.aj:46) - [after throwing]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute throwing=simulation throw a exception dzg-server-runner-prod: 2018-03-06 20:42:00.014 [DefaultQuartzScheduler_Worker-2] ERROR (JobRunShell.java:211) - Job cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob threw an unhandled Exception: java.lang.RuntimeException: simulation throw a exception at cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute(EveryMinuteTestJob.java:20) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) dzg-server-runner-prod: 2018-03-06 20:42:00.014 [DefaultQuartzScheduler_Worker-2] ERROR (QuartzScheduler.java:2425) - Job (cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob threw an exception. org.quartz.SchedulerException: Job threw an unhandled exception. at org.quartz.core.JobRunShell.run(JobRunShell.java:213) at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) Caused by: java.lang.RuntimeException: simulation throw a exception at cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute(EveryMinuteTestJob.java:20) at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ... 1 common frames omitted dzg-server-runner-prod: 2018-03-06 20:43:00.002 [DefaultQuartzScheduler_Worker-3] INFO (LoggingAspectJ.aj:19) - [before]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:43:00.010 [DefaultQuartzScheduler_Worker-3] INFO (EveryMinuteTestJob.java:17) - execute method invoked------- dzg-server-runner-prod: 2018-03-06 20:43:00.011 [DefaultQuartzScheduler_Worker-3] INFO (LoggingAspectJ.aj:30) - [after returning]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute dzg-server-runner-prod: 2018-03-06 20:43:00.022 [DefaultQuartzScheduler_Worker-3] INFO (LoggingAspectJ.aj:38) - [after]cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute
以上的实际上会有非常严重的线程安全问题。因为切面是个单例,所以其成员变量在不同的通知中有修改的话,就可能造成某个job1插入一条记录之后,又被另外的一个job2插入一条记录,成员变量execute就变为另外一个了,job1在方法调用完成后去更新其字段,实际就不是更新原来的记录的字段了。所以如果需要跨通知保存修改变量的话,就有安全问题。必须使用around通知。
public aspect LoggingAspectAround { private static final Logger logger = LoggerFactory.getLogger(LoggingAspectAround.class); public pointcut jobs() : execution(public void cn.esstx.dzg.runner.tinyrunner.job..*.execute(org.quartz.JobExecutionContext)); void around(): jobs(){ JobExecute execute = null; try { logger.info("[before]"+ this + ":" + thisJoinPoint.getTarget().getClass() .getCanonicalName() + "." + thisJoinPoint.getSignature().getName()); Date start = new Date(); String jobName = thisJoinPoint.getTarget().getClass().getName(); String simpleName = thisJoinPoint.getTarget().getClass().getSimpleName(); execute = JobExecute.dao.insertStart(start, start, jobName,simpleName); proceed(); logger.info("[after returning]" + this + ":" + thisJoinPoint.getTarget().getClass().getCanonicalName() + "." + thisJoinPoint.getSignature().getName()); execute.updateEnd(); } catch (Exception e) { logger.error("[after throwing]"+ this + ":" + thisJoinPoint.getTarget().getClass().getCanonicalName() + "." + thisJoinPoint.getSignature().getName() , e); execute.updateException(e); } } }