一個普通的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.executedzg-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.executedzg-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.executedzg-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.executedzg-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.executedzg-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 exceptiondzg-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 exceptionat 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 exceptionat cn.esstx.dzg.runner.tinyrunner.job.EveryMinuteTestJob.execute(EveryMinuteTestJob.java:20)at org.quartz.core.JobRunShell.run(JobRunShell.java:202)... 1 common frames omitteddzg-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.executedzg-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.executedzg-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); } }}