一、应用背景
项目中需要后端以无入侵的方式,调用调度中心API服务。然而调度中心设置了登录,调度中心API接口对cookie进行了验证,feign访问调度中心API服务时,需通过其登录验证。
二、实现原理
通过FeignClient客户端声明式调用调度中心Api服务与普通FeignClient相比作了一下几点处理:
- 调度中心登录Api服务返回值改为feign.Response,原始的http请求响应,方便获取cookie值;
- 调度中心其他Api服务,新增@RequestHeader("Cookie") String cookie参数,传递cookie值,通过调度中心登录验证;
三、潜在问题
- 网络开销:
每次调用接口如果都请求一次登录接口,难免会产生额外的网络开销,可以通过redis缓存cookie值去处理。 - 登录失效:
由于调度中心cookie有效时间为2小时,需每两小时登录一次,获取新的cookie,可以通过重试机制,实现过期重新登录
解决方案:可以参考 XxlJobComponent.java
四、代码实现
- 引入相关jar
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
2.HttpResultForXxlJob.java
/**
* xxl-job Api接口响应包装类
*
* @author liudong
* @date 2021/4/25 16:37
*/
@Data
public class HttpResultForXxlJob<T> implements Serializable {
private static final long serialVersionUID = 6512789515344894483L;
/**
* 请求状态码
*/
private int code;
/**
* 消息
*/
private String msg;
/**
* 返回数据信息
*/
private T content;
/**
* 序列化为Json
*
* @return json字符串
*/
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
- XxlJobClient.java
/**
* xxl-job客户端
*
* @author liudong
* @date 2021/4/25 16:33
*/
@FeignClient(name = "xxlJobClient", url = "${third-party.config.xxl-job.host:not found xxl-job service url}")
public interface XxlJobClient {
/**
* xxl-job登录接口
*
* @param params 参数
* @return 响应信息
*/
@PostMapping(value = "/xxl-job-admin/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
Response login(@RequestBody Map<String, ?> params);
/**
* 创建定时任务
*
* @param cookie cookie
* @param params 定时任务参数
* @return 定时任务ID
*/
@PostMapping(value = "/xxl-job-admin/jobinfo/add", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
HttpResultForXxlJob<Integer> add(@RequestHeader("Cookie") String cookie, @RequestBody Map<String, ?> params);
/**
* 更新定时任务
*
* @param cookie cookie
* @param params 定时任务更新参数
* @return 执行结果
*/
@PutMapping(value = "/xxl-job-admin/jobinfo/update", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
HttpResultForXxlJob<String> update(@RequestHeader("Cookie") String cookie, @RequestBody Map<String, ?> params);
/**
* 删除定时任务
*
* @param cookie cookie
* @param id 定时任务更新参数
* @return 执行结果
*/
@DeleteMapping(value = "/xxl-job-admin/jobinfo/remove")
HttpResultForXxlJob<String> remove(@RequestHeader("Cookie") String cookie, @RequestParam("id") int id);
/**
* 开启任务
*
* @param cookie cookie
* @param id 定时任务ID
* @return 执行结果
*/
@PutMapping(value = "/xxl-job-admin/jobinfo/start")
HttpResultForXxlJob<String> start(@RequestHeader("Cookie") String cookie, @RequestParam("id") int id);
/**
* 结束任务
*
* @param cookie cookie
* @param id 定时任务ID
* @return 执行结果
*/
@PutMapping(value = "/xxl-job-admin/jobinfo/stop")
HttpResultForXxlJob<String> stop(@RequestHeader("Cookie") String cookie, @RequestParam("id") int id);
/**
* 结束任务
*
* @param cookie cookie
* @param params 查询参数
* @return 执行结果
*/
@GetMapping(value = "/xxl-job-admin/joblog/pageList")
JSONObject log(@RequestHeader("Cookie") String cookie, @RequestParam("params") Map<String, Object> params);
}
- XxlJobComponent.java
/**
* 任务管理处理器
*
* @author liudong
* @date 2021/4/25 10:56
*/
@Slf4j
@Component
public class XxlJobComponent {
/**
* xxl job 账号
*/
@Value("${xxl.job.user-name}")
private String userName;
/**
* xxl job 密码
*/
@Value("${xxl.job.password}")
private String password;
/**
* xxl-job客户端
*/
@Resource
private XxlJobClient xxlJobClient;
/**
* 应用全局配置
*/
@Resource
private ApplicationConfig applicationConfig;
/**
* redis操作类
*/
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 登录xxl-job
*/
public void login() {
Map<String, Object> userInfo = new HashMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY);
userInfo.put("userName", userName);
userInfo.put("password", password);
// 设置cookie永久有效,对应xxl-job记住密码
userInfo.put("ifRemember", "on");
Response response = xxlJobClient.login(userInfo);
if (HttpStatus.HTTP_OK == response.status()) {
response.headers().get(XxlJobConstant.COOKIE_KEY).forEach(e -> {
if (e.contains(XxlJobConstant.XXL_JOB_LOGIN_IDENTITY)) {
redisTemplate.opsForValue().set(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY, e, CacheConstant.XXL_JOB_COOKIE_REDIS_TIMEOUT, TimeUnit.HOURS);
}
});
} else {
throw ExceptionFactory.systemException(ErrorCode.LOGIN_XXL_JOB_FAILURE_EXCEPTION);
}
}
/**
* 创建任务
*
* @param addOrUpdateXxlJobInfoRequest 任务参数
* @return 任务ID
*/
public Integer add(AddOrUpdateXxlJobInfoRequest addOrUpdateXxlJobInfoRequest) {
if (Boolean.FALSE.equals(redisTemplate.hasKey(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY))) {
login();
}
if (ObjectUtils.isNotEmpty(addOrUpdateXxlJobInfoRequest)) {
HttpResultForXxlJob result = xxlJobClient.add(String.valueOf(redisTemplate.opsForValue().get(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY)), BeanUtil.beanToMap(addOrUpdateXxlJobInfoRequest));
if (HttpStatus.HTTP_OK == result.getCode()) {
log.info(result.getMsg());
return (int) result.getContent();
}
}
return null;
}
/**
* 更新任务
*
* @param addOrUpdateXxlJobInfoRequest 任务参数
*/
public void update(AddOrUpdateXxlJobInfoRequest addOrUpdateXxlJobInfoRequest) {
if (Boolean.FALSE.equals(redisTemplate.hasKey(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY))) {
login();
}
if (ObjectUtils.isNotEmpty(addOrUpdateXxlJobInfoRequest)) {
HttpResultForXxlJob result = xxlJobClient.update(String.valueOf(redisTemplate.opsForValue().get(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY)), BeanUtil.beanToMap(addOrUpdateXxlJobInfoRequest));
log.info(result.getMsg());
AbstractAssert.isTrue(HttpStatus.HTTP_OK == result.getCode(), ErrorCode.UPDATE_JOB_FAILURE_EXCEPTION);
}
}
/**
* 删除任务
*
* @param id 任务ID
*/
public void remove(Integer id) {
if (Boolean.FALSE.equals(redisTemplate.hasKey(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY))) {
login();
}
if (ObjectUtils.isNotEmpty(id)) {
HttpResultForXxlJob result = xxlJobClient.remove(String.valueOf(redisTemplate.opsForValue().get(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY)), id);
log.info(result.getMsg());
AbstractAssert.isTrue(HttpStatus.HTTP_OK == result.getCode(), ErrorCode.REMOVE_JOB_FAILURE_EXCEPTION);
}
}
/**
* 启动任务
*
* @param id 任务ID
*/
public void start(Integer id) {
if (Boolean.FALSE.equals(redisTemplate.hasKey(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY))) {
login();
}
if (ObjectUtils.isNotEmpty(id)) {
HttpResultForXxlJob result = xxlJobClient.start(String.valueOf(redisTemplate.opsForValue().get(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY)), id);
log.info(result.getMsg());
AbstractAssert.isTrue(HttpStatus.HTTP_OK == result.getCode(), ErrorCode.START_JOB_FAILURE_EXCEPTION);
}
}
/**
* 停止任务
*
* @param id 任务ID
*/
public void stop(Integer id) {
if (Boolean.FALSE.equals(redisTemplate.hasKey(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY))) {
login();
}
if (ObjectUtils.isNotEmpty(id)) {
HttpResultForXxlJob result = xxlJobClient.stop(String.valueOf(redisTemplate.opsForValue().get(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY)), id);
log.info(result.getMsg());
AbstractAssert.isTrue(HttpStatus.HTTP_OK == result.getCode(), ErrorCode.STOP_JOB_FAILURE_EXCEPTION);
}
}
/**
* 查询日志
*
* @param params 查询参数
* @return 结果集
*/
@Retryable(value = SystemException.class, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public List<XxlJobLogDTO> log(Map<String, Object> params) {
try {
if (Boolean.FALSE.equals(redisTemplate.hasKey(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY))) {
login();
}
if (ObjectUtils.isEmpty(params)) {
return Lists.newArrayList();
}
JSONObject result = xxlJobClient.log(String.valueOf(redisTemplate.opsForValue().get(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY)), params);
if (ObjectUtils.isNotEmpty(result) && ObjectUtils.isNotEmpty(result.getString("data"))) {
return JSON.parseArray(result.getString("data"), XxlJobLogDTO.class);
}
} catch (FeignException e) {
redisTemplate.delete(CacheConstant.XXL_JOB_COOKIE_REDIS_KEY);
throw ExceptionFactory.systemException("远程操作xxl-job失败,进行重试!", e);
}
return Lists.newArrayList();
}
/**
* 重试次数达到最大后回调处理
*
* @param systemException 重试异常
*/
@Recover
public void recoverCallback(SystemException systemException) {
log.error("远程操作xxl-job异常!", systemException);
}
}
五、测试用例:
/**
* xxl-job api Test
*
* @author liudong
* @date 2021/4/26 9:45
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CdpApplication.class)
public class XxlJobClientTest {
@Resource
private XxlJobClient xxlJobClient;
@Test
public void login() {
}
@Test
public void add() {
Map<String, Object> hashMap = new HashMap(2);
hashMap.put("userName", "admin");
hashMap.put("password", "123456");
Response response = xxlJobClient.login(hashMap);
AddOrUpdateXxlJobInfoRequest addOrUpdateXxlJobInfoRequest = new AddOrUpdateXxlJobInfoRequest();
addOrUpdateXxlJobInfoRequest.setJobGroup(2);
addOrUpdateXxlJobInfoRequest.setJobDesc("test");
addOrUpdateXxlJobInfoRequest.setAuthor("liudong");
addOrUpdateXxlJobInfoRequest.setAlarmEmail("");
addOrUpdateXxlJobInfoRequest.setScheduleType("CRON");
addOrUpdateXxlJobInfoRequest.setScheduleConf("0/6 * * * * ?");
addOrUpdateXxlJobInfoRequest.setGlueType("BEAN");
addOrUpdateXxlJobInfoRequest.setExecutorHandler("testHandler");
addOrUpdateXxlJobInfoRequest.setExecutorRouteStrategy("FIRST");
addOrUpdateXxlJobInfoRequest.setMisfireStrategy(MisfireStrategyEnum.DO_NOTHING.toString());
addOrUpdateXxlJobInfoRequest.setExecutorBlockStrategy("SERIAL_EXECUTION");
XxlJobInfoDTO xxlJobInfoDTO = new XxlJobInfoDTO();
BeanUtils.copyProperties(addOrUpdateXxlJobInfoRequest, xxlJobInfoDTO);
Map<String, Object> stringObjectMap = BeanUtil.beanToMap(addOrUpdateXxlJobInfoRequest);
response.headers().get("set-cookie").forEach(e -> {
if (e.contains("XXL_JOB_LOGIN_IDENTITY")) {
System.out.println(xxlJobClient.add(e, stringObjectMap).toString());
}
});
}
@Test
public void update() {
Map<String, Object> hashMap = new HashMap(2);
hashMap.put("userName", "admin");
hashMap.put("password", "123456");
Response response = xxlJobClient.login(hashMap);
AddOrUpdateXxlJobInfoRequest addOrUpdateXxlJobInfoRequest = new AddOrUpdateXxlJobInfoRequest();
addOrUpdateXxlJobInfoRequest.setId(14);
addOrUpdateXxlJobInfoRequest.setJobGroup(2);
addOrUpdateXxlJobInfoRequest.setJobDesc("update");
addOrUpdateXxlJobInfoRequest.setAuthor("liudong");
addOrUpdateXxlJobInfoRequest.setAlarmEmail("");
addOrUpdateXxlJobInfoRequest.setScheduleType("CRON");
addOrUpdateXxlJobInfoRequest.setScheduleConf("0/6 * * * * ?");
addOrUpdateXxlJobInfoRequest.setGlueType("BEAN");
addOrUpdateXxlJobInfoRequest.setExecutorHandler("testHandler");
addOrUpdateXxlJobInfoRequest.setExecutorRouteStrategy("FIRST");
addOrUpdateXxlJobInfoRequest.setMisfireStrategy(MisfireStrategyEnum.DO_NOTHING.toString());
addOrUpdateXxlJobInfoRequest.setExecutorBlockStrategy("SERIAL_EXECUTION");
XxlJobInfoDTO xxlJobInfoDTO = new XxlJobInfoDTO();
BeanUtils.copyProperties(addOrUpdateXxlJobInfoRequest, xxlJobInfoDTO);
Map<String, Object> stringObjectMap = BeanUtil.beanToMap(addOrUpdateXxlJobInfoRequest);
response.headers().get("set-cookie").forEach(e -> {
if (e.contains("XXL_JOB_LOGIN_IDENTITY")) {
System.out.println(xxlJobClient.update(e, stringObjectMap).toString());
}
});
}
@Test
public void remove() {
Map<String, Object> hashMap = new HashMap(2);
hashMap.put("userName", "admin");
hashMap.put("password", "123456");
Response response = xxlJobClient.login(hashMap);
response.headers().get("set-cookie").forEach(e -> {
if (e.contains("XXL_JOB_LOGIN_IDENTITY")) {
System.out.println(xxlJobClient.remove(e, 23).toString());
}
});
}
@Test
public void start() {
Map<String, Object> hashMap = new HashMap(2);
hashMap.put("userName", "admin");
hashMap.put("password", "123456");
Response response = xxlJobClient.login(hashMap);
response.headers().get("set-cookie").forEach(e -> {
if (e.contains("XXL_JOB_LOGIN_IDENTITY")) {
System.out.println(xxlJobClient.start(e, 22).toString());
}
});
}
@Test
public void stop() {
Map<String, Object> hashMap = new HashMap(2);
hashMap.put("userName", "admin");
hashMap.put("password", "123456");
Response response = xxlJobClient.login(hashMap);
response.headers().get("set-cookie").forEach(e -> {
if (e.contains("XXL_JOB_LOGIN_IDENTITY")) {
System.out.println(xxlJobClient.stop(e, 22).toString());
}
});
}
}