情景:
数据库中的数据需要根据formula在一定时间上计算得到相应的结果数据,就是说,根据formula去计算,得到相应结果保存在相应字段上,这个job是定时触发的,计算按照一定的事件类型。
代码:
public class DashboardDataCalculationJob {
private final static Logger log = LoggerFactory.getLogger(DashboardDataCalculationJob.class);
protected String cronJobKey;
private DashboardDataCalculationService dashboardDataCalculationService;
public void setCronJobKey(final String cronJobKey) {
this.cronJobKey = cronJobKey;
}
public void setDashboardDataCalculationService(
final DashboardDataCalculationService dashboardDataCalculationService) {
this.dashboardDataCalculationService = dashboardDataCalculationService;
}
public void execute() {
long start = System.currentTimeMillis();
Date date = new Date();
log.info("DashboardDataCalculationJob executing Daily...");
boolean success = dashboardDataCalculationService.calculate(cronJobKey, PeriodType.Daily,
date);
if (success) {
log.info("DashboardDataCalculationJob executing Monthly...");
success = dashboardDataCalculationService.calculate(cronJobKey, PeriodType.Monthly,
date);
}
if (success) {
log.info("DashboardDataCalculationJob executing Yearly...");
dashboardDataCalculationService.calculate(cronJobKey, PeriodType.Yearly, date);
}
log.info("DashboardDataCalculationJob took {} ms.", System.currentTimeMillis() - start);
}
}
这个类负责调用正确的cronJob去执行定时任务。我们可以看到,传过来相应的cronJobKey,首先从daily开始计算,只要成功,就会触发一系列的计算
接下来,我们看看DashboardDataCalculationService这个接口.
@WebService
public interface DashboardDataCalculationService {
public boolean calculate(String cronJobKey, PeriodType periodType, Date date);
public boolean calculateFields(List<String> fieldList, PeriodType periodType, Date date);
public Double doCalculate(String formula, Date calculateDate, PeriodType periodType);
}
这个接口中定义了三个calculate方法,用于计算,再看看它的实现类中
public boolean calculate(String cronJobKey, PeriodType periodType, Date date);方法
public boolean calculate(final String cronJobKey, final PeriodType periodType, final Date date) {
try {
init();
Date calculateDate = JobUtil.getCalculateDate(periodType, date);
SystemCronJob systemCronJob = systemCronJobService.getSystemCronJobByJobKey(cronJobKey);
if (systemCronJob == null) {
log.error("SystemCronJob not found by cronJobKey: {}", cronJobKey);
String subject = "SystemCronJob not found by cronJobKey: " + cronJobKey;
String content = subject;
try {
emailService.sendEmail(subject, content);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return false;
}
List<DashboardDataDefinition> defList = systemCronJob.getDashboardDataDefinitions();
for (DashboardDataDefinition def : defList) {
calculateDef(periodType, calculateDate, def);
}
return true;
} catch (Exception ex) {
log.error(ex.getMessage());
String subject = "DashboardDataCalculationService Failed: " + ex.getMessage();
StringWriter writer = new StringWriter();
ex.printStackTrace(new PrintWriter(writer));
String content = writer.toString();
try {
emailService.sendEmail(subject, content);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return false;
} finally {
release();
}
}
看下这个方法,init()方法初始化了ThreadLocal变量中的参数,
private static final ThreadLocal<Map<String, Double>> cachedValue = new ThreadLocal<Map<String, Double>>();
private static final ThreadLocal<Map<String, Boolean>> cachedValueSumLast = new ThreadLocal<Map<String, Boolean>>();
public void init() {
cachedValue.set(new HashMap<String, Double>());
cachedValueSumLast.set(new HashMap<String, Boolean>());
}
接下来,systemCronJobService 负责去数据库拿到相应的SystemCronJob 对象,如果没拿到,则直跳出返回。
拿到SystemCronJob 对象后,通过它可以得到相应的需要计算的数据表对象,得到要计算的记录,分条执行calculateDef(periodType, calculateDate, def)。
private void calculateDef(final PeriodType periodType, final Date calculateDate,
final DashboardDataDefinition def) {
log.info("ChartField: {}, FormulaText: {}", def.getChartField(), def.getFormulaText());
String formula = def.getFormulaText();
if (StringUtils.isBlank(formula)) {
log.warn("'{}' FormulaText is blank", def.getChartField());
return;
}
log.info("formula: {}={}", def.getChartField(), formula);
Double value = doCalculate(formula, calculateDate, periodType);
在方法public boolean calculate(final String cronJobKey, final PeriodType periodType, final Date date) 调用 private void calculateDef(final PeriodType periodType, final Date calculateDate,
final DashboardDataDefinition def)方法,在此方法中,最终会调用
Double value = doCalculate(formula, calculateDate, periodType);
doCalculate()方法,所以,关键的计算在这里面。
public Double doCalculate(final String formula, final Date calculateDate,
final PeriodType periodType) {
String cacheKey = getCacheKey(formula, calculateDate, periodType);
if (cachedValue.get() == null) {
init();
}
if (cachedValue.get().containsKey(cacheKey)) {
Double cached = cachedValue.get().get(cacheKey);
Boolean sumLastCal = cachedValueSumLast.get().get(cacheKey);
if (sumLastCal != null && sumLastCal.booleanValue() == true) {
FormulaMethod.sumLastCal.set(Boolean.TRUE);
}
log.info("use cached value {} :{}", formula, cached);
return cached;
}
long start = System.currentTimeMillis();
log.info("calculateDate: {}", calculateDate);
log.info("calculateDate.time: {}", calculateDate.getTime());
log.info("periodType: {}", periodType);
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(formula);
StandardEvaluationContext context = new StandardEvaluationContext();
DashboardDataAccessor accessor = new DashboardDataAccessor(calculateDate, periodType);
context.setRootObject(new FormulaMethod(calculateDate, periodType, accessor));
List<PropertyAccessor> propertyAccessors = new ArrayList<PropertyAccessor>();
propertyAccessors.add(accessor);
context.setPropertyAccessors(propertyAccessors);
Object value = exp.getValue(context);
log.info("{} = {}", formula, value);
log.info("took {} ms.", System.currentTimeMillis() - start);
double v = Double.parseDouble(value.toString());
cachedValue.get().put(cacheKey, v);
cachedValueSumLast.get().put(cacheKey, FormulaMethod.sumLastCal.get());
return v;
}
这个方法中,首先把要计算的条件组成一个key,放在本地线程当中,其中有一个相应的计算结果,如果下次计算的条件与其相同,则不需要计算了,直接拿出结果就OK了,这样就节省了很多时间。
关键的计算是调用了spring的ExpressionParser及 Expression,让ExpressionParser去解析我们的formula,得到Expression 对象,最后有Expression对象调用它的getValue()方法得到计算结果。
当然,其中用到StandardEvaluationContext,PropertyAccessor,这些都是计算不可少的spring自带对象, DashboardDataAccessor以及FormulaMethod 都是自己编写的用于计算的条件对象,看名字应该知道它们是用来干什么的。