xxl-job是非常流行的分布式调度中心,通常我们直接拿来用或做二开与自己的服务集成,作为微服务架构里的统一调度组件,对定时调度类作业集中管理,将调度与业务剥离。
xxl-job的架构里,xxl-job-admin是负责调度的组件,根据时间轮算法触发任务,通过RPC方式调用xxl-job-executor执行具体的任务,可以制定不同的JobHandler实现不同的执行方式。通常为了业务解耦,xxl-job-executor会独立部署,与业务通过RPC调用方式来实现任务的业务逻辑执行。
以dubbo为RPC通信组件的微服务集群中,实现以xxl-job-executor作为consumer的JobHandler,调用provider的dubbo接口,业务人员只需要关注业务逻辑实现,像其它微服务接口一样提供定时作业的接口,即可方便的实现定时任务。dubbo提供了泛化调用的方式,可以在不引入provider方的接口定义,只需要知道接口签名、方法形参、实参就可以实现dubbo调用。以下是示例,基于dubbo2.7版本。
引入dubbo依赖
org.apache.dubbo
dubbo-spring-boot-starter
org.apache.dubbo
dubbo-dependencies-zookeeper
pom
org.slf4j
slf4j-log4j12
定义DubboJobHandler
核心是genericService.$invoke
@Component
public class DubboJobHandler {
private static final Logger logger = LoggerFactory.getLogger(DubboJobHandler.class);
// ReferenceConfig比较重,建议缓存
private final Map> referenceConfigMap = new ConcurrentHashMap<>();
@Autowired
private ObjectProvider applicationConfigObjectProvider;
@Autowired
private ObjectProvider registryConfigObjectProvider;
/**
* 兼容2.7.3版本与2.7.13版本,二者版本的入参类型不一致,会导致NoSuchMethod异常
*/
private static final SupplierHolder> REFERENCE_CONFIG_CACHE_GET_METHOD =
new SupplierHolder<>(() -> Arrays.stream(ReferenceConfigCache.class.getDeclaredMethods())
.filter(m -> "get".equals(m.getName()))
.filter(m -> m.getParameterTypes().length == 1)
.filter(m -> !m.getParameterTypes()[0].equals(Class.class))
.filter(m -> !m.getParameterTypes()[0].equals(String.class))
.peek(m -> {
if (m.isAccessible()) {
m.setAccessible(true);
}
})
.collect(Collectors.toList()));
/**
* 兼容2.7.3版本与2.7.13版本,二者版本的入参类型不一致,会导致NoSuchMethod异常
*/
private static final SupplierHolder> REFERENCE_CONFIG_CACHE_DESTROY_METHOD =
new SupplierHolder<>(() -> Arrays.stream(ReferenceConfigCache.class.getDeclaredMethods())
.filter(m -> "destroy".equals(m.getName()))
.filter(m -> m.getParameterTypes().length == 1)
.filter(m -> !m.getParameterTypes()[0].equals(Class.class))
.filter(m -> !m.getParameterTypes()[0].equals(String.class))
.peek(m -> {
if (m.isAccessible()) {
m.setAccessible(true);
}
})
.collect(Collectors.toList()));
/**
* 3、dubbo调用任务,定义handler名称为dubboJobHandler
*/
@XxlJob("dubboJobHandler")
public void dubboJobHandler() throws Exception {
logger.info("E|任务执行|dubboJobHandler|{}|{},{}", XxlJobHelper.getJobId(),
XxlJobHelper.getJobTag(), XxlJobHelper.getJobDesc());
try {
final DubboParams dubboParams = GsonTool.fromJson(XxlJobHelper.getJobParam(), DubboParams.class);
final ReferenceConfig referenceConfig = buildReferenceConfig(dubboParams);
final ReferenceConfigCache referenceConfigCache = ReferenceConfigCache.getCache();
// 获取泛化方法
GenericService genericService = null;
try {
genericService = referenceConfigCache.get(referenceConfig); // 直接调用get方法
} catch (NoSuchMethodError e) {
final List methods = REFERENCE_CONFIG_CACHE_GET_METHOD.get();
if (methods.isEmpty()) {
XxlJobHelper.handleFail("获取调用接口失败");
return;
}
final Optional methodAny = methods.stream().filter(m -> {
final Class type = m.getParameterTypes()[0];
return type.equals(referenceConfig.getClass())
|| type.isAssignableFrom(referenceConfig.getClass());
}).findAny();
if (!methodAny.isPresent()) {
XxlJobHelper.handleFail("获取调用接口失败");
return;
}
// 通过反射拿到get方法再调用,拿到泛化服务
try {
genericService = (GenericService) methodAny.get().invoke(referenceConfigCache, referenceConfig);
} catch (Exception e1) {
logger.error("invoke ReferenceConfigCache#get(ReferenceConfig) error", e1);
XxlJobHelper.handleFail("获取调用接口失败");
return;
}
} catch (Exception e) {
logger.error("invoke ReferenceConfigCache#get(ReferenceConfig) error", e);
try {
referenceConfigCache.destroy(referenceConfig); // 直接调用destroy方法
} catch (NoSuchMethodError e1) {
logger.warn("invoke ReferenceConfigCache#destroy(ReferenceConfig) cause NoSuchMethodError");
final List methods = REFERENCE_CONFIG_CACHE_DESTROY_METHOD.get();
methods.stream().filter(m -> {
final Class type = m.getParameterTypes()[0];
return type.equals(referenceConfig.getClass())
|| type.isAssignableFrom(referenceConfig.getClass());
}).findAny().ifPresent(m -> {
try {
m.invoke(referenceConfigCache, referenceConfig);
} catch (Exception e2) {
logger.warn("invoke ReferenceConfigCache#destroy(ReferenceConfig) fail, msg={}",
e2.getMessage());
}
});
}
// 服务不正常时清理缓存
removeReferenceConfig(dubboParams);
XxlJobHelper.handleFail("获取调用接口失败");
return;
}
// 传递attachment
Optional.ofNullable(dubboParams.getAttachments())
.ifPresent(m -> m.forEach(RpcContext.getContext()::setAttachment));
// 使用泛化方式调用dubbo接口
final Object result = genericService.$invoke(dubboParams.getInvoke().split("#")[1],
dubboParams.getTypes(), dubboParams.getArgs());
XxlJobHelper.log(result == null ? null : result.toString());
} catch (Exception e) {
logger.error("execute fail for jobId={}, param={}", XxlJobHelper.getJobId(), XxlJobHelper.getJobParam(), e);
XxlJobHelper.log(e);
XxlJobHelper.handleFail("Exception(" + e.getClass().getName() + "," + e.getMessage() + ")");
}
}
private ReferenceConfig buildReferenceConfig(DubboParams dubboParams) {
final String key = Stream.of(dubboParams.getInvoke(), dubboParams.getGroup(),
dubboParams.getVersion(), dubboParams.getTimeout(), dubboParams.getRetry())
.map(String::valueOf).collect(Collectors.joining(":"));
return referenceConfigMap.computeIfAbsent(key, k -> {
ReferenceConfig reference = new ReferenceConfig<>();
reference.setGeneric("true");
reference.setRetries(dubboParams.getRetry());
reference.setTimeout(dubboParams.getTimeout());
reference.setApplication(applicationConfigObjectProvider.getIfAvailable());
reference.setRegistry(registryConfigObjectProvider.getIfAvailable());
reference.setInterface(dubboParams.getInvoke().split("#")[0]);
reference.setVersion(dubboParams.getVersion());
reference.setGroup(dubboParams.getGroup());
return reference;
});
}
private void removeReferenceConfig(DubboParams dubboParams) {
final String key = Stream.of(dubboParams.getInvoke(), dubboParams.getGroup(),
dubboParams.getVersion(), dubboParams.getTimeout(), dubboParams.getRetry())
.map(String::valueOf).collect(Collectors.joining(":"));
referenceConfigMap.remove(key);
}
}
实现很简单,如果不考虑跨版本的兼容性问题,通过反射获取到泛化方法那些处理都可以省掉。至于为什么会有版本兼容性问题,一来是版本管理做得不好,版本没有统一;二来无非是想支持多个版本。
DubboParams是调用参数,配置在xxl-job的任务参数中,如
{
"args": [
"statTag#181"
],
"types": [
"java.lang.String"
],
"invoke": "com.xxx.facade.admin.CompensateTaskService#doCompensate",
"version": "1.0",
"timeout": 30000
}
DubboParams对象定义如下:
@Data
@Accessors(chain = true)
public class DubboParams implements Serializable {
// 请求接口:接口名#方法名
private String invoke;
// 版本号
private String version = "1.0";
// 分组
private String group;
// 参数类型,如[java.lang.Integer]
private String[] types;
// 参数,如[1]
private Object[] args;
// 超时时间
private Integer timeout;
// 重试次数
private int retry;
// 附加参数
private Map attachments;
}
xxl-job-admin中配置如下: