分布式定时任务—xxl-job学习(四)——调度中心web页面端api调用源码分析

前言

接上篇:分布式定时任务—xxl-job学习(三)——调度中心(xxl-job-admin)的启动和任务调度过程源码分析

本篇我们从调度中心的wen页面端分析下api调用的源码。
api类
也就是上图中的一些api调用类。

一、controller目录下非controller类

1.1 PermissionLimit自定义注解

/**
 * 权限限制
 * @author xuxueli 2015-12-12 18:29:02
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionLimit {
	
	/**
	 * 登录拦截 (默认拦截)
	 */
	boolean limit() default true;

	/**
	 * 要求管理员权限
	 *
	 * @return
	 */
	boolean adminuser() default false;

}

分析:
自定义了一个方法上的权限限制注解,主要控制登录拦截和是否要求有管理员权限。

1.2 拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private PermissionInterceptor permissionInterceptor;
    @Resource
    private CookieInterceptor cookieInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");
        registry.addInterceptor(cookieInterceptor).addPathPatterns("/**");
    }

}

分析:
WebMvcConfig 配置类实现了WebMvcConfigurer接口,进行框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter等。

在这里重写了void addInterceptors(InterceptorRegistry registry)方法增加拦截器配置

  • addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
  • addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截
  • excludePathPatterns:用于设置不需要拦截的过滤规则。

1.2.1 PermissionInterceptor权限拦截器

@Component
public class PermissionInterceptor extends HandlerInterceptorAdapter {

	@Resource
	private LoginService loginService;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		
		if (!(handler instanceof HandlerMethod)) {
			return super.preHandle(request, response, handler);
		}

		// if need login
		boolean needLogin = true;
		boolean needAdminuser = false;
		HandlerMethod method = (HandlerMethod)handler;
		PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class);
		if (permission!=null) {
			needLogin = permission.limit();
			needAdminuser = permission.adminuser();
		}

		if (needLogin) {
			XxlJobUser loginUser = loginService.ifLogin(request, response);
			if (loginUser == null) {
				response.sendRedirect(request.getContextPath() + "/toLogin");
				//request.getRequestDispatcher("/toLogin").forward(request, response);
				return false;
			}
			if (needAdminuser && loginUser.getRole()!=1) {
				throw new RuntimeException(I18nUtil.getString("system_permission_limit"));
			}
			request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser);
		}

		return super.preHandle(request, response, handler);
	}
	
}

分析: 该类继承HandlerInterceptorAdapter适配器类,此处只重写了boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)方法,预处理回调方法,在方法被调用前执行。

  1. 判断方法是带有映射的方法,不是的话不需要走权限控制;
  2. handler强转为HandlerMethod,并取到方法上的PermissionLimit注解;
  3. 根据方法上的注解得到是否需要登录、是否需要有管理员权限;
  4. 如果需要登录则调用loginService.ifLogin(request, response)方法判断Cookie中是否有用户信息,没有则跳转到登录页面;
  5. 如果Cookie中取到的用户角色id不是1则认为无管理员权限;
  6. 调用父类的super.preHandle(request, response, handler)

1.2 CookieInterceptor Cookie缓存拦截器

@Component
public class CookieInterceptor extends HandlerInterceptorAdapter {

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {

		// cookie
		if (modelAndView!=null && request.getCookies()!=null && request.getCookies().length>0) {
			HashMap<String, Cookie> cookieMap = new HashMap<String, Cookie>();
			for (Cookie ck : request.getCookies()) {
				cookieMap.put(ck.getName(), ck);
			}
			modelAndView.addObject("cookieMap", cookieMap);
		}

		// static method
		if (modelAndView != null) {
			modelAndView.addObject("I18nUtil", FtlUtil.generateStaticModel(I18nUtil.class.getName()));
		}
		
		super.postHandle(request, response, handler, modelAndView);
	}
	
}

分析: 该类继承HandlerInterceptorAdapter适配器类,此处只重写了postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)后处理方法,在主方法执行后渲染视图前执行。

  1. 如果视图不为null,将请求中的Cookie信息以HashMap<String, Cookie> cookieMap的形式存入视图中;
  2. 如果视图不为null,将国际化工具类存入模板信息中再放入视图。

二、controller目录下controller类

2.1 IndexController类

包括登录、图表展示等方法

	@InitBinder
	public void initBinder(WebDataBinder binder) {
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		dateFormat.setLenient(false);
		binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
	}

注意:
该类有一个数据绑定方法:Spring在绑定请求参数到HandlerMethod的时候会借助WebDataBinder进行数据转换,"yyyy-MM-dd HH:mm:ss"这种类型的字符串直接使用Date类型接收。

2.2 JobApiController类

该类提供了三个api方法供执行器组件端调用,具体包括"callback"、“registry"和"registryRemove”。

2.2.1 AdminBizImpl.callback(callbackParamList)

    @Override
    public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
        for (HandleCallbackParam handleCallbackParam: callbackParamList) {
            ReturnT<String> callbackResult = callback(handleCallbackParam);
            logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}",
                    (callbackResult.getCode()==IJobHandler.SUCCESS.getCode()?"success":"fail"), handleCallbackParam, callbackResult);
        }

        return ReturnT.SUCCESS;
    }

    private ReturnT<String> callback(HandleCallbackParam handleCallbackParam) {
        // valid log item
        XxlJobLog log = xxlJobLogDao.load(handleCallbackParam.getLogId());
        if (log == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "log item not found.");
        }
        if (log.getHandleCode() > 0) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "log repeate callback.");     // avoid repeat callback, trigger child job etc
        }

        // trigger success, to trigger child job
        String callbackMsg = null;
        if (IJobHandler.SUCCESS.getCode() == handleCallbackParam.getExecuteResult().getCode()) {
            XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(log.getJobId());
            if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) {
                callbackMsg = "<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<< </span><br>";

                String[] childJobIds = xxlJobInfo.getChildJobId().split(",");
                for (int i = 0; i < childJobIds.length; i++) {
                    int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1;
                    if (childJobId > 0) {

                        JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null);
                        ReturnT<String> triggerChildResult = ReturnT.SUCCESS;

                        // add msg
                        callbackMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"),
                                (i+1),
                                childJobIds.length,
                                childJobIds[i],
                                (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")),
                                triggerChildResult.getMsg());
                    } else {
                        callbackMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"),
                                (i+1),
                                childJobIds.length,
                                childJobIds[i]);
                    }
                }

            }
        }

        // handle msg
        StringBuffer handleMsg = new StringBuffer();
        if (log.getHandleMsg()!=null) {
            handleMsg.append(log.getHandleMsg()).append("<br>");
        }
        if (handleCallbackParam.getExecuteResult().getMsg() != null) {
            handleMsg.append(handleCallbackParam.getExecuteResult().getMsg());
        }
        if (callbackMsg != null) {
            handleMsg.append(callbackMsg);
        }

        if (handleMsg.length() > 15000) {
            handleMsg = new StringBuffer(handleMsg.substring(0, 15000));  // text最大64kb 避免长度过长
        }

        // success, save log
        log.setHandleTime(new Date());
        log.setHandleCode(handleCallbackParam.getExecuteResult().getCode());
        log.setHandleMsg(handleMsg.toString());
        xxlJobLogDao.updateHandleInfo(log);

        return ReturnT.SUCCESS;
    }

分析:

  1. 循环处理回调参数集合;
  2. 根据回调入参里的logId从数据库xxl_job_log表查询XxlJobLog;
  3. 校验XxlJobLog是否为空?handleCode字段是否大于0?(避免重复回调,触发子作业等);
  4. 如果触发调度成功,判断是否有子任务,如果有则触发调度子任务;
  5. 处理触发调度信息(text最大64kb 避免长度过长,长度截取到15000);
  6. 更新到日志记录中。

2.2.2 AdminBizImpl.registry(registryParam)

    @Override
    public ReturnT<String> registry(RegistryParam registryParam) {

        // valid
        if (!StringUtils.hasText(registryParam.getRegistryGroup())
                || !StringUtils.hasText(registryParam.getRegistryKey())
                || !StringUtils.hasText(registryParam.getRegistryValue())) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
        }

        int ret = xxlJobRegistryDao.registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
        if (ret < 1) {
            xxlJobRegistryDao.registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());

            // fresh
            freshGroupRegistryInfo(registryParam);
        }
        return ReturnT.SUCCESS;
    }

分析:

  1. 校验注册参数中的类型、appname、执行器地址;
  2. 更新xxl_job_registry表,更新结果 < 1表示没有则新增,
  3. 调用freshGroupRegistryInfo方法------------作者还在考虑中,这个方法无具体内容
        private void freshGroupRegistryInfo(RegistryParam registryParam){
            // Under consideration, prevent affecting core tables
        }
    

2.2.3 AdminBizImpl.registryRemove(registryParam)

    @Override
    public ReturnT<String> registryRemove(RegistryParam registryParam) {

        // valid
        if (!StringUtils.hasText(registryParam.getRegistryGroup())
                || !StringUtils.hasText(registryParam.getRegistryKey())
                || !StringUtils.hasText(registryParam.getRegistryValue())) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
        }

        int ret = xxlJobRegistryDao.registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
        if (ret > 0) {

            // fresh
            freshGroupRegistryInfo(registryParam);
        }
        return ReturnT.SUCCESS;
    }

分析:

  1. 校验注册参数中的类型、appname、执行器地址;
  2. 删除xxl_job_registry表信息,
  3. 删除成功则调用freshGroupRegistryInfo方法------------作者还在考虑中,这个方法无具体内容
        private void freshGroupRegistryInfo(RegistryParam registryParam){
            // Under consideration, prevent affecting core tables
        }
    

2.3 JobCodeController类

该类主要是GLUE(xxx)模式的可在线编辑的任务的查询、修改和保存;
涉及:xxl_job_info任务表和xxl_job_logglue任务日志表(glue类型);
注意:
每次保存会清楚30次之前的备份记录。

2.4 JobGroupController类

该类主要是执行器管理控制层:包括执行器的查询、保存、更新、删除。涉及xxl_job_group表。
注意:

  1. 当选择自动注册时,需要根据appname从xxl_job_registry表匹配出最近90秒更新的执行器地址列表。
  2. 删除的时候会进行校验,如果挂载了任务则不能删除;如果执行器列表只有这一个也不能删除。

2.5 JobInfoController类

该类主要负责任务的新增、修改、删除、查询下次调度时间、启动、停止、调度一次。涉及xxl_job_info表。
注意:

  1. xxl_job_infotrigger_status字段0-停止,1-运行;
  2. 计算下次调度时间需要加5s,避开预读周期;

2.6 JobLogController类

该类负责调度日志的查询、清理、中止任务等。
日志展示和中止会调用ExecutorBizClient的log、kill方法远程调度执行器。

2.7 UserController类

该类主要负责用户的新增、更新和删除。

三、总结

xxl-job这款轻量级的分布式定时任务还是比较适合我们大多数场景的。后续我们继续学习下其他框架(比如:Elastic-Job等)

猜你喜欢

转载自blog.csdn.net/RabbitInTheGrass/article/details/107036008