基于mall4j产品的二开项目后端
e5e06aef5f1a38c24820cc6574745d2d0f8a61f5..efdb99f8cecc4afb592afad79c761081d5d5cf22
2024-12-18 lee
init
efdb99 diff | tree
2024-12-18 lee
init
921461 diff | tree
2024-12-18 lee
init
799bbf diff | tree
353 files added
53760 ■■■■■ changed files
system-im-client/pom.xml 35 ●●●●● patch | view | raw | blame | history
system-im-client/src/main/java/com/yami/shop/im/client/LuckIMClient.java 113 ●●●●● patch | view | raw | blame | history
system-im-client/src/main/java/com/yami/shop/im/client/LuckSystemIMClientTest.java 81 ●●●●● patch | view | raw | blame | history
system-im-client/src/main/java/com/yami/shop/im/client/LuckUserIMClientTest.java 96 ●●●●● patch | view | raw | blame | history
system-im-client/src/main/java/com/yami/shop/im/client/constant/LuckIMClientConstant.java 14 ●●●●● patch | view | raw | blame | history
system-im-client/src/main/java/com/yami/shop/im/client/exception/LuckImException.java 17 ●●●●● patch | view | raw | blame | history
system-im-client/src/test/java/com/mita/im/client/AppTest.java 38 ●●●●● patch | view | raw | blame | history
system-test/pom.xml 36 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/base/BaseTest.java 6 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/base/BaseTestHandler.java 25 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/base/BaseTestTemplate.java 88 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/constants/Constant.java 17 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/data/ProductData.java 58 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/template/DiscountTestTemplate.java 68 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/test/DiscountTest.java 13 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/test/LoginTest.java 30 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/util/CheckUtil.java 50 ●●●●● patch | view | raw | blame | history
system-test/src/main/java/com/yami/shop/systemtest/util/HttpUtil.java 119 ●●●●● patch | view | raw | blame | history
xxl-job/doc/XXL-JOB-English-Documentation.md 1247 ●●●●● patch | view | raw | blame | history
xxl-job/doc/XXL-JOB官方文档.md 2324 ●●●●● patch | view | raw | blame | history
xxl-job/doc/XXL-JOB架构图.pptx patch | view | raw | blame | history
xxl-job/doc/db/tables_xxl_job.sql 122 ●●●●● patch | view | raw | blame | history
xxl-job/doc/images/cnblog-首页-每日一博-第一.png patch | view | raw | blame | history
xxl-job/doc/images/cnblog-首页-热门动弹-第一.png patch | view | raw | blame | history
xxl-job/doc/images/donate-alipay.jpg patch | view | raw | blame | history
xxl-job/doc/images/donate-paypal.png patch | view | raw | blame | history
xxl-job/doc/images/donate-wechat.png patch | view | raw | blame | history
xxl-job/doc/images/gitee-gvp.jpg patch | view | raw | blame | history
xxl-job/doc/images/img_1001.png patch | view | raw | blame | history
xxl-job/doc/images/img_1002.png patch | view | raw | blame | history
xxl-job/doc/images/img_6yC0.png patch | view | raw | blame | history
xxl-job/doc/images/img_BPLG.png patch | view | raw | blame | history
xxl-job/doc/images/img_EB65.png patch | view | raw | blame | history
xxl-job/doc/images/img_Fgql.png patch | view | raw | blame | history
xxl-job/doc/images/img_Hr2T.png patch | view | raw | blame | history
xxl-job/doc/images/img_Qohm.png patch | view | raw | blame | history
xxl-job/doc/images/img_UDSo.png patch | view | raw | blame | history
xxl-job/doc/images/img_V3vF.png patch | view | raw | blame | history
xxl-job/doc/images/img_Wb2o.png patch | view | raw | blame | history
xxl-job/doc/images/img_Ypik.png patch | view | raw | blame | history
xxl-job/doc/images/img_Z9Qr.png patch | view | raw | blame | history
xxl-job/doc/images/img_ZAhX.png patch | view | raw | blame | history
xxl-job/doc/images/img_ZAsz.png patch | view | raw | blame | history
xxl-job/doc/images/img_dNUJ.png patch | view | raw | blame | history
xxl-job/doc/images/img_eYrv.png patch | view | raw | blame | history
xxl-job/doc/images/img_hIci.png patch | view | raw | blame | history
xxl-job/doc/images/img_iUw0.png patch | view | raw | blame | history
xxl-job/doc/images/img_inc8.png patch | view | raw | blame | history
xxl-job/doc/images/img_jOAU.png patch | view | raw | blame | history
xxl-job/doc/images/img_jrdI.png patch | view | raw | blame | history
xxl-job/doc/images/img_o8HQ.png patch | view | raw | blame | history
xxl-job/doc/images/img_tJOq.png patch | view | raw | blame | history
xxl-job/doc/images/img_tvGI.png patch | view | raw | blame | history
xxl-job/doc/images/qq群-一个xxl同学进了58.png patch | view | raw | blame | history
xxl-job/doc/images/xxl-logo.jpg patch | view | raw | blame | history
xxl-job/doc/images/xxl-logo.png patch | view | raw | blame | history
xxl-job/xxl-job-admin/Dockerfile 11 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/pom.xml 113 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java 16 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java 96 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java 72 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java 96 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java 197 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java 180 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java 233 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java 179 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java 29 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java 42 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java 59 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java 28 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java 66 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java 20 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java 65 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java 118 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java 99 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java 158 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java 1666 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java 14 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java 77 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java 237 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java 157 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java 75 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java 54 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java 55 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java 73 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java 32 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java 413 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java 58 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java 48 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java 24 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java 48 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java 85 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java 48 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java 19 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java 79 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java 76 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java 19 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java 23 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java 46 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java 39 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java 46 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java 101 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java 184 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java 110 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java 152 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java 204 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java 369 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java 150 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java 27 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java 226 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java 98 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java 31 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java 79 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java 92 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java 133 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java 37 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java 49 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java 62 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java 24 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java 26 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java 38 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java 31 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java 107 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java 86 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java 35 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java 434 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/application.properties 65 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties 276 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties 276 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties 276 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/logback.xml 29 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml 91 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml 240 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml 71 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml 273 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml 62 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml 62 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml 87 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js 10 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css 62 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js 156 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/index.js 207 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js 97 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js 359 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js 739 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js 91 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js 396 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js 66 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js 328 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js 41 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css 36 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js 434 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css 346 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js 9698 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js 879 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js 899 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js 234 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js 398 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js 409 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js 152 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js 1106 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js 1106 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js 22 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js 117 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js 4 ●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js 2 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css 1 ●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl 31 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl 239 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl 47 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl 147 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl 164 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl 172 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl 540 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl 72 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl 180 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl 45 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl 188 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java 22 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java 50 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java 40 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobGroupDaoTest.java 44 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobInfoDaoTest.java 86 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogDaoTest.java 52 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogGlueDaoTest.java 36 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobRegistryDaoTest.java 30 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java 25 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/adminbiz/AdminBizTest.java 75 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-admin/src/test/java/com/xxl/job/executorbiz/ExecutorBizTest.java 105 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/pom.xml 64 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java 48 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java 45 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java 50 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java 56 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java 172 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java 67 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java 28 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java 28 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java 47 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java 56 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java 54 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java 57 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java 144 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java 122 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java 255 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java 35 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java 13 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java 271 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java 75 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java 126 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java 90 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java 53 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java 80 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java 38 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java 24 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java 30 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java 38 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java 53 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java 93 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java 220 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java 256 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java 129 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java 124 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java 252 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java 260 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java 156 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java 181 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java 88 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java 203 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java 73 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java 70 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java 228 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java 46 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java 24 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java 159 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/pom.xml 18 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml 45 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/FramelessApplication.java 38 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java 93 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java 251 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml 27 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties 17 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java 12 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile 11 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml 74 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java 16 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java 78 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/mvc/controller/IndexController.java 18 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java 253 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties 26 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml 29 ●●●●● patch | view | raw | blame | history
xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java 14 ●●●●● patch | view | raw | blame | history
yami-shop-api/Dockerfile 19 ●●●●● patch | view | raw | blame | history
yami-shop-api/Dockerfile-test 19 ●●●●● patch | view | raw | blame | history
yami-shop-api/pom.xml 148 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/ApiApplication.java 36 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/config/OrderThreadConfig.java 37 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/config/SwaggerConfiguration.java 67 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/config/ThreadPoolConfigProperties.java 50 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/config/ValidationsConfiguration.java 46 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/AddrController.java 165 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/AgentController.java 100 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/AreaController.java 59 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/C2cRealNameController.java 140 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/CategoryController.java 322 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/CompanyAuditingController.java 84 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/CpsController.java 274 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/DeliveryController.java 90 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/DiplomaController.java 29 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/FileController.java 95 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/FlowLogController.java 54 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/FlowLogLoginController.java 58 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/IndexImgController.java 54 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/LuckyCoinController.java 112 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsHandCardController.java 146 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsSalesController.java 156 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsTranLeveController.java 46 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsTranOrderController.java 184 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsWantBuyController.java 145 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MyNoticeController.java 133 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MyOrderController.java 443 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/MyShopDetailController.java 158 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/NoticeController.java 166 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/NotifyLogController.java 73 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/OrderController.java 359 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/OrderInvoiceController.java 93 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/OrderRefundController.java 669 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/PayBalanceController.java 138 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/PayController.java 71 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/PayLevelController.java 124 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/PayNoticeController.java 95 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdBrowseLogController.java 66 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdCommController.java 120 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdController.java 285 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdRecommendController.java 129 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdTagController.java 47 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/QrcodeTicketController.java 109 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/SearchController.java 80 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopBankCardController.java 168 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopCartController.java 320 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopCompanyController.java 98 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopDetailController.java 405 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopManagementController.java 90 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopRenovationController.java 86 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopUserRegisterController.java 199 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/SkuController.java 49 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/SmsController.java 51 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/TaskRuleController.java 36 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/TestsController.java 361 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/UserCollectionController.java 196 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/UserCollectionShopController.java 122 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/UserController.java 275 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/UserRegisterController.java 417 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/WebConfigController.java 82 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnFlowController.java 96 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnIncomeBangController.java 119 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnIndexController.java 106 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnMyWalletController.java 638 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnQrCodeController.java 118 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnRealNameController.java 163 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnUserWalletController.java 171 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/im/LuckImFriendController.java 183 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/im/LuckImGroupController.java 210 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/controller/im/LuckImMsgController.java 76 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/listener/SubmitOrderListener.java 393 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/manager/SubmitOrderImplManager.java 385 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/java/com/yami/shop/api/task/V1BoxTask.java 71 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/application-dev.yml 35 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/application-docker.yml 33 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/application-prod.yml 34 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/application-test.yml 35 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/application.yml 53 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/banner.txt 11 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/cert/alipayCertPublicKey_RSA2.crt 43 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/cert/alipayRootCert.crt 88 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/cert/appCertPublicKey_2021004183677087.crt 23 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/cert/test/appPublicCert.crt 19 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/i18n/validations.properties patch | view | raw | blame | history
yami-shop-api/src/main/resources/i18n/validations_en.properties 47 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/i18n/validations_zh.properties 43 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/logback-spring-test.xml 81 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/logback-spring.xml 81 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/logback/logback-dev.xml 15 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/logback/logback-docker.xml 39 ●●●●● patch | view | raw | blame | history
yami-shop-api/src/main/resources/logback/logback-prod.xml 39 ●●●●● patch | view | raw | blame | history
system-im-client/pom.xml
New file
@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<project
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.yami.shop</groupId>
        <artifactId>yami-shop</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>system-im-client</artifactId>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.yami.shop</groupId>
            <artifactId>yami-shop-bean</artifactId>
            <version>${yami.shop.version}</version>
        </dependency>
    </dependencies>
</project>
system-im-client/src/main/java/com/yami/shop/im/client/LuckIMClient.java
New file
@@ -0,0 +1,113 @@
package com.yami.shop.im.client;
import java.net.URI;
import javax.annotation.PostConstruct;
import javax.websocket.ClientEndpoint;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import org.springframework.stereotype.Component;
import com.yami.shop.bean.im.MsgAction;
import com.yami.shop.bean.im.MsgBody;
import com.yami.shop.bean.im.MsgType;
import com.yami.shop.bean.im.RecipientType;
import com.yami.shop.bean.im.SystemMsgType;
import com.yami.shop.bean.im.security.MsgAuth;
import com.yami.shop.im.client.constant.LuckIMClientConstant;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
/**
 * 幸运集社im客户端
 */
@Slf4j
@Component
@ClientEndpoint
public class LuckIMClient {
    private Session session;
    @PostConstruct
    public void init() {
        try {
            String url = LuckIMClientConstant.LUCK_IM_MSG_URL.replace("{auth}", MsgAuth.genTodaySystemAuthCode()).replace("{clientId}", IdUtil.fastSimpleUUID());
            WebSocketContainer container = javax.websocket.ContainerProvider.getWebSocketContainer();
            session = container.connectToServer(LuckIMClient.class, URI.create(url));
        } catch (Exception e) {
            log.error("LuckIMClient init Exception: " + ExceptionUtil.stacktraceToOneLineString(e));
        }
    }
    @OnOpen
    public void onOpen(Session session) {
        log.info("Luck Im Server connected.");
        heartBeat();
    }
    @OnMessage
    public void onMessage(String message) {
        log.info("Luck Im Server onMessage: " + message);
    }
    @OnClose
    public void onClose(Session session) {
        log.info("Luck Im Server closed");
    }
    /**
     * 心跳处理
     */
    private void heartBeat() {
        while(session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(MsgBody.getHeartBeatMsgBody().toString());
                Thread.sleep(1000 * 30);//每30秒发送心跳
            } catch (Exception e) {
                log.error("heartBeat Exception: " + ExceptionUtil.stacktraceToString(e));
            }
        }
    }
    /**
     * 发送系统消息给用户
     * @param userId 用户id
     * @param systemMsgType 系统消息类型
     * @param content 内容
     */
    public void sendSystemMsgToUser(String userId,SystemMsgType systemMsgType,Object content) {
        if(ObjUtil.isAllEmpty(userId,systemMsgType,content)) {
            throw new IllegalArgumentException();
        }
        if(session == null || !session.isOpen()) {
            log.error("系统im初始化失败或链接已断开");
            return;
        }
        MsgBody msgBody = new MsgBody();
        msgBody.setAction(MsgAction.SEND_SYSTEM_MSG.name());
        msgBody.setContent(content);
        msgBody.setRecipientType(RecipientType.USER.name());
        msgBody.setFromId(MsgBody.getSystemId());
        msgBody.setMsgType(MsgType.SYSTEM.name());
        msgBody.setSystemType(systemMsgType.getValue());
        msgBody.setToId(userId);
        msgBody.setMsgId(MsgBody.randomMessageId());
        msgBody.setTimestamp(System.currentTimeMillis());
        try {
            session.getBasicRemote().sendText(msgBody.toString());
        } catch (Exception e) {
            log.error("sendSystemMsgToUser Exception: " + ExceptionUtil.stacktraceToString(e));
        }
    }
}
system-im-client/src/main/java/com/yami/shop/im/client/LuckSystemIMClientTest.java
New file
@@ -0,0 +1,81 @@
package com.yami.shop.im.client;
import java.net.URI;
import javax.websocket.ClientEndpoint;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import com.yami.shop.bean.im.MsgAction;
import com.yami.shop.bean.im.MsgBody;
import com.yami.shop.bean.im.MsgType;
import com.yami.shop.bean.im.RecipientType;
import com.yami.shop.bean.im.SystemMsgType;
import com.yami.shop.bean.im.security.MsgAuth;
import com.yami.shop.im.client.constant.LuckIMClientConstant;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
/**
 * 幸运集社im客户端  后台系统测试样例
 */
@Slf4j
@ClientEndpoint
public class LuckSystemIMClientTest {
    public void onOpen(Session session) {
        log.info("Luck Im Server connected.");
    }
    @OnMessage
    public void onMessage(String message) {
        log.info("Luck Im Server onMessage: " + message);
    }
    @OnClose
    public void onClose(Session session) {
        log.info("Luck Im Server closed");
    }
    public static void main(String[] args) {
        try {
            // WebSocket服务器的URL
            String url = LuckIMClientConstant.LUCK_IM_MSG_URL.replace("{auth}", MsgAuth.genTodaySystemAuthCode()).replace("{clientId}", IdUtil.fastSimpleUUID());
            WebSocketContainer container = javax.websocket.ContainerProvider.getWebSocketContainer();
            Session session = container.connectToServer(LuckSystemIMClientTest.class, URI.create(url));
            session.getBasicRemote().sendText(buildMsg());
            session.getBasicRemote().sendText(buildMsg());
            session.getBasicRemote().sendText(buildMsg());
            session.getBasicRemote().sendText(buildMsg());
            session.getBasicRemote().sendText(buildMsg());
            session.getBasicRemote().sendText(buildMsg());
            // 等待一段时间,以便接收服务器发送的消息
            Thread.sleep(1000000);
            // 关闭连接
            session.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static String buildMsg() {
        MsgBody msg = new MsgBody();
        msg.setAction(MsgAction.SEND_SYSTEM_MSG.name());
        msg.setContent(RandomUtil.randomString(50));
        msg.setSystemType(SystemMsgType.GROUP_MEMBER_JOIN.getValue());
        msg.setFromId("0");
        msg.setToId("1803696537288138754");
        msg.setMsgType(MsgType.TEXT.name());
        msg.setRecipientType(RecipientType.USER.name());
        return msg.toString();
    }
}
system-im-client/src/main/java/com/yami/shop/im/client/LuckUserIMClientTest.java
New file
@@ -0,0 +1,96 @@
package com.yami.shop.im.client;
import java.net.URI;
import javax.annotation.PostConstruct;
import javax.websocket.ClientEndpoint;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import org.springframework.stereotype.Component;
import com.yami.shop.bean.im.MsgAction;
import com.yami.shop.bean.im.MsgBody;
import com.yami.shop.bean.im.MsgType;
import com.yami.shop.bean.im.RecipientType;
import com.yami.shop.bean.im.SystemMsgType;
import com.yami.shop.bean.im.security.MsgAuth;
import com.yami.shop.im.client.constant.LuckIMClientConstant;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
/**
 * 幸运集社im客户端  用户测试样例
 */
@Slf4j
@ClientEndpoint
public class LuckUserIMClientTest {
    public void onOpen(Session session) {
        log.info("Luck Im Server connected.");
    }
    @OnMessage
    public void onMessage(String message) {
        log.info("Luck Im Server onMessage: " + message);
    }
    @OnClose
    public void onClose(Session session) {
        log.info("Luck Im Server closed");
    }
    public static void main(String[] args) {
        try {
            // WebSocket服务器的URL
            String url = LuckIMClientConstant.LUCK_IM_USER_URL
                    .replace("{token}", "VTN5ZTlwZWo0anh1QnBlZjZGNzVKVXlweU1vU0c5OE8zN0x6OUhRVzFTL25mRFdEYjJtRmRtZ2JZamtuVUF6Zw==")
                    .replace("{userId}", "1808419187023753218");
            WebSocketContainer container = javax.websocket.ContainerProvider.getWebSocketContainer();
            Session session = container.connectToServer(LuckUserIMClientTest.class, URI.create(url));
            session.getBasicRemote().sendText(buildGroupMsg());
            // 等待一段时间,以便接收服务器发送的消息
            Thread.sleep(1000000);
            // 关闭连接
            session.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static String buildMsg() {
        MsgBody msg = new MsgBody();
        msg.setAction(MsgAction.SEND_MSG.name());
        msg.setContent(RandomUtil.randomString(100));
        msg.setSystemType(SystemMsgType.USER.getValue());
        msg.setFromId("0");
        msg.setToId("1803696537288138754");
        msg.setMsgType(MsgType.TEXT.name());
        msg.setRecipientType(RecipientType.USER.name());
        return msg.toString();
    }
    private static String buildGroupMsg() {
        MsgBody msg = new MsgBody();
        msg.setAction(MsgAction.SEND_MSG.name());
        msg.setContent(RandomUtil.randomString(100));
        msg.setSystemType(SystemMsgType.USER.getValue());
        msg.setFromId("1808419187023753218");
        msg.setToId("1809485947580796928");
        msg.setMsgType(MsgType.TEXT.name());
        msg.setRecipientType(RecipientType.GROUP.name());
        return msg.toString();
    }
}
system-im-client/src/main/java/com/yami/shop/im/client/constant/LuckIMClientConstant.java
New file
@@ -0,0 +1,14 @@
package com.yami.shop.im.client.constant;
public class LuckIMClientConstant {
    public static final String LUCK_IM_HOST = "ws://47.121.136.124:8089/";
//    public static final String LUCK_IM_HOST = "ws://127.0.0.1:8089/";
    /**im websocket 通讯 url**/
    public static final String LUCK_IM_MSG_URL = LUCK_IM_HOST + "im/luck/system/{auth}/{clientId}";
    /**im websocket 通讯 url**/
    public static final String LUCK_IM_USER_URL = LUCK_IM_HOST + "im/luck/user/{token}/{userId}";
}
system-im-client/src/main/java/com/yami/shop/im/client/exception/LuckImException.java
New file
@@ -0,0 +1,17 @@
package com.yami.shop.im.client.exception;
/**
 * IM异常类
 */
public class LuckImException extends RuntimeException{
    private static final long serialVersionUID = 2790327436198670240L;
    public LuckImException(String message) {
        super(message);
    }
    public LuckImException() {
        super();
    }
}
system-im-client/src/test/java/com/mita/im/client/AppTest.java
New file
@@ -0,0 +1,38 @@
package com.mita.im.client;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
 * Unit test for simple App.
 */
public class AppTest
    extends TestCase
{
    /**
     * Create the test case
     *
     * @param testName name of the test case
     */
    public AppTest( String testName )
    {
        super( testName );
    }
    /**
     * @return the suite of tests being tested
     */
    public static Test suite()
    {
        return new TestSuite( AppTest.class );
    }
    /**
     * Rigourous Test :-)
     */
    public void testApp()
    {
        assertTrue( true );
    }
}
system-test/pom.xml
New file
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>yami-shop</artifactId>
        <groupId>com.yami.shop</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>system-test</artifactId>
    <packaging>jar</packaging>
    <name>system-test</name>
    <description>用于做系统测试的模块</description>
    <dependencies>
        <dependency>
            <groupId>com.yami.shop</groupId>
            <artifactId>yami-shop-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.yami.shop</groupId>
            <artifactId>yami-shop-multishop</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.testng</groupId>-->
<!--            <artifactId>testng</artifactId>-->
<!--            <version>6.14.3</version>-->
<!--        </dependency>-->
    </dependencies>
</project>
system-test/src/main/java/com/yami/shop/systemtest/base/BaseTest.java
New file
@@ -0,0 +1,6 @@
package com.yami.shop.systemtest.base;
public interface BaseTest {
    void doTest();
}
system-test/src/main/java/com/yami/shop/systemtest/base/BaseTestHandler.java
New file
@@ -0,0 +1,25 @@
package com.yami.shop.systemtest.base;
import cn.hutool.core.collection.CollectionUtil;
import java.util.ArrayList;
import java.util.List;
public class BaseTestHandler {
    private final List<BaseTest> baseTestList = new ArrayList<>();
    public void add(BaseTest baseTest) {
        baseTestList.add(baseTest);
    }
    public void doTest() {
        if (CollectionUtil.isEmpty(baseTestList)) {
            return;
        }
        for (BaseTest test : baseTestList) {
            System.out.println("----------testing: " + test.getClass().getSimpleName()+ "----------");
            test.doTest();
        }
    }
}
system-test/src/main/java/com/yami/shop/systemtest/base/BaseTestTemplate.java
New file
@@ -0,0 +1,88 @@
package com.yami.shop.systemtest.base;
import java.util.List;
/**
 * 这个是一个模板方法,继承这个类的时候,会需要实现创建商品到计算分账的方法,需要根据需要进行实现
 */
public abstract class BaseTestTemplate implements BaseTest{
    @Override
    public void doTest() {
        List<Long> prodIds = createProduct();
        getProduct(prodIds);
        doMarketing();
        confirmOrder();
        submitOrder();
        payOrder();
        checkAccountByPay();
        receiptOrder();
        checkAccountByReceipt();
        refundOrder();
        checkAccountByRefund();
        deleteProduct(prodIds);
    }
    /**
     * 创建商品
     * @return 创建的几个商品的id列表
     */
    public abstract List<Long> createProduct();
    /**
     * 获取商品详情
     */
    public abstract void getProduct(List<Long> prodIds);
    /**
     * 进行营销,创建完这个商品,究竟是用来干嘛的,满减?优惠券?团购?秒杀?
     */
    public abstract void doMarketing();
    /**
     * 立即购买 确认订单
     */
    public abstract void confirmOrder();
    /**
     * 提交订单
     */
    public abstract void submitOrder();
    /**
     * 支付订单
     */
    public abstract void payOrder();
    /**
     * 查看结算信息
     */
    public abstract void checkAccountByPay();
    /**
     * 确认收货
     */
    public abstract void receiptOrder();
    /**
     * 查看结算信息
     */
    public abstract void checkAccountByReceipt();
    /**
     * 退款
     */
    public abstract void refundOrder();
    /**
     * 查看结算信息
     */
    public abstract void checkAccountByRefund();
    /**
     *
     */
    public abstract void deleteProduct(List<Long> prodIds);
}
system-test/src/main/java/com/yami/shop/systemtest/constants/Constant.java
New file
@@ -0,0 +1,17 @@
package com.yami.shop.systemtest.constants;
public class Constant {
    public static final String API_URL = "http://127.0.0.1:8086";
    public static String API_TOKEN = "";
    public static final String SHOP_URL = "http://127.0.0.1:8087";
    public static final String SHOP_TOKEN = "hv7f6ztf91//dj90C9tt362IbjbGTlQdpfQhfrAkbwvTxMtYv1qx0cueWlUYCc9L";
    public static final String PLATFORM_URL = "http://127.0.0.1:8088";
    public static final String PLATFORM_TOKEN = "FSh9pgzpC7htv8YD/4qs+JdRrNigo85MQ5jflY5Ga956iPVsmfW/hn+hlqH17AGT";
}
system-test/src/main/java/com/yami/shop/systemtest/data/ProductData.java
New file
@@ -0,0 +1,58 @@
package com.yami.shop.systemtest.data;
import com.yami.shop.bean.model.Product;
import com.yami.shop.bean.model.Sku;
import com.yami.shop.bean.param.ProductParam;
import com.yami.shop.systemtest.base.BaseTest;
import com.yami.shop.systemtest.constants.Constant;
import com.yami.shop.systemtest.util.HttpUtil;
import java.util.ArrayList;
import java.util.List;
/**
 * 商品数据,为啥叫商品数据不叫商品测试呢,因为这个商品在订单里面都会用到
 *
 */
public class ProductData {
    public static Long getProduct(double price,double deliveryAmount) {
        String url = Constant.SHOP_URL + "/prod/prod";
        return HttpUtil.post(url, getProductParam(price, deliveryAmount), Long.class);
    }
    public static ProductParam getProductParam(double price,double deliveryAmount){
        ProductParam productParam = new ProductParam();
        productParam.setCategoryId(107L);
        productParam.setShopCategoryId(197L);
        productParam.setDeliveryAmount(deliveryAmount);
        productParam.setPrice(price);
        // 实体商品
        productParam.setMold(0);
        productParam.setProdName("接口测试商品");
        productParam.setProdNameCn("接口测试商品");
        productParam.setProdNameEn("en");
        productParam.setPic("/test.png");
        productParam.setImgs("/test.png");
        productParam.setOriPrice(0d);
        // 配送方式
        Product.DeliveryModeVO deliveryModeVO = new Product.DeliveryModeVO();
        deliveryModeVO.setHasShopDelivery(true);
        deliveryModeVO.setHasUserPickUp(true);
        deliveryModeVO.setHasCityDelivery(true);
        productParam.setDeliveryModeVo(deliveryModeVO);
        List<Sku> skuList = new ArrayList<>();
        Sku sku = new Sku();
        sku.setPrice(price);
        sku.setStocks(100);
        skuList.add(sku);
        productParam.setSkuList(skuList);
        return productParam;
    }
}
system-test/src/main/java/com/yami/shop/systemtest/template/DiscountTestTemplate.java
New file
@@ -0,0 +1,68 @@
package com.yami.shop.systemtest.template;
import com.yami.shop.systemtest.base.BaseTestTemplate;
import java.util.List;
public class DiscountTestTemplate extends BaseTestTemplate {
    @Override
    public List<Long> createProduct() {
        return null;
    }
    @Override
    public void getProduct(List<Long> prodIds) {
    }
    @Override
    public void doMarketing() {
    }
    @Override
    public void confirmOrder() {
    }
    @Override
    public void submitOrder() {
    }
    @Override
    public void payOrder() {
    }
    @Override
    public void checkAccountByPay() {
    }
    @Override
    public void receiptOrder() {
    }
    @Override
    public void checkAccountByReceipt() {
    }
    @Override
    public void refundOrder() {
    }
    @Override
    public void checkAccountByRefund() {
    }
    @Override
    public void deleteProduct(List<Long> prodIds) {
    }
}
system-test/src/main/java/com/yami/shop/systemtest/test/DiscountTest.java
New file
@@ -0,0 +1,13 @@
package com.yami.shop.systemtest.test;
import com.yami.shop.systemtest.template.DiscountTestTemplate;
public class DiscountTest {
//    @Test
    public void doTest(){
        DiscountTestTemplate discountTestTemplate = new DiscountTestTemplate();
        discountTestTemplate.doTest();
    }
}
system-test/src/main/java/com/yami/shop/systemtest/test/LoginTest.java
New file
@@ -0,0 +1,30 @@
package com.yami.shop.systemtest.test;
import cn.hutool.crypto.symmetric.AES;
import com.yami.shop.security.common.dto.AuthenticationDTO;
import com.yami.shop.security.common.vo.TokenInfoVO;
import com.yami.shop.systemtest.constants.Constant;
import com.yami.shop.systemtest.util.HttpUtil;
import java.nio.charset.StandardCharsets;
public class LoginTest {
//    @Test
    public void doTest() {
        String url = Constant.API_URL + "/login";
        String userName = "ping";
        String password = "ping";
        AES aes = new AES("-mall4j-password".getBytes(StandardCharsets.UTF_8));
        password = aes.encryptBase64(System.currentTimeMillis() + password);
        AuthenticationDTO authenticationDTO = new AuthenticationDTO();
        authenticationDTO.setUserName(userName);
        authenticationDTO.setPassWord(password);
        TokenInfoVO token = HttpUtil.post(url, authenticationDTO, TokenInfoVO.class);
    }
}
system-test/src/main/java/com/yami/shop/systemtest/util/CheckUtil.java
New file
@@ -0,0 +1,50 @@
package com.yami.shop.systemtest.util;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.yami.shop.common.response.ServerResponseEntity;
import com.yami.shop.common.util.Json;
import java.util.List;
public class CheckUtil {
    public static void check(HttpResponse response) {
        String body = getBody(response);
        ServerResponseEntity serverResponseEntity = Json.parseObject(body, ServerResponseEntity.class);
        if (!serverResponseEntity.isSuccess()) {
            throw new RuntimeException(serverResponseEntity.getMsg());
        }
    }
    public static <T>  T checkAndGetObject(HttpResponse response, Class<T> clazz) {
        String body = getBody(response);
        TypeReference<ServerResponseEntity<T>> typeReference = new TypeReference<ServerResponseEntity<T>>(clazz) {};
        ServerResponseEntity<T> serverResponseEntity = JSON.parseObject(body, typeReference);
        if (!serverResponseEntity.isSuccess()) {
            throw new RuntimeException(serverResponseEntity.getMsg());
        }
        return serverResponseEntity.getData();
    }
    public static <T> List<T> checkAndGetArray(HttpResponse response, Class<T[]> clazz) {
        String body = getBody(response);
        ServerResponseEntity<T> serverResponseEntity = Json.parseObject(body, ServerResponseEntity.class);
        if (!serverResponseEntity.isSuccess()) {
            throw new RuntimeException(serverResponseEntity.getMsg());
        }
        return Json.parseArray(Json.toJsonString(serverResponseEntity.getData()), clazz);
    }
    private static String getBody(HttpResponse response){
        if (!response.isOk()) {
            throw new RuntimeException("请求异常");
        }
        System.out.println(response.body());
        return response.body();
    }
}
system-test/src/main/java/com/yami/shop/systemtest/util/HttpUtil.java
New file
@@ -0,0 +1,119 @@
package com.yami.shop.systemtest.util;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.yami.shop.common.util.Json;
import com.yami.shop.systemtest.constants.Constant;
import java.util.List;
public class HttpUtil {
    public static void get(String url) {
        HttpResponse response = doGet(url);
        CheckUtil.check(response);
    }
    public static <T> T get(String url, Class<T> clazz) {
        HttpResponse response = doGet(url);
        return CheckUtil.checkAndGetObject(response,clazz);
    }
    public static <T> List<T> getResultArray(String url, Class<T[]> clazz) {
        HttpResponse response = doGet(url);
        return CheckUtil.checkAndGetArray(response,clazz);
    }
    public static void post(String url, Object obj) {
        HttpResponse response = doPost(url, obj);
        CheckUtil.check(response);
    }
    public static <T> T post(String url, Object obj, Class<T> clazz) {
        HttpResponse response = doPost(url, obj);
        return CheckUtil.checkAndGetObject(response,clazz);
    }
    public static <T> List<T> postResultArray(String url, Object obj, Class<T[]> clazz) {
        HttpResponse response = doPost(url, obj);
        return CheckUtil.checkAndGetArray(response,clazz);
    }
    public static void put(String url, Object obj) {
        HttpResponse response = doPut(url, obj);
        CheckUtil.check(response);
    }
    public static <T> T doPut(String url, Object obj, Class<T> clazz) {
        HttpResponse response = doPut(url, obj);
        return CheckUtil.checkAndGetObject(response,clazz);
    }
    public static <T> List<T> putResultArray(String url, Object obj, Class<T[]> clazz) {
        HttpResponse response = doPut(url, obj);
        return CheckUtil.checkAndGetArray(response,clazz);
    }
    public static void delete(String url) {
        HttpResponse response = doDelete(url);
        CheckUtil.check(response);
    }
    public static <T> T delete(String url, Class<T> clazz) {
        HttpResponse response = doDelete(url);
        return CheckUtil.checkAndGetObject(response,clazz);
    }
    public static <T> List<T> deleteResultArray(String url, Class<T[]> clazz) {
        HttpResponse response = doDelete(url);
        return CheckUtil.checkAndGetArray(response,clazz);
    }
    private static HttpResponse doGet(String url) {
        HttpRequest postRequest = HttpRequest.get(url);
        setHeader(postRequest);
        return postRequest.execute();
    }
    private static HttpResponse doPost(String url, Object obj) {
        HttpRequest postRequest = HttpRequest.post(url);
        setHeader(postRequest);
        return postRequest
                .body(Json.toJsonString(obj))
                .execute();
    }
    private static HttpResponse doPut(String url, Object obj) {
        HttpRequest postRequest = HttpRequest.put(url);
        setHeader(postRequest);
        return postRequest
                .body(Json.toJsonString(obj))
                .execute();
    }
    private static HttpResponse doDelete(String url) {
        HttpRequest postRequest = HttpRequest.get(url);
        setHeader(postRequest);
        return postRequest.execute();
    }
    private static HttpRequest setHeader(HttpRequest httpRequest) {
        String url = httpRequest.getUrl();
        if (url.startsWith(Constant.API_URL)) {
            httpRequest.header("authorization", Constant.API_TOKEN);
        }
        if (url.startsWith(Constant.SHOP_URL)) {
            httpRequest.header("authorization", Constant.SHOP_TOKEN);
        }
        if (url.startsWith(Constant.PLATFORM_URL)) {
            httpRequest.header("authorization", Constant.PLATFORM_TOKEN);
        }
        return httpRequest;
    }
}
xxl-job/doc/XXL-JOB-English-Documentation.md
New file
@@ -0,0 +1,1247 @@
## 《Distributed task scheduling framework XXL-JOB》
[![Actions Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/)
[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-job.svg)](https://github.com/xuxueli/xxl-job/releases)
[![GitHub stars](https://img.shields.io/github/stars/xuxueli/xxl-job)](https://github.com/xuxueli/xxl-job/)
[![Docker Status](https://img.shields.io/docker/pulls/xuxueli/xxl-job-admin)](https://hub.docker.com/r/xuxueli/xxl-job-admin/)
[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html)
[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat)](https://www.xuxueli.com/page/donate.html)
[TOCM]
[TOC]
## 1. Brief introduction
### 1.1 Overview
XXL-JOB is a distributed task scheduling framework, the core design goal is to develop quickly, learning simple, lightweight, easy to expand. Is now open source and access to a number of companies online product line, download and use it now.
> English document update slightly delayed, Please check the Chinese version for the latest document.
### 1.2 Features
- 1.Simple: support through the Web page on the task CRUD operation, simple operation, a minute to get started;
- 2.Dynamic: support dynamic modification of task status, pause / resume tasks, and termination of running tasks,immediate effect;
- 3.Dispatch center HA (center type): Dispatch with central design, "dispatch center" based on the cluster of Quartz implementation, can guarantee the scheduling - center HA;
- 4.Executor HA (Distributed): Task Distributed Execution, Task " Executer " supports cluster deployment to ensure that tasks perform HA;
- 5.Task Failover: Deploy the Excutor cluster,tasks will be smooth to switch excuter when the strategy of the router choose ‘failover’;
- 6.Consistency: "Dispatch Center" through the DB lock to ensure the consistency of cluster distributed scheduling,one task excuted for once;
- 7.Custom task parameters: support online configuration scheduling tasks into the parameters, immediate effect;
- 8.Scheduling thread pool: scheduling system multi-threaded trigger scheduling operation, to ensure accurate scheduling, not blocked;
- 9.Elastic expansion capacity: once the new executor machine on the line or off the assembly line, the next time scheduling will be re-assigned tasks;
- 10.Mail alarm: the task fails to support e-mail alarm, support configuring multiple email addresses to send bulk alert messages;
- 11.Status monitoring: support real-time monitoring of the progress of the task;
- 12.Rolling execution log: support online view scheduling results, and support Rolling real-time view of the executer output of the complete implementation of the log;
- 13.GLUE: provide Web IDE, support online development task logic code, dynamic release, real-time compiler effective, omit the deployment of the on-line process. Supports historical versions of 30 versions back;
- 14.Data Encryption: The communication between the dispatching center and the executor is used for data encryption, Enhancing the security of dispatching information;
- 15.Task Dependency: Support configuration subtask dependencies, When the parent task executed end and after the success of the implementation will take the initiative to trigger a second task execution, multiple sub tasks are separated by commas;
- 16.Push the Maven central warehouse: The latest stable version will be sent to the Maven central warehouse to facilitate user access and use;
- 17.Task registration: The executor automatically registers tasks periodically, and the dispatch center automatically finds the registered tasks and triggers execution. It also supports manual input of executor address;
- 18.Router strategy: A rich routing strategy is provided when the executor cluster is deployed, these include: first, last, poll, random, consistent HASH, least frequently used, least recently used, failover, busy over, sharding broadcast,etc.;
- 19.Report monitor: Support real-time view of running data, such as the number of tasks, the number of dispatch, the number of executors, etc .; and scheduling reports, such as scheduling date distribution, scheduling success map;
- 20.Script task: Support the development and operation of script tasks in GLUE mode, including shell, Python and other types of script;
- 21.Blocking handling strategy: The scheduling is too dense and the executor is too late to handle. The strategy includes: single machine serial (default), discarding the following scheduling, and Override the previous scheduling;
- 22.Failure handling strategy:Handling strategy when scheduling fails, the strategy includes: failure alarm (default), failure retry;
- 23.Sharding broadcast task: When an executor cluster is deployed, task routing strategy select "sharding broadcast", a task schedule will broadcast all the actuators in the cluster to perform it once, you can develop sharding tasks based on sharding parameters;
- 24.Dynamic sharding: The sharding broadcast task is sharded by the executors to support the dynamic expansion of the executor cluster to dynamically increase the number of shardings and cooperate with the business handle; In the large amount of data operations can significantly improve the task processing capacity and speed.
- 25、Event trigger:In addition to "Cron" and "Task Dependency" to trigger tasks, support event-based triggering tasks. The dispatch center provides API service that triggers a single execution of the task, it can be triggered flexibly according to business events.
###  1.3 Development
In 2015, I created the XXL-JOB project repository on github and submitted the first commit, followed by the system structure design, UI selection, interactive design ...
In 2015 - November, XXL-JOB finally RELEASE the first big version of V1.0, then I will be released to OSCHINA, XXL-JOB OSCHINA won the popular recommendation of @红薯, the same period reached OSCHINA's " Popular move "ranked first and git.oschina open source software monthly heat ranked first, especially thanks for @红薯, thank you for the attention and support.
In 2015 - December, I will XXL-JOB published to our internal knowledge base, and get internal colleagues recognized.
In 2016 - 01 months, my company started XXL-JOB internal access and custom work, in this thank Yuan and Yin two colleagues contribution, but also to thank the internal other attention and support colleagues.
In 2017-05-13, the link of "let the code run" in "[the 62nd source of open source China Genesis](https://www.oschina.net/event/2236961)" held in Shanghai,, I stepped on and made a speech about the XXL-JOB, five hundred spectators in the audience reacted enthusiastically ([pictorial review](https://www.oschina.net/question/2686220_2242120)).
> Our company have access to XXL-JOB, internal alias "Ferrari" (Ferrari based on XXL-JOB V1.1 version customization, new access application recommended to upgrade the latest version).
According to the latest statistics, from 2016-01-21 to 2017-07-07 period, the system has been scheduled about 600,000 times, outstanding performance. New access applications recommend the latest version, because after several major updates, the system's task model, UI interaction model and the underlying scheduling communication model has a greater optimization and upgrading, the core function more stable and efficient.
So far, XXL-JOB has access to a number of companies online product line, access to scenes such as electronic commerce, O2O business and large data operations, as of 2016-07-19, XXL-JOB has access to the company But not limited to:
    - 1、大众点评【美团点评】
    - 2、山东学而网络科技有限公司;
    - 3、安徽慧通互联科技有限公司;
    - 4、人人聚财金服;
    - 5、上海棠棣信息科技股份有限公司
    - 6、运满满【运满满】
    - 7、米其林 (中国区)【米其林】
    - 8、妈妈联盟
    - 9、九樱天下(北京)信息技术有限公司
    - 10、万普拉斯科技有限公司【一加手机】
    - 11、上海亿保健康管理有限公司
    - 12、海尔馨厨【海尔】
    - 13、河南大红包电子商务有限公司
    - 14、成都顺点科技有限公司
    - 15、深圳市怡亚通
    - 16、深圳麦亚信科技股份有限公司
    - 17、上海博莹科技信息技术有限公司
    - 18、中国平安科技有限公司【中国平安】
    - 19、杭州知时信息科技有限公司
    - 20、博莹科技(上海)有限公司
    - 21、成都依能股份有限责任公司
    - 22、湖南高阳通联信息技术有限公司
    - 23、深圳市邦德文化发展有限公司
    - 24、福建阿思可网络教育有限公司
    - 25、优信二手车【优信】
    - 26、上海悠游堂投资发展股份有限公司【悠游堂】
    - 27、北京粉笔蓝天科技有限公司
    - 28、中秀科技(无锡)有限公司
    - 29、武汉空心科技有限公司
    - 30、北京蚂蚁风暴科技有限公司
    - 31、四川互宜达科技有限公司
    - 32、钱包行云(北京)科技有限公司
    - 33、重庆欣才集团
    - 34、咪咕互动娱乐有限公司【中国移动】
    - 35、北京诺亦腾科技有限公司
    - 36、增长引擎(北京)信息技术有限公司
    - 37、北京英贝思科技有限公司
    - 38、刚泰集团
    - 39、深圳泰久信息系统股份有限公司
    - 40、随行付支付有限公司
    - 41、广州瀚农网络科技有限公司
    - 42、享点科技有限公司
    - 43、杭州比智科技有限公司
    - 44、圳临界线网络科技有限公司
    - 45、广州知识圈网络科技有限公司
    - 46、国誉商业上海有限公司
    - 47、海尔消费金融有限公司,嗨付、够花【海尔】
    - 48、广州巴图鲁信息科技有限公司
    - 49、深圳市鹏海运电子数据交换有限公司
    - 50、深圳市亚飞电子商务有限公司
    - 51、上海趣医网络有限公司
    - 52、聚金资本
    - 53、北京父母邦网络科技有限公司
    - 54、中山元赫软件科技有限公司
    - 55、中商惠民(北京)电子商务有限公司
    - 56、凯京集团
    - 57、华夏票联(北京)科技有限公司
    - 58、拍拍贷【拍拍贷】
    - 59、北京尚德机构在线教育有限公司
    - 60、任子行股份有限公司
    - 61、北京时态电子商务有限公司
    - 62、深圳卷皮网络科技有限公司
    - 63、北京安博通科技股份有限公司
    - 64、未来无线网
    - 65、厦门瓷禧网络有限公司
    - 66、北京递蓝科软件股份有限公司
    - 67、郑州创海软件科技公司
    - 68、北京国槐信息科技有限公司
    - 69、浪潮软件集团
    - 70、多立恒(北京)信息技术有限公司
    - 71、广州极迅客信息科技有限公司
    - 72、赫基(中国)集团股份有限公司
    - 73、海投汇
    - 74、上海润益创业孵化器管理股份有限公司
    - 75、汉纳森(厦门)数据股份有限公司
    - 76、安信信托
    - 77、岚儒财富
    - 78、捷道软件
    - 79、湖北享七网络科技有限公司
    - 80、湖南创发科技责任有限公司
    - 81、深圳小安时代互联网金融服务有限公司
    - 82、湖北享七网络科技有限公司
    - 83、钱包行云(北京)科技有限公司
    - 84、360金融【360】
    - 85、易企秀
    - 86、摩贝(上海)生物科技有限公司
    - 87、广东芯智慧科技有限公司
    - 88、联想集团【联想】
    - 89、怪兽充电
    - 90、行圆汽车
    - 91、深圳店店通科技邮箱公司
    - 92、京东【京东】
    - 93、米庄理财
    - 94、咖啡易融
    - 95、梧桐诚选
    - 96、恒大地产【恒大】
    - 97、昆明龙慧
    - 98、上海涩瑶软件
    - 99、易信【网易】
    - 100、铜板街
    - 101、杭州云若网络科技有限公司
    - 102、特百惠(中国)有限公司
    - 103、常山众卡运力供应链管理有限公司
    - 104、深圳立创电子商务有限公司
    - 105、杭州智诺科技股份有限公司
    - 106、北京云漾信息科技有限公司
    - 107、深圳市多银科技有限公司
    - 108、亲宝宝
    - 109、上海博卡软件科技有限公司
    - 110、智慧树在线教育平台
    - 111、米族金融
    - 112、北京辰森世纪
    - 113、云南滇医通
    - 114、广州市分领网络科技有限责任公司
    - 115、浙江微能科技有限公司
    - 116、上海馨飞电子商务有限公司
    - 117、上海宝尊电子商务有限公司
    - 118、直客通科技技术有限公司
    - 119、科度科技有限公司
    - 120、上海数慧系统技术有限公司
    - 121、我的医药网
    - 122、多粉平台
    - 123、铁甲二手机
    - 124、上海海新得数据技术有限公司
    - 125、深圳市珍爱网信息技术有限公司【珍爱网】
    - 126、小蜜蜂
    - 127、吉荣数科技
    - 128、上海恺域信息科技有限公司
    - 129、广州荔支网络有限公司【荔枝FM】
    - 130、杭州闪宝科技有限公司
    - 131、北京互联新网科技发展有限公司
    - 132、誉道科技
    - 133、山西兆盛房地产开发有限公司
    - 134、北京蓝睿通达科技有限公司
    - 135、月亮小屋(中国)有限公司【蓝月亮】
    - 136、青岛国瑞信息技术有限公司
    - 137、博雅云计算(北京)有限公司
    - 138、华泰证券香港子公司
    - 139、杭州东方通信软件技术有限公司
    - 140、武汉博晟安全技术股份有限公司
    - 141、深圳市六度人和科技有限公司
    - 142、杭州趣维科技有限公司(小影)
    - 143、宁波单车侠之家科技有限公司【单车侠】
    - 144、丁丁云康信息科技(北京)有限公司
    - 145、云钱袋
    - 146、南京中兴力维
    - 147、上海矽昌通信技术有限公司
    - 148、深圳萨科科技
    - 149、中通服创立科技有限责任公司
    - 150、深圳市对庄科技有限公司
    - 151、上证所信息网络有限公司
    - 152、杭州火烧云科技有限公司【婚礼纪】
    - 153、天津青芒果科技有限公司【芒果头条】
    - 154、长飞光纤光缆股份有限公司
    - 155、世纪凯歌(北京)医疗科技有限公司
    - 156、浙江霖梓控股有限公司
    - 157、江西腾飞网络技术有限公司
    - 158、安迅物流有限公司
    - 159、肉联网
    - 160、北京北广梯影广告传媒有限公司
    - 161、上海数慧系统技术有限公司
    - 162、大志天成
    - 163、上海云鹊医
    - 164、上海云鹊医
    - 165、墨迹天气【墨迹天气】
    - 166、上海逸橙信息科技有限公司
    - 167、沅朋物联
    - 168、杭州恒生云融网络科技有限公司
    - 169、绿米联创
    - 170、重庆易宠科技有限公司
    - 171、安徽引航科技有限公司(乐职网)
    - 172、上海数联医信企业发展有限公司
    - 173、良彬建材
    - 174、杭州求是同创网络科技有限公司
    - 175、荷马国际
    - 176、点雇网
    - 177、深圳市华星光电技术有限公司
    - 178、厦门神州鹰软件科技有限公司
    - 179、深圳市招商信诺人寿保险有限公司
    - 180、上海好屋网信息技术有限公司
    - 181、海信集团【海信】
    - 182、信凌可信息科技(上海)有限公司
    - 183、长春天成科技发展有限公司
    - 184、用友金融信息技术股份有限公司【用友】
    - 185、北京咖啡易融有限公司
    - 186、国投瑞银基金管理有限公司
    - 187、晋松(上海)网络信息技术有限公司
    - 188、深圳市随手科技有限公司【随手记】
    - 189、深圳水务科技有限公司
    - 190、易企秀【易企秀】
    - 191、北京磁云科技
    - 192、南京蜂泰互联网科技有限公司
    - 193、章鱼直播
    - 194、奖多多科技
    - 195、天津市神州商龙科技股份有限公司
    - 196、岩心科技
    - 197、车码科技(北京)有限公司
    - 198、贵阳市投资控股集团
    - 199、康旗股份
    - 200、龙腾出行
    - 201、杭州华量软件
    - 202、合肥顶岭医疗科技有限公司
    - 203、重庆表达式科技有限公司
    - 204、上海米道信息科技有限公司
    - 205、北京益友会科技有限公司
    - 206、北京融贯电子商务有限公司
    - 207、中国外汇交易中心
    - 208、中国外运股份有限公司
    - 209、中国上海晓圈教育科技有限公司
    - 210、普联软件股份有限公司
    - 211、北京科蓝软件股份有限公司
    - 212、江苏斯诺物联科技有限公司
    - 213、北京搜狐-狐友【搜狐】
    - 214、新大陆网商金融
    - 215、山东神码中税信息科技有限公司
    - 216、河南汇顺网络科技有限公司
    - 217、北京华夏思源科技发展有限公司
    - 218、上海东普信息科技有限公司
    - 219、上海鸣勃网络科技有限公司
    - 220、广东学苑教育发展有限公司
    - 221、深圳强时科技有限公司
    - 222、上海云砺信息科技有限公司
    - 223、重庆愉客行网络有限公司
    - 224、数云
    - 225、国家电网运检部
    - 226、杭州找趣
    - 227、浩鲸云计算科技股份有限公司
    - 228、科大讯飞【科大讯飞】
    - 229、杭州行装网络科技有限公司
    - 230、即有分期金融
    - 231、深圳法司德信息科技有限公司
    - 232、上海博复信息科技有限公司
    - 233、杭州云嘉云计算有限公司
    - 234、有家民宿(有家美宿)
    - 235、北京赢销通软件技术有限公司
    - 236、浙江聚有财金融服务外包有限公司
    - 237、易族智汇(北京)科技有限公司
    - 238、合肥顶岭医疗科技开发有限公司
    - 239、车船宝(深圳)旭珩科技有限公司)
    - 240、广州富力地产有限公司
    - 241、氢课(上海)教育科技有限公司
    - 242、武汉氪细胞网络技术有限公司
    - 243、杭州有云科技有限公司
    - 244、上海仙豆智能机器人有限公司
    - 245、拉卡拉支付股份有限公司【拉卡拉】
    - 246、虎彩印艺股份有限公司
    - 247、北京数微科技有限公司
    - 248、广东智瑞科技有限公司
    - 249、找钢网
    - 250、九机网
    - 251、杭州跑跑网络科技有限公司
    - 252、深圳未来云集
    - 253、杭州每日给力科技有限公司
    - 254、上海齐犇信息科技有限公司
    - 255、滴滴出行【滴滴】
    - 256、合肥云诊信息科技有限公司
    - 257、云知声智能科技股份有限公司
    - 258、南京坦道科技有限公司
    - 259、爱乐优(二手平台)
    - 260、猫眼电影(私有化部署)【猫眼电影】
    - 261、美团大象(私有化部署)【美团大象】
    - 262、作业帮教育科技(北京)有限公司【作业帮】
    - 263、北京小年糕互联网技术有限公司
    - 264、山东矩阵软件工程股份有限公司
    - 265、陕西国驿软件科技有限公司
    - 266、君开信息科技
    - 267、村鸟网络科技有限责任公司
    - 268、云南国际信托有限公司
    - 269、金智教育
    - 270、珠海市筑巢科技有限公司
    - 271、上海百胜软件股份有限公司
    - 272、深圳市科盾科技有限公司
    - 273、哈啰出行
    - 274、途虎养车
    - 275、卡思优派人力资源集团
    - 276、南京观为智慧软件科技有限公司
    - 277、杭州城市大脑科技有限公司
    - 278、猿辅导
    - ……
> The company that access and use this product is welcome to register at the [address](https://github.com/xuxueli/xxl-job/issues/1 ), only for product promotion.
Welcome everyone's attention and use, XXL-JOB will also embrace changes, sustainable development.
### 1.4 Download
#### Documentation
- [中文文档](https://www.xuxueli.com/xxl-job/)
- [English Documentation](https://www.xuxueli.com/xxl-job/en/)
#### Source repository address (The latest code will be released in the two git warehouse in the same time)
Source repository address | Release Download
--- | ---
[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job) | [Download](https://github.com/xuxueli/xxl-job/releases)
[http://gitee.com/xuxueli0323/xxl-job](http://gitee.com/xuxueli0323/xxl-job) | [Download](http://gitee.com/xuxueli0323/xxl-job/releases)
#### Center repository address (The latest Release version:1.8.1)
```
<!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>1.8.2</version>
</dependency>
```
#### Technical exchange group
- [社区交流](https://www.xuxueli.com/page/community.html)
- [Gitter](https://gitter.im/xuxueli/xxl-job)
### 1.5 Environment
- JDK:1.7+
- Servlet/JSP Spec:3.1/2.3
- Tomcat:8.5.x/Jetty9.2.x
- Spring-boot:1.5.x/Spring4.x
- Mysql:5.6+
- Maven:3+
## 2. Quick Start
### 2.1 Init database
Please download project source code,get db scripts and execute, it will generate 16 tables if succeed.
The relative path of db scripts is as follows:
    /xxl-job/doc/db/tables_xxl_job.sql
The xxl-job-admin can be deployed as a cluster,all nodes of the cluster must connect to the same mysql instance.
If mysql instances is deployed in master-slave mode,all nodes of the cluster must connect to master instace.
### 2.2 Compile
Source code is organized by maven,unzip it and structure is as follows:
    xxl-job-admin:schedule admin center
    xxl-job-core:public common dependent library
    xxl-job-executor:executor Sample(Select appropriate version of executor,Can be used directly,You can also refer to it and transform existing projects into executors)
        :xxl-job-executor-sample-spring:Spring version,executors managed by Spring,general and recommend;
        :xxl-job-executor-sample-springboot:Springboot version,executors managed by Springboot;
### 2.3 Configure and delploy "Schedule Center"
    schedule center project:xxl-job-admin
    target:Centralized management、Schedule and trigger task
#### Step 1:Configure Schedule Center
Configure file’s path of schedule center is as follows:
    /xxl-job/xxl-job-admin/src/main/resources/application.properties
The concrete contet describe as follows:
    ### JDBC connection info of schedule center:keep Consistent with chapter 2.1
    xxl.job.db.driverClass=com.mysql.jdbc.Driver
    xxl.job.db.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    xxl.job.db.user=root
    xxl.job.db.password=root_pwd
    ### Alarm mailbox
    xxl.job.mail.host=smtp.163.com
    xxl.job.mail.port=25
    xxl.job.mail.username=ovono802302@163.com
    xxl.job.mail.password=asdfzxcv
    xxl.job.mail.sendFrom=ovono802302@163.com
    xxl.job.mail.sendNick=《任务调度平台XXL-JOB》
    ### Login account
    xxl.job.login.username=admin
    xxl.job.login.password=123456
    ### TOKEN used for communication between the executor and schedule center, enabled if it’s not null
    xxl.job.accessToken=
    ### Internationalized Settings, the default is Chinese version,Switch to English when the value is "en".
    xxl.job.i18n=en
#### Step 2:Deploy:
If you has finished step 1,then you can compile the project in maven and deploy the war package to tomcat.
the url to visit is :http://localhost:8080/xxl-job-admin (this address will be used by executor and use it as callback url),the index page after login in is as follow
![index page after login in](https://www.xuxueli.com/doc/static/xxl-job/images/img_6yC0.png "index page after login in")
Now,the “xxl-job-admin” project is deployed success.
#### Step3:schedule center Cluster(Option):
xxl-job-admin can be deployed as a cluster to improve system availability.
Prerequisites for cluster is to keep all node configuration(db and login account info) consistent with each other. Different xxl-job-admin cluster distinguish with each other by db configuration.
xxl-job-admin can be visited through nginx proxy and configure a domain for nginx,and the domain url can be configured as the executor’s callback url.
### 2.4 Configur and Deploy "xxl-job-executor-example"
    Executor Project:xxl-job-executor-example (if you want to create new executor project you can refer this demo);
    Target:receive xxl-job-admin’s schedule command and execute it;
#### Step 1:import maven dependence
Pleast confirm import xxl-job-core jar in pom.xml;
#### Step 2:Executor Configuration
Relative path of the executor configuration file is as follows:
    /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-spring/src/main/resources/xxl-job-executor.properties
The concret content of configuration file as follows:
    ### xxl-job admin address list:xxl-job-admin address list: Multiple addresses are separated by commas,this address is used for "heart beat and register" and "task execution result callback" between the executor and xxl-job-admin.
    xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
    ### xxl.job.executor.appname is used to group by executors
    xxl.job.executor.appname=xxl-job-executor-sample
    ### xxl.job.executor.ip :1,used to register with xxl-job-admin;2,xxl-job-admin dispatch task to executor through it;3,if it is blank executor will get ip automatically, multi network card need to be configured.
    xxl.job.executor.ip=
    ### xxl.job.executor.port :the port of the executor runned by,if multiple executor instance run on the same computer the port must different with each other
    xxl.job.executor.port=9999
    ### xxl-job log path:runtime log path of the job instance
    xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler/
    ### xxl-job, access token:xxl-job access token,enabled if it not blank
    xxl.job.accessToken=
#### Step 3:executor configuration
configure file path of executor:
    /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-spring/src/main/resources/applicationcontext-xxl-job.xml
Concrete contet describe as follows:
```
<!-- configure 01、JobHandler scan path:auto scan JobHandler bean managed by container -->
<context:component-scan base-package="com.xxl.job.executor.service.jobhandler" />
<!-- configure 02、Excutor:executer core configure -->
<bean id="xxlJobExecutor" class="com.xxl.job.core.executor.XxlJobExecutor" init-method="start" destroy-method="destroy" >
    <!-- executor IP[required],auto get if it blank -->
    <property name="ip" value="${xxl.job.executor.ip}" />
    <!-- executor port[required] -->
    <property name="port" value="${xxl.job.executor.port}" />
    <!-- executor AppName[required],auto register will be closed if it blank -->
    <property name="appname" value="${xxl.job.executor.appname}" />
    <!-- register center address of executor [required],auto register will be closed if it blank -->
    <property name="adminAddresses" value="${xxl.job.admin.addresses}" />
    <!-- log path of executor[required] -->
    <property name="logPath" value="${xxl.job.executor.logpath}" />
    <!-- access token, match check enabled if it not blank[required] -->
    <property name="accessToken" value="${xxl.job.accessToken}" />
</bean>
```
#### Step 4:deploy executor project
You can compile and package the project If have done all the steps above successfully,the project supply two executor demo projects,you can choose any one to deploy:
    xxl-job-executor-sample-spring:compile and package in WAR,can be deployed to tomcat;
    xxl-job-executor-sample-springboot:compile and package in JAR,and run in springboot mode;
Now you have deployed the executor project.
#### Step 5:executor cluster(optional)
In order to improve system availability and job process capacity,executor project can be deployed as cluster.
Prerequisites:keep all node’s configuration item "xxl.job.admin.addresses" exactly the same with each other,all executors can be register automatically.
### 2.5 Start first job "Hello World"
Now let’s create a "GLUE模式(Java)" job,if you want to learn more about it , please see “chapter 3:Task details”。( "GLUE模式(Java)"'s code is maintained online through xxl-job-admin,compare with "Bean模式任务" it’s not need to develop, deploy the code on the executor and it’s not need to restart the executor, so it’s lightweight)
#### Prerequisites:please confirm xxl-job-admin and executor project has been deployed successfully.
#### Step 1:Create new job
Login in xxl-job-admin,click on the"新建任务" button, configure the job params as follows and click "保存" button to save the job info.
![task management](https://www.xuxueli.com/doc/static/xxl-job/images/img_o8HQ.png "task management")
![create task](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "create task")
#### Step 2:develop “GLUE模式(Java)” job
Click “GLUE” button on the right of the job to go to GLUE editor view as shown below。“GLUE模式(Java)” mode task has been inited with default task code for printing Hello World。 ( “GLUE模式(Java)” mode task is a java code fragment implements IJobHandler interface,it will be executed in executor,you can use @Resource/@Autowire to inject other java bean instance,if you want to see more info please go to chapter 3)
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Fgql.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题")
#### Step 3:trigger task
If you want to run the job manually please click "执行" button on the right of the job(usually we trigger job by Cron expression)
#### Step 4:view log
Click “日志” button on the right side of the task you will go to the task log list ,you will see the schedule history records of the task and the schedule detail info,execution info and execution params.If you click the “执行日志” button on the right side of the task log record,you will go to log console and view the execute log in the course of task execution.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题")
On the log console,you can view task execution log on the executor immediately after it dump to log file,so you can monitor the task execution process by Rolling way.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_eYrv.png "在这里输入图片标题")
## 3. Task details
### Description of configuration item:
    - 执行器:the container where job executed in,it will be discovered automaticly if it has registered success when job was scheduled,and the job will be executed automaticly through this way.On the other side all tasks was grouped by this way.Tasks must be binded to a executor and it can be configured on "执行器管理"  page;
    - 描述:the decription of task
    - 路由策略:when executors deployed as a cluster,it can configure multi route policys,include:
        FIRST(第一个):default select the first executor;
        LAST(最后一个):default select the last executor;
        ROUND(轮询):round select the executor;;
        RANDOM(随机):random select the executor;
        CONSISTENT_HASH(一致性HASH):all jobs was evenly scheduled on different machines,make sure load balance of executors under the same group and the same job will be scheduled to the same machine.
        LEAST_FREQUENTLY_USED(最不经常使用):default select the least often used executor.
        LEAST_RECENTLY_USED(最近最久未使用):defalut select the longest not used executor.
        FAILOVER(故障转移):beat with the executor in order and select the first beat success executor as target executor.
        BUSYOVER(忙碌转移):check the executor busy or not in order,the first executor checked not busy is to be select as the target scheduled executor.
        SHARDING_BROADCAST(分片广播):broadcast all executor nodes under the same executor group execute the job, slice number will be transferred at the same time,shard task will be executed accordate with the shard number.
    - Cron:Cron expression used to trigger job execution;
    - 运行模式:
        BEAN模式:job was maintained on the side of executor by  as JobHandler instance,it will be executed accordate with "JobHandler" properties.
        GLUE模式(Java):task source code is maintened in the schedule center,it must implement IJobHandler and explain by "groovy" in the executor instance,inject other bean instace by annotation @Resource/@Autowire.
        GLUE模式(Shell):it’s source code is a shell script and maintained in the schedule center.
        GLUE模式(Python):it’s source code is a python script and maintained in the schedule center.
    - JobHandler:it’s used in  "BEAN模式",it’s instance is defined by annotation @JobHandler on the JobHandler class name.
    - 子任务Key:every task has a unique key (task Key can acquire from task list),when main task is done successfully it’s child task stand for by this key will be scheduled.
    - 阻塞处理策略:the stategy handle the task when this task is scheduled too frequently and the task is block to wait for cpu time.
        单机串行(默认):task schedule request go into the FIFO queue and execute serially.
        丢弃后续调度:the schedule request will be discarded and marked as fail when the same task’s  instance scheduled befor is running in the target executor.
        覆盖之前调度:the schedule request will be executed and clear before task queue when the same task’s  instance scheduled befor is running in the target executor.
    - 失败处理策略:handle policy for schedule fail
        失败告警(默认):it will trigger alarm such as send alarm mail when it’s scheduled fail.
        失败重试:it will try another time when it’s scheduled fai,if try fail it will trigger alarm for fail.every time it will trigger a new schedule request.
    - 执行参数:the params needed in the run time of the task, multiple values are separated by commas,it will be passed to task instace as an array when task is scheduled.
    - 报警邮件:the email used to receive the alarm mail when task is scheduled fail or execute fail, multiple values are separated by commas.
    - 负责人:The person name response for the task.
### 3.1 BEAN模式
The task logic exist in the executor project as JobHandler,the develop steps as shown below:
#### Step 1:develp obHandler in the executor project
    - 1, create new java class implent com.xxl.job.core.handler.IJobHandler;
    - 2, if you add @Component annotation on the top of the class name it’s will be managed as a bean instance by spring container;
    - 3, add  “@JobHandler(value=" customize jobhandler name")” annotation,the value stand for JobHandler name,it will be used as JobHandler property when create a new task in the schedule center.
#### Step 2:create task in schedule center
If you want learn more about configure item please go and sedd “Description of configuration item”,select  "BEAN模式" as run mode,property JobHandler please fill in the value defined by @JobHande.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题")
### 3.2 GLUE模式(Java)
Task source code is maintained in the schedule center and can be updated by Web IDE online, it will be compiled and effective real-time,didn’t need to assign JobHandler,develop flow shown as below:
#### Step 1:create task in schedule center
If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Java)" as run mode.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tJOq.png "在这里输入图片标题")
#### Step 2:develop task source code
Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page).
Version backtrack(support 30 versions while backtrack):on the WEB IDE page of GLUE task,on upper right corner drop down box please select “版本回溯”,it will display GLUE updated history,select the version you want it will display the source code of this version,it will backtrace the version while click save button.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题")
### 3.3 GLUE模式(Shell)
#### Step 1:create new task in schedule center
If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Shell)"as run mode.
#### Step 2:develop task source code
Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page).
Actually it is a shell script fragment.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_iUw0.png "在这里输入图片标题")
### 3.4 GLUE模式(Python)
#### Step 1:create new task in schedule center
If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Python)"as run mode.
#### Step 2:develop task source code
Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page).
Actually it is a python script fragment.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_BPLG.png "在这里输入图片标题")
## 4. Task Management
### 4.0 configure executor
click"执行器管理" on the left menu,it will go to the page as shown below:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Hr2T.png "在这里输入图片标题")
    1,"调度中心OnLine”:display schedule center machine list,when task is scheduled it will callback schedule center for notify the execution result in failover mode, so that it can avoid a single point scheduler;
    2,"执行器列表" :display all nodes under this executor group.
If you want to create a new executor,please click "+新增执行器" button:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_V3vF.png "在这里输入图片标题")
### Description of executor attributes
    Appname: the unique identity of the executor cluster,executor will registe automatically and periodically by appname so that it can be scheduled.
    名称: the name of ther executor,it is used to describe the executor.
    排序: the order of executor,it will be used in the place where need to select executor.
    注册方式:which way the schedule center used to acquire executor address through;
        自动注册:executor will register automatically,through this schedule center can discover executor dynamically.
        手动录入:fill in executor address manually and it will be used by schedule center, multiple address separated by commas.
    机器地址:only effective when "注册方式" is "手动录入",support fill in executor address manually.
### 4.1 create new task
Go to task management list page,click “新增任务” button on the upper right corner,on the pop-up window“新增任务”page configure task property and save.learn more info please go and see "3,task details".
### 4.2 edit task
Go to task management list page and choose the task you want to edit ,click”编辑”button on the right side of the task,on the pop-up window “编辑任务”page edit task property and save.
### 4.3 edit GLUE source code
Only fit to GLUE task.
choose the task you want to edit and click” GLUE”button on the right side of the task, it will go to the Web IDE page of GLUE task,then you can edit task source code on this page.you can read "3.2 GLUE模式(Java)" for more info.
### 4.4 pause/recover task
You can pause or recover task but it just fit to follow up schedule trigger and won’t affect scheduled tasks,if you want to stop tasks which has been triggered,please go and see “4.8 stop the running task”
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题")
### 4.5 manually trigger
You can trigger a task manually by Click “执行”button,it won’t affect original scheduling rules.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z5wp.png "在这里输入图片标题")
### 4.6 view schedule log
You can view task’s history schedule log by click “日志” button,on the history schedule log list page you can view every time of task’s schedule result,execution result and so on,click “执行日志” button can view the task’s full execute log.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_9235.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题")
    调度时间:schedule center trigger time when schedule and send execution signal to executor;
    调度结果:schedule center trigger task’s result, 200 represent success,500 or other number stands for fail;
    调度备注:schedule center trigger task’s remark info;
    执行器地址:the machine address where the task was executed;
    运行模式:run mode of triggered task,go and see  "3,Task Details" for more info;
    任务参数:the input params of the executed task;
    执行时间:the callback time task was done in the executor;
    执行结果:task’s execute result in the executor, 200 represent success,500 or other number stands for fail;
    执行备注:task’s execute remark info in the executor;
    操作:
        "执行日志"button:click this button you can view task’s execution detail log,go and see chapter 4.7 “view execution log” for more info;
        "终止任务"button:click this button you can stop the task’s execution thread on this executor,include bloked task instance which didn’t has started;
### 4.7 view execution log
Click the “执行日志” button on the right side of the record,you can go to the execution log page,you can view the full execution log of the logic business code, shown as below:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tvGI.png "在这里输入图片标题")
### 4.8 stop running tasks
Just fit to running tasks,on the task log list page,click “终止任务” button on the right side of the record, it will send stop command to the executor where the task was executed,finally the task was killed and the task instance execute queue of this task will be clear.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_hIci.png "在这里输入图片标题")
It is implemented by interrupt execute thread, it will trigger InterruptedException.so if JobHandler catch this execuption and handle this exception this function is unavailable.
So if you want stop the running task ,the JobHandler need to handle InterruptedException separately by throw this exception.the right logic is as shown below:
```
try{
    // do something
} catch (Exception e) {
    if (e instanceof InterruptedException) {
        throw e;
    }
    logger.warn("{}", e);
}
```
If JobHandler start child thread,child thread also must not catch InterruptedException,and it should throw exception.
### 4.9 delete execution log
On the task log list page, after you select executor and task, you can click"删除" button on the right side and it will pop-up "日志清理" window,on the pop-up window you can choose different log delete policy,choose the policy you want to execute and click "确定" button it will delele relative logs:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Ypik.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_EB65.png "在这里输入图片标题")
### 4.10 delete task
Click the delete button on the right side of the task,the task will be deteted.
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z9Qr.png "在这里输入图片标题")
## 5. Overall design
### 5.1 Source directory introduction
    - /doc :documentation and material
    - /db :db scripts
    - /xxl-job-admin :schedule and admin center
    - /xxl-job-core :common core Jar
    - /xxl-job-executor-samples :executor,Demo project(you can develop on this demo project or adjust your own exist project to executor project)
### 5.2 configure database
XXL-JOB schedule module is implemented based on Quartz cluster,it’s “database” is extended based on Quartz’s 11 mysql tables.
XXL-JOB custom Quartz table structure prefix(XXL_JOB_QRTZ_).
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_bNwm.png "在这里输入图片标题")
The added tables as shown below:
    - XXL_JOB_QRTZ_TRIGGER_GROUP:executor basic table, maintain the info about the executor;
    - XXL_JOB_QRTZ_TRIGGER_REGISTRY:executor register table, maintain addressed of online executors and schedule center machines.
    - XXL_JOB_QRTZ_TRIGGER_INFO:schedule extend table,it is used to save XXL-JOB schedule extended info,such as task group,task name,machine address,executor,input params of task and alarm email and so on.
    - XXL_JOB_QRTZ_TRIGGER_LOG:schedule log table,it is used to save XXL-JOB task’s histry schedule info,such as :schedule result,execution result,input param of scheduled task,scheduled machine and executor and so on.
    - XXL_JOB_QRTZ_TRIGGER_LOGGLUE:schedule log table,it is used to save XXL-JOB task’s histry schedule info,such as :schedule result,execution result,input param of scheduled task,scheduled machine and executor and so on.
So XXL-JOB database total has 16 tables.
### 5.3 Architecture design
#### 5.3.1 Design target
All schedule behavior has been abstracted into “schedule center” common platform , it dosen’t include business logic and just responsible for starting schedule requests.
All tasks was abstracted into separate JobHandler and was managed by executors, executor is responsible for receiving schedule request and execute the relative JobHandler business.
So schedule and task can be decoupled from each other, by the way it can improve the overall stability and scalability of the system.
#### 5.3.2 System composition
- **Schedule module(schedule center)**:
    it is responsible for manage schedule info,send schedule request accord task configuration and it is not include an business code.schedule system decouple with the task, improve the overall stability and scalability of the system, at the same time schedule system performance is no longer limited to task modules.
    Support visualization, simple and dynamic management schedule information, include create,update,delete, GLUE develop and task alarm and so on, All of the above operations will take effect in real time,support monitor schedule result and execution log and executor failover.
- **Executor module(Executor)**:
    it is responsible for receive schedule request and execute task logic,task module focuses on the execution of the task, Development and maintenance is simpler and more efficient.
    Receive execution request, end request and log request from schedule center.
#### 5.3.3 Architecture diagram
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png "在这里输入图片标题")
### 5.4 Schedule module analysis
#### 5.4.1 Disadvantage of quartz
Quartz is a good open source project and was often as the first choice for job schedule.Tasks was managed by api in quartz cluster so it can avoid some  disadvantages of single quartz instance,but it also has some disadvantage as shown below:
    - problem 1:it is not humane while operate task by call apill.
    - problem 2:it is need to store business QuartzJobBean into database, System Invasion is quite serious.
    - problem 3:schedule logic and couple with QuartzJobBean in the same project,it will lead a problem in case that if schedule tasks gradually increased and task logic gradually increased,under this situation the performance of the schedule system will be greatly limited by business.
XXL-JOB solve above problems of quartz.
#### 5.4.2 RemoteHttpJobBean
Under Quartz develop,task logic often was maintained by QuartzJobBean, couple is very serious.in XXL-JOB"Schedule module" and "task module" are completely decoupled,all scheduled tasks in schedule module use the same QuartzJobBean called RemoteHttpJobBean.the params of the tasks was maintained in the extended tables,when trigger RemoteHttpJobBean,it will parse different params and start remote cal l and it wil call relative remote executor.
This call module is like RPC,RemoteHttpJobBean provide call proxy functionality,the executor is provided as remote service.
#### 5.4.3 Schedule Center HA(Cluster)
It is based on Quartz cluster,databse use Mysql;while QUARTZ task schedule is used in Clustered Distributed Concurrent Environment,all nodes will report task info and store into database.it will fetch trigger from database while execute task,if trigger name and execute time is the same only one node will execute the task.
```
# for cluster
org.quartz.jobStore.tablePrefix = XXL_JOB_QRTZ_
org.quartz.scheduler.instanceId: AUTO
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 1000
```
#### 5.4.4 Schedule threadpool
Default threads in the threadpool is 10 so it can avoid task schedule delay because of single thread block.
```
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
```
business logic was executed on remote executor in XXL-JOB,schedule center just start one schedule request at every schedule time,executor will inqueue the request and response schedule center immediately. There is a huge difference from run business logic in quartz’s  QuartzJobBean directly,just as Elephants and feathers;
the logic of task in XXL-JOB schedule center is very light and single job average run time alaways under 100ms,(most  is network time consume).so it can use limited threads to support a large mount of job run concurrently, 10 threads configured above can support at least 100 JOB normal execution.
#### 5.4.5 @DisallowConcurrentExecution
This annotation is not used default by the schedule center of XXL-JOB schedule module, it use concurrent policy default,because RemoteHttpJobBean is common QuartzJobBean,so it greatly improve the capacity of schedule system and decrease the blocked chance of schedule module in the case of multi-threaded schedule.
Every schedule module was scheduled and executed parallel in XXL-JOB,but tasks in executor is executed serially and support stop task.
#### 5.4.6 misfire
The handle policy when miss the job’s trigger time.
he reason may be:restart service,schedule thread was blocked by QuartzJobBean, threads was exhausted,some task enable @DisallowConcurrentExecution,the last schedule  was blocked and next schedule was missed.
The default value of misfire in quartz.properties as shown below, unit in milliseconds:
```
org.quartz.jobStore.misfireThreshold: 60000
```
Misfire rule:
    withMisfireHandlingInstructionDoNothing:does not trigger execute immediately and wait for next time schedule.
    withMisfireHandlingInstructionIgnoreMisfires:execute immediately at the first frequency of the missed time.
    withMisfireHandlingInstructionFireAndProceed:trigger task execution immediately at the frequency of the current time.
XXL-JOB’s default misfire rule:withMisfireHandlingInstructionDoNothing
```
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getJobCron()).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
```
#### 5.4.7 log callback service
When schedule center of the schedule module was deployed as web service, on one side it play as schedule center, on the other side it also provide api service for executor.
The source code location of schedule center’s “log callback api service” as shown below:
```
xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback
```
Executor will execute task when it receive task execute request.it will notify the task execute result to schedule center when the task is done.
#### 5.4.8 task HA(Failover)
If executor project was deployed as cluster schedule center will known all online executor nodes,such as:“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”.
When "路由策略" select "故障转移(FAILOVER)",it will send heart beat check request in order while schedule center start schedule request.  The first alive checked executor node will be selected and send schedule request to it.
“调度备注” can be viewed on the monitor page when schedule success. As shown below:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jrdI.png "在这里输入图片标题")
“调度备注” will display local schedule route path、executor’s "注册方式"、"地址列表" and task’s "路由策略"。Under "故障转移(FAILOVER)" policy, schedule center take first address to do heartbeat detection, heat beat fail will automatically skip, the second address heart beat fail…… until the third address “127.0.0.1:9999” heart beat success, it was selected as target executor, then send schedule request to target executor, now the schedule process is end wait for the executor’s callback execution result.
#### 5.4.9 schedule log
Every time when task was scheduled in the schedule center it will record a task log, the task log include three part as shown below:
- 任务信息:include executor address、JobHandler and executor params,accord these parameters it can locate specific machine and task code that the task will be executed.
- 调度信息:include schedule time、schedule result and  schedule log  and so on,accord these parameters you can understand some task schedule info of schedule center.
- 执行信息:include execute time、execute result and execute log and so on, accord these parameters you can understand the task execution info in the executor.
Schedule log stands fo single task schedule, attribute description is as follows:
- 执行器地址:machine addresses on which task will be executed.
- JobHandler:JobHandler name of task under Bean module.
- 任务参数:the input parameters of task
- 调度时间:the schedule time started by schedule center.
- 调度结果:schedule result of schedule center,SUCCESS or FAIL.
- 调度备注:remark info of task scheduled by schedule center, such as address heart beat log.
- 执行时间:the callback time when the task is done in the executor.
- 执行结果:task execute result in the executor,SUCCESS or FAIL.
- 执行备注:task execute remark info in the executor,such as exception log.
- 执行日志:full execution log of the business code during execution of the task,go and see “4.7 view execution log”.
#### 5.4.10 Task dependency
principle:every task has a task key in XXL-JOB, every task can configure property “child task Key”,it can match task dependency relationship through task key.
When parent task end execute and success, it will match child task dependency accord child task key, it will trigger child task execute once if it matched child task.
On the task log page ,you can see matched child task and triggered child task’s log info when you “查看”button of “执行备注”,otherwise the child task didin’t execute, as shown beleow:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Wb2o.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jOAU.png "在这里输入图片标题")
### 5.5 Task "run mode" analysis
#### 5.5.1 "Bean模式" task
Development steps:go and see "chapter 3" .
principle: every Bean mode task is a Spring Bean instance and it is maintained in executor project’s Spring container. task class nedd to add “@JobHandler(value="name")” annotation, because executor identify task bean instance in spring container through annotation. Task class nedd to implements interface IJobHandler, task logic code in method execute(), the task logic in execute() method will be executed when executor received a schedule request from schedule center.
#### 5.5.2 "GLUE模式(Java)" task
Development steps:go and see "chapter 3" .
Principle : every "GLUE模式(Java)" task code is a class implemets interface IJobHandler, when executor received schedule request from schedule center these code will be loaded by Groovy classloader and instantiate into a Java object and inject spring bean service declared in this code at the same time(please confirm service and class reference in Glue code exist in executor project), then call the object’s execute() method and execute task logic.
#### 5.5.3 GLUE模式(Shell) + GLUE模式(Python)
Development steps:go and see "chapter 3" .
principle:the source code of script task is maintained in schedule center and script logic will be executed in executor. when script task was triggered, executor will load script source code and generate a script file on the machine where executor was deployed, the script will be called by java code, the script output log will be written to the task log file in real time so that we can monitor script execution in real time through schedule center, the return code 0 stands for success other for fail.
All supported types of scripts as shown beloes:
    - shell script:shell script task will be enabled when select "GLUE模式(Shell)"as task run mode.
    - python script: python script task will be enabled when select " GLUE模式(Python)"as task run mode.
#### 5.5.4 executor
Executor is actually an embedded Jetty server with default port 9999, as shown below(parameter:xxl.job.executor.port).
Executor will identify Bean mode task in spring container through @JobHandler When project start, it will be managed use the value of annotation as key.
When executor received schedule request from schedule center, if task type is “Bean模式” it will match bean mode task in Spring container and call it’s execute() method and execute task logic. if task type is “GLUE模式”, it will load Glue code, instantiate a Java object and inject other spring service(notice: the spring service injected in Glue code must exist in the same executor project), then call execute() method and execute task logic.
#### 5.5.5 task log
XXL-JOB will generate a log file for every schedule request, the log info will be recorded by XxlJobLogger.log() method, the log file will be loaded when view log info through schedule center.
(history version is implemented by overriding LOG4J’s Appender so it exists dependency restrictions, The way has been discraded in the new version)
The location of log file can be specified in executor configuration file, default pattern is : /data/applogs/xxl-job/jobhandler/formatted date/primary key for database scheduling log records.log”.
When start child thread in JobHandler, child thread will print log in parent JobHandler thread’s execute log in order to trace execute log.
### 5.6 Communication module analysis
#### 5.6.1 A complete task schedule communication process
    - 1,schedule center send http request to executor, and the service in executor in fact is a jetty server with default port 9999.
    - 2,executor execute task logic.
    - 3,executor http callback with schedule center for schedule result, the service in schedule center used to receive callback request from executor is a set of api opended to executor.
#### 5.6.2 Encrypt Communication data
When scheduler center send request to executor, it will use RequestModel and ResponseModel object to encapsulate schedule request parameters and response data, these two object will be serialized before communication, data protocol and time stamp will be checked so that achieve data encryption target.
### 5.7 task register and task auto discover
Task executor machine property has been canceled from v1.5, instead of task register and auto discovery, get remote machine address dynamic.
    AppName: unique identify of executor cluster,  executor is minimal unite of task register, every task recognize machine addresses under the executor on which it was binded.
    Beat: heartbeat cycle of task register, default is 15s, and the time executor usedto register is twice the time, the time used to auto task discover is twice the beat time, the invalid time of register is twice the Beat time.
    registry table: see XXL_JOB_QRTZ_TRIGGER_REGISTRY table, it will maintain a register record periodically while task register, such as the bind relationship between machine address and AppName, so that schedule center can recognize machine list by AppName dynamicly.
To ensure system lightweight and reduce learning costs, it did not use Zookeeper as register center, Use DB as register center to do task registration.
### 5.8 task execute result
Since v1.6.2, the task execute result is recognized through ReturnT of IJobHandler, it executes success when return value meets the condition "ReturnT.code == ReturnT.SUCCESS_CODE" , or it executes fail, and it can callback error message info to schedule center through ReturnT.msg, so it can control task execute results in the task logic.
### 5.9 slice broadcat & dynamic slice
When “分片广播” is selected as route policy in executor cluster, one task schedule will broadcast all executor node in cluster to trigger task execute in every executor, pass slice parameter at the same time, so we can develop slice task by slice parameters.
"分片广播"  break the task by the dimensions of executor, support dynamic extend executor cluster so that it can add slice number dynamically to do business process, In case of large amount of data process can significantly improve task processing capacity and speed.
The develop process of "分片广播" is the same as general task, The difference is that you can get slice parameters,code as shown below(go and see ShardingJobHandler in execuotr example ):
    int shardIndex = XxlJobContext.getXxlJobContext().getShardIndex();
    int shardTotal = XxlJobContext.getXxlJobContext().getShardTotal();
This slice parameter object has two properties:
    index:the current slice number(start with 0),stands for the number of current executor in the executor cluster.
    total:total slice number,stands for total slices in the executor cluster.
This feature applies to scenes as shown below:
- 1、slice task scene:when 10 executor to handle 10w records,  1w records need to be handled per machine, time-consuming 10 times lower;
- 2、Broadcast task scene:broadcast all cluster nodes to execute shell script、broadcast all cluster nodes to update cache.
### 5.10 AccessToken
To improve system security it is need to check security between schedule center and executor, just allow communication between them when AccessToken of each other matched.
The AccessToken of scheduler center and executor can be configured by xxl.job.accessToken.
There are only two settings when communication between scheduler center and executor just:
- one:do not configure AccessToken on both, close security check.
- two:configure the same AccessToken on both;
### 5.11 Dispatching center API services
The scheduling center provides API services for executors and business parties to choose to use, and the currently available API services are available.
    1. Job result callback service;
    2. Executor registration service;
    3. Executor registration remove services;
    4. Triggers a single execution service, and support the task to be triggered according to the business event;
The scheduling center API service location: com.xxl.job.core.biz.AdminBiz.java
The scheduling center API service requests reference code:com.xxl.job.adminbiz.AdminBizTest.java
## 6 Version update log
### 6.1 version V1.1.x,New features [2015-12-05]
**【since V1.1.x,XXL-JOB was used by company hiring me,alias Ferrari inner company,the latest version is recommended for new project】**
- 1、simple:support CRUD operation through Web page, simple and one minute to get started;
- 2、dynamic:support dynamic update task status,pause/recover task and effective in real time;
- 3、service HA:task info stored in mysql, Job service support cluster to make sure service HA;
- 4、task HA:when some Job services hangs up, tasks will be assigned to some other alive machines, if all nodes of the cluster hangs up,  it will  compensate for the execution of lost task when restart;
- 5、one task instance will only be executed on one executor;
- 6、task is executed serially;
- 7、support for custom parameters;
- 8、Support pause task execution remotely .
### 6.2 version V1.2.x,New features [2016-01-17]
- 1、support task group;
- 2、suport local task, remote task;
- 3、support two types underlying communication ,Servlet or JETTY;
- 4、support task log;
- 5、support serially execution,parallel execution;
    Description:system architecture of V1.2 divided by function as shown below:
        - schedule module(schedule center):Responsible for managing schedule information,send schedule request according to the schedule configuration;
        - execute module(executor):Responsible for receiving schedule request and execute task logic;
        - communication module:Responsible for the communication between the schedule module and execute module;
    advantage:
        - Decouple:execute module supply task api, schedule module maintains schedule information, The business is independent of each other;
        - High scalability;
        - stability;
### 6.3 version V1.3.0,New features [2016-05-19]
- 1、discard local task module, remote task was recommended, easy to decouple system, the JobHandler of task was called executor.
- 2、dicard underlying communication type servlet, JETTY was recommended, schedule and callback bidirectional communication, rebuild the communication logic;
- 3、UI interactive optimization:optimize left menu expansion and menu item selected status , task list opens the table with compression optimization;
- 4、【important】executor is subdivided into two develop mode:BEAN、GLUE:
    Introduction to the executor mode:
        - BEAN mode executor:every executor is a Spring Bean instance,it was recognized and scheduled by XXL-JOB through @JobHandler annotation;
         -GLUE mode executor:every executor corresponds to a piece of code,edited and maintained online by Web, Dynamic compile and takes effect in real time, executor is responsible for loading GLUE code and executing;
### 6.4 version V1.3.1,New features [2016-05-23]
- 1、Update project directory structure:
    - /xxl-job-admin -------------------- 【schedule center】:Responsible for managing schedule information,send schedule request according to schedule configuration;
    - /xxl-job-core -----------------------  Public core dependence
    - /xxl-job-executor-example ------ 【executor】:Responsible for receiving scheduling request and execute task logic;
    - /db ---------------------------------- create table script
    - /doc --------------------------------- user manual
- 2、Upgrade the user manual under the new directory structure;
- 3、Optimize some interactions and UI;
### 6.5 version V1.3.2,New features [2016-05-28]
- 1、Schedule logic for transactional handle;
- 2、executor asynchronous callback execution log;
- 3、【important】based on HA support of schedule center,extend executor’s Failover support,Support configure multiple execution addresses;
### 6.6 version V1.4.0 New features [2016-07-24]
- 1、Task dependency: it is implemented by trigger event, it will automatically trigger a child task schedule after Task execute success and callback, multiple child tasks are separated by commas;
- 2、executor source code has been reconstructed, optimize underlying db script;
- 3、optimize task thread group logic of executor, before it is group by executor’s JobHandler so when multiple task reuse Jobhanlder will cause block with each other. Now it is grouped by task of schedule center so tasks are isolated from task execution.
- 4、optimize communication scheme between executor and schedule center, a simple RPC protocol was implemented through Hex + HC, optimize the maintenance and analysis process of communication parameters.
- 5、schedule center, create/edit task, page attribute adjustment:
    - 5.1、the property JobName was removed from task add/edit page and it is changed to automatically generate by system: this field before is used to identify a task in schedule center and did not use in other scenes, so remove it to simplify the task creation;
    - 5.2、adjust "GLUE模式" property in task add/edit page to near JobHandler input box;
    - 5.3、"报警阈值" property was removed from task add/edit page;
    - 5.4、"子任务Key" property was removed from task add/edit page, the key of task can be acquired from task list page, child task will be triggered by child task key when main task execute success.
- 6、bug fix:
    - 6.1、optimize jetty executor shutdown,  solve one problem may cause jetty could not shutdown.
    - 6.2、optimize callback of executor task queue when task execute finish. Solve a problem which may cause task could not callback.
    - 6.3、Optimize Page List Parameters of Schedule Center, solve one problem which may be caused by post length limit of server.
    - 6.4、optmize executor Jobhandler annotation, solve a problem that container could not load the JobHandler caused by the transaction proxy.
    - 6.5、optimize remote schedule, disable retry policy, solve a problem may caused repeat call;
Tips: V1.3.x release has been published , enter the maintenance phase, branch  address is [V1.3](https://github.com/xuxueli/xxl-job/tree/v1.3) .New features will be updated continuously in the master branch.
### 6.7 version V1.4.1 New features [2016-09-06]
- 1、project successfully pushed to maven central warehouse, Central warehouse address and dependency  as shown below:
    ```
    <!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ -->
    <dependency>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job-core</artifactId>
        <version>${最新稳定版}</version>
    </dependency>
    ```
- 2、To adapt to the rules of central warehouse, groupId has been changed from com.xxl to com.xuxueli.
- 3、to resolve the problem that sub-modules can not be compiled separately, system version is not maintained in the project root pom, each sub-module is configured separately for version configuration;
- 4、optimize data byte length statistics rule of RPC communication it may reduce 50% of data traffic;
- 5、IJobHandler cancel task return value, before the execution status is judged by the return value, now it instead of task was executed successfully by default only when exception was caught the task execution was judged failed.
- 6、optimize system public pop-up box as a plugin;
- 7、optimize table structure and the table name now is upper case;
- 8、modify ContentType of JSON response from exception handler of schedule center to fix the bug that it is could not recognized by browser.
### 6.8 version V1.4.2 New features [2016-09-29]
- 1、push V1.4.2 to maven central warehouse, main version V1.4 enter maintenance phase;
- 2、fix problem task list offset when add task;
- 3、fix a style disorder problem that caused by bootstrap does not support the modal frame overlap , the problem occurs when the task is edited;
- 4、optimize schedule status when schedule timeout and Handler could not matched;
- 5、the task could not stop problem caused by catch exception has given solution;
### 6.9 version V1.5.0 New features [2016-11-13]
- 1、task register: executor registers the task automatically, schedule center will automatically discover the registered task and trigger execution.
- 2、add parameter AppName for executor: AppName is the unique identifier of each executor cluster, register periodically and automatically with AppName.
- 3、add column executor management in schedule center : manage online executors, automatically discover registered executors via the property AppName。Only managed executors are allowed to be used;
- 4、change Task group attribute to executor : each task needs to be bound to the specified exector, schedule address is obtained by binded executor;
- 5、discard property task machine: by the way of binding task with executor, automatically discovers registered remote executor address and triggers schedule request.
- 6、add DBGlueLoader in public dependency, it implement GLUE source code calssloader based on native jdbc, Reduce third party reliance (mybatis,spring-orm etc); simplify and optimize executor configuration (for GLUE task), Reduce the difficulty of getting started;
- 7、adjust table structure, reconstruct the project;
- 8、schedule center automatically registered and found, failover: schedule center periodically registered automatically, task callback can recognize all online schedule center addresses, task callback support failover so that it can avoid single point of risk.
### 6.10 version V1.5.1 New features [2016-11-13]
- 1、Reconstruct the underlying code and optimize logic, clean POM and Clean Code;
- 2、Servlet/JSP Spec selected 3.0/2.2;
- 3、Spring updated to 3.2.17.RELEASE version;
- 4、Jetty updated to version 8.2.0.v20160908;
- 5、has push V1.5.0 and V1.5.1 to maven central warehouse;
### 6.10 version V1.5.2 New features [2017-02-28]
- 1、optimize IP tools class which used to gets IP address,IP static cache;
- 2、both executor and schedule center support customize registered IP address;Solve problem when machine has multiple network card and get the wrong card;
- 3、solve the problem that it will generate multiple log files when executed across days;
- 4、the non-sensitive log level is adjusted to debug;
- 5、Upgrade the database connection pool to c3p0;
- 6、optimize log4j property of executor,remove invalid attribute;
- 7、reconstruct underlying code and optimize logic and Clean Code;
- 8、optimize Dependency Injection Logic of GLUE, support injected as alias;
### 6.11 version V1.6.0 New features [2017-03-13]
- 1、upgrade communication scheme,the HEX communication model is adjusted to the B-RPC model based on HTTP;
- 2、executor supports set execution address list manually,provide switch to use automatically registered address or manually set address;
- 3、executor route rules:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移;
- 4、unified thread model and thread destruction scheme (by the way of listener or stop() method,Destroy the thread when container is destroyed;Daemon is sometimes not ideal);
- 5、unified system configuration data,Unified managed by configuration files;
- 6、CleanCode,Clean up invalid historical parameters;
- 7、extend data structure and adjust related table structure;
- 8、new created task defaults to a non-running state;
- 9、optimize update logic of GLUE mode task instance , The original update is based on the timeout value and now is updated according to the version number,version number plus one while source changed;
### 6.12 version V1.6.1 New features [2017-03-25]
- 1、Rolling log;
- 2、reconstruct WebIDE interactive;
- 3、enhanced communication check,filter unnormal requests effectively;
- 4、enhanced permission check,Using dynamic login TOKEN(recommend instead of internal SSO);
- 5、optimize database configuration,solve garbled problem;
### 6.13 version V1.6.2 New features [2017-04-25]
- 1、execution report:support view run time data in real time, such as task number, total schedule number, executor number etc., include schedule report , such as scheduled distribution graph on date, scheduled success distribution graph etc;
- 2、JobHandler support set return value for tasks, it is easy to control task execute result in task logic;
- 3、the problem could not view exception info when resource path include space or chinese word casused resource file could not be loaded;
- 4、optimize route policy:fix problems that Loop and LFU routing policy counters are no limit and first route is focused on the first machine;
### 6.14 version V1.7.0 New features [2017-05-02]
- 1、script task:support develop and run script task by GLUE, include script type such as Shell、Python and Groovy;
- 2、add spring-boot type executor example project;
- 3、upgrade jetty to version 9.2;
- 4、task execute log remove log4j dependency, instead of self-realization,Thus eliminate the dependency on the log component;
- 5、executor remove GlueLoader dependency,instead of push mode,thus GLUE source code load no longer rely on JDBC;
- 6、get the project name when login and redirect, solve 404 problem when it is not deployed by the directory;
### 6.15 version V1.7.1 New features [2017-05-08]
- 1、unified write and read code of execute log as UTF-8,solve log garbled problem under windows environment;
- 2、communication timeout period is limited to 10s,To avoid schedule thread is occupied under abnormal situation;
- 3、adjust executor , server stat, destroy and register logic;
- 4、optimize Jetty Server shutdown logic, repair port occupation caused by executor could not be closed normally and frequent printe c3p0 log probleam;
- 5、start child thread in JobHandler,support child thread print execute log and view by Rolling;
- 6、task log cleanup;
- 7、pop-up component is replaced by layer;
- 8、upgrade quartz to version 2.3.0;
### 6.16 version V1.7.2 New features [2017-05-17]
- 1、block handle policy:the policy when schedule is too frequently and the executor it too late to handle, include multiple strategies:single machine serially execute(default)、discard subsequent schedule、override before schedule;
- 2、fail handle policy:handle policy when scheduled fail, include :failure alarm(default)、failed to retry;
- 3、The communication timeout is adjusted to 180s;
- 4、executor and database are completely decoupled,But the executor needs to configure schedule center cluster address。schedule center provides APIs for executor callbacks and heartbeat registration services,cancel jetty inner schedule center, heartbeat cycle is adjusted to 30s,heartbeat failure is triple heartbeat;
- 5、fix executor parameters lost bug when edit;
- 6、add task test Demo to make task logic test easier;
### 6.17 version V1.8.0 New features [2017-07-17]
- 1、optimize update logic of task Cron,instead of rescheduleJob,at the same time preventing set cron repeatedly;
- 2、optimize API callback service failed status code,facilitate troubleshooting;
- 3、XxlJobLogger support multi-parameter;
- 4、route policy add "忙碌转移" mode:Perform idle detection in sequence,The first idle test successfully machine is selected as the target executor and trigger schedule;
- 5、reconstruct route policy code;
- 6、fix executor repeat registration problem;
- 7、Task thread will be destroyed after 30 times idle turn, reduce the inefficient thread consumption of low frequency tasks;
- 8、Executor task execution result batch callback so that reduce callback frequency to improve actuator performance;
- 9、cancle XML configuration of springboot executor project,instead of class configuration;
- 10、supports filter execute log based on running status;
- 11、optimize scheduling Center Task Registration Detection Logic;
### 6.18 version V1.8.1 New features [2017-07-30]
- 1、slice broadcast task:When slice broadcast is selected as route policy in executor cluster, one task schedule will broadcast all executor node in cluster to trigger task execute in every executor, pass slice parameter at the same time, so we can develop slice task by slice parameters;
- 2、dynamic slice: break the task by the dimensions of executor, support dynamic extend executor cluster so that it can add slice number dynamically to do business process, In case of large amount of data process can significantly improve task processing capacity and speed;
- 3、executor JobHandler disables name conflicts;
- 4、executor cluster address list for natural sorting;
- 5、add test cases and optimize DAO layer code for Scheduling center;
- 6、schedule Center API service change to self-study RPC framework to u nify communication model;
- 7、add schedule center API service test Demo, convenient in dispatch center API extension and testing;
- 8、Task list page interaction optimization,The task list is automatically refreshed when the executor group is replaced,create new job defaults to locate current executor position;
- 9、access Token:To improve system security,it is used for safety check between schedule center and executor, communication allowed just when Both Access Token matched;
- 10、upgrade springboot version to 1.5.6.RELEASE of executor;
- 11、unify maven version dependency management;
### 6.19 version V1.8.2 New features[Coding]
- 1,support configuring the HTTPS for executor callback URL;
- 2,Standardize project directory for extend multi executors;
- 3,add JFinal type executor sample project;
### TODO LIST
- 1,Task privilege management:control privilege on executor, check privilege on core operations;
- 2,Task slice routing:using consistent Hash algorithm to calculate slice order as stable as possible, even if there is fluctuation in the registration machine will not cause large fluctuations in the order of slice. Currently using IP natural sorting can meet the demand,to be determined;
- 3,Failure retry optimization:The current failure to retry logic is execute the request logic once again after the scheduled request fails。The optimization point is retry for both scheduling and execution failures, retry a full schedule when retrying,This may lead schedule failure to an infinite loop,to be determined;
- 4,write file when callback failed,read the log when viewing the log,callback confirm after rebooting;
- 5,Task dependency,flow chart,child task + aggregation task,log of each node;
- 6,Scheduled task priority;
- 7,Remove quartz dependencies and rewrite scheduld module:insert the next execution record into delayqueue when add or resume task, schedule center cluster compete distributed lock,successful nodes bulk load expired delayqueue data and batch execution;
- 8,springboot and docker image,and push docker image to the central warehouse,further realize product out of the box;
- 9,globalization:schedule center interface and Official documents,add English version;
- 10,executor removal:notify schedule center and remove the corresponding execute node when executor is destroyed, improve the timeliness of executor state recognized;
## 7. Other
### 7.1 Contributing
Contributions are welcome! Open a pull request to fix a bug, or open an [Issue](https://github.com/xuxueli/xxl-job/issues/) to discuss a new feature or change.
### 7.2 used records(record just for spread,Product is open source and free of charge)
Record for spread product and product is free and open source.
Welcome to [check in](https://github.com/xuxueli/xxl-job/issues/1 )on github.
### 7.3 Copyright and License
This product is open source and free, and will continue to provide free community technical support. Individual or enterprise users are free to access and use.
- Licensed under the GNU General Public License (GPL) v3.
- Copyright (c) 2015-present, xuxueli.
---
### Donate
No matter how much the amount is enough to express your thought, thank you very much :)     [To donate](https://www.xuxueli.com/page/donate.html )
xxl-job/doc/XXL-JOB官方文档.md
New file
@@ -0,0 +1,2324 @@
## 《分布式任务调度平台XXL-JOB》
[![Actions Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/)
[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-job.svg)](https://github.com/xuxueli/xxl-job/releases)
[![GitHub stars](https://img.shields.io/github/stars/xuxueli/xxl-job)](https://github.com/xuxueli/xxl-job/)
[![Docker Status](https://img.shields.io/docker/pulls/xuxueli/xxl-job-admin)](https://hub.docker.com/r/xuxueli/xxl-job-admin/)
[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html)
[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat)](https://www.xuxueli.com/page/donate.html)
[TOCM]
[TOC]
## 一、简介
### 1.1 概述
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
### 1.2 社区交流
- [社区交流](https://www.xuxueli.com/page/community.html)
### 1.3 特性
- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;
- 2、动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效;
- 3、调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA;
- 4、执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA;
- 5、注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址;
- 6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务;
- 7、触发策略:提供丰富的任务触发策略,包括:Cron触发、固定间隔触发、固定延时触发、API(事件)触发、人工触发、父子任务触发;
- 8、调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等;
- 9、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
- 10、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务;
- 11、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试;
- 12、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式;
- 13、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
- 14、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务;
- 15、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
- 16、故障转移:任务路由策略选择"故障转移"情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。
- 17、任务进度监控:支持实时监控任务进度;
- 18、Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志;
- 19、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。
- 20、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS、PHP、PowerShell等类型脚本;
- 21、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可;
- 22、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔;
- 23、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行;
- 24、自定义任务参数:支持在线配置调度任务入参,即时生效;
- 25、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞;
- 26、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性;
- 27、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件;
- 28、推送maven中央仓库: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用;
- 29、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等;
- 30、全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行;
- 31、跨语言:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案;
- 32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文;
- 33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用;
- 34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性;
- 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色;
- 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作;
### 1.4 发展
于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计……
于2015-11月,XXL-JOB终于RELEASE了第一个大版本V1.0, 随后我将之发布到OSCHINA,XXL-JOB在OSCHINA上获得了@红薯的热门推荐,同期分别达到了OSCHINA的“热门动弹”排行第一和git.oschina的开源软件月热度排行第一,在此特别感谢红薯,感谢大家的关注和支持。
于2015-12月,我将XXL-JOB发表到我司内部知识库,并且得到内部同事认可。
于2016-01月,我司展开XXL-JOB的内部接入和定制工作,在此感谢袁某和尹某两位同事的贡献,同时也感谢内部其他给与关注与支持的同事。
于2017-05-13,在上海举办的 "[第62期开源中国源创会](https://www.oschina.net/event/2236961)" 的 "放码过来" 环节,我登台对XXL-JOB做了演讲,台下五百位在场观众反响热烈([图文回顾](https://www.oschina.net/question/2686220_2242120) )。
于2017-10-22,又拍云 Open Talk 联合 Spring Cloud 中国社区举办的 "[进击的微服务实战派上海站](https://opentalk.upyun.com/303.html)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。
于2017-12-11,XXL-JOB有幸参会《[InfoQ ArchSummit全球架构师峰会](http://bj2017.archsummit.com/)》,并被拍拍贷架构总监"杨波老师"在专题 "[微服务原理、基础架构和开源实践](http://bj2017.archsummit.com/training/2)" 中现场介绍。
于2017-12-18,XXL-JOB参与"[2017年度最受欢迎中国开源软件](http://www.oschina.net/project/top_cn_2017?sort=1)"评比,在当时已录入的约九千个国产开源项目中角逐,最终进入了前30强。
于2018-01-15,XXL-JOB参与"[2017码云最火开源项目](https://www.oschina.net/news/92438/2017-mayun-top-50)"评比,在当时已录入的约六千五百个码云项目中角逐,最终进去了前20强。
于2018-04-14,iTechPlus在上海举办的 "[2018互联网开发者大会](http://www.itdks.com/eventlist/detail/2065)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。
于2018-05-27,在上海举办的 "[第75期开源中国源创会](https://www.oschina.net/event/2278742)" 的 "架构" 主题专场,我登台进行“基础架构与中间件图谱”主题演讲,台下上千位在场观众反响热烈([图文回顾](https://www.oschina.net/question/3802184_2280606) )。
于2018-12-05,XXL-JOB参与"[2018年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2018?sort=1)"评比,在当时已录入的一万多个开源项目中角逐,最终排名第19名。
于2019-12-10,XXL-JOB参与"[2019年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2019)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第9名。
于2020-11-16,XXL-JOB参与"[2020年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2020)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第8名。
于2021-12-06,XXL-JOB参与"[2021年度OSC中国开源项目评选](https://www.oschina.net/project/top_cn_2021) "评比,在当时已录入的一万多个开源项目中角逐,最终当选"最受欢迎项目"。
> 我司大众点评目前已接入XXL-JOB,内部别名《Ferrari》(Ferrari基于XXL-JOB的V1.1版本定制而成,新接入应用推荐升级最新版本)。
据最新统计, 自2016-01-21接入至2017-12-01期间,该系统已调度约100万次,表现优异。新接入应用推荐使用最新版本,因为经过数十个版本的更新,系统的任务模型、UI交互模型以及底层调度通讯模型都有了较大的优化和提升,核心功能更加稳定高效。
至今,XXL-JOB已接入多家公司的线上产品线,接入场景如电商业务,O2O业务和大数据作业等,截止最新统计时间为止,XXL-JOB已接入的公司包括不限于:
    - 1、大众点评【美团点评】
    - 2、山东学而网络科技有限公司;
    - 3、安徽慧通互联科技有限公司;
    - 4、人人聚财金服;
    - 5、上海棠棣信息科技股份有限公司
    - 6、运满满【运满满】
    - 7、米其林 (中国区)【米其林】
    - 8、妈妈联盟
    - 9、九樱天下(北京)信息技术有限公司
    - 10、万普拉斯科技有限公司【一加手机】
    - 11、上海亿保健康管理有限公司
    - 12、海尔馨厨【海尔】
    - 13、河南大红包电子商务有限公司
    - 14、成都顺点科技有限公司
    - 15、深圳市怡亚通
    - 16、深圳麦亚信科技股份有限公司
    - 17、上海博莹科技信息技术有限公司
    - 18、中国平安科技有限公司【中国平安】
    - 19、杭州知时信息科技有限公司
    - 20、博莹科技(上海)有限公司
    - 21、成都依能股份有限责任公司
    - 22、湖南高阳通联信息技术有限公司
    - 23、深圳市邦德文化发展有限公司
    - 24、福建阿思可网络教育有限公司
    - 25、优信二手车【优信】
    - 26、上海悠游堂投资发展股份有限公司【悠游堂】
    - 27、北京粉笔蓝天科技有限公司
    - 28、中秀科技(无锡)有限公司
    - 29、武汉空心科技有限公司
    - 30、北京蚂蚁风暴科技有限公司
    - 31、四川互宜达科技有限公司
    - 32、钱包行云(北京)科技有限公司
    - 33、重庆欣才集团
    - 34、咪咕互动娱乐有限公司【中国移动】
    - 35、北京诺亦腾科技有限公司
    - 36、增长引擎(北京)信息技术有限公司
    - 37、北京英贝思科技有限公司
    - 38、刚泰集团
    - 39、深圳泰久信息系统股份有限公司
    - 40、随行付支付有限公司
    - 41、广州瀚农网络科技有限公司
    - 42、享点科技有限公司
    - 43、杭州比智科技有限公司
    - 44、圳临界线网络科技有限公司
    - 45、广州知识圈网络科技有限公司
    - 46、国誉商业上海有限公司
    - 47、海尔消费金融有限公司,嗨付、够花【海尔】
    - 48、广州巴图鲁信息科技有限公司
    - 49、深圳市鹏海运电子数据交换有限公司
    - 50、深圳市亚飞电子商务有限公司
    - 51、上海趣医网络有限公司
    - 52、聚金资本
    - 53、北京父母邦网络科技有限公司
    - 54、中山元赫软件科技有限公司
    - 55、中商惠民(北京)电子商务有限公司
    - 56、凯京集团
    - 57、华夏票联(北京)科技有限公司
    - 58、拍拍贷【拍拍贷】
    - 59、北京尚德机构在线教育有限公司
    - 60、任子行股份有限公司
    - 61、北京时态电子商务有限公司
    - 62、深圳卷皮网络科技有限公司
    - 63、北京安博通科技股份有限公司
    - 64、未来无线网
    - 65、厦门瓷禧网络有限公司
    - 66、北京递蓝科软件股份有限公司
    - 67、郑州创海软件科技公司
    - 68、北京国槐信息科技有限公司
    - 69、浪潮软件集团
    - 70、多立恒(北京)信息技术有限公司
    - 71、广州极迅客信息科技有限公司
    - 72、赫基(中国)集团股份有限公司
    - 73、海投汇
    - 74、上海润益创业孵化器管理股份有限公司
    - 75、汉纳森(厦门)数据股份有限公司
    - 76、安信信托
    - 77、岚儒财富
    - 78、捷道软件
    - 79、湖北享七网络科技有限公司
    - 80、湖南创发科技责任有限公司
    - 81、深圳小安时代互联网金融服务有限公司
    - 82、湖北享七网络科技有限公司
    - 83、钱包行云(北京)科技有限公司
    - 84、360金融【360】
    - 85、易企秀
    - 86、摩贝(上海)生物科技有限公司
    - 87、广东芯智慧科技有限公司
    - 88、联想集团【联想】
    - 89、怪兽充电
    - 90、行圆汽车
    - 91、深圳店店通科技邮箱公司
    - 92、京东【京东】
    - 93、米庄理财
    - 94、咖啡易融
    - 95、梧桐诚选
    - 96、恒大地产【恒大】
    - 97、昆明龙慧
    - 98、上海涩瑶软件
    - 99、易信【网易】
    - 100、铜板街
    - 101、杭州云若网络科技有限公司
    - 102、特百惠(中国)有限公司
    - 103、常山众卡运力供应链管理有限公司
    - 104、深圳立创电子商务有限公司
    - 105、杭州智诺科技股份有限公司
    - 106、北京云漾信息科技有限公司
    - 107、深圳市多银科技有限公司
    - 108、亲宝宝
    - 109、上海博卡软件科技有限公司
    - 110、智慧树在线教育平台
    - 111、米族金融
    - 112、北京辰森世纪
    - 113、云南滇医通
    - 114、广州市分领网络科技有限责任公司
    - 115、浙江微能科技有限公司
    - 116、上海馨飞电子商务有限公司
    - 117、上海宝尊电子商务有限公司
    - 118、直客通科技技术有限公司
    - 119、科度科技有限公司
    - 120、上海数慧系统技术有限公司
    - 121、我的医药网
    - 122、多粉平台
    - 123、铁甲二手机
    - 124、上海海新得数据技术有限公司
    - 125、深圳市珍爱网信息技术有限公司【珍爱网】
    - 126、小蜜蜂
    - 127、吉荣数科技
    - 128、上海恺域信息科技有限公司
    - 129、广州荔支网络有限公司【荔枝FM】
    - 130、杭州闪宝科技有限公司
    - 131、北京互联新网科技发展有限公司
    - 132、誉道科技
    - 133、山西兆盛房地产开发有限公司
    - 134、北京蓝睿通达科技有限公司
    - 135、月亮小屋(中国)有限公司【蓝月亮】
    - 136、青岛国瑞信息技术有限公司
    - 137、博雅云计算(北京)有限公司
    - 138、华泰证券香港子公司
    - 139、杭州东方通信软件技术有限公司
    - 140、武汉博晟安全技术股份有限公司
    - 141、深圳市六度人和科技有限公司
    - 142、杭州趣维科技有限公司(小影)
    - 143、宁波单车侠之家科技有限公司【单车侠】
    - 144、丁丁云康信息科技(北京)有限公司
    - 145、云钱袋
    - 146、南京中兴力维
    - 147、上海矽昌通信技术有限公司
    - 148、深圳萨科科技
    - 149、中通服创立科技有限责任公司
    - 150、深圳市对庄科技有限公司
    - 151、上证所信息网络有限公司
    - 152、杭州火烧云科技有限公司【婚礼纪】
    - 153、天津青芒果科技有限公司【芒果头条】
    - 154、长飞光纤光缆股份有限公司
    - 155、世纪凯歌(北京)医疗科技有限公司
    - 156、浙江霖梓控股有限公司
    - 157、江西腾飞网络技术有限公司
    - 158、安迅物流有限公司
    - 159、肉联网
    - 160、北京北广梯影广告传媒有限公司
    - 161、上海数慧系统技术有限公司
    - 162、大志天成
    - 163、上海云鹊医
    - 164、上海云鹊医
    - 165、墨迹天气【墨迹天气】
    - 166、上海逸橙信息科技有限公司
    - 167、沅朋物联
    - 168、杭州恒生云融网络科技有限公司
    - 169、绿米联创
    - 170、重庆易宠科技有限公司
    - 171、安徽引航科技有限公司(乐职网)
    - 172、上海数联医信企业发展有限公司
    - 173、良彬建材
    - 174、杭州求是同创网络科技有限公司
    - 175、荷马国际
    - 176、点雇网
    - 177、深圳市华星光电技术有限公司
    - 178、厦门神州鹰软件科技有限公司
    - 179、深圳市招商信诺人寿保险有限公司
    - 180、上海好屋网信息技术有限公司
    - 181、海信集团【海信】
    - 182、信凌可信息科技(上海)有限公司
    - 183、长春天成科技发展有限公司
    - 184、用友金融信息技术股份有限公司【用友】
    - 185、北京咖啡易融有限公司
    - 186、国投瑞银基金管理有限公司
    - 187、晋松(上海)网络信息技术有限公司
    - 188、深圳市随手科技有限公司【随手记】
    - 189、深圳水务科技有限公司
    - 190、易企秀【易企秀】
    - 191、北京磁云科技
    - 192、南京蜂泰互联网科技有限公司
    - 193、章鱼直播
    - 194、奖多多科技
    - 195、天津市神州商龙科技股份有限公司
    - 196、岩心科技
    - 197、车码科技(北京)有限公司
    - 198、贵阳市投资控股集团
    - 199、康旗股份
    - 200、龙腾出行
    - 201、杭州华量软件
    - 202、合肥顶岭医疗科技有限公司
    - 203、重庆表达式科技有限公司
    - 204、上海米道信息科技有限公司
    - 205、北京益友会科技有限公司
    - 206、北京融贯电子商务有限公司
    - 207、中国外汇交易中心
    - 208、中国外运股份有限公司
    - 209、中国上海晓圈教育科技有限公司
    - 210、普联软件股份有限公司
    - 211、北京科蓝软件股份有限公司
    - 212、江苏斯诺物联科技有限公司
    - 213、北京搜狐-狐友【搜狐】
    - 214、新大陆网商金融
    - 215、山东神码中税信息科技有限公司
    - 216、河南汇顺网络科技有限公司
    - 217、北京华夏思源科技发展有限公司
    - 218、上海东普信息科技有限公司
    - 219、上海鸣勃网络科技有限公司
    - 220、广东学苑教育发展有限公司
    - 221、深圳强时科技有限公司
    - 222、上海云砺信息科技有限公司
    - 223、重庆愉客行网络有限公司
    - 224、数云
    - 225、国家电网运检部
    - 226、杭州找趣
    - 227、浩鲸云计算科技股份有限公司
    - 228、科大讯飞【科大讯飞】
    - 229、杭州行装网络科技有限公司
    - 230、即有分期金融
    - 231、深圳法司德信息科技有限公司
    - 232、上海博复信息科技有限公司
    - 233、杭州云嘉云计算有限公司
    - 234、有家民宿(有家美宿)
    - 235、北京赢销通软件技术有限公司
    - 236、浙江聚有财金融服务外包有限公司
    - 237、易族智汇(北京)科技有限公司
    - 238、合肥顶岭医疗科技开发有限公司
    - 239、车船宝(深圳)旭珩科技有限公司)
    - 240、广州富力地产有限公司
    - 241、氢课(上海)教育科技有限公司
    - 242、武汉氪细胞网络技术有限公司
    - 243、杭州有云科技有限公司
    - 244、上海仙豆智能机器人有限公司
    - 245、拉卡拉支付股份有限公司【拉卡拉】
    - 246、虎彩印艺股份有限公司
    - 247、北京数微科技有限公司
    - 248、广东智瑞科技有限公司
    - 249、找钢网
    - 250、九机网
    - 251、杭州跑跑网络科技有限公司
    - 252、深圳未来云集
    - 253、杭州每日给力科技有限公司
    - 254、上海齐犇信息科技有限公司
    - 255、滴滴出行【滴滴】
    - 256、合肥云诊信息科技有限公司
    - 257、云知声智能科技股份有限公司
    - 258、南京坦道科技有限公司
    - 259、爱乐优(二手平台)
    - 260、猫眼电影(私有化部署)【猫眼电影】
    - 261、美团大象(私有化部署)【美团大象】
    - 262、作业帮教育科技(北京)有限公司【作业帮】
    - 263、北京小年糕互联网技术有限公司
    - 264、山东矩阵软件工程股份有限公司
    - 265、陕西国驿软件科技有限公司
    - 266、君开信息科技
    - 267、村鸟网络科技有限责任公司
    - 268、云南国际信托有限公司
    - 269、金智教育
    - 270、珠海市筑巢科技有限公司
    - 271、上海百胜软件股份有限公司
    - 272、深圳市科盾科技有限公司
    - 273、哈啰出行【哈啰】
    - 274、途虎养车【途虎】
    - 275、卡思优派人力资源集团
    - 276、南京观为智慧软件科技有限公司
    - 277、杭州城市大脑科技有限公司
    - 278、猿辅导【猿辅导】
    - 279、洛阳健创网络科技有限公司
    - 280、魔力耳朵
    - 281、亿阳信通
    - 282、上海招鲤科技有限公司
    - 283、四川商旅无忧科技服务有限公司
    - 284、UU跑腿
    - 285、北京老虎证券【老虎证券】
    - 286、悠活省吧(北京)网络科技有限公司
    - 287、F5未来商店
    - 288、深圳环阳通信息技术有限公司
    - 289、遠傳電信
    - 290、作业帮(北京)教育科技有限公司【作业帮】
    - 291、成都科鸿智信科技有限公司
    - 292、北京木屋时代科技有限公司
    - 293、大学通(哈尔滨)科技有限责任公司
    - 294、浙江华坤道威数据科技有限公司
    - 295、吉祥航空【吉祥航空】
    - 296、南京圆周网络科技有限公司
    - 297、广州市洋葱omall电子商务
    - 298、天津联物科技有限公司
    - 299、跑哪儿科技(北京)有限公司
    - 300、深圳市美西西餐饮有限公司(喜茶)
    - 301、平安不动产有限公司【平安】
    - 302、江苏中海昇物联科技有限公司
    - 303、湖南牙医帮科技有限公司
    - 304、重庆民航凯亚信息技术有限公司(易通航)
    - 305、递易(上海)智能科技有限公司
    - 306、亚朵
    - 307、浙江新课堂教育股份有限公司
    - 308、北京蜂创科技有限公司
    - 309、德一智慧城市信息系统有限公司
    - 310、北京翼点科技有限公司
    - 311、湖南智数新维度信息科技有限公司
    - 312、北京玖扬博文文化发展有限公司
    - 313、上海宇珩信息科技有限公司
    - 314、全景智联(武汉)科技有限公司
    - 315、天津易客满国际物流有限公司
    - 316、南京爱福路汽车科技有限公司
    - 317、我房旅居集团
    - 318、湛江亲邻科技有限公司
    - 319、深圳市姜科网络有限公司
    - 320、青岛日日顺物流有限公司
    - 321、南京太川信息技术有限公司
    - 322、美图之家科技优先公司【美图】
    - 323、南京太川信息技术有限公司
    - 324、众薪科技(北京)有限公司
    - 325、武汉安安物联科技有限公司
    - 326、北京智客朗道网络科技有限公司
    - 327、深圳市超级猩猩健身管理管理有限公司
    - 328、重庆达志科技有限公司
    - 329、上海享评信息科技有限公司
    - 330、薪得付信息科技
    - 331、跟谁学
    - 332、中道(苏州)旅游网络科技有限公司
    - 333、广州小卫科技有限公司
    - 334、上海非码网络科技有限公司
    - 335、途家网网络技术(北京)有限公司【途家】
    - 336、广州辉凡信息科技有限公司
    - 337、天维尔信息科技股份有限公司
    - 338、上海极豆科技有限公司
    - 339、苏州触达信息技术有限公司
    - 340、北京热云科技有限公司
    - 341、中智企服(北京)科技有限公司
    - 342、易联云计算(杭州)有限责任公司
    - 343、青岛航空股份有限公司【青岛航空】
    - 344、山西博睿通科技有限公司
    - 345、网易杭州网络有限公司【网易】
    - 346、北京果果乐学科技有限公司
    - 347、百望股份有限公司
    - 348、中保金服(深圳)科技有限公司
    - 349、天津运友物流科技股份有限公司
    - 350、广东创能科技股份有限公司
    - 351、上海倚博信息科技有限公司
    - 352、深圳百果园实业(集团)股份有限公司
    - 353、广州细刻网络科技有限公司
    - 354、武汉鸿业众创科技有限公司
    - 355、金锡科技(广州)有限公司
    - 356、易瑞国际电子商务有限公司
    - 357、奇点云
    - 358、中视信息科技有限公司
    - 359、开源项目:datax-web
    - 360、云知声智能科技股份有限公司
    - 361、开源项目:bboss
    - 362、成都深驾科技有限公司
    - 363、FunPlus【趣加】
    - 364、杭州创匠信科技有限公司
    - 365、龙匠(北京)科技发展有限公司
    - 366、广州一链通互联网科技有限公司
    - 367、上海星艾网络科技有限公司
    - 368、虎博网络技术(上海)有限公司
    - 369、青岛优米信息技术有限公司
    - 370、八维通科技有限公司
    - 371、烟台合享智星数据科技有限公司
    - 372、东吴证券股份有限公司
    - 373、中通云仓股份有限公司【中通】
    - 374、北京加菲猫科技有限公司
    - 375、北京匠心演绎科技有限公司
    - 376、宝贝走天下
    - 377、厦门众库科技有限公司
    - 378、海通证券数据中心
    - 389、湖南快乐通宝小额贷款有限公司
    - 380、浙江大华技术股份有限公司
    - 381、杭州魔筷科技有限公司
    - 382、青岛掌讯通区块链科技有限公司
    - 383、新大陆金融科技
    - 384、常州玺拓软件科技有限公司
    - 385、北京正保网格教育科技有限公司
    - 386、统一企业(中国)投资有限公司【统一】
    - 387、微革网络科技有限公司
    - 388、杭州融易算科技有限公司
    - 399、青岛上啥班网络科技有限公司
    - 390、京东酒世界
    - 391、杭州爱博仕科技有限公司
    - 392、五星金服控股有限公司
    - 393、福建乐摩物联科技有限公司
    - 394、百炼智能科技有限公司
    - 395、山东能源数智云科技有限公司
    - 396、招商局能源运输股份有限公司
    - 397、三一集团【三一】
    - 398、东巴文(深圳)健康管理有限公司
    - 399、索易软件
    - 400、深圳市宁远科技有限公司
    - 401、熙牛医疗
    - 402、南京智鹤电子科技有限公司
    - 403、嘀嗒出行【嘀嗒出行】
    - 404、广州虎牙信息科技有限公司【虎牙】
    - 405、广州欧莱雅百库网络科技有限公司【欧莱雅】
    - 406、微微科技有限公司
    - 407、我爱我家房地产经纪有限公司【我爱我家】
    - 408、九号发现
    - 409、薪人薪事
    - 410、武汉氪细胞网络技术有限公司
    - 411、广州市斯凯奇商业有限公司
    - 412、微淼商学院
    - 413、杭州车盛科技有限公司
    - 414、深兰科技(上海)有限公司
    - 415、安徽中科美络信息技术有限公司
    - 416、比亚迪汽车工业有限公司【比亚迪】
    - 417、湖南小桔信息技术有限公司
    - 418、安徽科大国创软件科技有限公司
    - 419、克而瑞
    - 420、陕西云基华海信息技术有限公司
    - 421、安徽深宁科技有限公司
    - 422、广东康爱多数字健康有限公司
    - 423、嘉里电子商务
    - 424、上海时代光华教育发展有限公司
    - 425、CityDo
    - 426、上海禹知信息科技有限公司
    - 427、广东智瑞科技有限公司
    - 428、西安爱铭网络科技有限公司
    - 429、心医国际数字医疗系统(大连)有限公司
    - 430、乐其电商
    - 431、锐达科技
    - 432、天津长城滨银汽车金融有限公司
    - 433、代码网
    - 434、东莞市东城乔伦软件开发工作室
    - 435、浙江百应科技有限公司
    - 436、上海力爱帝信息技术有限公司(Red E)
    - 437、云徙科技有限公司
    - 438、北京康智乐思网络科技有限公司【大姨吗APP】
    - 439、安徽开元瞬视科技有限公司
    - 440、立方
    - 441、厦门纵行科技
    - 442、乐山-菲尼克斯半导体有限公司
    - 443、武汉光谷联合集团有限公司
    - 444、上海金仕达软件科技有限公司
    - 445、深圳易世通达科技有限公司
    - 446、爱动超越人工智能科技(北京)有限责任公司
    - 447、迪普信(北京)科技有限公司
    - 448、掌站科技(北京)有限公司
    - 449、深圳市华云中盛股份有限公司
    - 450、上海原圈科技有限公司
    - 451、广州赞赏信息科技有限公司
    - 452、Amber Group
    - 453、德威国际货运代理(上海)公司
    - 454、浙江杰夫兄弟智慧科技有限公司
    - 455、信也科技
    - 456、开思时代科技(深圳)有限公司
    - 457、大连槐德科技有限公司
    - 458、同程生活
    - 459、松果出行
    - 460、企鹅杏仁集团
    - 461、宁波科云信息科技有限公司
    - 462、上海格蓝威驰信息科技有限公司
    - 463、杭州趣淘鲸科技有限公司
    - 464、湖州市数字惠民科技有限公司
    - 465、乐普(北京)医疗器械股份有限公司
    - 466、广州市晴川高新技术开发有限公司
    - 467、山西缇客科技有限公司
    - 468、徐州卡西穆电子商务有限公司
    - 469、格创东智科技有限公司
    - 470、世纪龙信息网络有限责任公司
    - 471、邦道科技有限公司
    - 472、河南中盟新云科技股份有限公司
    - 473、横琴人寿保险有限公司
    - 474、上海海隆华钟信息技术有限公司
    - 475、上海久湛
    - 476、上海仙豆智能机器人有限公司
    - 477、广州汇尚网络科技有限公司
    - 478、深圳市阿卡索资讯股份有限公司
    - 479、青岛佳家康健康管理有限责任公司
    - 480、蓝城兄弟
    - 481、成都天府通金融服务股份有限公司
    - 482、深圳云镖网络科技有限公司
    - 483、上海影创科技
    - 484、成都艾拉物联
    - 485、北京客邻尚品网络技术有限公司
    - 486、IT实战联盟
    - 487、杭州尤拉夫科技有限公司
    - 488、中大检测(湖南)股份有限公司
    - 489、江苏电老虎工业互联网股份有限公司
    - 490、上海助通信息科技有限公司
    - 491、北京符节科技有限公司
    - 492、杭州英祐科技有限公司
    - 493、江苏电老虎工业互联网股份有限公司
    - 494、深圳市点猫科技有限公司
    - 495、杭州天音
    - 496、深圳市二十一科技互联网有限公司
    - 497、海南海口翎度科技
    - 498、北京小趣智品科技有限公司
    - 499、广州石竹计算机软件有限公司
    - 500、深圳市惟客数据科技有限公司
    - 501、中国医疗器械有限公司
    - 502、上海云谦科技有限公司
    - 503、上海磐农信息科技有限公司
    - 504、广州领航食品有限公司
    - 505、青岛掌讯通区块链科技有限公司
    - 506、北京新网数码信息技术有限公司
    - 507、超体信息科技(深圳)有限公司
    - 508、长沙店帮手信息科技有限公司
    - 509、上海助弓装饰工程有限公司
    - 510、杭州寻联网络科技有限公司
    - 511、成都大淘客科技有限公司
    - 512、松果出行
    - 513、深圳市唤梦科技有限公司
    - 514、上汽集团商用车技术中心
    - 515、北京中航讯科技股份有限公司
    - 516、北龙中网(北京)科技有限责任公司
    - 517、前海超级前台(深圳)信息技术有限公司
    - 518、上海中商网络股份有限公司
    - 519、上海助通信息科技有限公司
    - 520、宁波聚臻智能科技有限公司
    - 521、上海零动数码科技股份有限公司
    - 522、浙江学海教育科技有限公司
    - 523、聚学云(山东)信息技术有限公司
    - 524、多氟多新材料股份有限公司
    - 525、智慧眼科技股份有限公司
    - 526、广东智通人才连锁股份有限公司
    - 527、世纪开元智印互联科技集团股份有限公司
    - 528、北京理想汽车【理想汽车】
    - 529、巽逸科技(重庆)有限公司
    - 530、义乌购电子商务有限公司
    - 531、深圳市珂莱蒂尔服饰有限公司
    - 532、江西国泰利民信息科技有限公司
    - 533、广西广电大数据科技有限公司
    - 534、杭州艾麦科技有限公司
    - 535、广州小滴科技有限公司
    - 536、佳缘科技股份有限公司
    - 537、上海深擎信息科技有限公司
    - 538、武商网
    - 539、福建民本信息科技有限公司
    - 540、杭州惠合信息科技有限公司
    - 541、厦门爱立得科技有限公司
    - 542、成都拟合未来科技有限公司
    - 543、宁波聚臻智能科技有限公司
    - 544、广东百慧科技有限公司
    - 545、笨马网络
    - 546、深圳市信安数字科技有限公司
    - 547、深圳市思乐数据技术有限公司
    - 548、四川绿源集科技有限公司
    - 549、湖南云医链生物科技有限公司
    - 550、杭州源诚科技有限公司
    - 551、北京开课吧科技有限公司
    - 552、北京多来点信息技术有限公司
    - 553、JEECG BOOT低代码开发平台
    - 554、苏州同元软控信息技术有限公司
    - 555、江苏大泰信息技术有限公司
    - 556、北京大禹汇智
    - 557、北京盛哲科技有限公司
    - ……
> 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。
欢迎大家的关注和使用,XXL-JOB也将拥抱变化,持续发展。
### 1.5 下载
#### 文档地址
- [中文文档](https://www.xuxueli.com/xxl-job/)
- [English Documentation](https://www.xuxueli.com/xxl-job/en/)
#### 源码仓库地址
源码仓库地址 | Release Download
--- | ---
[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job) | [Download](https://github.com/xuxueli/xxl-job/releases)
[http://gitee.com/xuxueli0323/xxl-job](http://gitee.com/xuxueli0323/xxl-job) | [Download](http://gitee.com/xuxueli0323/xxl-job/releases)
#### 中央仓库地址
```
<!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>${最新稳定版本}</version>
</dependency>
```
### 1.6 环境
- Maven3+
- Jdk1.8+
- Mysql5.7+
## 二、快速入门
### 2.1 初始化“调度数据库”
请下载项目源码并解压,获取 "调度数据库初始化SQL脚本" 并执行即可。
"调度数据库初始化SQL脚本" 位置为:
    /xxl-job/doc/db/tables_xxl_job.sql
调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;
如果mysql做主从,调度中心集群节点务必强制走主库;
### 2.2 编译源码
解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可,源码结构如下:
    xxl-job-admin:调度中心
    xxl-job-core:公共依赖
    xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器)
        :xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式;
        :xxl-job-executor-sample-frameless:无框架版本;
### 2.3 配置部署“调度中心”
    调度中心项目:xxl-job-admin
    作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
#### 步骤一:调度中心配置:
调度中心配置文件地址:
    /xxl-job/xxl-job-admin/src/main/resources/application.properties
调度中心配置内容说明:
    ### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    spring.datasource.username=root
    spring.datasource.password=root_pwd
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    ### 报警邮箱
    spring.mail.host=smtp.qq.com
    spring.mail.port=25
    spring.mail.username=xxx@qq.com
    spring.mail.password=xxx
    spring.mail.properties.mail.smtp.auth=true
    spring.mail.properties.mail.smtp.starttls.enable=true
    spring.mail.properties.mail.smtp.starttls.required=true
    spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
    ### 调度中心通讯TOKEN [选填]:非空时启用;
    xxl.job.accessToken=
    ### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
    xxl.job.i18n=zh_CN
    ## 调度线程池最大线程配置【必填】
    xxl.job.triggerpool.fast.max=200
    xxl.job.triggerpool.slow.max=100
    ### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
    xxl.job.logretentiondays=30
#### 步骤二:部署项目:
如果已经正确进行上述配置,可将项目编译打包部署。
调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
默认登录账号 "admin/123456", 登录后运行界面如下图所示。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_6yC0.png "在这里输入图片标题")
至此“调度中心”项目已经部署成功。
#### 步骤三:调度中心集群(可选):
调度中心支持集群部署,提升调度系统容灾和可用性。
调度中心集群部署时,几点要求和建议:
- DB配置保持一致;
- 集群机器时钟保持一致(单机集群忽视);
- 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。
#### 其他:Docker 镜像方式搭建调度中心:
- 下载镜像
```
// Docker地址:https://hub.docker.com/r/xuxueli/xxl-job-admin/     (建议指定版本号)
docker pull xuxueli/xxl-job-admin
```
- 创建容器并运行
```
docker run -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin  -d xuxueli/xxl-job-admin:{指定版本}
/**
* 如需自定义 mysql 等配置,可通过 "-e PARAMS" 指定,参数格式 PARAMS="--key=value  --key2=value2" ;
* 配置项参考文件:/xxl-job/xxl-job-admin/src/main/resources/application.properties
* 如需自定义 JVM内存参数 等配置,可通过 "-e JAVA_OPTS" 指定,参数格式 JAVA_OPTS="-Xmx512m" ;
*/
docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin  -d xuxueli/xxl-job-admin:{指定版本}
```
### 2.4 配置部署“执行器项目”
    “执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例,可直接使用,也可以参考其并将现有项目改造成执行器)
    作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。
#### 步骤一:maven依赖
确认pom文件中引入了 "xxl-job-core" 的maven依赖;
#### 步骤二:执行器配置
执行器配置,配置文件地址:
    /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
执行器配置,配置内容说明:
    ### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
    xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
    ### 执行器通讯TOKEN [选填]:非空时启用;
    xxl.job.accessToken=
    ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
    xxl.job.executor.appname=xxl-job-executor-sample
    ### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
    xxl.job.executor.address=
    ### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
    xxl.job.executor.ip=
    ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
    xxl.job.executor.port=9999
    ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
    xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
    ### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
    xxl.job.executor.logretentiondays=30
#### 步骤三:执行器组件配置
执行器组件,配置文件地址:
    /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java
执行器组件,配置内容说明:
```
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    logger.info(">>>>>>>>>>> xxl-job config init.");
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appname);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
    return xxlJobSpringExecutor;
}
```
#### 步骤四:部署执行器项目:
如果已经正确进行上述配置,可将执行器项目编译打部署,系统提供多种执行器Sample示例项目,选择其中一个即可,各自的部署方式如下。
    xxl-job-executor-sample-springboot:项目编译打包成springboot类型的可执行JAR包,命令启动即可;
    xxl-job-executor-sample-frameless:项目编译打包成JAR包,命令启动即可;
至此“执行器”项目已经部署结束。
#### 步骤五:执行器集群(可选):
执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。
执行器集群部署时,几点要求和建议:
- 执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。
- 同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。
### 2.5 开发第一个任务“Hello World”
本示例以新建一个 “GLUE模式(Java)” 运行模式的任务为例。更多有关任务的详细配置,请查看“章节三:任务详解”。
( “GLUE模式(Java)”的执行代码托管到调度中心在线维护,相比“Bean模式任务”需要在执行器项目开发部署上线,更加简便轻量)
> 前提:请确认“调度中心”和“执行器”项目已经成功部署并启动;
#### 步骤一:新建任务:
登录调度中心,点击下图所示“新建任务”按钮,新建示例任务。然后,参考下面截图中任务的参数配置,点击保存。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_o8HQ.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题")
#### 步骤二:“GLUE模式(Java)” 任务开发:
请点击任务右侧 “GLUE” 按钮,进入 “GLUE编辑器开发界面” ,见下图。“GLUE模式(Java)” 运行模式的任务默认已经初始化了示例任务代码,即打印Hello World。
( “GLUE模式(Java)” 运行模式的任务实际上是一段继承自IJobHandler的Java类代码,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务,详细介绍请查看第三章节)
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Fgql.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题")
#### 步骤三:触发执行:
请点击任务右侧 “执行” 按钮,可手动触发一次任务执行(通常情况下,通过配置Cron表达式进行任务调度触发)。
#### 步骤四:查看日志:
请点击任务右侧 “日志” 按钮,可前往任务日志界面查看任务日志。
在任务日志界面中,可查看该任务的历史调度记录以及每一次调度的任务调度信息、执行参数和执行信息。运行中的任务点击右侧的“执行日志”按钮,可进入日志控制台查看实时执行日志。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题")
在日志控制台,可以Rolling方式实时查看任务在执行器一侧运行输出的日志信息,实时监控任务进度;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_eYrv.png "在这里输入图片标题")
## 三、任务详解
### 配置属性详细说明:
    基础配置:
        - 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置;
        - 任务描述:任务的描述信息,便于任务管理;
        - 负责人:任务的负责人;
        - 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔;
    触发配置:
        - 调度类型:
            无:该类型不会主动触发调度;
            CRON:该类型将会通过CRON,触发任务调度;
            固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发;
            固定延迟:该类型将会以固定延迟,触发任务调度;按照固定的延迟时间,从上次调度结束后开始计算延迟时间,到达延迟时间后触发下次调度;
        - CRON:触发任务执行的Cron表达式;
        - 固定速度:固件速度的时间间隔,单位为秒;
        - 固定延迟:固件延迟的时间间隔,单位为秒;
    任务配置:
        - 运行模式:
            BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
            GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
            GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本;
            GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本;
            GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本;
            GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本;
            GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本;
        - JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
        - 执行参数:任务执行所需的参数;
    高级配置:
        - 路由策略:当执行器集群部署时,提供丰富的路由策略,包括;
            FIRST(第一个):固定选择第一个机器;
            LAST(最后一个):固定选择最后一个机器;
            ROUND(轮询):;
            RANDOM(随机):随机选择在线的机器;
            CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
            LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
            LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
            FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
            BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
            SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
        - 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。
        - 调度过期策略:
            - 忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间;
            - 立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间;
        - 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;
            单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
            丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
            覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
        - 任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
        - 失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
### 3.1 BEAN模式(类形式)
Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。
- 优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持,可以参考示例项目 "xxl-job-executor-sample-frameless";
- 缺点:
    - 每个任务需要占用一个Java类,造成类的浪费;
    - 不支持自动扫描任务并注入到执行器容器,需要手动注入。
#### 步骤一:执行器项目中,开发Job类:
    1、开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。
    2、手动通过如下方式注入到执行器容器。
    ```
    XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());
    ```
#### 步骤二:调度中心,新建调度任务
后续步骤和 "3.2 BEAN模式(方法形式)"一致,可以前往参考。
### 3.2 BEAN模式(方法形式)
Bean模式任务,支持基于方法的开发方式,每个任务对应一个方法。
- 优点:
    - 每个任务只需要开发一个方法,并添加"@XxlJob"注解即可,更加方便、快速。
    - 支持自动扫描任务并注入到执行器容器。
- 缺点:要求Spring容器环境;
>基于方法开发的任务,底层会生成JobHandler代理,和基于类的方式一样,任务也会以JobHandler的形式存在于执行器任务容器中。
#### 步骤一:执行器项目中,开发Job方法:
    1、任务开发:在Spring Bean实例中,开发Job方法;
    2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
    3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
    4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
```
// 可参考Sample示例执行器中的 "com.xxl.job.executor.service.jobhandler.SampleXxlJob" ,如下:
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
    XxlJobHelper.log("XXL-JOB, Hello World.");
}
```
#### 步骤二:调度中心,新建调度任务
参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "BEAN模式",JobHandler属性填写任务注解“@XxlJob”中定义的值;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题")
#### 原生内置Bean模式任务
为方便用户参考与快速实用,示例执行器内原生提供多个Bean模式任务Handler,可以直接配置实用,如下:
- demoJobHandler:简单示例任务,任务内部模拟耗时任务逻辑,用户可在线体验Rolling Log等功能;
- shardingJobHandler:分片示例任务,任务内部模拟处理分片参数,可参考熟悉分片任务;
- httpJobHandler:通用HTTP任务Handler;业务方只需要提供HTTP链接等信息即可,不限制语言、平台。示例任务入参如下:
    ```
    url: http://www.xxx.com
    method: get 或 post
    data: post-data
    ```
- commandJobHandler:通用命令行任务Handler;业务方只需要提供命令行即可;如 “pwd”命令;
### 3.3 GLUE模式(Java)
任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。开发流程如下:
#### 步骤一:调度中心,新建调度任务:
参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Java)";
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tJOq.png "在这里输入图片标题")
#### 步骤二:开发任务代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。
版本回溯功能(支持30个版本的版本回溯):在GLUE任务的Web IDE界面,选择右上角下拉框“版本回溯”,会列出该GLUE的更新历史,选择相应版本即可显示该版本代码,保存后GLUE代码即回退到对应的历史版本;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题")
### 3.4 GLUE模式(Shell)
#### 步骤一:调度中心,新建调度任务
参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Shell)";
#### 步骤二:开发任务代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。
该模式的任务实际上是一段 "shell" 脚本;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_iUw0.png "在这里输入图片标题")
### 3.4 GLUE模式(Python)
#### 步骤一:调度中心,新建调度任务
参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Python)";
#### 步骤二:开发任务代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。
该模式的任务实际上是一段 "python" 脚本;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_BPLG.png "在这里输入图片标题")
### 3.5 GLUE模式(NodeJS)
#### 步骤一:调度中心,新建调度任务
参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(NodeJS)";
#### 步骤二:开发任务代码:
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。
该模式的任务实际上是一段 "nodeJS" 脚本;
### 3.6 GLUE模式(PHP)
同上
### 3.7 GLUE模式(PowerShell)
同上
## 四、操作指南
### 4.1 配置执行器
点击进入"执行器管理"界面, 如下图:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Hr2T.png "在这里输入图片标题")
    1、"调度中心OnLine:"右侧显示在线的"调度中心"列表, 任务执行结束后, 将会以failover的模式进行回调调度中心通知执行结果, 避免回调的单点风险;
    2、"执行器列表" 中显示在线的执行器列表, 可通过"OnLine 机器"查看对应执行器的集群机器。
点击按钮 "+新增执行器" 弹框如下图, 可新增执行器配置:
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_V3vF.png "在这里输入图片标题")
执行器属性说明
    AppName: 是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用;
    名称: 执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性;
    排序: 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表;
    注册方式:调度中心获取执行器地址的方式;
        自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址;
        手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用;
    机器地址:"注册方式"为"手动录入"时有效,支持人工维护执行器的地址信息;
### 4.2 新建任务
进入任务管理界面,点击“新增任务”按钮,在弹出的“新增任务”界面配置任务属性后保存即可。详情页参考章节 "三、任务详解"。
### 4.3 编辑任务
进入任务管理界面,选中指定任务。点击该任务右侧“编辑”按钮,在弹出的“编辑任务”界面更新任务属性后保存即可,可以修改设置的任务属性信息:
### 4.4 编辑GLUE代码
该操作仅针对GLUE任务。
选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发。可参考章节 "3.3 GLUE模式(Java)"。
### 4.5 启动/停止任务
可对任务进行“启动”和“停止”操作。
需要注意的是,此处的启动/停止仅针对任务的后续调度触发行为,不会影响到已经触发的调度任务,如需终止已经触发的调度任务,可查看“4.9 终止运行中的任务”
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题")
### 4.6 手动触发一次调度
点击“执行”按钮,可手动触发一次任务调度,不影响原有调度规则。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题")
### 4.7 查看调度日志
点击“日志”按钮,可以查看任务历史调度日志。在历史调入日志界面可查看每次任务调度的调度结果、执行结果等,点击“执行日志”按钮可查看执行器完整日志。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_UDSo.png "在这里输入图片标题")
    调度时间:"调度中心"触发本次调度并向"执行器"发送任务执行信号的时间;
    调度结果:"调度中心"触发本次调度的结果,200表示成功,500或其他表示失败;
    调度备注:"调度中心"触发本次调度的日志信息;
    执行器地址:本次任务执行的机器地址
    运行模式:触发调度时任务的运行模式,运行模式可参考章节 "三、任务详解";
    任务参数:本地任务执行的入参
    执行时间:"执行器"中本次任务执行结束后回调的时间;
    执行结果:"执行器"中本次任务执行的结果,200表示成功,500或其他表示失败;
    执行备注:"执行器"中本次任务执行的日志信息;
    操作:
        "执行日志"按钮:点击可查看本地任务执行的详细日志信息;详见“4.8 查看执行日志”;
        "终止任务"按钮:点击可终止本地调度对应执行器上本任务的执行线程,包括未执行的阻塞任务一并被终止;
### 4.8 查看执行日志
点击执行日志右侧的 “执行日志” 按钮,可跳转至执行日志界面,可以查看业务代码中打印的完整日志,如下图;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tvGI.png "在这里输入图片标题")
### 4.9 终止运行中的任务
仅针对执行中的任务。
在任务日志界面,点击右侧的“终止任务”按钮,将会向本次任务对应的执行器发送任务终止请求,将会终止掉本次任务,同时会清空掉整个任务执行队列。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_hIci.png "在这里输入图片标题")
任务终止时通过 "interrupt" 执行线程的方式实现, 将会触发 "InterruptedException" 异常。因此如果JobHandler内部catch到了该异常并消化掉的话, 任务终止功能将不可用。
因此, 如果遇到上述任务终止不可用的情况, 需要在JobHandler中应该针对 "InterruptedException" 异常进行特殊处理 (向上抛出) , 正确逻辑如下:
```
try{
    // do something
} catch (Exception e) {
    if (e instanceof InterruptedException) {
        throw e;
    }
    logger.warn("{}", e);
}
```
而且,在JobHandler中开启子线程时,子线程也不可catch处理"InterruptedException",应该主动向上抛出。
任务终止时会执行对应JobHandler的"destroy()"方法,可以借助该方法处理一些资源回收的逻辑。
### 4.10 删除执行日志
在任务日志界面,选中执行器和任务之后,点击右侧的"删除"按钮将会出现"日志清理"弹框,弹框中支持选择不同类型的日志清理策略,选中后点击"确定"按钮即可进行日志清理操作;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Ypik.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_EB65.png "在这里输入图片标题")
### 4.11 删除任务
点击删除按钮,可以删除对应任务。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z9Qr.png "在这里输入图片标题")
### 4.12 用户管理
进入 "用户管理" 界面,可查看和管理用户信息;
目前用户分为两种角色:
- 管理员:拥有全量权限,支持在线管理用户信息,为用户分配权限,权限分配粒度为执行器;
- 普通用户:仅拥有被分配权限的执行器,及相关任务的操作权限;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_1001.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_1002.png "在这里输入图片标题")
## 五、总体设计
### 5.1 源码目录介绍
    - /doc :文档资料
    - /db :“调度数据库”建表脚本
    - /xxl-job-admin :调度中心,项目源码
    - /xxl-job-core :公共Jar依赖
    - /xxl-job-executor-samples :执行器,Sample示例项目(大家可以在该项目上进行开发,也可以将现有项目改造生成执行器项目)
### 5.2 “调度数据库”配置
XXL-JOB调度模块基于自研调度组件并支持集群部署,调度数据库表说明如下:
    - xxl_job_lock:任务调度锁表;
    - xxl_job_group:执行器信息表,维护任务执行器信息;
    - xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
    - xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
    - xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
    - xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
    - xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
    - xxl_job_user:系统用户表;
### 5.3 架构设计
#### 5.3.1 设计思想
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
#### 5.3.2 系统组成
- **调度模块(调度中心)**:
    负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
    支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
- **执行模块(执行器)**:
    负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
    接收“调度中心”的执行请求、终止请求和日志请求等。
#### 5.3.3 架构图
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png "在这里输入图片标题")
### 5.4 调度模块剖析
#### 5.4.1 quartz的不足
Quartz作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中Quartz采用API的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题:
- 问题一:调用API的的方式操作任务,不人性化;
- 问题二:需要持久化业务QuartzJobBean到底层数据表中,系统侵入性相当严重。
- 问题三:调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务;
- 问题四:quartz底层以“抢占式”获取DB锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而XXL-JOB通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。
XXL-JOB弥补了quartz的上述不足之处。
#### 5.4.2 自研调度模块
XXL-JOB最终选择自研调度组件(早期调度组件基于Quartz);一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性;
XXL-JOB中“调度模块”和“任务模块”完全解耦,调度模块进行任务调度时,将会解析不同的任务参数发起远程调用,调用各自的远程执行器服务。这种调用模型类似RPC调用,调度中心提供调用代理的功能,而执行器提供远程服务的功能。
#### 5.4.3 调度中心HA(集群)
基于数据库的集群方案,数据库选用Mysql;集群分布式并发环境中进行定时任务调度时,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。
#### 5.4.4 调度线程池
调度采用线程池方式实现,避免单线程因阻塞而引起任务调度延迟。
#### 5.4.5 并行调度
XXL-JOB调度模块默认采用并行机制,在多线程调度的情况下,调度模块被阻塞的几率很低,大大提高了调度系统的承载量。
XXL-JOB的不同任务之间并行调度、并行执行。
XXL-JOB的单个任务,针对多个执行器是并行运行的,针对单个执行器是串行执行的。同时支持任务终止。
#### 5.4.6 过期处理策略
任务调度错过触发时间时的处理策略:
- 可能原因:服务重启;调度线程被阻塞,线程被耗尽;上次调度持续阻塞,下次调度被错过;
- 处理策略:
    - 过期超5s:本次忽略,当前时间开始计算下次触发时间
    - 过期5s内:立即触发一次,当前时间开始计算下次触发时间
#### 5.4.7 日志回调服务
调度模块的“调度中心”作为Web服务部署时,一方面承担调度中心功能,另一方面也为执行器提供API服务。
调度中心提供的"日志回调服务API服务"代码位置如下:
```
xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback
```
“执行器”在接收到任务执行请求后,执行任务,在执行结束之后会将执行结果回调通知“调度中心”:
#### 5.4.8 任务HA(Failover)
执行器如若集群部署,调度中心将会感知到在线的所有执行器,如“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”。
当任务"路由策略"选择"故障转移(FAILOVER)"时,当调度中心每次发起调度请求时,会按照顺序对执行器发出心跳检测请求,第一个检测为存活状态的执行器将会被选定并发送调度请求。
调度成功后,可在日志监控界面查看“调度备注”,如下;
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jrdI.png "在这里输入图片标题")
“调度备注”可以看出本地调度运行轨迹,执行器的"注册方式"、"地址列表"和任务的"路由策略"。"故障转移(FAILOVER)"路由策略下,调度中心首先对第一个地址进行心跳检测,心跳失败因此自动跳过,第二个依然心跳检测失败……
直至心跳检测第三个地址“127.0.0.1:9999”成功,选定为“目标执行器”;然后对“目标执行器”发送调度请求,调度流程结束,等待执行器回调执行结果。
#### 5.4.9 调度日志
调度中心每次进行任务调度,都会记录一条任务日志,任务日志主要包括以下三部分内容:
- 任务信息:包括“执行器地址”、“JobHandler”和“执行参数”等属性,点击任务ID按钮可查看,根据这些参数,可以精确的定位任务执行的具体机器和任务代码;
- 调度信息:包括“调度时间”、“调度结果”和“调度日志”等,根据这些参数,可以了解“调度中心”发起调度请求时具体情况。
- 执行信息:包括“执行时间”、“执行结果”和“执行日志”等,根据这些参数,可以了解在“执行器”端任务执行的具体情况;
调度日志,针对单次调度,属性说明如下:
- 执行器地址:任务执行的机器地址;
- JobHandler:Bean模式表示任务执行的JobHandler名称;
- 任务参数:任务执行的入参;
- 调度时间:调度中心,发起调度的时间;
- 调度结果:调度中心,发起调度的结果,SUCCESS或FAIL;
- 调度备注:调度中心,发起调度的备注信息,如地址心跳检测日志等;
- 执行时间:执行器,任务执行结束后回调的时间;
- 执行结果:执行器,任务执行的结果,SUCCESS或FAIL;
- 执行备注:执行器,任务执行的备注信息,如异常日志等;
- 执行日志:任务执行过程中,业务代码中打印的完整执行日志,见“4.8 查看执行日志”;
#### 5.4.10 任务依赖
原理:XXL-JOB中每个任务都对应有一个任务ID,同时,每个任务支持设置属性“子任务ID”,因此,通过“任务ID”可以匹配任务依赖关系。
当父任务执行结束并且执行成功时,将会根据“子任务ID”匹配子任务依赖,如果匹配到子任务,将会主动触发一次子任务的执行。
在任务日志界面,点击任务的“执行备注”的“查看”按钮,可以看到匹配子任务以及触发子任务执行的日志信息,如无信息则表示未触发子任务执行,可参考下图。
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Wb2o.png "在这里输入图片标题")
![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jOAU.png "在这里输入图片标题")
#### 5.4.11  全异步化 & 轻量级
- 全异步化设计:XXL-JOB系统中业务逻辑在远程执行器执行,触发流程全异步化设计。相比直接在调度中心内部执行业务逻辑,极大的降低了调度线程占用时间;
    - 异步调度:调度中心每次任务触发时仅发送一次调度请求,该调度请求首先推送“异步调度队列”,然后异步推送给远程执行器
    - 异步执行:执行器会将请求存入“异步执行队列”并且立即响应调度中心,异步运行。
- 轻量级设计:XXL-JOB调度中心中每个JOB逻辑非常 “轻”,在全异步化的基础上,单个JOB一次运行平均耗时基本在 "10ms" 之内(基本为一次请求的网络开销);因此,可以保证使用有限的线程支撑大量的JOB并发运行;
得益于上述两点优化,理论上默认配置下的调度中心,单机能够支撑 5000 任务并发运行稳定运行;
实际场景中,由于调度中心与执行器网络ping延迟不同、DB读写耗时不同、任务调度密集程度不同,会导致任务量上限会上下波动。
如若需要支撑更多的任务量,可以通过 "调大调度线程数" 、"降低调度中心与执行器ping延迟" 和 "提升机器配置" 几种方式优化。
#### 5.4.12 均衡调度
调度中心在集群部署时会自动进行任务平均分配,触发组件每次获取与线程池数量(调度中心支持自定义调度线程池大小)相关数量的任务,避免大量任务集中在单个调度中心集群节点;
### 5.5 任务 "运行模式" 剖析
#### 5.5.1 "Bean模式" 任务
开发步骤:可参考 "章节三" ;
原理:每个Bean模式任务都是一个Spring的Bean类实例,它被维护在“执行器”项目的Spring容器中。任务类需要加“@JobHandler(value="名称")”注解,因为“执行器”会根据该注解识别Spring容器中的任务。任务类需要继承统一接口“IJobHandler”,任务逻辑在execute方法中开发,因为“执行器”在接收到调度中心的调度请求时,将会调用“IJobHandler”的execute方法,执行任务逻辑。
#### 5.5.2 "GLUE模式(Java)" 任务
开发步骤:可参考 "章节三" ;
原理:每个 "GLUE模式(Java)" 任务的代码,实际上是“一个继承自“IJobHandler”的实现类的类代码”,“执行器”接收到“调度中心”的调度请求时,会通过Groovy类加载器加载此代码,实例化成Java对象,同时注入此代码中声明的Spring服务(请确保Glue代码中的服务和类引用在“执行器”项目中存在),然后调用该对象的execute方法,执行任务逻辑。
#### 5.5.3 GLUE模式(Shell) + GLUE模式(Python) + GLUE模式(PHP) + GLUE模式(NodeJS) + GLUE模式(Powershell)
开发步骤:可参考 "章节三" ;
原理:脚本任务的源码托管在调度中心,脚本逻辑在执行器运行。当触发脚本任务时,执行器会加载脚本源码在执行器机器上生成一份脚本文件,然后通过Java代码调用该脚本;并且实时将脚本输出日志写到任务日志文件中,从而在调度中心可以实时监控脚本运行情况;
目前支持的脚本类型如下:
    - shell脚本:任务运行模式选择为 "GLUE模式(Shell)"时支持 "Shell" 脚本任务;
    - python脚本:任务运行模式选择为 "GLUE模式(Python)"时支持 "Python" 脚本任务;
    - php脚本:任务运行模式选择为 "GLUE模式(PHP)"时支持 "PHP" 脚本任务;
    - nodejs脚本:任务运行模式选择为 "GLUE模式(NodeJS)"时支持 "NodeJS" 脚本任务;
    - powershell:任务运行模式选择为 "GLUE模式(PowerShell)"时支持 "PowerShell" 脚本任务;
脚本任务通过 Exit Code 判断任务执行结果,状态码可参考章节 "5.15 任务执行结果说明";
#### 5.5.4 执行器
执行器实际上是一个内嵌的Server,默认端口9999(配置项:xxl.job.executor.port)。
在项目启动时,执行器会通过“@JobHandler”识别Spring容器中“Bean模式任务”,以注解的value属性为key管理起来。
“执行器”接收到“调度中心”的调度请求时,如果任务类型为“Bean模式”,将会匹配Spring容器中的“Bean模式任务”,然后调用其execute方法,执行任务逻辑。如果任务类型为“GLUE模式”,将会加载GLue代码,实例化Java对象,注入依赖的Spring服务(注意:Glue代码中注入的Spring服务,必须存在与该“执行器”项目的Spring容器中),然后调用execute方法,执行任务逻辑。
#### 5.5.5 任务日志
XXL-JOB会为每次调度请求生成一个单独的日志文件,需要通过 "XxlJobHelper.log" 打印执行日志,“调度中心”查看执行日志时将会加载对应的日志文件。
(历史版本通过重写LOG4J的Appender实现,存在依赖限制,该方式在新版本已经被抛弃)
日志文件存放的位置可在“执行器”配置文件进行自定义,默认目录格式为:/data/applogs/xxl-job/jobhandler/“格式化日期”/“数据库调度日志记录的主键ID.log”。
在JobHandler中开启子线程时,子线程将会将会把日志打印在父线程即JobHandler的执行日志中,方便日志追踪。
### 5.6 通讯模块剖析
#### 5.6.1 一次完整的任务调度通讯流程
    - 1、“调度中心”向“执行器”发送http调度请求: “执行器”中接收请求的服务,实际上是一台内嵌Server,默认端口9999;
    - 2、“执行器”执行任务逻辑;
    - 3、“执行器”http回调“调度中心”调度结果: “调度中心”中接收回调的服务,是针对执行器开放一套API服务;
#### 5.6.2 通讯数据加密
调度中心向执行器发送的调度请求时使用RequestModel和ResponseModel两个对象封装调度请求参数和响应数据, 在进行通讯之前底层会将上述两个对象对象序列化,并进行数据协议以及时间戳检验,从而达到数据加密的功能;
### 5.7 任务注册, 任务自动发现
自v1.5版本之后, 任务取消了"任务执行机器"属性, 改为通过任务注册和自动发现的方式, 动态获取远程执行器地址并执行。
    AppName: 每个执行器机器集群的唯一标示, 任务注册以 "执行器" 为最小粒度进行注册; 每个任务通过其绑定的执行器可感知对应的执行器机器列表;
    注册表: 见"xxl_job_registry"表, "执行器" 在进行任务注册时将会周期性维护一条注册记录,即机器地址和AppName的绑定关系; "调度中心" 从而可以动态感知每个AppName在线的机器列表;
    执行器注册: 任务注册Beat周期默认30s; 执行器以一倍Beat进行执行器注册, 调度中心以一倍Beat进行动态任务发现; 注册信息的失效时间为三倍Beat;
    执行器注册摘除:执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性;
为保证系统"轻量级"并且降低学习部署成本,没有采用Zookeeper作为注册中心,采用DB方式进行任务注册发现;
### 5.8 任务执行结果
自v1.6.2之后,任务执行结果通过 "IJobHandler" 的返回值 "ReturnT" 进行判断;
当返回值符合 "ReturnT.code == ReturnT.SUCCESS_CODE" 时表示任务执行成功,否则表示任务执行失败,而且可以通过 "ReturnT.msg" 回调错误信息给调度中心;
从而,在任务逻辑中可以方便的控制任务执行结果;
### 5.9 分片广播 & 动态分片
执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
"分片广播" 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
"分片广播" 和普通任务开发流程一致,不同之处在于可以获取分片参数,获取分片参数进行分片业务处理。
- Java语言任务获取分片参数方式:BEAN、GLUE模式(Java)
```
// 可参考Sample示例执行器中的示例任务"ShardingJobHandler"了解试用
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
```
- 脚本语言任务获取分片参数方式:GLUE模式(Shell)、GLUE模式(Python)、GLUE模式(Nodejs)
```
// 脚本任务入参固定为三个,依次为:任务传参、分片序号、分片总数。以Shell模式任务为例,获取分片参数代码如下
echo "分片序号 index = $2"
echo "分片总数 total = $3"
```
分片参数属性说明:
    index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号;
    total:总分片数,执行器集群的总机器数量;
该特性适用场景如:
- 1、分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;
- 2、广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等
### 5.10 访问令牌(AccessToken)
为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯;
调度中心和执行器,可通过配置项 "xxl.job.accessToken" 进行AccessToken的设置。
调度中心和执行器,如果需要正常通讯,只有两种设置;
- 设置一:调度中心和执行器,均不设置AccessToken;关闭安全性校验;
- 设置二:调度中心和执行器,设置了相同的AccessToken;
### 5.11 故障转移 & 失败重试
一次完整任务流程包括"调度(调度中心) + 执行(执行器)"两个阶段。
- "故障转移"发生在调度阶段,在执行器集群部署时,如果某一台执行器发生故障,该策略支持自动进行Failover切换到一台正常的执行器机器并且完成调度请求流程。
- "失败重试"发生在"调度 + 执行"两个阶段,支持通过自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
### 5.12 执行器灰度上线
调度中心与业务解耦,只需部署一次后常年不需要维护。但是,执行器中托管运行着业务作业,作业上线和变更需要重启执行器,尤其是Bean模式任务。
执行器重启可能会中断运行中的任务。但是,XXL-JOB得益于自建执行器与自建注册中心,可以通过灰度上线的方式,避免因重启导致的任务中断的问题。
步骤如下:
- 1、执行器改为手动注册,下线一半机器列表(A组),线上运行另一半机器列表(B组);
- 2、等待A组机器任务运行结束并编译上线;执行器注册地址替换为A组;
- 3、等待B组机器任务运行结束并编译上线;执行器注册地址替换为A组+B组;
操作结束;
### 5.13 任务执行结果说明
系统根据以下标准判断任务执行结果,可参考之。
-- | Bean/Glue(Java) | Glue(Shell) 等脚本任务
--- | --- | ---
成功 | IJobHandler.SUCCESS | 0
失败 | IJobHandler.FAIL | -1(非0状态码)
### 5.14 任务超时控制
支持设置任务超时时间,任务运行超时的情况下,将会主动中断任务;
需要注意的是,任务超时中断时与任务终止机制(可查看“4.9 终止运行中的任务”)类似,也是通过 "interrupt" 中断任务,因此业务代码需要将 "InterruptedException" 外抛,否则功能不可用。
### 5.15 跨语言
XXL-JOB是一个跨语言的任务调度平台,主要体现在如下几个方面:
- 1、RESTful API:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。(可参考章节 “调度中心/执行器 RESTful API” )
- 2、多任务模式:提供Java、Python、PHP……等十来种任务模式,可参考章节 “5.5 任务 "运行模式" ”;理论上可扩展任意语言任务模式;
- 2、提供基于HTTP的任务Handler(Bean任务,JobHandler="httpJobHandler");业务方只需要提供HTTP链接等相关信息即可,不限制语言、平台;(可参考章节 “原生内置Bean模式任务” )
### 5.16 任务失败告警
默认提供邮件失败告警,可扩展短信、钉钉等方式。如果需要新增一种告警方式,只需要新增一个实现 "com.xxl.job.admin.core.alarm.JobAlarm" 接口的告警实现即可。可以参考默认提供邮箱告警实现 "EmailJobAlarm"。
### 5.17 调度中心Docker镜像构建
可以通过以下命令快速构建调度中心,并启动运行;
```
mvn clean package
docker build -t xuxueli/xxl-job-admin ./xxl-job-admin
docker run --name xxl-job-admin -p 8080:8080 -d xuxueli/xxl-job-admin
```
### 5.20 避免任务重复执行
调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发;
针对上述情况,可以通过结合 "单机路由策略(如:第一台、一致性哈希)" + "阻塞策略(如:单机串行、丢弃后续调度)" 来规避,最终避免任务重复执行。
### 5.21 命令行任务
原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可;
如任务参数 "pwd" 将会执行命令并输出数据;
### 5.22 日志自动清理
XXL-JOB日志主要包含如下两部分,均支持日志自动清理,说明如下:
- 调度中心日志表数据:可借助配置项 "xxl.job.logretentiondays" 设置日志表数据保存天数,过期日志自动清理;详情可查看上文配置说明;
- 执行器日志文件数据:可借助配置项 "xxl.job.executor.logretentiondays" 设置日志文件数据保存天数,过期日志自动清理;详情可查看上文配置说明;
### 5.23 调度结果丢失处理
执行器因网络抖动回调失败或宕机等异常情况,会导致任务调度结果丢失。由于调度中心依赖执行器回调来感知调度结果,因此会导致调度日志永远处于 "运行中" 状态。
针对该问题,调度中心提供内置组件进行处理,逻辑为:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
## 六、调度中心/执行器 RESTful API
XXL-JOB 目标是一种跨平台、跨语言的任务调度规范和协议。
针对Java应用,可以直接通过官方提供的调度中心与执行器,方便快速的接入和使用调度中心,可以参考上文 “快速入门” 章节。
针对非Java应用,可借助 XXL-JOB 的标准 RESTful API 方便的实现多语言支持。
- 调度中心 RESTful API:
    - 说明:调度中心提供给执行器使用的API;不局限于官方执行器使用,第三方可使用该API来实现执行器;
    - API列表:执行器注册、任务结果回调等;
- 执行器 RESTful API :
    - 说明:执行器提供给调度中心使用的API;官方执行器默认已实现,第三方执行器需要实现并对接提供给调度中心;
    - API列表:任务触发、任务终止、任务日志查询……等;
此处 RESTful API 主要用于非Java语言定制个性化执行器使用,实现跨语言。除此之外,如果有需要通过API操作调度中心,可以个性化扩展 “调度中心 RESTful API” 并使用。
### 6.1 调度中心 RESTful API
API服务位置:com.xxl.job.core.biz.AdminBiz ( com.xxl.job.admin.controller.JobApiController )
API服务请求参考代码:com.xxl.job.adminbiz.AdminBizTest
#### a、任务回调
```
说明:执行器执行完任务后,回调任务结果时使用
------
地址格式:{调度中心根地址}/api/callback
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    [{
        "logId":1,              // 本次调度日志ID
        "logDateTim":0,         // 本次调度日志时间
        "handleCode":200,       // 200 表示任务执行正常,500表示失败
        "handleMsg": null
        }
    }]
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null      // 错误提示消息
    }
```
#### b、执行器注册
```
说明:执行器注册时使用,调度中心会实时感知注册成功的执行器并发起任务调度
------
地址格式:{调度中心根地址}/api/registry
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    {
        "registryGroup":"EXECUTOR",                     // 固定值
        "registryKey":"xxl-job-executor-example",       // 执行器AppName
        "registryValue":"http://127.0.0.1:9999/"        // 执行器地址,内置服务跟地址
    }
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null      // 错误提示消息
    }
```
#### c、执行器注册摘除
```
说明:执行器注册摘除时使用,注册摘除后的执行器不参与任务调度与执行
------
地址格式:{调度中心根地址}/api/registryRemove
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    {
        "registryGroup":"EXECUTOR",                     // 固定值
        "registryKey":"xxl-job-executor-example",       // 执行器AppName
        "registryValue":"http://127.0.0.1:9999/"        // 执行器地址,内置服务跟地址
    }
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null      // 错误提示消息
    }
```
### 6.2 执行器 RESTful API
API服务位置:com.xxl.job.core.biz.ExecutorBiz
API服务请求参考代码:com.xxl.job.executorbiz.ExecutorBizTest
#### a、心跳检测
```
说明:调度中心检测执行器是否在线时使用
------
地址格式:{执行器内嵌服务根地址}/beat
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null       // 错误提示消息
    }
```
#### b、忙碌检测
```
说明:调度中心检测指定执行器上指定任务是否忙碌(运行中)时使用
------
地址格式:{执行器内嵌服务根地址}/idleBeat
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    {
        "jobId":1       // 任务ID
    }
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null       // 错误提示消息
    }
```
#### c、触发任务
```
说明:触发任务执行
------
地址格式:{执行器内嵌服务根地址}/run
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    {
        "jobId":1,                                  // 任务ID
        "executorHandler":"demoJobHandler",         // 任务标识
        "executorParams":"demoJobHandler",          // 任务参数
        "executorBlockStrategy":"COVER_EARLY",      // 任务阻塞策略,可选值参考 com.xxl.job.core.enums.ExecutorBlockStrategyEnum
        "executorTimeout":0,                        // 任务超时时间,单位秒,大于零时生效
        "logId":1,                                  // 本次调度日志ID
        "logDateTime":1586629003729,                // 本次调度日志时间
        "glueType":"BEAN",                          // 任务模式,可选值参考 com.xxl.job.core.glue.GlueTypeEnum
        "glueSource":"xxx",                         // GLUE脚本代码
        "glueUpdatetime":1586629003727,             // GLUE脚本更新时间,用于判定脚本是否变更以及是否需要刷新
        "broadcastIndex":0,                         // 分片参数:当前分片
        "broadcastTotal":0                          // 分片参数:总分片
    }
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null       // 错误提示消息
    }
```
#### f、终止任务
```
说明:终止任务
------
地址格式:{执行器内嵌服务根地址}/kill
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    {
        "jobId":1       // 任务ID
    }
响应数据格式:
    {
      "code": 200,      // 200 表示正常、其他失败
      "msg": null       // 错误提示消息
    }
```
#### d、查看执行日志
```
说明:终止任务,滚动方式加载
------
地址格式:{执行器内嵌服务根地址}/log
Header:
    XXL-JOB-ACCESS-TOKEN : {请求令牌}
请求数据格式如下,放置在 RequestBody 中,JSON格式:
    {
        "logDateTim":0,     // 本次调度日志时间
        "logId":0,          // 本次调度日志ID
        "fromLineNum":0     // 日志开始行号,滚动加载日志
    }
响应数据格式:
    {
        "code":200,         // 200 表示正常、其他失败
        "msg": null         // 错误提示消息
        "content":{
            "fromLineNum":0,        // 本次请求,日志开始行数
            "toLineNum":100,        // 本次请求,日志结束行号
            "logContent":"xxx",     // 本次请求日志内容
            "isEnd":true            // 日志是否全部加载完
        }
    }
```
## 七、版本更新日志
### 7.1 版本 V1.1.x,新特性[2015-12-05]
**【于V1.1.x版本,XXL-JOB正式应用于我司,内部定制别名为 “Ferrari”,新接入应用推荐使用最新版本】**
- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;
- 2、动态:支持动态修改任务状态,动态暂停/恢复任务,即时生效;
- 3、服务HA:任务信息持久化到mysql中,Job服务天然支持集群,保证服务HA;
- 4、任务HA:某台Job服务挂掉,任务会平滑分配给其他的某一台存活服务,即使所有服务挂掉,重启时或补偿执行丢失任务;
- 5、一个任务只会在其中一台服务器上执行;
- 6、任务串行执行;
- 7、支持自定义参数;
- 8、支持远程任务执行终止;
### 7.2 版本 V1.2.x,新特性[2016-01-17]
- 1、支持任务分组;
- 2、支持“本地任务”、“远程任务”;
- 3、底层通讯支持两种方式,Servlet方式 + JETTY方式;
- 4、支持“任务日志”;
- 5、支持“串行执行”,并行执行;
    说明:V1.2版本将系统架构按功能拆分为:
        - 调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求;
        - 执行模块(执行器):负责接收调度请求并执行任务逻辑;
        - 通讯模块:负责调度模块和任务模块之间的信息通讯;
    优点:
        - 解耦:任务模块提供任务接口,调度模块维护调度信息,业务相互独立;
        - 高扩展性;
        - 稳定性;
### 7.3 版本 V1.3.0,新特性[2016-05-19]
- 1、遗弃“本地任务”模式,推荐使用“远程任务”,易于系统解耦,任务对应的JobHandler统称为“执行器”;
- 2、遗弃“servlet”方式底层系统通讯,推荐使用JETTY方式,调度+回调双向通讯,重构通讯逻辑;
- 3、UI交互优化:左侧菜单展开状态优化,菜单项选中状态优化,任务列表打开表格有压缩优化;
- 4、【重要】“执行器”细分为:BEAN、GLUE两种开发模式,简介见下文:
    “执行器” 模式简介:
        - BEAN模式执行器:每个执行器都是Spring的一个Bean实例,XXL-JOB通过注解@JobHandler识别和调度执行器;
         -GLUE模式执行器:每个执行器对应一段代码,在线Web编辑和维护,动态编译生效,执行器负责加载GLUE代码和执行;
### 7.4 版本 V1.3.1,新特性[2016-05-23]
- 1、更新项目目录结构:
    - /xxl-job-admin -------------------- 【调度中心】:负责管理调度信息,按照调度配置发出调度请求;
    - /xxl-job-core ----------------------- 公共依赖
    - /xxl-job-executor-example ------ 【执行器】:负责接收调度请求并执行任务逻辑;
    - /db ---------------------------------- 建表脚本
    - /doc --------------------------------- 用户手册
- 2、在新的目录结构上,升级了用户手册;
- 3、优化了一些交互和UI;
### 7.5 版本 V1.3.2,新特性[2016-05-28]
- 1、调度逻辑进行事务包裹;
- 2、执行器异步回调执行日志;
- 3、【重要】在 “调度中心” 支持HA的基础上,扩展执行器的Failover支持,支持配置多执行期地址;
### 7.6 版本 V1.4.0 新特性[2016-07-24]
- 1、任务依赖: 通过事件触发方式实现, 任务执行成功并回调时会主动触发一次子任务的调度, 多个子任务用逗号分隔;
- 2、执行器底层实现代码进行重度重构, 优化底层建表脚本;
- 3、执行器中任务线程分组逻辑优化: 之前根据执行器JobHandler进行线程分组,当多个任务复用Jobhanlder会导致相互阻塞。现改为根据调度中心任务进行任务线程分组,任务与任务执行相互隔离;
- 4、执行器调度通讯方案优化, 通过Hex + HC实现建议RPC通讯协议, 优化了通讯参数的维护和解析流程;
- 5、调度中心, 新建/编辑任务, 界面属性调整:
    - 5.1、任务新增/编辑界面中去除 "任务名JobName"属性 ,该属性改为系统自动生成: 该字段之前主要用于在 "调度中心" 唯一标示一个任务, 现实意义不大, 因此计划淡化掉该字段,改为系统生成UUID,从而简化任务新建的操作;
    - 5.2、任务新增/编辑界面中去除 "GLUE模式" 复选框位置调整, 改为贴近"JobHandler"输入框右侧;
    - 5.3、任务新增/编辑界面中去除 "报警阈值" 属性;
    - 5.4、任务新增/编辑界面中去除 "子任务Key" 属性, 每个任务全局任务Key可以从任务列表获取, 当本任务执行结束且成功后, 将会根据子任务Key匹配子任务并主动触发一次子任务执行;
- 6、问题修复:
    - 6.1、执行器jetty关闭优化,解决一处可能导致jetty无法关闭的问题;
    - 6.2、执行器任务终止时,执行队列回调优化,解决一处导致任务无法回调的问题;
    - 6.3、调度中心中列表分页参数优化,解决一处因服务器限制post长度而引起的问题;
    - 6.4、执行器Jobhandler注解优化,解决一处因事务代理导致的容器无法加载JobHandler的问题;
    - 6.5、远程调度优化,禁用retry策略,解决一处可能导致重复调用的问题;
Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段, 地址见分支 [V1.3](https://github.com/xuxueli/xxl-job/tree/v1.3) 。新特性将会在master分支持续更新。
### 7.7 版本 V1.4.1 新特性[2016-09-06]
- 1、项目成功推送maven中央仓库, 中央仓库地址以及依赖如下:
    ```
    <!-- http://repo1.maven.org/maven2/com/xuxueli/xxl-job-core/ -->
    <dependency>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job-core</artifactId>
        <version>${最新稳定版}</version>
    </dependency>
    ```
- 2、为适配中央仓库规则, 项目groupId从com.xxl改为com.xuxueli。
- 3、系统版本不在维护在项目跟pom中,各个子模块单独配置版本配置,解决子模块无法单独编译的问题;
- 4、底层RPC通讯,传输数据的字节长度统计规则优化,可节省50%数据传输量;
- 5、IJobHandler取消任务返回值,原通过返回值判断执行状态,逻辑改为:默认任务执行成功,仅在捕获异常时认定任务执行失败。
- 6、系统公共弹框功能,插件化;
- 7、底层表结构,表明统一大写;
- 8、调度中心,异常处理器JSON响应的ContentType修改,修复浏览器不识别的问题;
### 7.8 版本 V1.4.2 新特性[2016-09-29]
- 1、推送新版本 V1.4.2 至中央仓库, 大版本 V1.4 进入维护阶段;
- 2、任务新增时,任务列表偏移问题修复;
- 3、修复一处因bootstrap不支持模态框重叠而导致的样式错乱的问题, 在任务编辑时会出现该问题;
- 4、调度超时和Handler匹配不到时,调度状态优化;
- 5、因catch异常,导致任务不可终止的问题,给出解决方案, 见文档;
### 7.9 版本 V1.5.0 特性[2016-11-13]
- 1、任务注册: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。
- 2、"执行器" 新增参数 "AppName" : 是每个执行器集群的唯一标示AppName, 并周期性以AppName为对象进行自动注册。
- 3、调度中心新增栏目 "执行器管理" : 管理在线的执行器, 通过属性AppName自动发现注册的执行器。只有被管理的执行器才允许被使用;
- 4、"任务组"属性改为"执行器": 每个任务需要绑定指定的执行器, 调度地址通过绑定的执行器获取;
- 5、抛弃"任务机器"属性: 通过任务绑定的执行器, 自动发现注册的远程执行器地址并触发调度请求。
- 6、"公共依赖"中新增DBGlueLoader,基于原生jdbc实现GLUE源码的加载器,减少第三方依赖(mybatis,spring-orm等);精简和优化执行器测配置(针对GLUE任务),降低上手难度;
- 7、表结构调整,底层重构优化;
- 8、"调度中心"自动注册和发现,failover: 调度中心周期性自动注册, 任务回调时可以感知在线的所有调度中心地址, 通过failover的方式进行任务回调,避免回调单点风险。
### 7.10 版本 V1.5.1 特性[2016-11-13]
- 1、底层代码重构和逻辑优化,POM清理以及CleanCode;
- 2、Servlet/JSP Spec设定为3.0/2.2
- 3、Spring升级至3.2.17.RELEASE版本;
- 4、Jetty升级版本至8.2.0.v20160908;
- 5、已推送V1.5.0和V1.5.1至Maven中央仓库;
### 7.11 版本 V1.5.2 特性[2017-02-28]
- 1、IP工具类获取IP逻辑优化,IP静态缓存;
- 2、执行器、调度中心,均支持自定义注册IP地址;解决机器多网卡时错误网卡注册的情况;
- 3、任务跨天执行时生成多份日志文件的问题修复;
- 4、底层日志底层日志调整,非敏感日志level调整为debug;
- 5、升级数据库连接池c3p0版本;
- 6、执行器log4j配置优化,去除无效属性;
- 7、底层代码重构和逻辑优化以及CleanCode;
- 8、GLUE依赖注入逻辑优化,支持别名注入;
### 7.12 版本 V1.6.0 特性[2017-03-13]
- 1、通讯方案升级,原基于HEX的通讯模型调整为基于HTTP的B-RPC的通讯模型;
- 2、执行器支持手动设置执行地址列表,提供开关切换使用注册地址还是手动设置的地址;
- 3、执行器路由规则:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移;
- 4、规范线程模型统一,统一线程销毁方案(通过listener或stop方法,容器销毁时销毁线程;Daemon方式有时不太理想);
- 5、规范系统配置数据,通过配置文件统一管理;
- 6、CleanCode,清理无效的历史参数;
- 7、底层扩展数据结构以及相关表结构调整;
- 8、新建任务默认为非运行状态;
- 9、GLUE模式任务实例更新逻辑优化,原根据超时时间更新改为根据版本号更新,源码变动版本号加一;
### 7.13 版本 V1.6.1 特性[2017-03-25]
- 1、Rolling日志;
- 2、WebIDE交互重构;
- 3、通讯增强校验,有效过滤非正常请求;
- 4、权限增强校验,采用动态登录TOKEN(推荐接入内部SSO);
- 5、数据库配置优化,解决乱码问题;
### 7.14 版本 V1.6.2 特性[2017-04-25]
- 1、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等;
- 2、JobHandler支持设置任务返回值,在任务逻辑中可以方便的控制任务执行结果;
- 3、资源路径包含空格或中文时资源文件无法加载时,无法准确查看异常信息的问题处理。
- 4、路由策越优化:循环和LFU路由策略计数器自增无上限问题和首次路由压力集中在首台机器的问题修复;
### 7.15 版本 V1.7.0 特性[2017-05-02]
- 1、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python和Groovy等类型脚本;
- 2、新增spring-boot类型执行器example项目;
- 3、升级jetty版本至9.2;
- 4、任务运行日志移除log4j组件依赖,改为底层自主实现,从而取消了对日志组件的依赖限制;
- 5、执行器移除GlueLoader依赖,改为推送方式实现,从而GLUE源码加载不再依赖JDBC;
- 6、登录拦截Redirect时获取项目名,解决非根据目录发布时跳转404问题;
### 7.16 版本 V1.7.1 特性[2017-05-08]
- 1、运行日志读写编码统一为UTF-8,解决windows环境下日志乱码问题;
- 2、通讯超时时间限定为10s,避免异常情况下调度线程占用;
- 3、执行器,server启动、销毁和注册逻辑调整;
- 4、JettyServer关闭逻辑优化,修复执行器无法正常关闭导致端口占用和频繁打印c3p0日志的问题;
- 5、JobHandler中开启子线程时,支持子线程输出执行日志并通过Rolling查看。
- 6、任务日志清理功能;
- 7、弹框组件统一替换为layer;
- 8、升级quartz版本至2.3.0;
### 7.17 版本 V1.7.2 特性[2017-05-17]
- 1、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
- 2、失败处理策略;调度失败时的处理策略,策略包括:失败告警(默认)、失败重试;
- 3、通讯时间戳超时时间调整为180s;
- 4、执行器与数据库彻底解耦,但是执行器需要配置调度中心集群地址。调度中心提供API供执行器回调和心跳注册服务,取消调度中心内部jetty,心跳周期调整为30s,心跳失效为三倍心跳;
- 5、执行参数编辑时丢失问题修复;
- 6、新增任务测试Demo,方便在开发时进行任务逻辑测试;
### 7.18 版本 V1.8.0 特性[2017-07-17]
- 1、任务Cron更新逻辑优化,改为rescheduleJob,同时防止cron重复设置;
- 2、API回调服务失败状态码优化,方便问题排查;
- 3、XxlJobLogger的日志多参数支持;
- 4、路由策略新增 "忙碌转移" 模式:按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
- 5、路由策略代码重构;
- 6、执行器重复注册问题修复;
- 7、任务线程轮空30次后自动销毁,降低低频任务的无效线程消耗。
- 8、执行器任务执行结果批量回调,降低回调频率提升执行器性能;
- 9、springboot版本执行器,取消XML配置,改为类配置方式;
- 10、执行日志,支持根据运行 "状态" 筛选日志;
- 11、调度中心任务注册检测逻辑优化;
### 7.19 版本 V1.8.1 特性[2017-07-30]
- 1、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数处理分片任务;
- 2、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
- 3、执行器JobHandler禁止命名冲突;
- 4、执行器集群地址列表进行自然排序;
- 5、调度中心,DAO层代码精简优化并且新增测试用例覆盖;
- 6、调度中心API服务改为自研RPC形式,统一底层通讯模型;
- 7、新增调度中心API服务测试Demo,方便在调度中心API扩展和测试;
- 8、任务列表页交互优化,更换执行器分组时自动刷新任务列表,新建任务时默认定位在当前执行器位置;
- 9、访问令牌(accessToken):为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯;
- 10、springboot版本执行器,升级至1.5.6.RELEASE版本;
- 11、统一maven依赖版本管理;
### 7.20 版本 V1.8.2 特性[2017-09-04]
- 1、项目主页搭建:提供中英文文档:https://www.xuxueli.com/xxl-job
- 2、JFinal执行器Sample示例项目;
- 3、事件触发:除了"Cron方式"和"任务依赖方式"触发任务执行之外,支持基于事件的触发任务方式。调度中心提供触发任务单次执行的API服务,可根据业务事件灵活触发。
- 4、执行器摘除:执行器销毁时,主动通知调度中心并摘除对应执行器节点,提高执行器状态感知的时效性。
- 5、执行器手动设置IP时将会绑定Host;
- 6、规范项目目录,方便扩展多执行器;
- 7、解决执行器回调URL不支持配置HTTPS时问题;
- 8、执行器回调线程销毁前, 批量回调队列中数据,防止任务结果丢失;
- 9、调度中心任务监控线程销毁时,批量对失败任务告警,防止告警信息丢失;
- 10、任务日志文件路径时间戳格式化时SimpleDateFormat并发问题解决;
### 7.21 版本 V1.9.0 特性[2017-12-29]
- 1、新增Nutz执行器Sample示例项目;
- 2、新增任务运行模式 "GLUE模式(NodeJS) ",支持NodeJS脚本任务;
- 3、脚本任务Shell、Python和Nodejs等支持获取分片参数;
- 4、失败重试,完整支持:调度中心调度失败且启用"失败重试"策略时,将会自动重试一次;执行器执行失败且回调失败重试状态(新增失败重试状态返回值)时,也将会自动重试一次;
- 5、失败告警策略扩展:默认提供邮件失败告警,可扩展短信等,扩展代码位置为 "JobFailMonitorHelper.failAlarm";
- 6、执行器端口支持自动生成(小于等于0时),避免端口定义冲突;
- 7、调度报表优化,支持时间区间筛选;
- 8、Log组件支持输出异常栈信息,底层实现优化;
- 9、告警邮件样式优化,调整为表格形式,邮件组件调整为commons-email简化邮件操作;
- 10、项目依赖全量升级至较新稳定版本,如spring、jackson等等;
- 11、任务日志,记录发起调度的机器信息;
- 12、交互优化,如登录注销;
- 13、任务Cron长度扩展支持至128位,支持负责类型Cron设置;
- 14、执行器地址录入交互优化,地址长度扩展支持至512位,支持大规模执行器集群配置;
- 15、任务参数“IJobHandler.execute”入参改为“String params”,增强入参通用性。
- 16、IJobHandler提供init/destroy方法,支持在相应任务线程初始化和销毁时进行附加操作;
- 17、任务注解调整为 “@JobHandler”,与任务抽象接口统一;
- 18、修复任务监控线程被耗时任务阻塞的问题;
- 19、修复任务监控线程无法监控任务触发和执行状态均未0的问题;
- 20、执行器动态代理对象,拦截非业务方法的执行;
- 21、修复JobThread捕获Error错误不更新JobLog的问题;
- 22、修复任务列表界面左侧菜单合并时样式错乱问题;
- 23、调度中心项目日志配置改为xml文件格式;
- 24、Log地址格式兼容,支持非"/"结尾路径配置;
- 25、底层系统日志级别规范调整,清理遗留代码;
- 26、建表SQL优化,支持同步创建制定编码的库和表;
- 27、系统安全性优化,登录Token写Cookie时进行MD5加密,同时Cookie启用HttpOnly;
- 28、新增"任务ID"属性,移除"JobKey"属性,前者承担所有功能,方便后续增强任务依赖功能。
- 29、任务循环依赖问题修复,避免子任务与父任务重复导致的调度死循环;
- 30、任务列表新增筛选条件 "任务描述",快速检索任务;
- 31、执行器Log文件定期清理功能:执行器新增配置项("xxl.job.executor.logretentiondays")日志保存天数,日志文件过期自动删除。
### 7.22 版本 V1.9.1 特性[2018-02-22]
- 1、国际化:调度中心实现国际化,支持中文、英文两种语言,默认为中文。
- 2、调度报表新增"运行中"中状态项;
- 3、调度报表优化,报表SQL调优并且新增LocalCache缓存(缓存时间60s),提高大数据量下报表加载速度;
- 4、修复打包部署时资源文件乱码问题;
- 5、修复新版本chrome滚动到顶部失效问题;
- 6、调度中心配置加载优化,取消对配置文件名的强依赖,支持加载磁盘配置;
- 7、修复脚本任务Log文件未正常close的问题;
- 8、项目依赖全量升级至较新稳定版本,如spring、jackson等等;
### 7.23 版本 V1.9.2 特性[2018-10-05]
- 1、任务超时控制:新增任务属性 "任务超时时间",并支持自定义,任务运行超时将会主动中断任务;
- 2、任务失败重试次数:新增任务属性 "失败重试次数",并支持自定义,当任务失败时将会按照预设的失败重试次数主动进行重试;同时收敛废弃其他失败重试策略,如调度失败、执行失败、状态码失败等;
- 3、新增任务运行模式 "GLUE模式(PHP) ",支持php脚本任务;
- 4、新增任务运行模式 "GLUE模式(PowerShell) ",支持PowerShell脚本任务;
- 5、调度全异步处理:任务触发之后,推送到调度队列,多线程并发处理调度请求,提高任务调度速率的同时,避免因网络问题导致quartz调度线程阻塞的问题;
- 6、执行器任务结果落盘优化:执行器回调失败时将任务结果写磁盘,待重启或网络恢复时重试回调任务结果,防止任务执行结果丢失;
- 7、任务日志查询速度大幅提升:百万级别数据量搜索速度提升1000倍;
- 8、调度中心提供API服务,支持通过API服务对任务进行查询、新增、更新、启停等操作;
- 9、底层自研Log组件参数占位符改为"{}",并修复打印有参日志时参数不匹配导致报错的问题;
- 10、任务回调结果优化,支持展示在Rolling log中,方便问题排查;
- 11、底层LocalCache组件兼容性优化,支持jdk9、jdk10及以上版本编译部署;
- 12、告警邮件固定使用 UTF-8 编码格式,修复由机器编码导致的邮件乱码问题;
- 13、告警邮件中展示失败告警信息;
- 14、告警邮箱支持SSL配置;
- 15、Window机器下File.separator不兼容问题修复;
- 16、脚本任务异常Log输出优化;
- 17、任务线程停止变量修饰符优化;
- 18、脚本任务Log文件流关闭优化;
- 19、任务报表成功、失败和进行中统计问题修复;
- 20、核心依赖Core内部国际化处理;
- 21、默认Quartz线程数调整为50;
- 22、新增左侧菜单"运行报表";
- 23、执行器手动设置IP时取消绑定Host的操作,该IP仅供执行器注册使用;修复指定外网IP时无法绑定执行器Host的问题;
- 24、取消父子任务不可重复的限制,支持循环任务触发等特殊场景;
- 25、任务调度备注中标注任务触发类型,如Cron触发、父任务触发、API触发等等,方便排查调度日志;
- 26、底层日志组件SimpleDateFormat线程安全问题修复;
- 27、执行器通讯线程优化,corePoolSize从256降低至32;
- 28、任务日志表状态字段类型优化;
- 29、GLUE脚本文件自动清理功能,及时清理过期脚本文件;
- 30、执行器注册方式切换优化,切换自动注册时主动同步在线机器,避免执行器为空的问题;
- 31、跨平台:除了提供Java、Python、PHP等十来种任务模式之外,新增提供基于HTTP的任务模式;
- 32、底层RPC序列化协议调整为hessian2;
- 33、修复表字段 “t.order”与数据库关键字冲突查询失败的问题,
- 34、任务属性枚举 "任务模式、阻塞策略" 国际化优化;
- 35、分片任务失败重试优化,仅重试当前失败的分片;
- 36、任务触发时支持动态传参,调度中心与API服务均提供提供动态参数功能;
- 37、任务执行日志、调度日志字段类型调整,改为text类型并取消字数限制;
- 38、GLUE任务脚本字段类型调整,改为mediumtext类型,提高GLUE长度上限;
- 39、任务监控线程Log输出优化,运行中任务的监控Log改为debug级别,减少非核心日志量;
- 40、项目依赖全量升级至较新稳定版本,如spring、Jackson、groovy等等;
- 41、docker支持:调度中心提供 Dockerfile 方便快速构建docker镜像;
### 7.24 版本 V2.0.0 Release Notes[2018-11-04]
- 1、调度中心迁移到 springboot;
- 2、底层通讯组件迁移至 xxl-rpc;
- 3、容器化:提供官方docker镜像,并实时更新推送dockerhub(docker pull xuxueli/xxl-job-admin),进一步实现产品开箱即用;
- 4、新增无框架执行器Sample示例项目 "xxl-job-executor-sample-frameless"。不依赖第三方框架,只需main方法即可启动运行执行器;
- 5、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可;
- 6、任务状态优化,仅运行状态"NORMAL"任务关联至quartz,降低quartz底层数据存储与调度压力;
- 7、任务状态规范:新增任务默认停止状态,任务更新时保持任务状态不变;
- 8、IP获取逻辑优化,优先遍历网卡来获取可用IP;
- 9、任务新增的API服务接口返回任务ID,方便调用方实用;
- 10、组件化优化,移除对 spring 的依赖:非spring应用选用 "XxlJobExecutor" 、spring应用选用 "XxlJobSpringExecutor" 作为执行器组件;
- 11、任务RollingLog展示逻辑优化,修复超时任务无法查看的问题;
- 12、多项UI组件升级到最新版本,如:CodeMirror、Echarts、Jquery 等;
- 13、项目依赖升级 groovy 至较新稳定版本;pom清理;
- 14、子任务失败重试重试逻辑优化,子任务失败时将会按照其预设的失败重试次数主动进行重试
### 7.25 版本 v2.0.1 Release Notes[2018-11-09]
- 1、左侧菜单折叠动画问题修复;
- 2、调度报表日期分布图默认值统一;
- 3、freemarker对数字默认加千分位问题修复,解决日志ID被分隔导致查看日志失败问题;
- 4、底层通讯组件升级,修复通讯异常时无效等待的问题;
- 5、执行器启动之后jetty停止的问题修复;
### 7.26 版本 v2.0.2 Release Notes[2019-04-20]
- 1、底层通讯方案优化:升级较新版本xxl-rpc,由"JETTY"方案调整为"NETTY_HTTP"方案,执行器内嵌netty-http-server提供服务,调度中心复用容器端口提供服务;
- 2、任务告警逻辑调整,改为通过扫描失败日志方式触发。一方面精确扫描失败任务,降低扫描范围;另一方面取消内存队列,降低线程内存消耗;
- 3、Quartz触发线程池废弃并替换为 "XxlJobThreadPool",降低线程切换、内存占用带来的消耗,提高调度性能;
- 4、调度线程池隔离,拆分为"Fast"和"Slow"两个线程池,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性;
- 5、执行器热部署时JobHandler重新初始化,修复由此导致的 "jobhandler naming conflicts." 问题;
- 6、新增Class的加载缓存,解决频繁加载Class会使jvm的方法区空间不足导致OOM的问题;
- 7、任务支持更换绑定执行器,方便任务分组转移和管理;
- 8、调度中心告警邮件发送组件改为 “spring-boot-starter-mail”;
- 9、记住密码功能优化,选中时永久记住;非选中时关闭浏览器即登出;
- 10、项目依赖升级至较新稳定版本,如quartz、spring、jackson、groovy、xxl-rpc等等;
- 11、精简项目,取消第三方依赖,如 commons-collections4、commons-lang3 ;
- 12、执行器回调日志落盘方案复用RPC序列化方案,并移除Jackson依赖;
- 13、底层Log调优,应用正常终止取消异常栈信息打印;
- 14、交互优化,尽量避免新开页面窗口;仅WebIDE支持新开页,并提供窗口快速关闭按钮;任务启、停、删除、触发等轻操作提示改为toast方式,
- 15、任务暂停、删除优化,避免quartz delete不完整导致任务脏数据;
- 16、任务回调、心跳注册成功日志优化,非核心常规日志调整为debug级别,降低冗余日志输出;
- 17、调整首页报表默认区间为本周,避免日志量太大查询缓慢;
- 18、LRU路由更新不及时问题修复;
- 19、任务失败告警邮件发送逻辑优化;
- 20、调度日志排序逻辑调整为按照调度时间倒序,兼容TIDB等主键不连续日志存储组件;
- 21、执行器优雅停机优化;
- 22、连接池配置优化,增强连接有效性验证;
- 23、JobHandler#msg长度限制,修复异常情况下日志超长导致内存溢出的问题;
- 24、升级xxl-rpc至较新版本,修复springboot 2.x版本兼容性问题;
### 7.27 版本 v2.1.0 Release Notes[2019-07-07]
- 1、自研调度组件,移除quartz依赖:一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性;
    - 触发:单节点周期性触发,运行事件如delayqueue;
    - 调度:集群竞争,负载方式协同处理,锁竞争-更新触发信息-推送时间轮-锁释放-锁竞争;
- 2、底层表结构重构:移除11张quartz相关表,并对现有表结构优化梳理;
- 3、任务日志主键调整为long数据类型,防止海量日志情况下数据溢出;
- 4、底层线程模型重构:移除Quartz线程池,降低系统线程与内存开销;
- 5、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色;
- 6、权限管理:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作;
- 7、调度线程池参数调优;
- 8、注册表索引优化,缓解锁表问题;
- 9、新增Jboot执行器Sample示例项目;
- 10、任务列表优化,支持根据 "任务状态"、"负责人" 属性筛选任务;
- 11、任务日志列表交互优化,操作按钮合并为分割按钮;
- 12、项目依赖升级至较新稳定版本,如spring、springboot、groovy、xxl-rpc等等;并清理冗余POM;
- 13、升级xxl-rpc至较新版本,修复代理服务初始化时远程服务不可用导致长连冗余创建的问题;
- 14、首页调度报表的日期排序在TIDB下乱序问题修复;
- 15、调度中心与执行器双向通讯超时时间调整为3s;
- 16、调度组件销毁流程优化,先停止调度线程,然后等待时间轮内存量任务处理完成,最终销毁时间轮线程;
- 17、执行器回调线程优化,回调地址为空时销毁问题修复;
- 18、HttpJobHandler优化,响应数据指定UTF-8格式,避免中文乱码;
- 19、代码优化,ConcurrentHashMap变量类型改为ConcurrentMap,避免因不同版本实现不同导致的兼容性问题;
### 7.28 版本 v2.1.1 Release Notes[2019-11-24]
- 1、 调度中心日志自动清理功能(至此,调度中心/执行器均支持日志自动清理,过期天数均默认设置为30天):调度中心新增配置项("xxl.job.logretentiondays")日志保存天数,过期日志自动清理;解决海量日志情况下日志表慢SQL问题;限制大于等于7时生效,否则关闭清理功能,默认为30;
- 2、 调度报表优化:新增日志报表的存储表,三天内的任务日志会以每分钟一次的频率异步同步至报表中;任务报表仅读取报表数据,极大提升加载速度;
- 3、 Cron在线生成工具:任务新增、编辑框通过组件在线生成Cron表达式;
- 4、 Cron下次执行时间查询:支持通过界面在线查看后续连续5次执行时间;
- 5、 调度中心新增应用健康检查功能,借助“spring-boot-starter-actuator”,相对地址 “/actuator/health”;
- 6、 DB脚本默认编码改为utf8mb4,修复字符乱码问题(建议Mysql版本5.7+);
- 7、 调度中心任务平均分配,触发组件每次获取与线程池数量相关数量的任务,避免大量任务集中在单个调度中心集群节点;
- 8、 任务触发组件优化,预加载频率正常1s一次,当预加载轮空时主动休眠一个加载周期,动态降低加载频率从而降低DB压力;
- 9、 调度组件优化:针对永远不会触发的Cron禁止配置和启动;任务Cron最后一次触发后再也不会触发时,比如一次性任务,主动停止相关任务;
- 10、DB重连优化,修复DB宕机重连后任务调度停止的问题,重连后自动加入调度集群触发任务调度;
- 11、注册监控线程优化,降低死锁几率;
- 12、调度中心日志删除优化,改为分页获取ID并根据ID删除的方式,避免批量删除海量日志导致死锁问题;
- 13、任务重试时参数丢失的问题修复;
- 14、调度中心移除SQL中的 "now()" 函数;集群部署时不再依赖DB时钟,仅需要保证调度中心应用节点时钟一致即可;
- 15、任务触发组件加载顺序调整,避免小概率情况下组件随机加载顺序导致的I18N的NPE问题;
- 16、JobThread自销毁优化,避免并发触发导致triggerQueue中任务丢失问题;
- 17、调度中心密码限制18位,修复修改密码超过18位无法登录的问题;
- 18、任务告警组件分页参数无效问题修复;
- 19、升级xxl-rpc版本:服务端线程优化,降低线程内存开销;IpUtil优化:增加连通性校,过滤明确非法的网卡;
- 20、调度中心回调API服务改为restful方式;
- 21、UI优化,任务列表和日志列表数据表格宽度比例调整,避免数据换行提升体验;
- 22、登录界面取消默认填写的登录账号密码;
- 23、执行器表属性调整,"顺序" 属性调整为整型,解决执行器数据较多时无法正确排序的问题;
- 24、任务列表交互优化,支持查看任务所属执行器的注册节点;
- 25、项目依赖升级至较新稳定版本,如spring、spring-boot、mybatis、slf4j、groovy等等;
- 26、日志组件优化:调度中心支持控制每次请求最大加载行数,日志量太大时分批请求,避免单次加载日志量太大阻塞页面;
### 7.29 版本 v2.1.2 Release Notes[2019-12-12]
- 1、方法任务支持:由原来基于JobHandler类任务开发方式,优化为支持基于方法的任务开发方式;因此,可以支持单个类中开发多个任务方法,进行类复用
```
@XxlJob("demoJobHandler")
public ReturnT<String> execute(String param) {
    XxlJobLogger.log("hello world");
    return ReturnT.SUCCESS;
}
```
- 2、移除commons-exec,采用原生方式实现,降低第三方依赖;
- 3、执行器回调乱码问题修复;
- 4、调度中心dispatcher servlet加载顺序优化;
- 5、执行器回调地址https兼容支持;
- 6、多个项目依赖升级至较新稳定版本;
- 注意:最新版本 "XxlJobSpringExecutor" 逻辑有调整,历史项目中该组件的配置方式请参考Sample示例项目进行调整,尤其注意需要移除组件的init和destroy方法;
### 7.30 版本 v2.2.0 Release Notes[2020-04-14]
- 1、RESTful API:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。
- 2、任务复制功能:点击复制是弹出新建任务弹框,并初始化被复制任务信息;
- 3、任务手动执行一次的时候,支持指定本次执行的机器地址,为空则从执行器获取;
- 4、任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
- 5、调度中心升级springboot2.x;因此,系统要求JDK8+;
- 6、XxlJob注解扫描方式优化,支持查找父类以及接口和基于类代理等常见情况;修复任务为空时小概率NPE问题;
- 7、移除旧类注解JobHandler,推荐使用基于方法注解 "@XxlJob" 的方式进行任务开发;(如需保留类注解JobHandler使用方式,可以参考旧版逻辑定制开发);
- 8、任务告警组件模块化:如果需要新增一种告警方式,只需要新增一个实现 "com.xxl.job.admin.core.alarm.JobAlarm" 接口的告警实现即可,更加灵活、方便定制;
- 9、调度中心国际化完善:新增 "中文繁体" 支持。默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
- 10、执行器注册逻辑优化:新增配置项 ”注册地址 / xxl.job.executor.address“,优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
- 11、默认数据库连接池调整为hikari,移除tomcat-jdbc依赖;
- 12、多个项目依赖升级至较新稳定版本,如mybatis、groovy和mysql驱动等;
- 13、执行器优雅停机优化,修复任务线程中断未join导致回调丢失的问题;
- 14、一致性哈希路由策略优化:默认虚拟节点数量调整为100,提高路由的均衡性;
- 15、通用HTTP任务Handler(httpJobHandler)优化,扩展自定义参数信息,示例参数如下;
```
url: http://www.xxx.com
method: get 或 post
data: post-data
```
- 16、SQL脚本编码默认utf8mb4执行,避免小概率下容器环境中乱码问题;
- 17、Web IDE交互问题修复:输入源码备注之后按回车跳转error问题处理;
- 18、执行器初始化逻辑优化:修复懒加载的Bean被提前初始化问题;
- 19、执行器注册默认值优化;
- 20、修复bootstrap.min.css.map 404问题;
- 21、执行器UI交互优化,移除冗余order属性;
- 22、执行备注消息长度限制,修复数据超长无法存储导致导致回调失败的问题;
注意:XxlJobSpringExecutor组件个别字段调整:“appName” 调整为 “appname” ,升级时该组件时需要注意;
### 7.31 版本 v2.3.0 Release Notes[2021-02-09]
- 1、【新增】调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等;
- 2、【新增】触发策略:除了常规Cron、API、父子任务触发方式外,新增提供 "固定间隔触发、(固定延时触发,实验中)" 新触发方式;
- 3、【新增】新增任务辅助工具 "XxlJobHelper":提供统一任务辅助能力,包括:任务上下文信息维护获取(任务参数、任务ID、分片参数)、日志输出、任务结果设置……等;
    - 3.1、"ShardingUtil" 组件废弃:改用 "XxlJobHelper.getShardIndex()/getShardTotal();" 获取分片参数;
    - 3.2、"XxlJobLogger" 组件废弃:改用 "XxlJobHelper.log" 进行日志输出;
- 4、【优化】任务核心类 "IJobHandler" 的 "execute" 方法取消出入参设计。改为通过 "XxlJobHelper.getJobParam" 获取任务参数并替代方法入参,通过 "XxlJobHelper.handleSuccess/handleFail" 设置任务结果并替代方法出参,示例代码如下;
```
@XxlJob("demoJobHandler")
public void execute() {
  String param = XxlJobHelper.getJobParam();    // 获取参数
  XxlJobHelper.handleSuccess();                 // 设置任务结果
}
```
- 5、【优化】Cron编辑器增强:Cron编辑器修改cron时可实时查看最近运行时间;
- 6、【优化】执行器示例项目规范整理;
- 7、【优化】任务调度生命周期重构:调度(schedule)、触发(trigger)、执行(handle)、回调(callback)、结束(complete);
- 8、【优化】执行器注册组件优化:注册逻辑调整为异步方式,提高注册性能;
- 9、【优化】执行器鉴权校验:执行器启动时主动校验accessToken,为空则主动Warn告警;(已规划安全强化:AccessToken动态生成、动态启停等)
- 10、【优化】邮箱告警配置优化:将"spring.mail.from"与"spring.mail.username"属性拆分开,更加灵活的支持一些无密码邮箱服务;
- 11、【优化】多个项目依赖升级至较新稳定版本,如netty、groovy、spring、springboot、mybatis等;
- 12、【优化】UI组件常规升级,提升组件稳定性;
- 13、【优化】调度中心页面交互优化:用户管理模块密码列取消;多处表达autocomplete取消;执行器管理模块XSS拦截校验等;
- 14、【优化】调度中心任务状态探测慢SQL问题优化;
- 15、【修复】GLUE-Java模式任务,init/destroy无法执行问题修复;
- 16、【修复】Cron编辑器问题修复:修复小概率情况下cron单个字段修改时导致其他字段被重置问题;
- 17、【修复】通用HTTP任务Handler(httpJobHandler)优化:修复 "setDoOutput(true)" 导致任务请求GetMethod失效问题;
- 18、【修复】执行器Commandhandler示例任务优化,修复极端情况下脚本进程挂起问题;
- 19、【修复】调度通讯组件优化,修复RestFul方式调用 DotNet 版本执行器时心跳检测失败问题;
- 20、【修复】调度中心远程执行日志查询乱码问题修复;
- 21、【修复】调度中心组件加载顺序优化,修复极端情况下调度组件初始慢导致的调度失败问题;
- 22、【修复】执行器注册线程优化,修复极端情况下初始化失败时导致NPE问题;
- 23、【修复】调度线程连接池优化,修复连接有效性校验超时问题;
- 24、【修复】执行器注册表字段优化,解决执行器注册节点过多导致注册信息存储和更新失败的问题;
- 25、【修复】轮训路由策略优化,修复小概率下并发问题;
- 26、【修复】页面redirect跳转后https变为http问题修复;
- 27、【修复】执行器日志清理优化,修复小概率下日志文件为空导致清理异常问题;
### 7.32 版本 v2.3.1 Release Notes[2022-05-21]
- 1、【修复】修复风险漏洞,升级问题低版本项目依赖:CVE-2021-2471、CVE-2022-22965等。
- 2、【修复】修复故障告警逻辑,邮箱校验逻辑下放至EmailJobAlarm中,避免对其他告警方式的干扰。
- 3、【优化】调度通讯默认启用accessToken,提升系统安全性(建议生产环境自定义accessToken)。
- 4、【优化】合并多项PR,项目代码结构、健壮性优化:PR-2833、PR-2812、PR-2541、PR-2537、PR-2514、PR-2509、PR-2591。
- 5、【优化】任务线程名优化,提升可读性与问题定位效率(ISSUE-2527)。
### 7.33 版本 v2.4.0 Release Notes[规划中]
- 1、【优化】[规划中]任务日志重构:一次调度只记录一条主任务,维护起止时间和状态。
    - 普通任务:只记录一条主任务;
    - 广播任务:记录一条主任务,每个分片任务记录一条次任务,关联在主任务上;
    - 重试任务:失败时,新增主任务。所有调度记录,包括入口调度和重试调度,均挂载主任务上。
- 2、【优化】[规划中]分片任务:全部完成后才会出发后置节点;
### 7.34 版本 v2.4.1 Release Notes[规划中]
- 1、[规划中]DAG流程任务
    - DAG任务:支持参数传递,共享数据:DAG任务创建、管理,DAG任务日志查看、操作;
    - 子任务:废弃
- 2、[规划中]多数据库支持,DAO层通过JPA实现,不限制数据库类型;
- 3、[规划中]告警增强:邮件告警 + webhook告警;
- 4、[规划中]安全强化:AccessToken动态生成、动态启停;控制调度、回调;
- 5、[规划中]任务导入导出工具,灵活支持版本升级、迁移等场景。
### TODO LIST
- 1、任务分片路由:分片采用一致性Hash算法计算出尽量稳定的分片顺序,即使注册机器存在波动也不会引起分批分片顺序大的波动;目前采用IP自然排序,可以满足需求,待定;
- 2、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。
- 3、调度任务优先级;
- 4、多数据库支持,DAO层通过JPA实现,不限制数据库类型;
- 5、执行器Log清理功能:调度中心Log删除时同步删除执行器中的Log文件;
- 6、延时任务:API触发,支持"动态传参、延时消费";该功能与 XXL-MQ 冲突,该场景建议用后者;
- 7、调度线程池改为协程方式实现,大幅降低系统内存消耗;
- 8、任务、执行器数据全量本地缓存;新增消息表广播通知;
- 9、忙碌转移优化,全部机器忙碌时不再直接失败;
- 10、任务触发参数优化:支持选择 "Cron触发"、"固定间隔时间触发"、"指定时间点触发"、"不选择" 等;
- 11、调度日志列表加上执行时长列,并支持排序;
- 12、DAG流程任务:
    - 替换子任务,支持参数传递,共享数据:
    - 配置并列的"a-b、b-c"路径列表,构成串行、并行、dag任务流程,"dagre-d3"绘图;任务依赖,流程图,子任务+会签任务,各节点日志;支持根据成功、失败选择分支;
    - 分片任务:全部完成后才会出发后置节点;
- 13、日期过滤:支持多个时间段排除;
- 14、告警增强:
    - 邮件告警:支持自定义标题、模板格式;
    - webhook告警:支持自定义告警URL、请求体格式;
- 15、新增任务运行模式 "GLUE模式(GO) ",支持GO任务;
- 16、GLUE 模式 Web Ide 版本对比功能;
- 17、注册中心优化,实时性注册发现:心跳注册间隔10s,refresh失败则首次注册并立即更新注册信息,心跳类似;30s过期销毁;
- 18、提供执行器Docker镜像;
- 19、脚本任务,支持数据参数,新版本仅支持单参数不支持需要兼容;
- 20、批量调度:调度请求入queue,调度线程批量获取调度请求并发起远程调度;提高线程效率;
- 21、执行器端口复用,复用容器端口提供通讯服务;
- 22、分片任务全部成功后触发子任务;
- 23、新增执行器描述属性;任务名称属性;
- 24、自定义失败重试时间间隔;
- 25、任务标签:方便搜索;
- 26、执行器:dag执行器,不需要注册机器;
## 八、其他
### 8.1 项目贡献
欢迎参与项目贡献!比如提交PR修复一个bug,或者新建 [Issue](https://github.com/xuxueli/xxl-job/issues/) 讨论新特性或者变更。
### 8.2 用户接入登记
更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。
### 8.3 开源协议和版权
产品开源免费,并且将持续提供免费的社区技术支持。个人或企业内部可自由的接入和使用。如有需要可邮件联系作者免费获取项目授权。
- Licensed under the GNU General Public License (GPL) v3.
- Copyright (c) 2015-present, xuxueli.
---
### 捐赠
无论捐赠金额多少都足够表达您这份心意,非常感谢 :)      [前往捐赠](https://www.xuxueli.com/page/donate.html )
xxl-job/doc/XXL-JOB架构图.pptx
Binary files differ
xxl-job/doc/db/tables_xxl_job.sql
New file
@@ -0,0 +1,122 @@
#
# XXL-JOB v2.3.1
# Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;
SET NAMES utf8mb4;
CREATE TABLE `xxl_job_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
  `job_desc` varchar(255) NOT NULL,
  `add_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `author` varchar(64) DEFAULT NULL COMMENT '作者',
  `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
  `schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
  `schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
  `misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
  `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
  `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
  `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
  `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
  `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
  `glue_source` mediumtext COMMENT 'GLUE源代码',
  `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
  `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
  `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
  `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
  `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
  `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
  `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
  `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
  `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
  `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
  `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
  `trigger_code` int(11) NOT NULL COMMENT '调度-结果',
  `trigger_msg` text COMMENT '调度-日志',
  `handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
  `handle_code` int(11) NOT NULL COMMENT '执行-状态',
  `handle_msg` text COMMENT '执行-日志',
  `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
  PRIMARY KEY (`id`),
  KEY `I_trigger_time` (`trigger_time`),
  KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log_report` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
  `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
  `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
  `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_logglue` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
  `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
  `glue_source` mediumtext COMMENT 'GLUE源代码',
  `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
  `add_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_registry` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `registry_group` varchar(50) NOT NULL,
  `registry_key` varchar(255) NOT NULL,
  `registry_value` varchar(255) NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_group` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
  `title` varchar(12) NOT NULL COMMENT '执行器名称',
  `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
  `address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '账号',
  `password` varchar(50) NOT NULL COMMENT '密码',
  `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
  `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
  PRIMARY KEY (`id`),
  UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_lock` (
  `lock_name` varchar(50) NOT NULL COMMENT '锁名称',
  PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' );
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
commit;
xxl-job/doc/images/cnblog-首页-每日一博-第一.png
xxl-job/doc/images/cnblog-首页-热门动弹-第一.png
xxl-job/doc/images/donate-alipay.jpg
xxl-job/doc/images/donate-paypal.png
xxl-job/doc/images/donate-wechat.png
xxl-job/doc/images/gitee-gvp.jpg
xxl-job/doc/images/img_1001.png
xxl-job/doc/images/img_1002.png
xxl-job/doc/images/img_6yC0.png
xxl-job/doc/images/img_BPLG.png
xxl-job/doc/images/img_EB65.png
xxl-job/doc/images/img_Fgql.png
xxl-job/doc/images/img_Hr2T.png
xxl-job/doc/images/img_Qohm.png
xxl-job/doc/images/img_UDSo.png
xxl-job/doc/images/img_V3vF.png
xxl-job/doc/images/img_Wb2o.png
xxl-job/doc/images/img_Ypik.png
xxl-job/doc/images/img_Z9Qr.png
xxl-job/doc/images/img_ZAhX.png
xxl-job/doc/images/img_ZAsz.png
xxl-job/doc/images/img_dNUJ.png
xxl-job/doc/images/img_eYrv.png
xxl-job/doc/images/img_hIci.png
xxl-job/doc/images/img_iUw0.png
xxl-job/doc/images/img_inc8.png
xxl-job/doc/images/img_jOAU.png
xxl-job/doc/images/img_jrdI.png
xxl-job/doc/images/img_o8HQ.png
xxl-job/doc/images/img_tJOq.png
xxl-job/doc/images/img_tvGI.png
xxl-job/doc/images/qq群-一个xxl同学进了58.png
xxl-job/doc/images/xxl-logo.jpg
xxl-job/doc/images/xxl-logo.png
xxl-job/xxl-job-admin/Dockerfile
New file
@@ -0,0 +1,11 @@
FROM openjdk:8-jre-slim
MAINTAINER xuxueli
ENV PARAMS=""
ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ADD target/xxl-job-admin-*.jar /application/app.jar
ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /application/app.jar $PARAMS"]
xxl-job/xxl-job-admin/pom.xml
New file
@@ -0,0 +1,113 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job</artifactId>
        <version>2.3.1</version>
    </parent>
    <artifactId>xxl-job-admin</artifactId>
    <packaging>jar</packaging>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <!-- starter-web:spring-webmvc + autoconfigure + logback + yaml + tomcat -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- starter-test:junit + spring-test + mockito -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- freemarker-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <!-- mail-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <!-- starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- mybatis-starter:mybatis + mybatis-spring + hikari(default) -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot-starter.version}</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector-java.version}</version>
        </dependency>
        <!-- xxl-job-core -->
        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>${project.parent.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- docker -->
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>0.4.13</version>
                <configuration>
                    <!-- made of '[a-z0-9-_.]' -->
                    <imageName>${project.artifactId}:${project.version}</imageName>
                    <dockerDirectory>${project.basedir}</dockerDirectory>
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java
New file
@@ -0,0 +1,16 @@
package com.xxl.job.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * @author xuxueli 2018-10-28 00:38:13
 */
@SpringBootApplication
public class XxlJobAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(XxlJobAdminApplication.class, args);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java
New file
@@ -0,0 +1,96 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.controller.annotation.PermissionLimit;
import com.xxl.job.admin.service.LoginService;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
/**
 * index controller
 * @author xuxueli 2015-12-19 16:13:16
 */
@Controller
public class IndexController {
    @Resource
    private XxlJobService xxlJobService;
    @Resource
    private LoginService loginService;
    @RequestMapping("/")
    public String index(Model model) {
        Map<String, Object> dashboardMap = xxlJobService.dashboardInfo();
        model.addAllAttributes(dashboardMap);
        return "index";
    }
    @RequestMapping("/chartInfo")
    @ResponseBody
    public ReturnT<Map<String, Object>> chartInfo(Date startDate, Date endDate) {
        ReturnT<Map<String, Object>> chartInfo = xxlJobService.chartInfo(startDate, endDate);
        return chartInfo;
    }
    @RequestMapping("/toLogin")
    @PermissionLimit(limit=false)
    public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) {
        if (loginService.ifLogin(request, response) != null) {
            modelAndView.setView(new RedirectView("/",true,false));
            return modelAndView;
        }
        return new ModelAndView("login");
    }
    @RequestMapping(value="login", method=RequestMethod.POST)
    @ResponseBody
    @PermissionLimit(limit=false)
    public ReturnT<String> loginDo(HttpServletRequest request, HttpServletResponse response, String userName, String password, String ifRemember){
        boolean ifRem = (ifRemember!=null && ifRemember.trim().length()>0 && "on".equals(ifRemember))?true:false;
        return loginService.login(request, response, userName, password, ifRem);
    }
    @RequestMapping(value="logout", method=RequestMethod.POST)
    @ResponseBody
    @PermissionLimit(limit=false)
    public ReturnT<String> logout(HttpServletRequest request, HttpServletResponse response){
        return loginService.logout(request, response);
    }
    @RequestMapping("/help")
    public String help() {
        /*if (!PermissionInterceptor.ifLogin(request)) {
            return "redirect:/toLogin";
        }*/
        return "help";
    }
    @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));
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java
New file
@@ -0,0 +1,72 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.controller.annotation.PermissionLimit;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.core.biz.AdminBiz;
import com.xxl.job.core.biz.model.HandleCallbackParam;
import com.xxl.job.core.biz.model.RegistryParam;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.util.GsonTool;
import com.xxl.job.core.util.XxlJobRemotingUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
 * Created by xuxueli on 17/5/10.
 */
@Controller
@RequestMapping("/api")
public class JobApiController {
    @Resource
    private AdminBiz adminBiz;
    /**
     * api
     *
     * @param uri
     * @param data
     * @return
     */
    @RequestMapping("/{uri}")
    @ResponseBody
    @PermissionLimit(limit=false)
    public ReturnT<String> api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) {
        // valid
        if (!"POST".equalsIgnoreCase(request.getMethod())) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
        }
        if (uri==null || uri.trim().length()==0) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
        }
        if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null
                && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0
                && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
        }
        // services mapping
        if ("callback".equals(uri)) {
            List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
            return adminBiz.callback(callbackParamList);
        } else if ("registry".equals(uri)) {
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registry(registryParam);
        } else if ("registryRemove".equals(uri)) {
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registryRemove(registryParam);
        } else {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java
New file
@@ -0,0 +1,96 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLogGlue;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobInfoDao;
import com.xxl.job.admin.dao.XxlJobLogGlueDao;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.glue.GlueTypeEnum;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.List;
/**
 * job code controller
 * @author xuxueli 2015-12-19 16:13:16
 */
@Controller
@RequestMapping("/jobcode")
public class JobCodeController {
    @Resource
    private XxlJobInfoDao xxlJobInfoDao;
    @Resource
    private XxlJobLogGlueDao xxlJobLogGlueDao;
    @RequestMapping
    public String index(HttpServletRequest request, Model model, int jobId) {
        XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId);
        List<XxlJobLogGlue> jobLogGlues = xxlJobLogGlueDao.findByJobId(jobId);
        if (jobInfo == null) {
            throw new RuntimeException(I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
        }
        if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())) {
            throw new RuntimeException(I18nUtil.getString("jobinfo_glue_gluetype_unvalid"));
        }
        // valid permission
        JobInfoController.validPermission(request, jobInfo.getJobGroup());
        // Glue类型-字典
        model.addAttribute("GlueTypeEnum", GlueTypeEnum.values());
        model.addAttribute("jobInfo", jobInfo);
        model.addAttribute("jobLogGlues", jobLogGlues);
        return "jobcode/jobcode.index";
    }
    @RequestMapping("/save")
    @ResponseBody
    public ReturnT<String> save(Model model, int id, String glueSource, String glueRemark) {
        // valid
        if (glueRemark==null) {
            return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) );
        }
        if (glueRemark.length()<4 || glueRemark.length()>100) {
            return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_remark_limit"));
        }
        XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(id);
        if (exists_jobInfo == null) {
            return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
        }
        // update new code
        exists_jobInfo.setGlueSource(glueSource);
        exists_jobInfo.setGlueRemark(glueRemark);
        exists_jobInfo.setGlueUpdatetime(new Date());
        exists_jobInfo.setUpdateTime(new Date());
        xxlJobInfoDao.update(exists_jobInfo);
        // log old code
        XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue();
        xxlJobLogGlue.setJobId(exists_jobInfo.getId());
        xxlJobLogGlue.setGlueType(exists_jobInfo.getGlueType());
        xxlJobLogGlue.setGlueSource(glueSource);
        xxlJobLogGlue.setGlueRemark(glueRemark);
        xxlJobLogGlue.setAddTime(new Date());
        xxlJobLogGlue.setUpdateTime(new Date());
        xxlJobLogGlueDao.save(xxlJobLogGlue);
        // remove code backup more than 30
        xxlJobLogGlueDao.removeOld(exists_jobInfo.getId(), 30);
        return ReturnT.SUCCESS;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java
New file
@@ -0,0 +1,197 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobRegistry;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.dao.XxlJobInfoDao;
import com.xxl.job.admin.dao.XxlJobRegistryDao;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.RegistryConfig;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
 * job group controller
 * @author xuxueli 2016-10-02 20:52:56
 */
@Controller
@RequestMapping("/jobgroup")
public class JobGroupController {
    @Resource
    public XxlJobInfoDao xxlJobInfoDao;
    @Resource
    public XxlJobGroupDao xxlJobGroupDao;
    @Resource
    private XxlJobRegistryDao xxlJobRegistryDao;
    @RequestMapping
    public String index(Model model) {
        return "jobgroup/jobgroup.index";
    }
    @RequestMapping("/pageList")
    @ResponseBody
    public Map<String, Object> pageList(HttpServletRequest request,
                                        @RequestParam(required = false, defaultValue = "0") int start,
                                        @RequestParam(required = false, defaultValue = "10") int length,
                                        String appname, String title) {
        // page query
        List<XxlJobGroup> list = xxlJobGroupDao.pageList(start, length, appname, title);
        int list_count = xxlJobGroupDao.pageListCount(start, length, appname, title);
        // package result
        Map<String, Object> maps = new HashMap<String, Object>();
        maps.put("recordsTotal", list_count);        // 总记录数
        maps.put("recordsFiltered", list_count);    // 过滤后的总记录数
        maps.put("data", list);                      // 分页列表
        return maps;
    }
    @RequestMapping("/save")
    @ResponseBody
    public ReturnT<String> save(XxlJobGroup xxlJobGroup){
        // valid
        if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) {
            return new ReturnT<String>(500, (I18nUtil.getString("system_please_input")+"AppName") );
        }
        if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) {
            return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_appname_length") );
        }
        if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) {
            return new ReturnT<String>(500, "AppName"+I18nUtil.getString("system_unvalid") );
        }
        if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) {
            return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) );
        }
        if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) {
            return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") );
        }
        if (xxlJobGroup.getAddressType()!=0) {
            if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) {
                return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_addressType_limit") );
            }
            if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) {
                return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") );
            }
            String[] addresss = xxlJobGroup.getAddressList().split(",");
            for (String item: addresss) {
                if (item==null || item.trim().length()==0) {
                    return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") );
                }
            }
        }
        // process
        xxlJobGroup.setUpdateTime(new Date());
        int ret = xxlJobGroupDao.save(xxlJobGroup);
        return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL;
    }
    @RequestMapping("/update")
    @ResponseBody
    public ReturnT<String> update(XxlJobGroup xxlJobGroup){
        // valid
        if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) {
            return new ReturnT<String>(500, (I18nUtil.getString("system_please_input")+"AppName") );
        }
        if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) {
            return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_appname_length") );
        }
        if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) {
            return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) );
        }
        if (xxlJobGroup.getAddressType() == 0) {
            // 0=自动注册
            List<String> registryList = findRegistryByAppName(xxlJobGroup.getAppname());
            String addressListStr = null;
            if (registryList!=null && !registryList.isEmpty()) {
                Collections.sort(registryList);
                addressListStr = "";
                for (String item:registryList) {
                    addressListStr += item + ",";
                }
                addressListStr = addressListStr.substring(0, addressListStr.length()-1);
            }
            xxlJobGroup.setAddressList(addressListStr);
        } else {
            // 1=手动录入
            if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) {
                return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_addressType_limit") );
            }
            String[] addresss = xxlJobGroup.getAddressList().split(",");
            for (String item: addresss) {
                if (item==null || item.trim().length()==0) {
                    return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") );
                }
            }
        }
        // process
        xxlJobGroup.setUpdateTime(new Date());
        int ret = xxlJobGroupDao.update(xxlJobGroup);
        return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL;
    }
    private List<String> findRegistryByAppName(String appnameParam){
        HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
        List<XxlJobRegistry> list = xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
        if (list != null) {
            for (XxlJobRegistry item: list) {
                if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
                    String appname = item.getRegistryKey();
                    List<String> registryList = appAddressMap.get(appname);
                    if (registryList == null) {
                        registryList = new ArrayList<String>();
                    }
                    if (!registryList.contains(item.getRegistryValue())) {
                        registryList.add(item.getRegistryValue());
                    }
                    appAddressMap.put(appname, registryList);
                }
            }
        }
        return appAddressMap.get(appnameParam);
    }
    @RequestMapping("/remove")
    @ResponseBody
    public ReturnT<String> remove(int id){
        // valid
        int count = xxlJobInfoDao.pageListCount(0, 10, id, -1,  null, null, null);
        if (count > 0) {
            return new ReturnT<String>(500, I18nUtil.getString("jobgroup_del_limit_0") );
        }
        List<XxlJobGroup> allList = xxlJobGroupDao.findAll();
        if (allList.size() == 1) {
            return new ReturnT<String>(500, I18nUtil.getString("jobgroup_del_limit_1") );
        }
        int ret = xxlJobGroupDao.remove(id);
        return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL;
    }
    @RequestMapping("/loadById")
    @ResponseBody
    public ReturnT<XxlJobGroup> loadById(int id){
        XxlJobGroup jobGroup = xxlJobGroupDao.load(id);
        return jobGroup!=null?new ReturnT<XxlJobGroup>(jobGroup):new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java
New file
@@ -0,0 +1,180 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.core.cron.CronExpression;
import com.xxl.job.admin.core.exception.XxlJobException;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobUser;
import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.thread.JobScheduleHelper;
import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.service.LoginService;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.glue.GlueTypeEnum;
import com.xxl.job.core.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.util.*;
/**
 * index controller
 * @author xuxueli 2015-12-19 16:13:16
 */
@Controller
@RequestMapping("/jobinfo")
public class JobInfoController {
    private static Logger logger = LoggerFactory.getLogger(JobInfoController.class);
    @Resource
    private XxlJobGroupDao xxlJobGroupDao;
    @Resource
    private XxlJobService xxlJobService;
    @RequestMapping
    public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) {
        // 枚举-字典
        model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values());        // 路由策略-列表
        model.addAttribute("GlueTypeEnum", GlueTypeEnum.values());                                // Glue类型-字典
        model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values());        // 阻塞处理策略-字典
        model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values());                        // 调度类型
        model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values());                    // 调度过期策略
        // 执行器列表
        List<XxlJobGroup> jobGroupList_all =  xxlJobGroupDao.findAll();
        // filter group
        List<XxlJobGroup> jobGroupList = filterJobGroupByRole(request, jobGroupList_all);
        if (jobGroupList==null || jobGroupList.size()==0) {
            throw new XxlJobException(I18nUtil.getString("jobgroup_empty"));
        }
        model.addAttribute("JobGroupList", jobGroupList);
        model.addAttribute("jobGroup", jobGroup);
        return "jobinfo/jobinfo.index";
    }
    public static List<XxlJobGroup> filterJobGroupByRole(HttpServletRequest request, List<XxlJobGroup> jobGroupList_all){
        List<XxlJobGroup> jobGroupList = new ArrayList<>();
        if (jobGroupList_all!=null && jobGroupList_all.size()>0) {
            XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
            if (loginUser.getRole() == 1) {
                jobGroupList = jobGroupList_all;
            } else {
                List<String> groupIdStrs = new ArrayList<>();
                if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) {
                    groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(","));
                }
                for (XxlJobGroup groupItem:jobGroupList_all) {
                    if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) {
                        jobGroupList.add(groupItem);
                    }
                }
            }
        }
        return jobGroupList;
    }
    public static void validPermission(HttpServletRequest request, int jobGroup) {
        XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
        if (!loginUser.validPermission(jobGroup)) {
            throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]");
        }
    }
    @RequestMapping("/pageList")
    @ResponseBody
    public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start,
            @RequestParam(required = false, defaultValue = "10") int length,
            int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) {
        return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
    }
    @RequestMapping("/add")
    @ResponseBody
    public ReturnT<String> add(XxlJobInfo jobInfo) {
        return xxlJobService.add(jobInfo);
    }
    @RequestMapping("/update")
    @ResponseBody
    public ReturnT<String> update(XxlJobInfo jobInfo) {
        return xxlJobService.update(jobInfo);
    }
    @RequestMapping("/remove")
    @ResponseBody
    public ReturnT<String> remove(int id) {
        return xxlJobService.remove(id);
    }
    @RequestMapping("/stop")
    @ResponseBody
    public ReturnT<String> pause(int id) {
        return xxlJobService.stop(id);
    }
    @RequestMapping("/start")
    @ResponseBody
    public ReturnT<String> start(int id) {
        return xxlJobService.start(id);
    }
    @RequestMapping("/trigger")
    @ResponseBody
    //@PermissionLimit(limit = false)
    public ReturnT<String> triggerJob(int id, String executorParam, String addressList) {
        // force cover job param
        if (executorParam == null) {
            executorParam = "";
        }
        JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList);
        return ReturnT.SUCCESS;
    }
    @RequestMapping("/nextTriggerTime")
    @ResponseBody
    public ReturnT<List<String>> nextTriggerTime(String scheduleType, String scheduleConf) {
        XxlJobInfo paramXxlJobInfo = new XxlJobInfo();
        paramXxlJobInfo.setScheduleType(scheduleType);
        paramXxlJobInfo.setScheduleConf(scheduleConf);
        List<String> result = new ArrayList<>();
        try {
            Date lastTime = new Date();
            for (int i = 0; i < 5; i++) {
                lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime);
                if (lastTime != null) {
                    result.add(DateUtil.formatDateTime(lastTime));
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT<List<String>>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage());
        }
        return new ReturnT<List<String>>(result);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java
New file
@@ -0,0 +1,233 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.core.exception.XxlJobException;
import com.xxl.job.admin.core.complete.XxlJobCompleter;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.dao.XxlJobInfoDao;
import com.xxl.job.admin.dao.XxlJobLogDao;
import com.xxl.job.core.biz.ExecutorBiz;
import com.xxl.job.core.biz.model.KillParam;
import com.xxl.job.core.biz.model.LogParam;
import com.xxl.job.core.biz.model.LogResult;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * index controller
 * @author xuxueli 2015-12-19 16:13:16
 */
@Controller
@RequestMapping("/joblog")
public class JobLogController {
    private static Logger logger = LoggerFactory.getLogger(JobLogController.class);
    @Resource
    private XxlJobGroupDao xxlJobGroupDao;
    @Resource
    public XxlJobInfoDao xxlJobInfoDao;
    @Resource
    public XxlJobLogDao xxlJobLogDao;
    @RequestMapping
    public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "0") Integer jobId) {
        // 执行器列表
        List<XxlJobGroup> jobGroupList_all =  xxlJobGroupDao.findAll();
        // filter group
        List<XxlJobGroup> jobGroupList = JobInfoController.filterJobGroupByRole(request, jobGroupList_all);
        if (jobGroupList==null || jobGroupList.size()==0) {
            throw new XxlJobException(I18nUtil.getString("jobgroup_empty"));
        }
        model.addAttribute("JobGroupList", jobGroupList);
        // 任务
        if (jobId > 0) {
            XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId);
            if (jobInfo == null) {
                throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid"));
            }
            model.addAttribute("jobInfo", jobInfo);
            // valid permission
            JobInfoController.validPermission(request, jobInfo.getJobGroup());
        }
        return "joblog/joblog.index";
    }
    @RequestMapping("/getJobsByGroup")
    @ResponseBody
    public ReturnT<List<XxlJobInfo>> getJobsByGroup(int jobGroup){
        List<XxlJobInfo> list = xxlJobInfoDao.getJobsByGroup(jobGroup);
        return new ReturnT<List<XxlJobInfo>>(list);
    }
    @RequestMapping("/pageList")
    @ResponseBody
    public Map<String, Object> pageList(HttpServletRequest request,
                                        @RequestParam(required = false, defaultValue = "0") int start,
                                        @RequestParam(required = false, defaultValue = "10") int length,
                                        int jobGroup, int jobId, int logStatus, String filterTime) {
        // valid permission
        JobInfoController.validPermission(request, jobGroup);    // 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup
        // parse param
        Date triggerTimeStart = null;
        Date triggerTimeEnd = null;
        if (filterTime!=null && filterTime.trim().length()>0) {
            String[] temp = filterTime.split(" - ");
            if (temp.length == 2) {
                triggerTimeStart = DateUtil.parseDateTime(temp[0]);
                triggerTimeEnd = DateUtil.parseDateTime(temp[1]);
            }
        }
        // page query
        List<XxlJobLog> list = xxlJobLogDao.pageList(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus);
        int list_count = xxlJobLogDao.pageListCount(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus);
        // package result
        Map<String, Object> maps = new HashMap<String, Object>();
        maps.put("recordsTotal", list_count);        // 总记录数
        maps.put("recordsFiltered", list_count);    // 过滤后的总记录数
        maps.put("data", list);                      // 分页列表
        return maps;
    }
    @RequestMapping("/logDetailPage")
    public String logDetailPage(int id, Model model){
        // base check
        ReturnT<String> logStatue = ReturnT.SUCCESS;
        XxlJobLog jobLog = xxlJobLogDao.load(id);
        if (jobLog == null) {
            throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid"));
        }
        model.addAttribute("triggerCode", jobLog.getTriggerCode());
        model.addAttribute("handleCode", jobLog.getHandleCode());
        model.addAttribute("executorAddress", jobLog.getExecutorAddress());
        model.addAttribute("triggerTime", jobLog.getTriggerTime().getTime());
        model.addAttribute("logId", jobLog.getId());
        return "joblog/joblog.detail";
    }
    @RequestMapping("/logDetailCat")
    @ResponseBody
    public ReturnT<LogResult> logDetailCat(String executorAddress, long triggerTime, long logId, int fromLineNum){
        try {
            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(executorAddress);
            ReturnT<LogResult> logResult = executorBiz.log(new LogParam(triggerTime, logId, fromLineNum));
            // is end
            if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) {
                XxlJobLog jobLog = xxlJobLogDao.load(logId);
                if (jobLog.getHandleCode() > 0) {
                    logResult.getContent().setEnd(true);
                }
            }
            return logResult;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT<LogResult>(ReturnT.FAIL_CODE, e.getMessage());
        }
    }
    @RequestMapping("/logKill")
    @ResponseBody
    public ReturnT<String> logKill(int id){
        // base check
        XxlJobLog log = xxlJobLogDao.load(id);
        XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId());
        if (jobInfo==null) {
            return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
        }
        if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) {
            return new ReturnT<String>(500, I18nUtil.getString("joblog_kill_log_limit"));
        }
        // request of kill
        ReturnT<String> runResult = null;
        try {
            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress());
            runResult = executorBiz.kill(new KillParam(jobInfo.getId()));
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            runResult = new ReturnT<String>(500, e.getMessage());
        }
        if (ReturnT.SUCCESS_CODE == runResult.getCode()) {
            log.setHandleCode(ReturnT.FAIL_CODE);
            log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():""));
            log.setHandleTime(new Date());
            XxlJobCompleter.updateHandleInfoAndFinish(log);
            return new ReturnT<String>(runResult.getMsg());
        } else {
            return new ReturnT<String>(500, runResult.getMsg());
        }
    }
    @RequestMapping("/clearLog")
    @ResponseBody
    public ReturnT<String> clearLog(int jobGroup, int jobId, int type){
        Date clearBeforeTime = null;
        int clearBeforeNum = 0;
        if (type == 1) {
            clearBeforeTime = DateUtil.addMonths(new Date(), -1);    // 清理一个月之前日志数据
        } else if (type == 2) {
            clearBeforeTime = DateUtil.addMonths(new Date(), -3);    // 清理三个月之前日志数据
        } else if (type == 3) {
            clearBeforeTime = DateUtil.addMonths(new Date(), -6);    // 清理六个月之前日志数据
        } else if (type == 4) {
            clearBeforeTime = DateUtil.addYears(new Date(), -1);    // 清理一年之前日志数据
        } else if (type == 5) {
            clearBeforeNum = 1000;        // 清理一千条以前日志数据
        } else if (type == 6) {
            clearBeforeNum = 10000;        // 清理一万条以前日志数据
        } else if (type == 7) {
            clearBeforeNum = 30000;        // 清理三万条以前日志数据
        } else if (type == 8) {
            clearBeforeNum = 100000;    // 清理十万条以前日志数据
        } else if (type == 9) {
            clearBeforeNum = 0;            // 清理所有日志数据
        } else {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_clean_type_unvalid"));
        }
        List<Long> logIds = null;
        do {
            logIds = xxlJobLogDao.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000);
            if (logIds!=null && logIds.size()>0) {
                xxlJobLogDao.clearLog(logIds);
            }
        } while (logIds!=null && logIds.size()>0);
        return ReturnT.SUCCESS;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java
New file
@@ -0,0 +1,179 @@
package com.xxl.job.admin.controller;
import com.xxl.job.admin.controller.annotation.PermissionLimit;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobUser;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.dao.XxlJobUserDao;
import com.xxl.job.admin.service.LoginService;
import com.xxl.job.core.biz.model.ReturnT;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @author xuxueli 2019-05-04 16:39:50
 */
@Controller
@RequestMapping("/user")
public class UserController {
    @Resource
    private XxlJobUserDao xxlJobUserDao;
    @Resource
    private XxlJobGroupDao xxlJobGroupDao;
    @RequestMapping
    @PermissionLimit(adminuser = true)
    public String index(Model model) {
        // 执行器列表
        List<XxlJobGroup> groupList = xxlJobGroupDao.findAll();
        model.addAttribute("groupList", groupList);
        return "user/user.index";
    }
    @RequestMapping("/pageList")
    @ResponseBody
    @PermissionLimit(adminuser = true)
    public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start,
                                        @RequestParam(required = false, defaultValue = "10") int length,
                                        String username, int role) {
        // page list
        List<XxlJobUser> list = xxlJobUserDao.pageList(start, length, username, role);
        int list_count = xxlJobUserDao.pageListCount(start, length, username, role);
        // filter
        if (list!=null && list.size()>0) {
            for (XxlJobUser item: list) {
                item.setPassword(null);
            }
        }
        // package result
        Map<String, Object> maps = new HashMap<String, Object>();
        maps.put("recordsTotal", list_count);        // 总记录数
        maps.put("recordsFiltered", list_count);    // 过滤后的总记录数
        maps.put("data", list);                      // 分页列表
        return maps;
    }
    @RequestMapping("/add")
    @ResponseBody
    @PermissionLimit(adminuser = true)
    public ReturnT<String> add(XxlJobUser xxlJobUser) {
        // valid username
        if (!StringUtils.hasText(xxlJobUser.getUsername())) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") );
        }
        xxlJobUser.setUsername(xxlJobUser.getUsername().trim());
        if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
        }
        // valid password
        if (!StringUtils.hasText(xxlJobUser.getPassword())) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") );
        }
        xxlJobUser.setPassword(xxlJobUser.getPassword().trim());
        if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
        }
        // md5 password
        xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes()));
        // check repeat
        XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername());
        if (existUser != null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") );
        }
        // write
        xxlJobUserDao.save(xxlJobUser);
        return ReturnT.SUCCESS;
    }
    @RequestMapping("/update")
    @ResponseBody
    @PermissionLimit(adminuser = true)
    public ReturnT<String> update(HttpServletRequest request, XxlJobUser xxlJobUser) {
        // avoid opt login seft
        XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
        if (loginUser.getUsername().equals(xxlJobUser.getUsername())) {
            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit"));
        }
        // valid password
        if (StringUtils.hasText(xxlJobUser.getPassword())) {
            xxlJobUser.setPassword(xxlJobUser.getPassword().trim());
            if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
            }
            // md5 password
            xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes()));
        } else {
            xxlJobUser.setPassword(null);
        }
        // write
        xxlJobUserDao.update(xxlJobUser);
        return ReturnT.SUCCESS;
    }
    @RequestMapping("/remove")
    @ResponseBody
    @PermissionLimit(adminuser = true)
    public ReturnT<String> remove(HttpServletRequest request, int id) {
        // avoid opt login seft
        XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
        if (loginUser.getId() == id) {
            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit"));
        }
        xxlJobUserDao.delete(id);
        return ReturnT.SUCCESS;
    }
    @RequestMapping("/updatePwd")
    @ResponseBody
    public ReturnT<String> updatePwd(HttpServletRequest request, String password){
        // valid password
        if (password==null || password.trim().length()==0){
            return new ReturnT<String>(ReturnT.FAIL.getCode(), "密码不可为空");
        }
        password = password.trim();
        if (!(password.length()>=4 && password.length()<=20)) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
        }
        // md5 password
        String md5Password = DigestUtils.md5DigestAsHex(password.getBytes());
        // update pwd
        XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
        // do write
        XxlJobUser existUser = xxlJobUserDao.loadByUserName(loginUser.getUsername());
        existUser.setPassword(md5Password);
        xxlJobUserDao.update(existUser);
        return ReturnT.SUCCESS;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java
New file
@@ -0,0 +1,29 @@
package com.xxl.job.admin.controller.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 权限限制
 * @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;
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java
New file
@@ -0,0 +1,42 @@
package com.xxl.job.admin.controller.interceptor;
import com.xxl.job.admin.core.util.FtlUtil;
import com.xxl.job.admin.core.util.I18nUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
/**
 * push cookies to model as cookieMap
 *
 * @author xuxueli 2015-12-12 18:09:04
 */
@Component
public class CookieInterceptor implements AsyncHandlerInterceptor {
    @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()));
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java
New file
@@ -0,0 +1,59 @@
package com.xxl.job.admin.controller.interceptor;
import com.xxl.job.admin.controller.annotation.PermissionLimit;
import com.xxl.job.admin.core.model.XxlJobUser;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.service.LoginService;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 权限拦截
 *
 * @author xuxueli 2015-12-12 18:09:04
 */
@Component
public class PermissionInterceptor implements AsyncHandlerInterceptor {
    @Resource
    private LoginService loginService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;    // proceed with the next interceptor
        }
        // 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.setStatus(302);
                response.setHeader("location", request.getContextPath()+"/toLogin");
                return false;
            }
            if (needAdminuser && loginUser.getRole()!=1) {
                throw new RuntimeException(I18nUtil.getString("system_permission_limit"));
            }
            request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser);
        }
        return true;    // proceed with the next interceptor
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java
New file
@@ -0,0 +1,28 @@
package com.xxl.job.admin.controller.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
 * web mvc config
 *
 * @author xuxueli 2018-04-02 20:48:20
 */
@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("/**");
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java
New file
@@ -0,0 +1,66 @@
package com.xxl.job.admin.controller.resolver;
import com.xxl.job.admin.core.exception.XxlJobException;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.admin.core.util.JacksonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * common exception resolver
 *
 * @author xuxueli 2016-1-6 19:22:18
 */
@Component
public class WebExceptionResolver implements HandlerExceptionResolver {
    private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class);
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex) {
        if (!(ex instanceof XxlJobException)) {
            logger.error("WebExceptionResolver:{}", ex);
        }
        // if json
        boolean isJson = false;
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod)handler;
            ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class);
            if (responseBody != null) {
                isJson = true;
            }
        }
        // error result
        ReturnT<String> errorResult = new ReturnT<String>(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "<br/>"));
        // response
        ModelAndView mv = new ModelAndView();
        if (isJson) {
            try {
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().print(JacksonUtil.writeValueAsString(errorResult));
            } catch (IOException e) {
                logger.error(e.getMessage(), e);
            }
            return mv;
        } else {
            mv.addObject("exceptionMsg", errorResult.getMsg());
            mv.setViewName("/common/common.exception");
            return mv;
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java
New file
@@ -0,0 +1,20 @@
package com.xxl.job.admin.core.alarm;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
/**
 * @author xuxueli 2020-01-19
 */
public interface JobAlarm {
    /**
     * job alarm
     *
     * @param info
     * @param jobLog
     * @return
     */
    public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java
New file
@@ -0,0 +1,65 @@
package com.xxl.job.admin.core.alarm;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class JobAlarmer implements ApplicationContextAware, InitializingBean {
    private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class);
    private ApplicationContext applicationContext;
    private List<JobAlarm> jobAlarmList;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, JobAlarm> serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class);
        if (serviceBeanMap != null && serviceBeanMap.size() > 0) {
            jobAlarmList = new ArrayList<JobAlarm>(serviceBeanMap.values());
        }
    }
    /**
     * job alarm
     *
     * @param info
     * @param jobLog
     * @return
     */
    public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) {
        boolean result = false;
        if (jobAlarmList!=null && jobAlarmList.size()>0) {
            result = true;  // success means all-success
            for (JobAlarm alarm: jobAlarmList) {
                boolean resultItem = false;
                try {
                    resultItem = alarm.doAlarm(info, jobLog);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
                if (!resultItem) {
                    result = false;
                }
            }
        }
        return result;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java
New file
@@ -0,0 +1,118 @@
package com.xxl.job.admin.core.alarm.impl;
import com.xxl.job.admin.core.alarm.JobAlarm;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.model.ReturnT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.mail.internet.MimeMessage;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
 * job alarm by email
 *
 * @author xuxueli 2020-01-19
 */
@Component
public class EmailJobAlarm implements JobAlarm {
    private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class);
    /**
     * fail alarm
     *
     * @param jobLog
     */
    @Override
    public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){
        boolean alarmResult = true;
        // send monitor email
        if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) {
            // alarmContent
            String alarmContent = "Alarm Job LogId=" + jobLog.getId();
            if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) {
                alarmContent += "<br>TriggerMsg=<br>" + jobLog.getTriggerMsg();
            }
            if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) {
                alarmContent += "<br>HandleCode=" + jobLog.getHandleMsg();
            }
            // email info
            XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(Integer.valueOf(info.getJobGroup()));
            String personal = I18nUtil.getString("admin_name_full");
            String title = I18nUtil.getString("jobconf_monitor");
            String content = MessageFormat.format(loadEmailJobAlarmTemplate(),
                    group!=null?group.getTitle():"null",
                    info.getId(),
                    info.getJobDesc(),
                    alarmContent);
            Set<String> emailSet = new HashSet<String>(Arrays.asList(info.getAlarmEmail().split(",")));
            for (String email: emailSet) {
                // make mail
                try {
                    MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage();
                    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
                    helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal);
                    helper.setTo(email);
                    helper.setSubject(title);
                    helper.setText(content, true);
                    XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage);
                } catch (Exception e) {
                    logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e);
                    alarmResult = false;
                }
            }
        }
        return alarmResult;
    }
    /**
     * load email job alarm template
     *
     * @return
     */
    private static final String loadEmailJobAlarmTemplate(){
        String mailBodyTemplate = "<h5>" + I18nUtil.getString("jobconf_monitor_detail") + ":</span>" +
                "<table border=\"1\" cellpadding=\"3\" style=\"border-collapse:collapse; width:80%;\" >\n" +
                "   <thead style=\"font-weight: bold;color: #ffffff;background-color: #ff8c00;\" >" +
                "      <tr>\n" +
                "         <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobgroup") +"</td>\n" +
                "         <td width=\"10%\" >"+ I18nUtil.getString("jobinfo_field_id") +"</td>\n" +
                "         <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobdesc") +"</td>\n" +
                "         <td width=\"10%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_title") +"</td>\n" +
                "         <td width=\"40%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_content") +"</td>\n" +
                "      </tr>\n" +
                "   </thead>\n" +
                "   <tbody>\n" +
                "      <tr>\n" +
                "         <td>{0}</td>\n" +
                "         <td>{1}</td>\n" +
                "         <td>{2}</td>\n" +
                "         <td>"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"</td>\n" +
                "         <td>{3}</td>\n" +
                "      </tr>\n" +
                "   </tbody>\n" +
                "</table>";
        return mailBodyTemplate;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java
New file
@@ -0,0 +1,99 @@
package com.xxl.job.admin.core.complete;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.context.XxlJobContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
/**
 * @author xuxueli 2020-10-30 20:43:10
 */
public class XxlJobCompleter {
    private static Logger logger = LoggerFactory.getLogger(XxlJobCompleter.class);
    /**
     * common fresh handle entrance (limit only once)
     *
     * @param xxlJobLog
     * @return
     */
    public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) {
        // finish
        finishJob(xxlJobLog);
        // text最大64kb 避免长度过长
        if (xxlJobLog.getHandleMsg().length() > 15000) {
            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) );
        }
        // fresh handle
        return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog);
    }
    /**
     * do somethind to finish job
     */
    private static void finishJob(XxlJobLog xxlJobLog){
        // 1、handle success, to trigger child job
        String triggerChildMsg = null;
        if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) {
            XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId());
            if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) {
                triggerChildMsg = "<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
                        triggerChildMsg += 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 {
                        triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"),
                                (i+1),
                                childJobIds.length,
                                childJobIds[i]);
                    }
                }
            }
        }
        if (triggerChildMsg != null) {
            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg );
        }
        // 2、fix_delay trigger next
        // on the way
    }
    private static boolean isNumeric(String str){
        try {
            int result = Integer.valueOf(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java
New file
@@ -0,0 +1,158 @@
package com.xxl.job.admin.core.conf;
import com.xxl.job.admin.core.alarm.JobAlarmer;
import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
import com.xxl.job.admin.dao.*;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Arrays;
/**
 * xxl-job config
 *
 * @author xuxueli 2017-04-28
 */
@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
    private static XxlJobAdminConfig adminConfig = null;
    public static XxlJobAdminConfig getAdminConfig() {
        return adminConfig;
    }
    // ---------------------- XxlJobScheduler ----------------------
    private XxlJobScheduler xxlJobScheduler;
    @Override
    public void afterPropertiesSet() throws Exception {
        adminConfig = this;
        xxlJobScheduler = new XxlJobScheduler();
        xxlJobScheduler.init();
    }
    @Override
    public void destroy() throws Exception {
        xxlJobScheduler.destroy();
    }
    // ---------------------- XxlJobScheduler ----------------------
    // conf
    @Value("${xxl.job.i18n}")
    private String i18n;
    @Value("${xxl.job.accessToken}")
    private String accessToken;
    @Value("${spring.mail.from}")
    private String emailFrom;
    @Value("${xxl.job.triggerpool.fast.max}")
    private int triggerPoolFastMax;
    @Value("${xxl.job.triggerpool.slow.max}")
    private int triggerPoolSlowMax;
    @Value("${xxl.job.logretentiondays}")
    private int logretentiondays;
    // dao, service
    @Resource
    private XxlJobLogDao xxlJobLogDao;
    @Resource
    private XxlJobInfoDao xxlJobInfoDao;
    @Resource
    private XxlJobRegistryDao xxlJobRegistryDao;
    @Resource
    private XxlJobGroupDao xxlJobGroupDao;
    @Resource
    private XxlJobLogReportDao xxlJobLogReportDao;
    @Resource
    private JavaMailSender mailSender;
    @Resource
    private DataSource dataSource;
    @Resource
    private JobAlarmer jobAlarmer;
    public String getI18n() {
        if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) {
            return "zh_CN";
        }
        return i18n;
    }
    public String getAccessToken() {
        return accessToken;
    }
    public String getEmailFrom() {
        return emailFrom;
    }
    public int getTriggerPoolFastMax() {
        if (triggerPoolFastMax < 200) {
            return 200;
        }
        return triggerPoolFastMax;
    }
    public int getTriggerPoolSlowMax() {
        if (triggerPoolSlowMax < 100) {
            return 100;
        }
        return triggerPoolSlowMax;
    }
    public int getLogretentiondays() {
        if (logretentiondays < 7) {
            return -1;  // Limit greater than or equal to 7, otherwise close
        }
        return logretentiondays;
    }
    public XxlJobLogDao getXxlJobLogDao() {
        return xxlJobLogDao;
    }
    public XxlJobInfoDao getXxlJobInfoDao() {
        return xxlJobInfoDao;
    }
    public XxlJobRegistryDao getXxlJobRegistryDao() {
        return xxlJobRegistryDao;
    }
    public XxlJobGroupDao getXxlJobGroupDao() {
        return xxlJobGroupDao;
    }
    public XxlJobLogReportDao getXxlJobLogReportDao() {
        return xxlJobLogReportDao;
    }
    public JavaMailSender getMailSender() {
        return mailSender;
    }
    public DataSource getDataSource() {
        return dataSource;
    }
    public JobAlarmer getJobAlarmer() {
        return jobAlarmer;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
New file
@@ -0,0 +1,1666 @@
/*
 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 */
package com.xxl.job.admin.core.cron;
import java.io.Serializable;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
/**
 * Provides a parser and evaluator for unix-like cron expressions. Cron
 * expressions provide the ability to specify complex time combinations such as
 * &quot;At 8:00am every Monday through Friday&quot; or &quot;At 1:30am every
 * last Friday of the month&quot;.
 * <P>
 * Cron expressions are comprised of 6 required fields and one optional field
 * separated by white space. The fields respectively are described as follows:
 *
 * <table cellspacing="8">
 * <tr>
 * <th align="left">Field Name</th>
 * <th align="left">&nbsp;</th>
 * <th align="left">Allowed Values</th>
 * <th align="left">&nbsp;</th>
 * <th align="left">Allowed Special Characters</th>
 * </tr>
 * <tr>
 * <td align="left"><code>Seconds</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-59</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Minutes</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-59</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Hours</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-23</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Day-of-month</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>1-31</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * ? / L W</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Month</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-11 or JAN-DEC</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Day-of-Week</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>1-7 or SUN-SAT</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * ? / L #</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Year (Optional)</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>empty, 1970-2199</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * </table>
 * <P>
 * The '*' character is used to specify all values. For example, &quot;*&quot;
 * in the minute field means &quot;every minute&quot;.
 * <P>
 * The '?' character is allowed for the day-of-month and day-of-week fields. It
 * is used to specify 'no specific value'. This is useful when you need to
 * specify something in one of the two fields, but not the other.
 * <P>
 * The '-' character is used to specify ranges For example &quot;10-12&quot; in
 * the hour field means &quot;the hours 10, 11 and 12&quot;.
 * <P>
 * The ',' character is used to specify additional values. For example
 * &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
 * Wednesday, and Friday&quot;.
 * <P>
 * The '/' character is used to specify increments. For example &quot;0/15&quot;
 * in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And
 * &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
 * 50&quot;.  Specifying '*' before the  '/' is equivalent to specifying 0 is
 * the value to start with. Essentially, for each field in the expression, there
 * is a set of numbers that can be turned on or off. For seconds and minutes,
 * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
 * 31, and for months 0 to 11 (JAN to DEC). The &quot;/&quot; character simply helps you turn
 * on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
 * month field only turns on month &quot;7&quot;, it does NOT mean every 6th
 * month, please note that subtlety.
 * <P>
 * The 'L' character is allowed for the day-of-month and day-of-week fields.
 * This character is short-hand for &quot;last&quot;, but it has different
 * meaning in each of the two fields. For example, the value &quot;L&quot; in
 * the day-of-month field means &quot;the last day of the month&quot; - day 31
 * for January, day 28 for February on non-leap years. If used in the
 * day-of-week field by itself, it simply means &quot;7&quot; or
 * &quot;SAT&quot;. But if used in the day-of-week field after another value, it
 * means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
 * means &quot;the last friday of the month&quot;. You can also specify an offset
 * from the last day of the month, such as "L-3" which would mean the third-to-last
 * day of the calendar month. <i>When using the 'L' option, it is important not to
 * specify lists, or ranges of values, as you'll get confusing/unexpected results.</i>
 * <P>
 * The 'W' character is allowed for the day-of-month field.  This character
 * is used to specify the weekday (Monday-Friday) nearest the given day.  As an
 * example, if you were to specify &quot;15W&quot; as the value for the
 * day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
 * the month&quot;. So if the 15th is a Saturday, the trigger will fire on
 * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
 * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
 * However if you specify &quot;1W&quot; as the value for day-of-month, and the
 * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
 * 'jump' over the boundary of a month's days.  The 'W' character can only be
 * specified when the day-of-month is a single day, not a range or list of days.
 * <P>
 * The 'L' and 'W' characters can also be combined for the day-of-month
 * expression to yield 'LW', which translates to &quot;last weekday of the
 * month&quot;.
 * <P>
 * The '#' character is allowed for the day-of-week field. This character is
 * used to specify &quot;the nth&quot; XXX day of the month. For example, the
 * value of &quot;6#3&quot; in the day-of-week field means the third Friday of
 * the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month).
 * Other examples: &quot;2#1&quot; = the first Monday of the month and
 * &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
 * &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
 * no firing will occur that month.  If the '#' character is used, there can
 * only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is
 * not valid, since there are two expressions).
 * <P>
 * <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
 * This character is short-hand for "calendar". This means values are
 * calculated against the associated calendar, if any. If no calendar is
 * associated, then it is equivalent to having an all-inclusive calendar. A
 * value of "5C" in the day-of-month field means "the first day included by the
 * calendar on or after the 5th". A value of "1C" in the day-of-week field
 * means "the first day included by the calendar on or after Sunday".-->
 * <P>
 * The legal characters and the names of months and days of the week are not
 * case sensitive.
 *
 * <p>
 * <b>NOTES:</b>
 * <ul>
 * <li>Support for specifying both a day-of-week and a day-of-month value is
 * not complete (you'll need to use the '?' character in one of these fields).
 * </li>
 * <li>Overflowing ranges is supported - that is, having a larger number on
 * the left hand side than the right. You might do 22-2 to catch 10 o'clock
 * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
 * very important to note that overuse of overflowing ranges creates ranges
 * that don't make sense and no effort has been made to determine which
 * interpretation CronExpression chooses. An example would be
 * "0 0 14-6 ? * FRI-MON". </li>
 * </ul>
 * </p>
 *
 *
 * @author Sharada Jambula, James House
 * @author Contributions from Mads Henderson
 * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
 *
 * Borrowed from quartz v2.3.1
 *
 */
public final class CronExpression implements Serializable, Cloneable {
    private static final long serialVersionUID = 12423409423L;
    protected static final int SECOND = 0;
    protected static final int MINUTE = 1;
    protected static final int HOUR = 2;
    protected static final int DAY_OF_MONTH = 3;
    protected static final int MONTH = 4;
    protected static final int DAY_OF_WEEK = 5;
    protected static final int YEAR = 6;
    protected static final int ALL_SPEC_INT = 99; // '*'
    protected static final int NO_SPEC_INT = 98; // '?'
    protected static final Integer ALL_SPEC = ALL_SPEC_INT;
    protected static final Integer NO_SPEC = NO_SPEC_INT;
    protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20);
    protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60);
    static {
        monthMap.put("JAN", 0);
        monthMap.put("FEB", 1);
        monthMap.put("MAR", 2);
        monthMap.put("APR", 3);
        monthMap.put("MAY", 4);
        monthMap.put("JUN", 5);
        monthMap.put("JUL", 6);
        monthMap.put("AUG", 7);
        monthMap.put("SEP", 8);
        monthMap.put("OCT", 9);
        monthMap.put("NOV", 10);
        monthMap.put("DEC", 11);
        dayMap.put("SUN", 1);
        dayMap.put("MON", 2);
        dayMap.put("TUE", 3);
        dayMap.put("WED", 4);
        dayMap.put("THU", 5);
        dayMap.put("FRI", 6);
        dayMap.put("SAT", 7);
    }
    private final String cronExpression;
    private TimeZone timeZone = null;
    protected transient TreeSet<Integer> seconds;
    protected transient TreeSet<Integer> minutes;
    protected transient TreeSet<Integer> hours;
    protected transient TreeSet<Integer> daysOfMonth;
    protected transient TreeSet<Integer> months;
    protected transient TreeSet<Integer> daysOfWeek;
    protected transient TreeSet<Integer> years;
    protected transient boolean lastdayOfWeek = false;
    protected transient int nthdayOfWeek = 0;
    protected transient boolean lastdayOfMonth = false;
    protected transient boolean nearestWeekday = false;
    protected transient int lastdayOffset = 0;
    protected transient boolean expressionParsed = false;
    public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
    /**
     * Constructs a new <CODE>CronExpression</CODE> based on the specified
     * parameter.
     *
     * @param cronExpression String representation of the cron expression the
     *                       new object should represent
     * @throws java.text.ParseException
     *         if the string expression cannot be parsed into a valid
     *         <CODE>CronExpression</CODE>
     */
    public CronExpression(String cronExpression) throws ParseException {
        if (cronExpression == null) {
            throw new IllegalArgumentException("cronExpression cannot be null");
        }
        this.cronExpression = cronExpression.toUpperCase(Locale.US);
        buildExpression(this.cronExpression);
    }
    /**
     * Constructs a new {@code CronExpression} as a copy of an existing
     * instance.
     *
     * @param expression
     *            The existing cron expression to be copied
     */
    public CronExpression(CronExpression expression) {
        /*
         * We don't call the other constructor here since we need to swallow the
         * ParseException. We also elide some of the sanity checking as it is
         * not logically trippable.
         */
        this.cronExpression = expression.getCronExpression();
        try {
            buildExpression(cronExpression);
        } catch (ParseException ex) {
            throw new AssertionError();
        }
        if (expression.getTimeZone() != null) {
            setTimeZone((TimeZone) expression.getTimeZone().clone());
        }
    }
    /**
     * Indicates whether the given date satisfies the cron expression. Note that
     * milliseconds are ignored, so two Dates falling on different milliseconds
     * of the same second will always have the same result here.
     *
     * @param date the date to evaluate
     * @return a boolean indicating whether the given date satisfies the cron
     *         expression
     */
    public boolean isSatisfiedBy(Date date) {
        Calendar testDateCal = Calendar.getInstance(getTimeZone());
        testDateCal.setTime(date);
        testDateCal.set(Calendar.MILLISECOND, 0);
        Date originalDate = testDateCal.getTime();
        testDateCal.add(Calendar.SECOND, -1);
        Date timeAfter = getTimeAfter(testDateCal.getTime());
        return ((timeAfter != null) && (timeAfter.equals(originalDate)));
    }
    /**
     * Returns the next date/time <I>after</I> the given date/time which
     * satisfies the cron expression.
     *
     * @param date the date/time at which to begin the search for the next valid
     *             date/time
     * @return the next valid date/time
     */
    public Date getNextValidTimeAfter(Date date) {
        return getTimeAfter(date);
    }
    /**
     * Returns the next date/time <I>after</I> the given date/time which does
     * <I>not</I> satisfy the expression
     *
     * @param date the date/time at which to begin the search for the next
     *             invalid date/time
     * @return the next valid date/time
     */
    public Date getNextInvalidTimeAfter(Date date) {
        long difference = 1000;
        //move back to the nearest second so differences will be accurate
        Calendar adjustCal = Calendar.getInstance(getTimeZone());
        adjustCal.setTime(date);
        adjustCal.set(Calendar.MILLISECOND, 0);
        Date lastDate = adjustCal.getTime();
        Date newDate;
        //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
        //keep getting the next included time until it's farther than one second
        // apart. At that point, lastDate is the last valid fire time. We return
        // the second immediately following it.
        while (difference == 1000) {
            newDate = getTimeAfter(lastDate);
            if(newDate == null)
                break;
            difference = newDate.getTime() - lastDate.getTime();
            if (difference == 1000) {
                lastDate = newDate;
            }
        }
        return new Date(lastDate.getTime() + 1000);
    }
    /**
     * Returns the time zone for which this <code>CronExpression</code>
     * will be resolved.
     */
    public TimeZone getTimeZone() {
        if (timeZone == null) {
            timeZone = TimeZone.getDefault();
        }
        return timeZone;
    }
    /**
     * Sets the time zone for which  this <code>CronExpression</code>
     * will be resolved.
     */
    public void setTimeZone(TimeZone timeZone) {
        this.timeZone = timeZone;
    }
    /**
     * Returns the string representation of the <CODE>CronExpression</CODE>
     *
     * @return a string representation of the <CODE>CronExpression</CODE>
     */
    @Override
    public String toString() {
        return cronExpression;
    }
    /**
     * Indicates whether the specified cron expression can be parsed into a
     * valid cron expression
     *
     * @param cronExpression the expression to evaluate
     * @return a boolean indicating whether the given expression is a valid cron
     *         expression
     */
    public static boolean isValidExpression(String cronExpression) {
        try {
            new CronExpression(cronExpression);
        } catch (ParseException pe) {
            return false;
        }
        return true;
    }
    public static void validateExpression(String cronExpression) throws ParseException {
        new CronExpression(cronExpression);
    }
    ////////////////////////////////////////////////////////////////////////////
    //
    // Expression Parsing Functions
    //
    ////////////////////////////////////////////////////////////////////////////
    protected void buildExpression(String expression) throws ParseException {
        expressionParsed = true;
        try {
            if (seconds == null) {
                seconds = new TreeSet<Integer>();
            }
            if (minutes == null) {
                minutes = new TreeSet<Integer>();
            }
            if (hours == null) {
                hours = new TreeSet<Integer>();
            }
            if (daysOfMonth == null) {
                daysOfMonth = new TreeSet<Integer>();
            }
            if (months == null) {
                months = new TreeSet<Integer>();
            }
            if (daysOfWeek == null) {
                daysOfWeek = new TreeSet<Integer>();
            }
            if (years == null) {
                years = new TreeSet<Integer>();
            }
            int exprOn = SECOND;
            StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
                    false);
            while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
                String expr = exprsTok.nextToken().trim();
                // throw an exception if L is used with other days of the month
                if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
                    throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
                }
                // throw an exception if L is used with other days of the week
                if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1  && expr.contains(",")) {
                    throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
                }
                if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) {
                    throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1);
                }
                StringTokenizer vTok = new StringTokenizer(expr, ",");
                while (vTok.hasMoreTokens()) {
                    String v = vTok.nextToken();
                    storeExpressionVals(0, v, exprOn);
                }
                exprOn++;
            }
            if (exprOn <= DAY_OF_WEEK) {
                throw new ParseException("Unexpected end of expression.",
                            expression.length());
            }
            if (exprOn <= YEAR) {
                storeExpressionVals(0, "*", YEAR);
            }
            TreeSet<Integer> dow = getSet(DAY_OF_WEEK);
            TreeSet<Integer> dom = getSet(DAY_OF_MONTH);
            // Copying the logic from the UnsupportedOperationException below
            boolean dayOfMSpec = !dom.contains(NO_SPEC);
            boolean dayOfWSpec = !dow.contains(NO_SPEC);
            if (!dayOfMSpec || dayOfWSpec) {
                if (!dayOfWSpec || dayOfMSpec) {
                    throw new ParseException(
                            "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);
                }
            }
        } catch (ParseException pe) {
            throw pe;
        } catch (Exception e) {
            throw new ParseException("Illegal cron expression format ("
                    + e.toString() + ")", 0);
        }
    }
    protected int storeExpressionVals(int pos, String s, int type)
        throws ParseException {
        int incr = 0;
        int i = skipWhiteSpace(pos, s);
        if (i >= s.length()) {
            return i;
        }
        char c = s.charAt(i);
        if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) {
            String sub = s.substring(i, i + 3);
            int sval = -1;
            int eval = -1;
            if (type == MONTH) {
                sval = getMonthNumber(sub) + 1;
                if (sval <= 0) {
                    throw new ParseException("Invalid Month value: '" + sub + "'", i);
                }
                if (s.length() > i + 3) {
                    c = s.charAt(i + 3);
                    if (c == '-') {
                        i += 4;
                        sub = s.substring(i, i + 3);
                        eval = getMonthNumber(sub) + 1;
                        if (eval <= 0) {
                            throw new ParseException("Invalid Month value: '" + sub + "'", i);
                        }
                    }
                }
            } else if (type == DAY_OF_WEEK) {
                sval = getDayOfWeekNumber(sub);
                if (sval < 0) {
                    throw new ParseException("Invalid Day-of-Week value: '"
                                + sub + "'", i);
                }
                if (s.length() > i + 3) {
                    c = s.charAt(i + 3);
                    if (c == '-') {
                        i += 4;
                        sub = s.substring(i, i + 3);
                        eval = getDayOfWeekNumber(sub);
                        if (eval < 0) {
                            throw new ParseException(
                                    "Invalid Day-of-Week value: '" + sub
                                        + "'", i);
                        }
                    } else if (c == '#') {
                        try {
                            i += 4;
                            nthdayOfWeek = Integer.parseInt(s.substring(i));
                            if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
                                throw new Exception();
                            }
                        } catch (Exception e) {
                            throw new ParseException(
                                    "A numeric value between 1 and 5 must follow the '#' option",
                                    i);
                        }
                    } else if (c == 'L') {
                        lastdayOfWeek = true;
                        i++;
                    }
                }
            } else {
                throw new ParseException(
                        "Illegal characters for this position: '" + sub + "'",
                        i);
            }
            if (eval != -1) {
                incr = 1;
            }
            addToSet(sval, eval, incr, type);
            return (i + 3);
        }
        if (c == '?') {
            i++;
            if ((i + 1) < s.length()
                    && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
                throw new ParseException("Illegal character after '?': "
                            + s.charAt(i), i);
            }
            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
                throw new ParseException(
                            "'?' can only be specified for Day-of-Month or Day-of-Week.",
                            i);
            }
            if (type == DAY_OF_WEEK && !lastdayOfMonth) {
                int val = daysOfMonth.last();
                if (val == NO_SPEC_INT) {
                    throw new ParseException(
                                "'?' can only be specified for Day-of-Month -OR- Day-of-Week.",
                                i);
                }
            }
            addToSet(NO_SPEC_INT, -1, 0, type);
            return i;
        }
        if (c == '*' || c == '/') {
            if (c == '*' && (i + 1) >= s.length()) {
                addToSet(ALL_SPEC_INT, -1, incr, type);
                return i + 1;
            } else if (c == '/'
                    && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
                            .charAt(i + 1) == '\t')) {
                throw new ParseException("'/' must be followed by an integer.", i);
            } else if (c == '*') {
                i++;
            }
            c = s.charAt(i);
            if (c == '/') { // is an increment specified?
                i++;
                if (i >= s.length()) {
                    throw new ParseException("Unexpected end of string.", i);
                }
                incr = getNumericValue(s, i);
                i++;
                if (incr > 10) {
                    i++;
                }
                checkIncrementRange(incr, type, i);
            } else {
                incr = 1;
            }
            addToSet(ALL_SPEC_INT, -1, incr, type);
            return i;
        } else if (c == 'L') {
            i++;
            if (type == DAY_OF_MONTH) {
                lastdayOfMonth = true;
            }
            if (type == DAY_OF_WEEK) {
                addToSet(7, 7, 0, type);
            }
            if(type == DAY_OF_MONTH && s.length() > i) {
                c = s.charAt(i);
                if(c == '-') {
                    ValueSet vs = getValue(0, s, i+1);
                    lastdayOffset = vs.value;
                    if(lastdayOffset > 30)
                        throw new ParseException("Offset from last day must be <= 30", i+1);
                    i = vs.pos;
                }
                if(s.length() > i) {
                    c = s.charAt(i);
                    if(c == 'W') {
                        nearestWeekday = true;
                        i++;
                    }
                }
            }
            return i;
        } else if (c >= '0' && c <= '9') {
            int val = Integer.parseInt(String.valueOf(c));
            i++;
            if (i >= s.length()) {
                addToSet(val, -1, -1, type);
            } else {
                c = s.charAt(i);
                if (c >= '0' && c <= '9') {
                    ValueSet vs = getValue(val, s, i);
                    val = vs.value;
                    i = vs.pos;
                }
                i = checkNext(i, s, val, type);
                return i;
            }
        } else {
            throw new ParseException("Unexpected character: " + c, i);
        }
        return i;
    }
    private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException {
        if (incr > 59 && (type == SECOND || type == MINUTE)) {
            throw new ParseException("Increment > 60 : " + incr, idxPos);
        } else if (incr > 23 && (type == HOUR)) {
            throw new ParseException("Increment > 24 : " + incr, idxPos);
        } else if (incr > 31 && (type == DAY_OF_MONTH)) {
            throw new ParseException("Increment > 31 : " + incr, idxPos);
        } else if (incr > 7 && (type == DAY_OF_WEEK)) {
            throw new ParseException("Increment > 7 : " + incr, idxPos);
        } else if (incr > 12 && (type == MONTH)) {
            throw new ParseException("Increment > 12 : " + incr, idxPos);
        }
    }
    protected int checkNext(int pos, String s, int val, int type)
        throws ParseException {
        int end = -1;
        int i = pos;
        if (i >= s.length()) {
            addToSet(val, end, -1, type);
            return i;
        }
        char c = s.charAt(pos);
        if (c == 'L') {
            if (type == DAY_OF_WEEK) {
                if(val < 1 || val > 7)
                    throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
                lastdayOfWeek = true;
            } else {
                throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
            }
            TreeSet<Integer> set = getSet(type);
            set.add(val);
            i++;
            return i;
        }
        if (c == 'W') {
            if (type == DAY_OF_MONTH) {
                nearestWeekday = true;
            } else {
                throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
            }
            if(val > 31)
                throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
            TreeSet<Integer> set = getSet(type);
            set.add(val);
            i++;
            return i;
        }
        if (c == '#') {
            if (type != DAY_OF_WEEK) {
                throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
            }
            i++;
            try {
                nthdayOfWeek = Integer.parseInt(s.substring(i));
                if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
                    throw new Exception();
                }
            } catch (Exception e) {
                throw new ParseException(
                        "A numeric value between 1 and 5 must follow the '#' option",
                        i);
            }
            TreeSet<Integer> set = getSet(type);
            set.add(val);
            i++;
            return i;
        }
        if (c == '-') {
            i++;
            c = s.charAt(i);
            int v = Integer.parseInt(String.valueOf(c));
            end = v;
            i++;
            if (i >= s.length()) {
                addToSet(val, end, 1, type);
                return i;
            }
            c = s.charAt(i);
            if (c >= '0' && c <= '9') {
                ValueSet vs = getValue(v, s, i);
                end = vs.value;
                i = vs.pos;
            }
            if (i < s.length() && ((c = s.charAt(i)) == '/')) {
                i++;
                c = s.charAt(i);
                int v2 = Integer.parseInt(String.valueOf(c));
                i++;
                if (i >= s.length()) {
                    addToSet(val, end, v2, type);
                    return i;
                }
                c = s.charAt(i);
                if (c >= '0' && c <= '9') {
                    ValueSet vs = getValue(v2, s, i);
                    int v3 = vs.value;
                    addToSet(val, end, v3, type);
                    i = vs.pos;
                    return i;
                } else {
                    addToSet(val, end, v2, type);
                    return i;
                }
            } else {
                addToSet(val, end, 1, type);
                return i;
            }
        }
        if (c == '/') {
            if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') {
                throw new ParseException("'/' must be followed by an integer.", i);
            }
            i++;
            c = s.charAt(i);
            int v2 = Integer.parseInt(String.valueOf(c));
            i++;
            if (i >= s.length()) {
                checkIncrementRange(v2, type, i);
                addToSet(val, end, v2, type);
                return i;
            }
            c = s.charAt(i);
            if (c >= '0' && c <= '9') {
                ValueSet vs = getValue(v2, s, i);
                int v3 = vs.value;
                checkIncrementRange(v3, type, i);
                addToSet(val, end, v3, type);
                i = vs.pos;
                return i;
            } else {
                throw new ParseException("Unexpected character '" + c + "' after '/'", i);
            }
        }
        addToSet(val, end, 0, type);
        i++;
        return i;
    }
    public String getCronExpression() {
        return cronExpression;
    }
    public String getExpressionSummary() {
        StringBuilder buf = new StringBuilder();
        buf.append("seconds: ");
        buf.append(getExpressionSetSummary(seconds));
        buf.append("\n");
        buf.append("minutes: ");
        buf.append(getExpressionSetSummary(minutes));
        buf.append("\n");
        buf.append("hours: ");
        buf.append(getExpressionSetSummary(hours));
        buf.append("\n");
        buf.append("daysOfMonth: ");
        buf.append(getExpressionSetSummary(daysOfMonth));
        buf.append("\n");
        buf.append("months: ");
        buf.append(getExpressionSetSummary(months));
        buf.append("\n");
        buf.append("daysOfWeek: ");
        buf.append(getExpressionSetSummary(daysOfWeek));
        buf.append("\n");
        buf.append("lastdayOfWeek: ");
        buf.append(lastdayOfWeek);
        buf.append("\n");
        buf.append("nearestWeekday: ");
        buf.append(nearestWeekday);
        buf.append("\n");
        buf.append("NthDayOfWeek: ");
        buf.append(nthdayOfWeek);
        buf.append("\n");
        buf.append("lastdayOfMonth: ");
        buf.append(lastdayOfMonth);
        buf.append("\n");
        buf.append("years: ");
        buf.append(getExpressionSetSummary(years));
        buf.append("\n");
        return buf.toString();
    }
    protected String getExpressionSetSummary(java.util.Set<Integer> set) {
        if (set.contains(NO_SPEC)) {
            return "?";
        }
        if (set.contains(ALL_SPEC)) {
            return "*";
        }
        StringBuilder buf = new StringBuilder();
        Iterator<Integer> itr = set.iterator();
        boolean first = true;
        while (itr.hasNext()) {
            Integer iVal = itr.next();
            String val = iVal.toString();
            if (!first) {
                buf.append(",");
            }
            buf.append(val);
            first = false;
        }
        return buf.toString();
    }
    protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) {
        if (list.contains(NO_SPEC)) {
            return "?";
        }
        if (list.contains(ALL_SPEC)) {
            return "*";
        }
        StringBuilder buf = new StringBuilder();
        Iterator<Integer> itr = list.iterator();
        boolean first = true;
        while (itr.hasNext()) {
            Integer iVal = itr.next();
            String val = iVal.toString();
            if (!first) {
                buf.append(",");
            }
            buf.append(val);
            first = false;
        }
        return buf.toString();
    }
    protected int skipWhiteSpace(int i, String s) {
        for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {
        }
        return i;
    }
    protected int findNextWhiteSpace(int i, String s) {
        for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
        }
        return i;
    }
    protected void addToSet(int val, int end, int incr, int type)
        throws ParseException {
        TreeSet<Integer> set = getSet(type);
        if (type == SECOND || type == MINUTE) {
            if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
                throw new ParseException(
                        "Minute and Second values must be between 0 and 59",
                        -1);
            }
        } else if (type == HOUR) {
            if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
                throw new ParseException(
                        "Hour values must be between 0 and 23", -1);
            }
        } else if (type == DAY_OF_MONTH) {
            if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
                    && (val != NO_SPEC_INT)) {
                throw new ParseException(
                        "Day of month values must be between 1 and 31", -1);
            }
        } else if (type == MONTH) {
            if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
                throw new ParseException(
                        "Month values must be between 1 and 12", -1);
            }
        } else if (type == DAY_OF_WEEK) {
            if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
                    && (val != NO_SPEC_INT)) {
                throw new ParseException(
                        "Day-of-Week values must be between 1 and 7", -1);
            }
        }
        if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
            if (val != -1) {
                set.add(val);
            } else {
                set.add(NO_SPEC);
            }
            return;
        }
        int startAt = val;
        int stopAt = end;
        if (val == ALL_SPEC_INT && incr <= 0) {
            incr = 1;
            set.add(ALL_SPEC); // put in a marker, but also fill values
        }
        if (type == SECOND || type == MINUTE) {
            if (stopAt == -1) {
                stopAt = 59;
            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 0;
            }
        } else if (type == HOUR) {
            if (stopAt == -1) {
                stopAt = 23;
            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 0;
            }
        } else if (type == DAY_OF_MONTH) {
            if (stopAt == -1) {
                stopAt = 31;
            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 1;
            }
        } else if (type == MONTH) {
            if (stopAt == -1) {
                stopAt = 12;
            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 1;
            }
        } else if (type == DAY_OF_WEEK) {
            if (stopAt == -1) {
                stopAt = 7;
            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 1;
            }
        } else if (type == YEAR) {
            if (stopAt == -1) {
                stopAt = MAX_YEAR;
            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 1970;
            }
        }
        // if the end of the range is before the start, then we need to overflow into
        // the next day, month etc. This is done by adding the maximum amount for that
        // type, and using modulus max to determine the value being added.
        int max = -1;
        if (stopAt < startAt) {
            switch (type) {
              case       SECOND : max = 60; break;
              case       MINUTE : max = 60; break;
              case         HOUR : max = 24; break;
              case        MONTH : max = 12; break;
              case  DAY_OF_WEEK : max = 7;  break;
              case DAY_OF_MONTH : max = 31; break;
              case         YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
              default           : throw new IllegalArgumentException("Unexpected type encountered");
            }
            stopAt += max;
        }
        for (int i = startAt; i <= stopAt; i += incr) {
            if (max == -1) {
                // ie: there's no max to overflow over
                set.add(i);
            } else {
                // take the modulus to get the real value
                int i2 = i % max;
                // 1-indexed ranges should not include 0, and should include their max
                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) {
                    i2 = max;
                }
                set.add(i2);
            }
        }
    }
    TreeSet<Integer> getSet(int type) {
        switch (type) {
            case SECOND:
                return seconds;
            case MINUTE:
                return minutes;
            case HOUR:
                return hours;
            case DAY_OF_MONTH:
                return daysOfMonth;
            case MONTH:
                return months;
            case DAY_OF_WEEK:
                return daysOfWeek;
            case YEAR:
                return years;
            default:
                return null;
        }
    }
    protected ValueSet getValue(int v, String s, int i) {
        char c = s.charAt(i);
        StringBuilder s1 = new StringBuilder(String.valueOf(v));
        while (c >= '0' && c <= '9') {
            s1.append(c);
            i++;
            if (i >= s.length()) {
                break;
            }
            c = s.charAt(i);
        }
        ValueSet val = new ValueSet();
        val.pos = (i < s.length()) ? i : i + 1;
        val.value = Integer.parseInt(s1.toString());
        return val;
    }
    protected int getNumericValue(String s, int i) {
        int endOfVal = findNextWhiteSpace(i, s);
        String val = s.substring(i, endOfVal);
        return Integer.parseInt(val);
    }
    protected int getMonthNumber(String s) {
        Integer integer = monthMap.get(s);
        if (integer == null) {
            return -1;
        }
        return integer;
    }
    protected int getDayOfWeekNumber(String s) {
        Integer integer = dayMap.get(s);
        if (integer == null) {
            return -1;
        }
        return integer;
    }
    ////////////////////////////////////////////////////////////////////////////
    //
    // Computation Functions
    //
    ////////////////////////////////////////////////////////////////////////////
    public Date getTimeAfter(Date afterTime) {
        // Computation is based on Gregorian year only.
        Calendar cl = new java.util.GregorianCalendar(getTimeZone());
        // move ahead one second, since we're computing the time *after* the
        // given time
        afterTime = new Date(afterTime.getTime() + 1000);
        // CronTrigger does not deal with milliseconds
        cl.setTime(afterTime);
        cl.set(Calendar.MILLISECOND, 0);
        boolean gotOne = false;
        // loop until we've computed the next time, or we've past the endTime
        while (!gotOne) {
            //if (endTime != null && cl.getTime().after(endTime)) return null;
            if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop...
                return null;
            }
            SortedSet<Integer> st = null;
            int t = 0;
            int sec = cl.get(Calendar.SECOND);
            int min = cl.get(Calendar.MINUTE);
            // get second.................................................
            st = seconds.tailSet(sec);
            if (st != null && st.size() != 0) {
                sec = st.first();
            } else {
                sec = seconds.first();
                min++;
                cl.set(Calendar.MINUTE, min);
            }
            cl.set(Calendar.SECOND, sec);
            min = cl.get(Calendar.MINUTE);
            int hr = cl.get(Calendar.HOUR_OF_DAY);
            t = -1;
            // get minute.................................................
            st = minutes.tailSet(min);
            if (st != null && st.size() != 0) {
                t = min;
                min = st.first();
            } else {
                min = minutes.first();
                hr++;
            }
            if (min != t) {
                cl.set(Calendar.SECOND, 0);
                cl.set(Calendar.MINUTE, min);
                setCalendarHour(cl, hr);
                continue;
            }
            cl.set(Calendar.MINUTE, min);
            hr = cl.get(Calendar.HOUR_OF_DAY);
            int day = cl.get(Calendar.DAY_OF_MONTH);
            t = -1;
            // get hour...................................................
            st = hours.tailSet(hr);
            if (st != null && st.size() != 0) {
                t = hr;
                hr = st.first();
            } else {
                hr = hours.first();
                day++;
            }
            if (hr != t) {
                cl.set(Calendar.SECOND, 0);
                cl.set(Calendar.MINUTE, 0);
                cl.set(Calendar.DAY_OF_MONTH, day);
                setCalendarHour(cl, hr);
                continue;
            }
            cl.set(Calendar.HOUR_OF_DAY, hr);
            day = cl.get(Calendar.DAY_OF_MONTH);
            int mon = cl.get(Calendar.MONTH) + 1;
            // '+ 1' because calendar is 0-based for this field, and we are
            // 1-based
            t = -1;
            int tmon = mon;
            // get day...................................................
            boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
            boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
            if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
                st = daysOfMonth.tailSet(day);
                if (lastdayOfMonth) {
                    if(!nearestWeekday) {
                        t = day;
                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                        day -= lastdayOffset;
                        if(t > day) {
                            mon++;
                            if(mon > 12) {
                                mon = 1;
                                tmon = 3333; // ensure test of mon != tmon further below fails
                                cl.add(Calendar.YEAR, 1);
                            }
                            day = 1;
                        }
                    } else {
                        t = day;
                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                        day -= lastdayOffset;
                        java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
                        tcal.set(Calendar.SECOND, 0);
                        tcal.set(Calendar.MINUTE, 0);
                        tcal.set(Calendar.HOUR_OF_DAY, 0);
                        tcal.set(Calendar.DAY_OF_MONTH, day);
                        tcal.set(Calendar.MONTH, mon - 1);
                        tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
                        int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                        int dow = tcal.get(Calendar.DAY_OF_WEEK);
                        if(dow == Calendar.SATURDAY && day == 1) {
                            day += 2;
                        } else if(dow == Calendar.SATURDAY) {
                            day -= 1;
                        } else if(dow == Calendar.SUNDAY && day == ldom) {
                            day -= 2;
                        } else if(dow == Calendar.SUNDAY) {
                            day += 1;
                        }
                        tcal.set(Calendar.SECOND, sec);
                        tcal.set(Calendar.MINUTE, min);
                        tcal.set(Calendar.HOUR_OF_DAY, hr);
                        tcal.set(Calendar.DAY_OF_MONTH, day);
                        tcal.set(Calendar.MONTH, mon - 1);
                        Date nTime = tcal.getTime();
                        if(nTime.before(afterTime)) {
                            day = 1;
                            mon++;
                        }
                    }
                } else if(nearestWeekday) {
                    t = day;
                    day = daysOfMonth.first();
                    java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
                    tcal.set(Calendar.SECOND, 0);
                    tcal.set(Calendar.MINUTE, 0);
                    tcal.set(Calendar.HOUR_OF_DAY, 0);
                    tcal.set(Calendar.DAY_OF_MONTH, day);
                    tcal.set(Calendar.MONTH, mon - 1);
                    tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
                    int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                    int dow = tcal.get(Calendar.DAY_OF_WEEK);
                    if(dow == Calendar.SATURDAY && day == 1) {
                        day += 2;
                    } else if(dow == Calendar.SATURDAY) {
                        day -= 1;
                    } else if(dow == Calendar.SUNDAY && day == ldom) {
                        day -= 2;
                    } else if(dow == Calendar.SUNDAY) {
                        day += 1;
                    }
                    tcal.set(Calendar.SECOND, sec);
                    tcal.set(Calendar.MINUTE, min);
                    tcal.set(Calendar.HOUR_OF_DAY, hr);
                    tcal.set(Calendar.DAY_OF_MONTH, day);
                    tcal.set(Calendar.MONTH, mon - 1);
                    Date nTime = tcal.getTime();
                    if(nTime.before(afterTime)) {
                        day = daysOfMonth.first();
                        mon++;
                    }
                } else if (st != null && st.size() != 0) {
                    t = day;
                    day = st.first();
                    // make sure we don't over-run a short month, such as february
                    int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                    if (day > lastDay) {
                        day = daysOfMonth.first();
                        mon++;
                    }
                } else {
                    day = daysOfMonth.first();
                    mon++;
                }
                if (day != t || mon != tmon) {
                    cl.set(Calendar.SECOND, 0);
                    cl.set(Calendar.MINUTE, 0);
                    cl.set(Calendar.HOUR_OF_DAY, 0);
                    cl.set(Calendar.DAY_OF_MONTH, day);
                    cl.set(Calendar.MONTH, mon - 1);
                    // '- 1' because calendar is 0-based for this field, and we
                    // are 1-based
                    continue;
                }
            } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
                if (lastdayOfWeek) { // are we looking for the last XXX day of
                    // the month?
                    int dow = daysOfWeek.first(); // desired
                    // d-o-w
                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
                    int daysToAdd = 0;
                    if (cDow < dow) {
                        daysToAdd = dow - cDow;
                    }
                    if (cDow > dow) {
                        daysToAdd = dow + (7 - cDow);
                    }
                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                    if (day + daysToAdd > lDay) { // did we already miss the
                        // last one?
                        cl.set(Calendar.SECOND, 0);
                        cl.set(Calendar.MINUTE, 0);
                        cl.set(Calendar.HOUR_OF_DAY, 0);
                        cl.set(Calendar.DAY_OF_MONTH, 1);
                        cl.set(Calendar.MONTH, mon);
                        // no '- 1' here because we are promoting the month
                        continue;
                    }
                    // find date of last occurrence of this day in this month...
                    while ((day + daysToAdd + 7) <= lDay) {
                        daysToAdd += 7;
                    }
                    day += daysToAdd;
                    if (daysToAdd > 0) {
                        cl.set(Calendar.SECOND, 0);
                        cl.set(Calendar.MINUTE, 0);
                        cl.set(Calendar.HOUR_OF_DAY, 0);
                        cl.set(Calendar.DAY_OF_MONTH, day);
                        cl.set(Calendar.MONTH, mon - 1);
                        // '- 1' here because we are not promoting the month
                        continue;
                    }
                } else if (nthdayOfWeek != 0) {
                    // are we looking for the Nth XXX day in the month?
                    int dow = daysOfWeek.first(); // desired
                    // d-o-w
                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
                    int daysToAdd = 0;
                    if (cDow < dow) {
                        daysToAdd = dow - cDow;
                    } else if (cDow > dow) {
                        daysToAdd = dow + (7 - cDow);
                    }
                    boolean dayShifted = false;
                    if (daysToAdd > 0) {
                        dayShifted = true;
                    }
                    day += daysToAdd;
                    int weekOfMonth = day / 7;
                    if (day % 7 > 0) {
                        weekOfMonth++;
                    }
                    daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
                    day += daysToAdd;
                    if (daysToAdd < 0
                            || day > getLastDayOfMonth(mon, cl
                                    .get(Calendar.YEAR))) {
                        cl.set(Calendar.SECOND, 0);
                        cl.set(Calendar.MINUTE, 0);
                        cl.set(Calendar.HOUR_OF_DAY, 0);
                        cl.set(Calendar.DAY_OF_MONTH, 1);
                        cl.set(Calendar.MONTH, mon);
                        // no '- 1' here because we are promoting the month
                        continue;
                    } else if (daysToAdd > 0 || dayShifted) {
                        cl.set(Calendar.SECOND, 0);
                        cl.set(Calendar.MINUTE, 0);
                        cl.set(Calendar.HOUR_OF_DAY, 0);
                        cl.set(Calendar.DAY_OF_MONTH, day);
                        cl.set(Calendar.MONTH, mon - 1);
                        // '- 1' here because we are NOT promoting the month
                        continue;
                    }
                } else {
                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
                    int dow = daysOfWeek.first(); // desired
                    // d-o-w
                    st = daysOfWeek.tailSet(cDow);
                    if (st != null && st.size() > 0) {
                        dow = st.first();
                    }
                    int daysToAdd = 0;
                    if (cDow < dow) {
                        daysToAdd = dow - cDow;
                    }
                    if (cDow > dow) {
                        daysToAdd = dow + (7 - cDow);
                    }
                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
                    if (day + daysToAdd > lDay) { // will we pass the end of
                        // the month?
                        cl.set(Calendar.SECOND, 0);
                        cl.set(Calendar.MINUTE, 0);
                        cl.set(Calendar.HOUR_OF_DAY, 0);
                        cl.set(Calendar.DAY_OF_MONTH, 1);
                        cl.set(Calendar.MONTH, mon);
                        // no '- 1' here because we are promoting the month
                        continue;
                    } else if (daysToAdd > 0) { // are we swithing days?
                        cl.set(Calendar.SECOND, 0);
                        cl.set(Calendar.MINUTE, 0);
                        cl.set(Calendar.HOUR_OF_DAY, 0);
                        cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd);
                        cl.set(Calendar.MONTH, mon - 1);
                        // '- 1' because calendar is 0-based for this field,
                        // and we are 1-based
                        continue;
                    }
                }
            } else { // dayOfWSpec && !dayOfMSpec
                throw new UnsupportedOperationException(
                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
            }
            cl.set(Calendar.DAY_OF_MONTH, day);
            mon = cl.get(Calendar.MONTH) + 1;
            // '+ 1' because calendar is 0-based for this field, and we are
            // 1-based
            int year = cl.get(Calendar.YEAR);
            t = -1;
            // test for expressions that never generate a valid fire date,
            // but keep looping...
            if (year > MAX_YEAR) {
                return null;
            }
            // get month...................................................
            st = months.tailSet(mon);
            if (st != null && st.size() != 0) {
                t = mon;
                mon = st.first();
            } else {
                mon = months.first();
                year++;
            }
            if (mon != t) {
                cl.set(Calendar.SECOND, 0);
                cl.set(Calendar.MINUTE, 0);
                cl.set(Calendar.HOUR_OF_DAY, 0);
                cl.set(Calendar.DAY_OF_MONTH, 1);
                cl.set(Calendar.MONTH, mon - 1);
                // '- 1' because calendar is 0-based for this field, and we are
                // 1-based
                cl.set(Calendar.YEAR, year);
                continue;
            }
            cl.set(Calendar.MONTH, mon - 1);
            // '- 1' because calendar is 0-based for this field, and we are
            // 1-based
            year = cl.get(Calendar.YEAR);
            t = -1;
            // get year...................................................
            st = years.tailSet(year);
            if (st != null && st.size() != 0) {
                t = year;
                year = st.first();
            } else {
                return null; // ran out of years...
            }
            if (year != t) {
                cl.set(Calendar.SECOND, 0);
                cl.set(Calendar.MINUTE, 0);
                cl.set(Calendar.HOUR_OF_DAY, 0);
                cl.set(Calendar.DAY_OF_MONTH, 1);
                cl.set(Calendar.MONTH, 0);
                // '- 1' because calendar is 0-based for this field, and we are
                // 1-based
                cl.set(Calendar.YEAR, year);
                continue;
            }
            cl.set(Calendar.YEAR, year);
            gotOne = true;
        } // while( !done )
        return cl.getTime();
    }
    /**
     * Advance the calendar to the particular hour paying particular attention
     * to daylight saving problems.
     *
     * @param cal the calendar to operate on
     * @param hour the hour to set
     */
    protected void setCalendarHour(Calendar cal, int hour) {
        cal.set(java.util.Calendar.HOUR_OF_DAY, hour);
        if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) {
            cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1);
        }
    }
    /**
     * NOT YET IMPLEMENTED: Returns the time before the given time
     * that the <code>CronExpression</code> matches.
     */
    public Date getTimeBefore(Date endTime) {
        // FUTURE_TODO: implement QUARTZ-423
        return null;
    }
    /**
     * NOT YET IMPLEMENTED: Returns the final time that the
     * <code>CronExpression</code> will match.
     */
    public Date getFinalFireTime() {
        // FUTURE_TODO: implement QUARTZ-423
        return null;
    }
    protected boolean isLeapYear(int year) {
        return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
    }
    protected int getLastDayOfMonth(int monthNum, int year) {
        switch (monthNum) {
            case 1:
                return 31;
            case 2:
                return (isLeapYear(year)) ? 29 : 28;
            case 3:
                return 31;
            case 4:
                return 30;
            case 5:
                return 31;
            case 6:
                return 30;
            case 7:
                return 31;
            case 8:
                return 31;
            case 9:
                return 30;
            case 10:
                return 31;
            case 11:
                return 30;
            case 12:
                return 31;
            default:
                throw new IllegalArgumentException("Illegal month number: "
                        + monthNum);
        }
    }
    private void readObject(java.io.ObjectInputStream stream)
        throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();
        try {
            buildExpression(cronExpression);
        } catch (Exception ignore) {
        } // never happens
    }
    @Override
    @Deprecated
    public Object clone() {
        return new CronExpression(this);
    }
}
class ValueSet {
    public int value;
    public int pos;
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java
New file
@@ -0,0 +1,14 @@
package com.xxl.job.admin.core.exception;
/**
 * @author xuxueli 2019-05-04 23:19:29
 */
public class XxlJobException extends RuntimeException {
    public XxlJobException() {
    }
    public XxlJobException(String message) {
        super(message);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java
New file
@@ -0,0 +1,77 @@
package com.xxl.job.admin.core.model;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
 * Created by xuxueli on 16/9/30.
 */
public class XxlJobGroup {
    private int id;
    private String appname;
    private String title;
    private int addressType;        // 执行器地址类型:0=自动注册、1=手动录入
    private String addressList;     // 执行器地址列表,多地址逗号分隔(手动录入)
    private Date updateTime;
    // registry list
    private List<String> registryList;  // 执行器地址列表(系统注册)
    public List<String> getRegistryList() {
        if (addressList!=null && addressList.trim().length()>0) {
            registryList = new ArrayList<String>(Arrays.asList(addressList.split(",")));
        }
        return registryList;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getAppname() {
        return appname;
    }
    public void setAppname(String appname) {
        this.appname = appname;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public int getAddressType() {
        return addressType;
    }
    public void setAddressType(int addressType) {
        this.addressType = addressType;
    }
    public String getAddressList() {
        return addressList;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
    public void setAddressList(String addressList) {
        this.addressList = addressList;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java
New file
@@ -0,0 +1,237 @@
package com.xxl.job.admin.core.model;
import java.util.Date;
/**
 * xxl-job info
 *
 * @author xuxueli  2016-1-12 18:25:49
 */
public class XxlJobInfo {
    private int id;                // 主键ID
    private int jobGroup;        // 执行器主键ID
    private String jobDesc;
    private Date addTime;
    private Date updateTime;
    private String author;        // 负责人
    private String alarmEmail;    // 报警邮件
    private String scheduleType;            // 调度类型
    private String scheduleConf;            // 调度配置,值含义取决于调度类型
    private String misfireStrategy;            // 调度过期策略
    private String executorRouteStrategy;    // 执行器路由策略
    private String executorHandler;            // 执行器,任务Handler名称
    private String executorParam;            // 执行器,任务参数
    private String executorBlockStrategy;    // 阻塞处理策略
    private int executorTimeout;             // 任务执行超时时间,单位秒
    private int executorFailRetryCount;        // 失败重试次数
    private String glueType;        // GLUE类型    #com.xxl.job.core.glue.GlueTypeEnum
    private String glueSource;        // GLUE源代码
    private String glueRemark;        // GLUE备注
    private Date glueUpdatetime;    // GLUE更新时间
    private String childJobId;        // 子任务ID,多个逗号分隔
    private int triggerStatus;        // 调度状态:0-停止,1-运行
    private long triggerLastTime;    // 上次调度时间
    private long triggerNextTime;    // 下次调度时间
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getJobGroup() {
        return jobGroup;
    }
    public void setJobGroup(int jobGroup) {
        this.jobGroup = jobGroup;
    }
    public String getJobDesc() {
        return jobDesc;
    }
    public void setJobDesc(String jobDesc) {
        this.jobDesc = jobDesc;
    }
    public Date getAddTime() {
        return addTime;
    }
    public void setAddTime(Date addTime) {
        this.addTime = addTime;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public String getAlarmEmail() {
        return alarmEmail;
    }
    public void setAlarmEmail(String alarmEmail) {
        this.alarmEmail = alarmEmail;
    }
    public String getScheduleType() {
        return scheduleType;
    }
    public void setScheduleType(String scheduleType) {
        this.scheduleType = scheduleType;
    }
    public String getScheduleConf() {
        return scheduleConf;
    }
    public void setScheduleConf(String scheduleConf) {
        this.scheduleConf = scheduleConf;
    }
    public String getMisfireStrategy() {
        return misfireStrategy;
    }
    public void setMisfireStrategy(String misfireStrategy) {
        this.misfireStrategy = misfireStrategy;
    }
    public String getExecutorRouteStrategy() {
        return executorRouteStrategy;
    }
    public void setExecutorRouteStrategy(String executorRouteStrategy) {
        this.executorRouteStrategy = executorRouteStrategy;
    }
    public String getExecutorHandler() {
        return executorHandler;
    }
    public void setExecutorHandler(String executorHandler) {
        this.executorHandler = executorHandler;
    }
    public String getExecutorParam() {
        return executorParam;
    }
    public void setExecutorParam(String executorParam) {
        this.executorParam = executorParam;
    }
    public String getExecutorBlockStrategy() {
        return executorBlockStrategy;
    }
    public void setExecutorBlockStrategy(String executorBlockStrategy) {
        this.executorBlockStrategy = executorBlockStrategy;
    }
    public int getExecutorTimeout() {
        return executorTimeout;
    }
    public void setExecutorTimeout(int executorTimeout) {
        this.executorTimeout = executorTimeout;
    }
    public int getExecutorFailRetryCount() {
        return executorFailRetryCount;
    }
    public void setExecutorFailRetryCount(int executorFailRetryCount) {
        this.executorFailRetryCount = executorFailRetryCount;
    }
    public String getGlueType() {
        return glueType;
    }
    public void setGlueType(String glueType) {
        this.glueType = glueType;
    }
    public String getGlueSource() {
        return glueSource;
    }
    public void setGlueSource(String glueSource) {
        this.glueSource = glueSource;
    }
    public String getGlueRemark() {
        return glueRemark;
    }
    public void setGlueRemark(String glueRemark) {
        this.glueRemark = glueRemark;
    }
    public Date getGlueUpdatetime() {
        return glueUpdatetime;
    }
    public void setGlueUpdatetime(Date glueUpdatetime) {
        this.glueUpdatetime = glueUpdatetime;
    }
    public String getChildJobId() {
        return childJobId;
    }
    public void setChildJobId(String childJobId) {
        this.childJobId = childJobId;
    }
    public int getTriggerStatus() {
        return triggerStatus;
    }
    public void setTriggerStatus(int triggerStatus) {
        this.triggerStatus = triggerStatus;
    }
    public long getTriggerLastTime() {
        return triggerLastTime;
    }
    public void setTriggerLastTime(long triggerLastTime) {
        this.triggerLastTime = triggerLastTime;
    }
    public long getTriggerNextTime() {
        return triggerNextTime;
    }
    public void setTriggerNextTime(long triggerNextTime) {
        this.triggerNextTime = triggerNextTime;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java
New file
@@ -0,0 +1,157 @@
package com.xxl.job.admin.core.model;
import java.util.Date;
/**
 * xxl-job log, used to track trigger process
 * @author xuxueli  2015-12-19 23:19:09
 */
public class XxlJobLog {
    private long id;
    // job info
    private int jobGroup;
    private int jobId;
    // execute info
    private String executorAddress;
    private String executorHandler;
    private String executorParam;
    private String executorShardingParam;
    private int executorFailRetryCount;
    // trigger info
    private Date triggerTime;
    private int triggerCode;
    private String triggerMsg;
    // handle info
    private Date handleTime;
    private int handleCode;
    private String handleMsg;
    // alarm info
    private int alarmStatus;
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public int getJobGroup() {
        return jobGroup;
    }
    public void setJobGroup(int jobGroup) {
        this.jobGroup = jobGroup;
    }
    public int getJobId() {
        return jobId;
    }
    public void setJobId(int jobId) {
        this.jobId = jobId;
    }
    public String getExecutorAddress() {
        return executorAddress;
    }
    public void setExecutorAddress(String executorAddress) {
        this.executorAddress = executorAddress;
    }
    public String getExecutorHandler() {
        return executorHandler;
    }
    public void setExecutorHandler(String executorHandler) {
        this.executorHandler = executorHandler;
    }
    public String getExecutorParam() {
        return executorParam;
    }
    public void setExecutorParam(String executorParam) {
        this.executorParam = executorParam;
    }
    public String getExecutorShardingParam() {
        return executorShardingParam;
    }
    public void setExecutorShardingParam(String executorShardingParam) {
        this.executorShardingParam = executorShardingParam;
    }
    public int getExecutorFailRetryCount() {
        return executorFailRetryCount;
    }
    public void setExecutorFailRetryCount(int executorFailRetryCount) {
        this.executorFailRetryCount = executorFailRetryCount;
    }
    public Date getTriggerTime() {
        return triggerTime;
    }
    public void setTriggerTime(Date triggerTime) {
        this.triggerTime = triggerTime;
    }
    public int getTriggerCode() {
        return triggerCode;
    }
    public void setTriggerCode(int triggerCode) {
        this.triggerCode = triggerCode;
    }
    public String getTriggerMsg() {
        return triggerMsg;
    }
    public void setTriggerMsg(String triggerMsg) {
        this.triggerMsg = triggerMsg;
    }
    public Date getHandleTime() {
        return handleTime;
    }
    public void setHandleTime(Date handleTime) {
        this.handleTime = handleTime;
    }
    public int getHandleCode() {
        return handleCode;
    }
    public void setHandleCode(int handleCode) {
        this.handleCode = handleCode;
    }
    public String getHandleMsg() {
        return handleMsg;
    }
    public void setHandleMsg(String handleMsg) {
        this.handleMsg = handleMsg;
    }
    public int getAlarmStatus() {
        return alarmStatus;
    }
    public void setAlarmStatus(int alarmStatus) {
        this.alarmStatus = alarmStatus;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java
New file
@@ -0,0 +1,75 @@
package com.xxl.job.admin.core.model;
import java.util.Date;
/**
 * xxl-job log for glue, used to track job code process
 * @author xuxueli 2016-5-19 17:57:46
 */
public class XxlJobLogGlue {
    private int id;
    private int jobId;                // 任务主键ID
    private String glueType;        // GLUE类型    #com.xxl.job.core.glue.GlueTypeEnum
    private String glueSource;
    private String glueRemark;
    private Date addTime;
    private Date updateTime;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getJobId() {
        return jobId;
    }
    public void setJobId(int jobId) {
        this.jobId = jobId;
    }
    public String getGlueType() {
        return glueType;
    }
    public void setGlueType(String glueType) {
        this.glueType = glueType;
    }
    public String getGlueSource() {
        return glueSource;
    }
    public void setGlueSource(String glueSource) {
        this.glueSource = glueSource;
    }
    public String getGlueRemark() {
        return glueRemark;
    }
    public void setGlueRemark(String glueRemark) {
        this.glueRemark = glueRemark;
    }
    public Date getAddTime() {
        return addTime;
    }
    public void setAddTime(Date addTime) {
        this.addTime = addTime;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java
New file
@@ -0,0 +1,54 @@
package com.xxl.job.admin.core.model;
import java.util.Date;
public class XxlJobLogReport {
    private int id;
    private Date triggerDay;
    private int runningCount;
    private int sucCount;
    private int failCount;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public Date getTriggerDay() {
        return triggerDay;
    }
    public void setTriggerDay(Date triggerDay) {
        this.triggerDay = triggerDay;
    }
    public int getRunningCount() {
        return runningCount;
    }
    public void setRunningCount(int runningCount) {
        this.runningCount = runningCount;
    }
    public int getSucCount() {
        return sucCount;
    }
    public void setSucCount(int sucCount) {
        this.sucCount = sucCount;
    }
    public int getFailCount() {
        return failCount;
    }
    public void setFailCount(int failCount) {
        this.failCount = failCount;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java
New file
@@ -0,0 +1,55 @@
package com.xxl.job.admin.core.model;
import java.util.Date;
/**
 * Created by xuxueli on 16/9/30.
 */
public class XxlJobRegistry {
    private int id;
    private String registryGroup;
    private String registryKey;
    private String registryValue;
    private Date updateTime;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getRegistryGroup() {
        return registryGroup;
    }
    public void setRegistryGroup(String registryGroup) {
        this.registryGroup = registryGroup;
    }
    public String getRegistryKey() {
        return registryKey;
    }
    public void setRegistryKey(String registryKey) {
        this.registryKey = registryKey;
    }
    public String getRegistryValue() {
        return registryValue;
    }
    public void setRegistryValue(String registryValue) {
        this.registryValue = registryValue;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java
New file
@@ -0,0 +1,73 @@
package com.xxl.job.admin.core.model;
import org.springframework.util.StringUtils;
/**
 * @author xuxueli 2019-05-04 16:43:12
 */
public class XxlJobUser {
    private int id;
    private String username;        // 账号
    private String password;        // 密码
    private int role;                // 角色:0-普通用户、1-管理员
    private String permission;    // 权限:执行器ID列表,多个逗号分割
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public int getRole() {
        return role;
    }
    public void setRole(int role) {
        this.role = role;
    }
    public String getPermission() {
        return permission;
    }
    public void setPermission(String permission) {
        this.permission = permission;
    }
    // plugin
    public boolean validPermission(int jobGroup){
        if (this.role == 1) {
            return true;
        } else {
            if (StringUtils.hasText(this.permission)) {
                for (String permissionItem : this.permission.split(",")) {
                    if (String.valueOf(jobGroup).equals(permissionItem)) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java
New file
@@ -0,0 +1,32 @@
//package com.xxl.job.admin.core.jobbean;
//
//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
//import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
//import org.quartz.JobExecutionContext;
//import org.quartz.JobExecutionException;
//import org.quartz.JobKey;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.scheduling.quartz.QuartzJobBean;
//
///**
// * http job bean
// * “@DisallowConcurrentExecution” disable concurrent, thread size can not be only one, better given more
// * @author xuxueli 2015-12-17 18:20:34
// */
////@DisallowConcurrentExecution
//public class RemoteHttpJobBean extends QuartzJobBean {
//    private static Logger logger = LoggerFactory.getLogger(RemoteHttpJobBean.class);
//
//    @Override
//    protected void executeInternal(JobExecutionContext context)
//            throws JobExecutionException {
//
//        // load jobId
//        JobKey jobKey = context.getTrigger().getJobKey();
//        Integer jobId = Integer.valueOf(jobKey.getName());
//
//
//    }
//
//}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java
New file
@@ -0,0 +1,413 @@
//package com.xxl.job.admin.core.schedule;
//
//import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
//import com.xxl.job.admin.core.jobbean.RemoteHttpJobBean;
//import com.xxl.job.admin.core.model.XxlJobInfo;
//import com.xxl.job.admin.core.thread.JobFailMonitorHelper;
//import com.xxl.job.admin.core.thread.JobRegistryMonitorHelper;
//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
//import com.xxl.job.admin.core.util.I18nUtil;
//import com.xxl.job.core.biz.AdminBiz;
//import com.xxl.job.core.biz.ExecutorBiz;
//import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
//import com.xxl.rpc.remoting.invoker.XxlRpcInvokerFactory;
//import com.xxl.rpc.remoting.invoker.call.CallType;
//import com.xxl.rpc.remoting.invoker.reference.XxlRpcReferenceBean;
//import com.xxl.rpc.remoting.invoker.route.LoadBalance;
//import com.xxl.rpc.remoting.net.NetEnum;
//import com.xxl.rpc.remoting.net.impl.servlet.server.ServletServerHandler;
//import com.xxl.rpc.remoting.provider.XxlRpcProviderFactory;
//import com.xxl.rpc.serialize.Serializer;
//import org.quartz.*;
//import org.quartz.Trigger.TriggerState;
//import org.quartz.impl.triggers.CronTriggerImpl;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.util.Assert;
//
//import javax.servlet.ServletException;
//import javax.servlet.http.HttpServletRequest;
//import javax.servlet.http.HttpServletResponse;
//import java.io.IOException;
//import java.util.Date;
//import java.util.concurrent.ConcurrentHashMap;
//
///**
// * base quartz scheduler util
// * @author xuxueli 2015-12-19 16:13:53
// */
//public final class XxlJobDynamicScheduler {
//    private static final Logger logger = LoggerFactory.getLogger(XxlJobDynamicScheduler_old.class);
//
//    // ---------------------- param ----------------------
//
//    // scheduler
//    private static Scheduler scheduler;
//    public void setScheduler(Scheduler scheduler) {
//        XxlJobDynamicScheduler_old.scheduler = scheduler;
//    }
//
//
//    // ---------------------- init + destroy ----------------------
//    public void start() throws Exception {
//        // valid
//        Assert.notNull(scheduler, "quartz scheduler is null");
//
//        // init i18n
//        initI18n();
//
//        // admin registry monitor run
//        JobRegistryMonitorHelper.getInstance().start();
//
//        // admin monitor run
//        JobFailMonitorHelper.getInstance().start();
//
//        // admin-server
//        initRpcProvider();
//
//        logger.info(">>>>>>>>> init xxl-job admin success.");
//    }
//
//
//    public void destroy() throws Exception {
//        // admin trigger pool stop
//        JobTriggerPoolHelper.toStop();
//
//        // admin registry stop
//        JobRegistryMonitorHelper.getInstance().toStop();
//
//        // admin monitor stop
//        JobFailMonitorHelper.getInstance().toStop();
//
//        // admin-server
//        stopRpcProvider();
//    }
//
//
//    // ---------------------- I18n ----------------------
//
//    private void initI18n(){
//        for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) {
//            item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
//        }
//    }
//
//
//    // ---------------------- admin rpc provider (no server version) ----------------------
//    private static ServletServerHandler servletServerHandler;
//    private void initRpcProvider(){
//        // init
//        XxlRpcProviderFactory xxlRpcProviderFactory = new XxlRpcProviderFactory();
//        xxlRpcProviderFactory.initConfig(
//                NetEnum.NETTY_HTTP,
//                Serializer.SerializeEnum.HESSIAN.getSerializer(),
//                null,
//                0,
//                XxlJobAdminConfig.getAdminConfig().getAccessToken(),
//                null,
//                null);
//
//        // add services
//        xxlRpcProviderFactory.addService(AdminBiz.class.getName(), null, XxlJobAdminConfig.getAdminConfig().getAdminBiz());
//
//        // servlet handler
//        servletServerHandler = new ServletServerHandler(xxlRpcProviderFactory);
//    }
//    private void stopRpcProvider() throws Exception {
//        XxlRpcInvokerFactory.getInstance().stop();
//    }
//    public static void invokeAdminService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
//        servletServerHandler.handle(null, request, response);
//    }
//
//
//    // ---------------------- executor-client ----------------------
//    private static ConcurrentHashMap<String, ExecutorBiz> executorBizRepository = new ConcurrentHashMap<String, ExecutorBiz>();
//    public static ExecutorBiz getExecutorBiz(String address) throws Exception {
//        // valid
//        if (address==null || address.trim().length()==0) {
//            return null;
//        }
//
//        // load-cache
//        address = address.trim();
//        ExecutorBiz executorBiz = executorBizRepository.get(address);
//        if (executorBiz != null) {
//            return executorBiz;
//        }
//
//        // set-cache
//        executorBiz = (ExecutorBiz) new XxlRpcReferenceBean(
//                NetEnum.NETTY_HTTP,
//                Serializer.SerializeEnum.HESSIAN.getSerializer(),
//                CallType.SYNC,
//                LoadBalance.ROUND,
//                ExecutorBiz.class,
//                null,
//                5000,
//                address,
//                XxlJobAdminConfig.getAdminConfig().getAccessToken(),
//                null,
//                null).getObject();
//
//        executorBizRepository.put(address, executorBiz);
//        return executorBiz;
//    }
//
//
//    // ---------------------- schedule util ----------------------
//
//    /**
//     * fill job info
//     *
//     * @param jobInfo
//     */
//    public static void fillJobInfo(XxlJobInfo jobInfo) {
//
//        String name = String.valueOf(jobInfo.getId());
//
//        // trigger key
//        TriggerKey triggerKey = TriggerKey.triggerKey(name);
//        try {
//
//            // trigger cron
//            Trigger trigger = scheduler.getTrigger(triggerKey);
//            if (trigger!=null && trigger instanceof CronTriggerImpl) {
//                String cronExpression = ((CronTriggerImpl) trigger).getCronExpression();
//                jobInfo.setJobCron(cronExpression);
//            }
//
//            // trigger state
//            TriggerState triggerState = scheduler.getTriggerState(triggerKey);
//            if (triggerState!=null) {
//                jobInfo.setJobStatus(triggerState.name());
//            }
//
//            //JobKey jobKey = new JobKey(jobInfo.getJobName(), String.valueOf(jobInfo.getJobGroup()));
//            //JobDetail jobDetail = scheduler.getJobDetail(jobKey);
//            //String jobClass = jobDetail.getJobClass().getName();
//
//        } catch (SchedulerException e) {
//            logger.error(e.getMessage(), e);
//        }
//    }
//
//
//    /**
//     * add trigger + job
//     *
//     * @param jobName
//     * @param cronExpression
//     * @return
//     * @throws SchedulerException
//     */
//    public static boolean addJob(String jobName, String cronExpression) throws SchedulerException {
//        // 1、job key
//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
//        JobKey jobKey = new JobKey(jobName);
//
//        // 2、valid
//        if (scheduler.checkExists(triggerKey)) {
//            return true;    // PASS
//        }
//
//        // 3、corn trigger
//        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();   // withMisfireHandlingInstructionDoNothing 忽略掉调度终止过程中忽略的调度
//        CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
//
//        // 4、job detail
//        Class<? extends Job> jobClass_ = RemoteHttpJobBean.class;   // Class.forName(jobInfo.getJobClass());
//        JobDetail jobDetail = JobBuilder.newJob(jobClass_).withIdentity(jobKey).build();
//
//        /*if (jobInfo.getJobData()!=null) {
//            JobDataMap jobDataMap = jobDetail.getJobDataMap();
//            jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class));
//            // JobExecutionContext context.getMergedJobDataMap().get("mailGuid");
//        }*/
//
//        // 5、schedule job
//        Date date = scheduler.scheduleJob(jobDetail, cronTrigger);
//
//        logger.info(">>>>>>>>>>> addJob success(quartz), jobDetail:{}, cronTrigger:{}, date:{}", jobDetail, cronTrigger, date);
//        return true;
//    }
//
//
//    /**
//     * remove trigger + job
//     *
//     * @param jobName
//     * @return
//     * @throws SchedulerException
//     */
//    public static boolean removeJob(String jobName) throws SchedulerException {
//
//        JobKey jobKey = new JobKey(jobName);
//        scheduler.deleteJob(jobKey);
//
//        /*TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
//        if (scheduler.checkExists(triggerKey)) {
//            scheduler.unscheduleJob(triggerKey);    // trigger + job
//        }*/
//
//        logger.info(">>>>>>>>>>> removeJob success(quartz), jobKey:{}", jobKey);
//        return true;
//    }
//
//
//    /**
//     * updateJobCron
//     *
//     * @param jobName
//     * @param cronExpression
//     * @return
//     * @throws SchedulerException
//     */
//    public static boolean updateJobCron(String jobName, String cronExpression) throws SchedulerException {
//
//        // 1、job key
//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
//
//        // 2、valid
//        if (!scheduler.checkExists(triggerKey)) {
//            return true;    // PASS
//        }
//
//        CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//
//        // 3、avoid repeat cron
//        String oldCron = oldTrigger.getCronExpression();
//        if (oldCron.equals(cronExpression)){
//            return true;    // PASS
//        }
//
//        // 4、new cron trigger
//        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
//        oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
//
//        // 5、rescheduleJob
//        scheduler.rescheduleJob(triggerKey, oldTrigger);
//
//        /*
//        JobKey jobKey = new JobKey(jobName);
//
//        // old job detail
//        JobDetail jobDetail = scheduler.getJobDetail(jobKey);
//
//        // new trigger
//        HashSet<Trigger> triggerSet = new HashSet<Trigger>();
//        triggerSet.add(cronTrigger);
//        // cover trigger of job detail
//        scheduler.scheduleJob(jobDetail, triggerSet, true);*/
//
//        logger.info(">>>>>>>>>>> resumeJob success, JobName:{}", jobName);
//        return true;
//    }
//
//
//    /**
//     * pause
//     *
//     * @param jobName
//     * @return
//     * @throws SchedulerException
//     */
//    /*public static boolean pauseJob(String jobName) throws SchedulerException {
//
//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
//
//        boolean result = false;
//        if (scheduler.checkExists(triggerKey)) {
//            scheduler.pauseTrigger(triggerKey);
//            result =  true;
//        }
//
//        logger.info(">>>>>>>>>>> pauseJob {}, triggerKey:{}", (result?"success":"fail"),triggerKey);
//        return result;
//    }*/
//
//
//    /**
//     * resume
//     *
//     * @param jobName
//     * @return
//     * @throws SchedulerException
//     */
//    /*public static boolean resumeJob(String jobName) throws SchedulerException {
//
//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
//
//        boolean result = false;
//        if (scheduler.checkExists(triggerKey)) {
//            scheduler.resumeTrigger(triggerKey);
//            result = true;
//        }
//
//        logger.info(">>>>>>>>>>> resumeJob {}, triggerKey:{}", (result?"success":"fail"), triggerKey);
//        return result;
//    }*/
//
//
//    /**
//     * run
//     *
//     * @param jobName
//     * @return
//     * @throws SchedulerException
//     */
//    /*public static boolean triggerJob(String jobName) throws SchedulerException {
//        // TriggerKey : name + group
//        JobKey jobKey = new JobKey(jobName);
//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
//
//        boolean result = false;
//        if (scheduler.checkExists(triggerKey)) {
//            scheduler.triggerJob(jobKey);
//            result = true;
//            logger.info(">>>>>>>>>>> runJob success, jobKey:{}", jobKey);
//        } else {
//            logger.info(">>>>>>>>>>> runJob fail, jobKey:{}", jobKey);
//        }
//        return result;
//    }*/
//
//
//    /**
//     * finaAllJobList
//     *
//     * @return
//     *//*
//    @Deprecated
//    public static List<Map<String, Object>> finaAllJobList(){
//        List<Map<String, Object>> jobList = new ArrayList<Map<String,Object>>();
//
//        try {
//            if (scheduler.getJobGroupNames()==null || scheduler.getJobGroupNames().size()==0) {
//                return null;
//            }
//            String groupName = scheduler.getJobGroupNames().get(0);
//            Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName));
//            if (jobKeys!=null && jobKeys.size()>0) {
//                for (JobKey jobKey : jobKeys) {
//                    TriggerKey triggerKey = TriggerKey.triggerKey(jobKey.getName(), Scheduler.DEFAULT_GROUP);
//                    Trigger trigger = scheduler.getTrigger(triggerKey);
//                    JobDetail jobDetail = scheduler.getJobDetail(jobKey);
//                    TriggerState triggerState = scheduler.getTriggerState(triggerKey);
//                    Map<String, Object> jobMap = new HashMap<String, Object>();
//                    jobMap.put("TriggerKey", triggerKey);
//                    jobMap.put("Trigger", trigger);
//                    jobMap.put("JobDetail", jobDetail);
//                    jobMap.put("TriggerState", triggerState);
//                    jobList.add(jobMap);
//                }
//            }
//
//        } catch (SchedulerException e) {
//            logger.error(e.getMessage(), e);
//            return null;
//        }
//        return jobList;
//    }*/
//
//}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java
New file
@@ -0,0 +1,58 @@
//package com.xxl.job.admin.core.quartz;
//
//import org.quartz.SchedulerConfigException;
//import org.quartz.spi.ThreadPool;
//
///**
// * single thread pool, for async trigger
// *
// * @author xuxueli 2019-03-06
// */
//public class XxlJobThreadPool implements ThreadPool {
//
//    @Override
//    public boolean runInThread(Runnable runnable) {
//
//        // async run
//        runnable.run();
//        return true;
//
//        //return false;
//    }
//
//    @Override
//    public int blockForAvailableThreads() {
//        return 1;
//    }
//
//    @Override
//    public void initialize() throws SchedulerConfigException {
//
//    }
//
//    @Override
//    public void shutdown(boolean waitForJobsToComplete) {
//
//    }
//
//    @Override
//    public int getPoolSize() {
//        return 1;
//    }
//
//    @Override
//    public void setInstanceId(String schedInstId) {
//
//    }
//
//    @Override
//    public void setInstanceName(String schedName) {
//
//    }
//
//    // support
//    public void setThreadCount(int count) {
//        //
//    }
//
//}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java
New file
@@ -0,0 +1,48 @@
package com.xxl.job.admin.core.route;
import com.xxl.job.admin.core.route.strategy.*;
import com.xxl.job.admin.core.util.I18nUtil;
/**
 * Created by xuxueli on 17/3/10.
 */
public enum ExecutorRouteStrategyEnum {
    FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()),
    LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()),
    ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()),
    RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()),
    CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()),
    LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()),
    LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()),
    FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()),
    BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()),
    SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null);
    ExecutorRouteStrategyEnum(String title, ExecutorRouter router) {
        this.title = title;
        this.router = router;
    }
    private String title;
    private ExecutorRouter router;
    public String getTitle() {
        return title;
    }
    public ExecutorRouter getRouter() {
        return router;
    }
    public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){
        if (name != null) {
            for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) {
                if (item.name().equals(name)) {
                    return item;
                }
            }
        }
        return defaultItem;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java
New file
@@ -0,0 +1,24 @@
package com.xxl.job.admin.core.route;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
 * Created by xuxueli on 17/3/10.
 */
public abstract class ExecutorRouter {
    protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class);
    /**
     * route address
     *
     * @param addressList
     * @return  ReturnT.content=address
     */
    public abstract ReturnT<String> route(TriggerParam triggerParam, List<String> addressList);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java
New file
@@ -0,0 +1,48 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.ExecutorBiz;
import com.xxl.job.core.biz.model.IdleBeatParam;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.List;
/**
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteBusyover extends ExecutorRouter {
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        StringBuffer idleBeatResultSB = new StringBuffer();
        for (String address : addressList) {
            // beat
            ReturnT<String> idleBeatResult = null;
            try {
                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
                idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                idleBeatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
            }
            idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"<br><br>":"")
                    .append(I18nUtil.getString("jobconf_idleBeat") + ":")
                    .append("<br>address:").append(address)
                    .append("<br>code:").append(idleBeatResult.getCode())
                    .append("<br>msg:").append(idleBeatResult.getMsg());
            // beat success
            if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
                idleBeatResult.setMsg(idleBeatResultSB.toString());
                idleBeatResult.setContent(address);
                return idleBeatResult;
            }
        }
        return new ReturnT<String>(ReturnT.FAIL_CODE, idleBeatResultSB.toString());
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java
New file
@@ -0,0 +1,85 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
 * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;
 *      a、virtual node:解决不均衡问题
 *      b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteConsistentHash extends ExecutorRouter {
    private static int VIRTUAL_NODE_NUM = 100;
    /**
     * get hash code on 2^32 ring (md5散列的方式计算hash值)
     * @param key
     * @return
     */
    private static long hash(String key) {
        // md5 byte
        MessageDigest md5;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 not supported", e);
        }
        md5.reset();
        byte[] keyBytes = null;
        try {
            keyBytes = key.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unknown string :" + key, e);
        }
        md5.update(keyBytes);
        byte[] digest = md5.digest();
        // hash code, Truncate to 32-bits
        long hashCode = ((long) (digest[3] & 0xFF) << 24)
                | ((long) (digest[2] & 0xFF) << 16)
                | ((long) (digest[1] & 0xFF) << 8)
                | (digest[0] & 0xFF);
        long truncateHashCode = hashCode & 0xffffffffL;
        return truncateHashCode;
    }
    public String hashJob(int jobId, List<String> addressList) {
        // ------A1------A2-------A3------
        // -----------J1------------------
        TreeMap<Long, String> addressRing = new TreeMap<Long, String>();
        for (String address: addressList) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                long addressHash = hash("SHARD-" + address + "-NODE-" + i);
                addressRing.put(addressHash, address);
            }
        }
        long jobHash = hash(String.valueOf(jobId));
        SortedMap<Long, String> lastRing = addressRing.tailMap(jobHash);
        if (!lastRing.isEmpty()) {
            return lastRing.get(lastRing.firstKey());
        }
        return addressRing.firstEntry().getValue();
    }
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        String address = hashJob(triggerParam.getJobId(), addressList);
        return new ReturnT<String>(address);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java
New file
@@ -0,0 +1,48 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.ExecutorBiz;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.List;
/**
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteFailover extends ExecutorRouter {
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        StringBuffer beatResultSB = new StringBuffer();
        for (String address : addressList) {
            // beat
            ReturnT<String> beatResult = null;
            try {
                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
                beatResult = executorBiz.beat();
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                beatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
            }
            beatResultSB.append( (beatResultSB.length()>0)?"<br><br>":"")
                    .append(I18nUtil.getString("jobconf_beat") + ":")
                    .append("<br>address:").append(address)
                    .append("<br>code:").append(beatResult.getCode())
                    .append("<br>msg:").append(beatResult.getMsg());
            // beat success
            if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {
                beatResult.setMsg(beatResultSB.toString());
                beatResult.setContent(address);
                return beatResult;
            }
        }
        return new ReturnT<String>(ReturnT.FAIL_CODE, beatResultSB.toString());
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java
New file
@@ -0,0 +1,19 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.List;
/**
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteFirst extends ExecutorRouter {
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList){
        return new ReturnT<String>(addressList.get(0));
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java
New file
@@ -0,0 +1,79 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
 * 单个JOB对应的每个执行器,使用频率最低的优先被选举
 *      a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数
 *      b、LRU(Least Recently Used):最近最久未使用,时间
 *
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteLFU extends ExecutorRouter {
    private static ConcurrentMap<Integer, HashMap<String, Integer>> jobLfuMap = new ConcurrentHashMap<Integer, HashMap<String, Integer>>();
    private static long CACHE_VALID_TIME = 0;
    public String route(int jobId, List<String> addressList) {
        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            jobLfuMap.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
        }
        // lfu item init
        HashMap<String, Integer> lfuItemMap = jobLfuMap.get(jobId);     // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList;
        if (lfuItemMap == null) {
            lfuItemMap = new HashMap<String, Integer>();
            jobLfuMap.putIfAbsent(jobId, lfuItemMap);   // 避免重复覆盖
        }
        // put new
        for (String address: addressList) {
            if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) {
                lfuItemMap.put(address, new Random().nextInt(addressList.size()));  // 初始化时主动Random一次,缓解首次压力
            }
        }
        // remove old
        List<String> delKeys = new ArrayList<>();
        for (String existKey: lfuItemMap.keySet()) {
            if (!addressList.contains(existKey)) {
                delKeys.add(existKey);
            }
        }
        if (delKeys.size() > 0) {
            for (String delKey: delKeys) {
                lfuItemMap.remove(delKey);
            }
        }
        // load least userd count address
        List<Map.Entry<String, Integer>> lfuItemList = new ArrayList<Map.Entry<String, Integer>>(lfuItemMap.entrySet());
        Collections.sort(lfuItemList, new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                return o1.getValue().compareTo(o2.getValue());
            }
        });
        Map.Entry<String, Integer> addressItem = lfuItemList.get(0);
        String minAddress = addressItem.getKey();
        addressItem.setValue(addressItem.getValue() + 1);
        return addressItem.getKey();
    }
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        String address = route(triggerParam.getJobId(), addressList);
        return new ReturnT<String>(address);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java
New file
@@ -0,0 +1,76 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
 * 单个JOB对应的每个执行器,最久为使用的优先被选举
 *      a、LFU(Least Frequently Used):最不经常使用,频率/次数
 *      b(*)、LRU(Least Recently Used):最近最久未使用,时间
 *
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteLRU extends ExecutorRouter {
    private static ConcurrentMap<Integer, LinkedHashMap<String, String>> jobLRUMap = new ConcurrentHashMap<Integer, LinkedHashMap<String, String>>();
    private static long CACHE_VALID_TIME = 0;
    public String route(int jobId, List<String> addressList) {
        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            jobLRUMap.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
        }
        // init lru
        LinkedHashMap<String, String> lruItem = jobLRUMap.get(jobId);
        if (lruItem == null) {
            /**
             * LinkedHashMap
             *      a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;
             *      b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法;
             */
            lruItem = new LinkedHashMap<String, String>(16, 0.75f, true);
            jobLRUMap.putIfAbsent(jobId, lruItem);
        }
        // put new
        for (String address: addressList) {
            if (!lruItem.containsKey(address)) {
                lruItem.put(address, address);
            }
        }
        // remove old
        List<String> delKeys = new ArrayList<>();
        for (String existKey: lruItem.keySet()) {
            if (!addressList.contains(existKey)) {
                delKeys.add(existKey);
            }
        }
        if (delKeys.size() > 0) {
            for (String delKey: delKeys) {
                lruItem.remove(delKey);
            }
        }
        // load
        String eldestKey = lruItem.entrySet().iterator().next().getKey();
        String eldestValue = lruItem.get(eldestKey);
        return eldestValue;
    }
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        String address = route(triggerParam.getJobId(), addressList);
        return new ReturnT<String>(address);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java
New file
@@ -0,0 +1,19 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.List;
/**
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteLast extends ExecutorRouter {
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        return new ReturnT<String>(addressList.get(addressList.size()-1));
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java
New file
@@ -0,0 +1,23 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.List;
import java.util.Random;
/**
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteRandom extends ExecutorRouter {
    private static Random localRandom = new Random();
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        String address = addressList.get(localRandom.nextInt(addressList.size()));
        return new ReturnT<String>(address);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java
New file
@@ -0,0 +1,46 @@
package com.xxl.job.admin.core.route.strategy;
import com.xxl.job.admin.core.route.ExecutorRouter;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * Created by xuxueli on 17/3/10.
 */
public class ExecutorRouteRound extends ExecutorRouter {
    private static ConcurrentMap<Integer, AtomicInteger> routeCountEachJob = new ConcurrentHashMap<>();
    private static long CACHE_VALID_TIME = 0;
    private static int count(int jobId) {
        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            routeCountEachJob.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
        }
        AtomicInteger count = routeCountEachJob.get(jobId);
        if (count == null || count.get() > 1000000) {
            // 初始化时主动Random一次,缓解首次压力
            count = new AtomicInteger(new Random().nextInt(100));
        } else {
            // count++
            count.addAndGet(1);
        }
        routeCountEachJob.put(jobId, count);
        return count.get();
    }
    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
        String address = addressList.get(count(triggerParam.getJobId())%addressList.size());
        return new ReturnT<String>(address);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java
New file
@@ -0,0 +1,39 @@
package com.xxl.job.admin.core.scheduler;
import com.xxl.job.admin.core.util.I18nUtil;
/**
 * @author xuxueli 2020-10-29 21:11:23
 */
public enum MisfireStrategyEnum {
    /**
     * do nothing
     */
    DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing")),
    /**
     * fire once now
     */
    FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now"));
    private String title;
    MisfireStrategyEnum(String title) {
        this.title = title;
    }
    public String getTitle() {
        return title;
    }
    public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){
        for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) {
            if (item.name().equals(name)) {
                return item;
            }
        }
        return defaultItem;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java
New file
@@ -0,0 +1,46 @@
package com.xxl.job.admin.core.scheduler;
import com.xxl.job.admin.core.util.I18nUtil;
/**
 * @author xuxueli 2020-10-29 21:11:23
 */
public enum ScheduleTypeEnum {
    NONE(I18nUtil.getString("schedule_type_none")),
    /**
     * schedule by cron
     */
    CRON(I18nUtil.getString("schedule_type_cron")),
    /**
     * schedule by fixed rate (in seconds)
     */
    FIX_RATE(I18nUtil.getString("schedule_type_fix_rate")),
    /**
     * schedule by fix delay (in seconds), after the last time
     */
    /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/;
    private String title;
    ScheduleTypeEnum(String title) {
        this.title = title;
    }
    public String getTitle() {
        return title;
    }
    public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){
        for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) {
            if (item.name().equals(name)) {
                return item;
            }
        }
        return defaultItem;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java
New file
@@ -0,0 +1,101 @@
package com.xxl.job.admin.core.scheduler;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.thread.*;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.ExecutorBiz;
import com.xxl.job.core.biz.client.ExecutorBizClient;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
 * @author xuxueli 2018-10-28 00:18:17
 */
public class XxlJobScheduler  {
    private static final Logger logger = LoggerFactory.getLogger(XxlJobScheduler.class);
    public void init() throws Exception {
        // init i18n
        initI18n();
        // admin trigger pool start
        JobTriggerPoolHelper.toStart();
        // admin registry monitor run
        JobRegistryHelper.getInstance().start();
        // admin fail-monitor run
        JobFailMonitorHelper.getInstance().start();
        // admin lose-monitor run ( depend on JobTriggerPoolHelper )
        JobCompleteHelper.getInstance().start();
        // admin log report start
        JobLogReportHelper.getInstance().start();
        // start-schedule  ( depend on JobTriggerPoolHelper )
        JobScheduleHelper.getInstance().start();
        logger.info(">>>>>>>>> init xxl-job admin success.");
    }
    public void destroy() throws Exception {
        // stop-schedule
        JobScheduleHelper.getInstance().toStop();
        // admin log report stop
        JobLogReportHelper.getInstance().toStop();
        // admin lose-monitor stop
        JobCompleteHelper.getInstance().toStop();
        // admin fail-monitor stop
        JobFailMonitorHelper.getInstance().toStop();
        // admin registry stop
        JobRegistryHelper.getInstance().toStop();
        // admin trigger pool stop
        JobTriggerPoolHelper.toStop();
    }
    // ---------------------- I18n ----------------------
    private void initI18n(){
        for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) {
            item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
        }
    }
    // ---------------------- executor-client ----------------------
    private static ConcurrentMap<String, ExecutorBiz> executorBizRepository = new ConcurrentHashMap<String, ExecutorBiz>();
    public static ExecutorBiz getExecutorBiz(String address) throws Exception {
        // valid
        if (address==null || address.trim().length()==0) {
            return null;
        }
        // load-cache
        address = address.trim();
        ExecutorBiz executorBiz = executorBizRepository.get(address);
        if (executorBiz != null) {
            return executorBiz;
        }
        // set-cache
        executorBiz = new ExecutorBizClient(address, XxlJobAdminConfig.getAdminConfig().getAccessToken());
        executorBizRepository.put(address, executorBiz);
        return executorBiz;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java
New file
@@ -0,0 +1,184 @@
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.complete.XxlJobCompleter;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobLog;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.model.HandleCallbackParam;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
/**
 * job lose-monitor instance
 *
 * @author xuxueli 2015-9-1 18:05:56
 */
public class JobCompleteHelper {
    private static Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class);
    private static JobCompleteHelper instance = new JobCompleteHelper();
    public static JobCompleteHelper getInstance(){
        return instance;
    }
    // ---------------------- monitor ----------------------
    private ThreadPoolExecutor callbackThreadPool = null;
    private Thread monitorThread;
    private volatile boolean toStop = false;
    public void start(){
        // for callback
        callbackThreadPool = new ThreadPoolExecutor(
                2,
                20,
                30L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode());
                    }
                },
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        r.run();
                        logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now).");
                    }
                });
        // for monitor
        monitorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                // wait for JobTriggerPoolHelper-init
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    if (!toStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                // monitor
                while (!toStop) {
                    try {
                        // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
                        Date losedTime = DateUtil.addMinutes(new Date(), -10);
                        List<Long> losedJobIds  = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime);
                        if (losedJobIds!=null && losedJobIds.size()>0) {
                            for (Long logId: losedJobIds) {
                                XxlJobLog jobLog = new XxlJobLog();
                                jobLog.setId(logId);
                                jobLog.setHandleTime(new Date());
                                jobLog.setHandleCode(ReturnT.FAIL_CODE);
                                jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") );
                                XxlJobCompleter.updateHandleInfoAndFinish(jobLog);
                            }
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e);
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(60);
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop");
            }
        });
        monitorThread.setDaemon(true);
        monitorThread.setName("xxl-job, admin JobLosedMonitorHelper");
        monitorThread.start();
    }
    public void toStop(){
        toStop = true;
        // stop registryOrRemoveThreadPool
        callbackThreadPool.shutdownNow();
        // stop monitorThread (interrupt and wait)
        monitorThread.interrupt();
        try {
            monitorThread.join();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
    }
    // ---------------------- helper ----------------------
    public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
        callbackThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                for (HandleCallbackParam handleCallbackParam: callbackParamList) {
                    ReturnT<String> callbackResult = callback(handleCallbackParam);
                    logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}",
                            (callbackResult.getCode()== ReturnT.SUCCESS_CODE?"success":"fail"), handleCallbackParam, callbackResult);
                }
            }
        });
        return ReturnT.SUCCESS;
    }
    private ReturnT<String> callback(HandleCallbackParam handleCallbackParam) {
        // valid log item
        XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().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
        }
        // handle msg
        StringBuffer handleMsg = new StringBuffer();
        if (log.getHandleMsg()!=null) {
            handleMsg.append(log.getHandleMsg()).append("<br>");
        }
        if (handleCallbackParam.getHandleMsg() != null) {
            handleMsg.append(handleCallbackParam.getHandleMsg());
        }
        // success, save log
        log.setHandleTime(new Date());
        log.setHandleCode(handleCallbackParam.getHandleCode());
        log.setHandleMsg(handleMsg.toString());
        XxlJobCompleter.updateHandleInfoAndFinish(log);
        return ReturnT.SUCCESS;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java
New file
@@ -0,0 +1,110 @@
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import com.xxl.job.admin.core.util.I18nUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
 * job monitor instance
 *
 * @author xuxueli 2015-9-1 18:05:56
 */
public class JobFailMonitorHelper {
    private static Logger logger = LoggerFactory.getLogger(JobFailMonitorHelper.class);
    private static JobFailMonitorHelper instance = new JobFailMonitorHelper();
    public static JobFailMonitorHelper getInstance(){
        return instance;
    }
    // ---------------------- monitor ----------------------
    private Thread monitorThread;
    private volatile boolean toStop = false;
    public void start(){
        monitorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                // monitor
                while (!toStop) {
                    try {
                        List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
                        if (failLogIds!=null && !failLogIds.isEmpty()) {
                            for (long failLogId: failLogIds) {
                                // lock log
                                int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
                                if (lockRet < 1) {
                                    continue;
                                }
                                XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
                                XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());
                                // 1、fail retry monitor
                                if (log.getExecutorFailRetryCount() > 0) {
                                    JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
                                    String retryMsg = "<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<< </span><br>";
                                    log.setTriggerMsg(log.getTriggerMsg() + retryMsg);
                                    XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log);
                                }
                                // 2、fail alarm monitor
                                int newAlarmStatus = 0;        // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
                                if (info != null) {
                                    boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);
                                    newAlarmStatus = alarmResult?2:3;
                                } else {
                                    newAlarmStatus = 1;
                                }
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus);
                            }
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e);
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop");
            }
        });
        monitorThread.setDaemon(true);
        monitorThread.setName("xxl-job, admin JobFailMonitorHelper");
        monitorThread.start();
    }
    public void toStop(){
        toStop = true;
        // interrupt and wait
        monitorThread.interrupt();
        try {
            monitorThread.join();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java
New file
@@ -0,0 +1,152 @@
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobLogReport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
 * job log report helper
 *
 * @author xuxueli 2019-11-22
 */
public class JobLogReportHelper {
    private static Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class);
    private static JobLogReportHelper instance = new JobLogReportHelper();
    public static JobLogReportHelper getInstance(){
        return instance;
    }
    private Thread logrThread;
    private volatile boolean toStop = false;
    public void start(){
        logrThread = new Thread(new Runnable() {
            @Override
            public void run() {
                // last clean log time
                long lastCleanLogTime = 0;
                while (!toStop) {
                    // 1、log-report refresh: refresh log report in 3 days
                    try {
                        for (int i = 0; i < 3; i++) {
                            // today
                            Calendar itemDay = Calendar.getInstance();
                            itemDay.add(Calendar.DAY_OF_MONTH, -i);
                            itemDay.set(Calendar.HOUR_OF_DAY, 0);
                            itemDay.set(Calendar.MINUTE, 0);
                            itemDay.set(Calendar.SECOND, 0);
                            itemDay.set(Calendar.MILLISECOND, 0);
                            Date todayFrom = itemDay.getTime();
                            itemDay.set(Calendar.HOUR_OF_DAY, 23);
                            itemDay.set(Calendar.MINUTE, 59);
                            itemDay.set(Calendar.SECOND, 59);
                            itemDay.set(Calendar.MILLISECOND, 999);
                            Date todayTo = itemDay.getTime();
                            // refresh log-report every minute
                            XxlJobLogReport xxlJobLogReport = new XxlJobLogReport();
                            xxlJobLogReport.setTriggerDay(todayFrom);
                            xxlJobLogReport.setRunningCount(0);
                            xxlJobLogReport.setSucCount(0);
                            xxlJobLogReport.setFailCount(0);
                            Map<String, Object> triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo);
                            if (triggerCountMap!=null && triggerCountMap.size()>0) {
                                int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0;
                                int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0;
                                int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0;
                                int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc;
                                xxlJobLogReport.setRunningCount(triggerDayCountRunning);
                                xxlJobLogReport.setSucCount(triggerDayCountSuc);
                                xxlJobLogReport.setFailCount(triggerDayCountFail);
                            }
                            // do refresh
                            int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport);
                            if (ret < 1) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport);
                            }
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e);
                        }
                    }
                    // 2、log-clean: switch open & once each day
                    if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0
                            && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) {
                        // expire-time
                        Calendar expiredDay = Calendar.getInstance();
                        expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays());
                        expiredDay.set(Calendar.HOUR_OF_DAY, 0);
                        expiredDay.set(Calendar.MINUTE, 0);
                        expiredDay.set(Calendar.SECOND, 0);
                        expiredDay.set(Calendar.MILLISECOND, 0);
                        Date clearBeforeTime = expiredDay.getTime();
                        // clean expired log
                        List<Long> logIds = null;
                        do {
                            logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000);
                            if (logIds!=null && logIds.size()>0) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds);
                            }
                        } while (logIds!=null && logIds.size()>0);
                        // update clean time
                        lastCleanLogTime = System.currentTimeMillis();
                    }
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, job log report thread stop");
            }
        });
        logrThread.setDaemon(true);
        logrThread.setName("xxl-job, admin JobLogReportHelper");
        logrThread.start();
    }
    public void toStop(){
        toStop = true;
        // interrupt and wait
        logrThread.interrupt();
        try {
            logrThread.join();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java
New file
@@ -0,0 +1,204 @@
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobRegistry;
import com.xxl.job.core.biz.model.RegistryParam;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.RegistryConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.concurrent.*;
/**
 * job registry instance
 * @author xuxueli 2016-10-02 19:10:24
 */
public class JobRegistryHelper {
    private static Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class);
    private static JobRegistryHelper instance = new JobRegistryHelper();
    public static JobRegistryHelper getInstance(){
        return instance;
    }
    private ThreadPoolExecutor registryOrRemoveThreadPool = null;
    private Thread registryMonitorThread;
    private volatile boolean toStop = false;
    public void start(){
        // for registry or remove
        registryOrRemoveThreadPool = new ThreadPoolExecutor(
                2,
                10,
                30L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(2000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
                    }
                },
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        r.run();
                        logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
                    }
                });
        // for monitor
        registryMonitorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!toStop) {
                    try {
                        // auto registry group
                        List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
                        if (groupList!=null && !groupList.isEmpty()) {
                            // remove dead address (admin/executor)
                            List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
                            if (ids!=null && ids.size()>0) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
                            }
                            // fresh online address (admin/executor)
                            HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
                            List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
                            if (list != null) {
                                for (XxlJobRegistry item: list) {
                                    if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
                                        String appname = item.getRegistryKey();
                                        List<String> registryList = appAddressMap.get(appname);
                                        if (registryList == null) {
                                            registryList = new ArrayList<String>();
                                        }
                                        if (!registryList.contains(item.getRegistryValue())) {
                                            registryList.add(item.getRegistryValue());
                                        }
                                        appAddressMap.put(appname, registryList);
                                    }
                                }
                            }
                            // fresh group address
                            for (XxlJobGroup group: groupList) {
                                List<String> registryList = appAddressMap.get(group.getAppname());
                                String addressListStr = null;
                                if (registryList!=null && !registryList.isEmpty()) {
                                    Collections.sort(registryList);
                                    StringBuilder addressListSB = new StringBuilder();
                                    for (String item:registryList) {
                                        addressListSB.append(item).append(",");
                                    }
                                    addressListStr = addressListSB.toString();
                                    addressListStr = addressListStr.substring(0, addressListStr.length()-1);
                                }
                                group.setAddressList(addressListStr);
                                group.setUpdateTime(new Date());
                                XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
                            }
                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                    } catch (InterruptedException e) {
                        if (!toStop) {
                            logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
            }
        });
        registryMonitorThread.setDaemon(true);
        registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread");
        registryMonitorThread.start();
    }
    public void toStop(){
        toStop = true;
        // stop registryOrRemoveThreadPool
        registryOrRemoveThreadPool.shutdownNow();
        // stop monitir (interrupt and wait)
        registryMonitorThread.interrupt();
        try {
            registryMonitorThread.join();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
    }
    // ---------------------- helper ----------------------
    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.");
        }
        // async execute
        registryOrRemoveThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
                if (ret < 1) {
                    XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
                    // fresh
                    freshGroupRegistryInfo(registryParam);
                }
            }
        });
        return ReturnT.SUCCESS;
    }
    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.");
        }
        // async execute
        registryOrRemoveThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
                if (ret > 0) {
                    // fresh
                    freshGroupRegistryInfo(registryParam);
                }
            }
        });
        return ReturnT.SUCCESS;
    }
    private void freshGroupRegistryInfo(RegistryParam registryParam){
        // Under consideration, prevent affecting core tables
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java
New file
@@ -0,0 +1,369 @@
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.cron.CronExpression;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
 * @author xuxueli 2019-05-21
 */
public class JobScheduleHelper {
    private static Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class);
    private static JobScheduleHelper instance = new JobScheduleHelper();
    public static JobScheduleHelper getInstance(){
        return instance;
    }
    public static final long PRE_READ_MS = 5000;    // pre read
    private Thread scheduleThread;
    private Thread ringThread;
    private volatile boolean scheduleThreadToStop = false;
    private volatile boolean ringThreadToStop = false;
    private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
    public void start(){
        // schedule thread
        scheduleThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>> init xxl-job admin scheduler success.");
                // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
                while (!scheduleThreadToStop) {
                    // Scan Job
                    long start = System.currentTimeMillis();
                    Connection conn = null;
                    Boolean connAutoCommit = null;
                    PreparedStatement preparedStatement = null;
                    boolean preReadSuc = true;
                    try {
                        conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        conn.setAutoCommit(false);
                        preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                        preparedStatement.execute();
                        // tx start
                        // 1、pre read
                        long nowTime = System.currentTimeMillis();
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList!=null && scheduleList.size()>0) {
                            // 2、push time-ring
                            for (XxlJobInfo jobInfo: scheduleList) {
                                // time-ring jump
                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                                    // 2.1、trigger-expire > 5s:pass && make next-trigger-time
                                    logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());
                                    // 1、misfire match
                                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                                        // FIRE_ONCE_NOW 》 trigger
                                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    }
                                    // 2、fresh next
                                    refreshNextValidTime(jobInfo, new Date());
                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time
                                    // 1、trigger
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    // 2、fresh next
                                    refreshNextValidTime(jobInfo, new Date());
                                    // next-trigger-time in 5s, pre-read again
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
                                        // 1、make ring second
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());
                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                                    }
                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time
                                    // 1、make ring second
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                                    // 2、push time ring
                                    pushTimeRing(ringSecond, jobInfo.getId());
                                    // 3、fresh next
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                                }
                            }
                            // 3、update trigger info
                            for (XxlJobInfo jobInfo: scheduleList) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                            }
                        } else {
                            preReadSuc = false;
                        }
                        // tx stop
                    } catch (Exception e) {
                        if (!scheduleThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {
                        // commit
                        if (conn != null) {
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                        // close PreparedStatement
                        if (null != preparedStatement) {
                            try {
                                preparedStatement.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                    }
                    long cost = System.currentTimeMillis()-start;
                    // Wait seconds, align second
                    if (cost < 1000) {  // scan-overtime, not wait
                        try {
                            // pre-read period: success > scan each second; fail > skip this period;
                            TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
                        } catch (InterruptedException e) {
                            if (!scheduleThreadToStop) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
            }
        });
        scheduleThread.setDaemon(true);
        scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
        scheduleThread.start();
        // ring thread
        ringThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!ringThreadToStop) {
                    // align second
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
                    } catch (InterruptedException e) {
                        if (!ringThreadToStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                    try {
                        // second data
                        List<Integer> ringItemData = new ArrayList<>();
                        int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                        for (int i = 0; i < 2; i++) {
                            List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                            if (tmpData != null) {
                                ringItemData.addAll(tmpData);
                            }
                        }
                        // ring trigger
                        logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                        if (ringItemData.size() > 0) {
                            // do trigger
                            for (int jobId: ringItemData) {
                                // do trigger
                                JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                            }
                            // clear
                            ringItemData.clear();
                        }
                    } catch (Exception e) {
                        if (!ringThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        ringThread.start();
    }
    private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
        Date nextValidTime = generateNextValidTime(jobInfo, fromTime);
        if (nextValidTime != null) {
            jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
            jobInfo.setTriggerNextTime(nextValidTime.getTime());
        } else {
            jobInfo.setTriggerStatus(0);
            jobInfo.setTriggerLastTime(0);
            jobInfo.setTriggerNextTime(0);
            logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}",
                    jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf());
        }
    }
    private void pushTimeRing(int ringSecond, int jobId){
        // push async ring
        List<Integer> ringItemData = ringData.get(ringSecond);
        if (ringItemData == null) {
            ringItemData = new ArrayList<Integer>();
            ringData.put(ringSecond, ringItemData);
        }
        ringItemData.add(jobId);
        logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) );
    }
    public void toStop(){
        // 1、stop schedule
        scheduleThreadToStop = true;
        try {
            TimeUnit.SECONDS.sleep(1);  // wait
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
        if (scheduleThread.getState() != Thread.State.TERMINATED){
            // interrupt and wait
            scheduleThread.interrupt();
            try {
                scheduleThread.join();
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
        }
        // if has ring data
        boolean hasRingData = false;
        if (!ringData.isEmpty()) {
            for (int second : ringData.keySet()) {
                List<Integer> tmpData = ringData.get(second);
                if (tmpData!=null && tmpData.size()>0) {
                    hasRingData = true;
                    break;
                }
            }
        }
        if (hasRingData) {
            try {
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
        }
        // stop ring (wait job-in-memory stop)
        ringThreadToStop = true;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
        if (ringThread.getState() != Thread.State.TERMINATED){
            // interrupt and wait
            ringThread.interrupt();
            try {
                ringThread.join();
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
        }
        logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop");
    }
    // ---------------------- tools ----------------------
    public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
        ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
        if (ScheduleTypeEnum.CRON == scheduleTypeEnum) {
            Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime);
            return nextValidTime;
        } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) {
            return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 );
        }
        return null;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java
New file
@@ -0,0 +1,150 @@
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import com.xxl.job.admin.core.trigger.XxlJobTrigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * job trigger thread pool helper
 *
 * @author xuxueli 2018-07-03 21:08:07
 */
public class JobTriggerPoolHelper {
    private static Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class);
    // ---------------------- trigger pool ----------------------
    // fast/slow thread pool
    private ThreadPoolExecutor fastTriggerPool = null;
    private ThreadPoolExecutor slowTriggerPool = null;
    public void start(){
        fastTriggerPool = new ThreadPoolExecutor(
                10,
                XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(),
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(1000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode());
                    }
                });
        slowTriggerPool = new ThreadPoolExecutor(
                10,
                XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(),
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(2000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode());
                    }
                });
    }
    public void stop() {
        //triggerPool.shutdown();
        fastTriggerPool.shutdownNow();
        slowTriggerPool.shutdownNow();
        logger.info(">>>>>>>>> xxl-job trigger thread pool shutdown success.");
    }
    // job timeout count
    private volatile long minTim = System.currentTimeMillis()/60000;     // ms > min
    private volatile ConcurrentMap<Integer, AtomicInteger> jobTimeoutCountMap = new ConcurrentHashMap<>();
    /**
     * add trigger
     */
    public void addTrigger(final int jobId,
                           final TriggerTypeEnum triggerType,
                           final int failRetryCount,
                           final String executorShardingParam,
                           final String executorParam,
                           final String addressList) {
        // choose thread pool
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {      // job-timeout 10 times in 1 min
            triggerPool_ = slowTriggerPool;
        }
        // trigger
        triggerPool_.execute(new Runnable() {
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                try {
                    // do trigger
                    XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                } finally {
                    // check timeout-count-map
                    long minTim_now = System.currentTimeMillis()/60000;
                    if (minTim != minTim_now) {
                        minTim = minTim_now;
                        jobTimeoutCountMap.clear();
                    }
                    // incr timeout-count-map
                    long cost = System.currentTimeMillis()-start;
                    if (cost > 500) {       // ob-timeout threshold 500ms
                        AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                        if (timeoutCount != null) {
                            timeoutCount.incrementAndGet();
                        }
                    }
                }
            }
        });
    }
    // ---------------------- helper ----------------------
    private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper();
    public static void toStart() {
        helper.start();
    }
    public static void toStop() {
        helper.stop();
    }
    /**
     * @param jobId
     * @param triggerType
     * @param failRetryCount
     *             >=0: use this param
     *             <0: use param from job info config
     * @param executorShardingParam
     * @param executorParam
     *          null: use job param
     *          not null: cover job param
     */
    public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) {
        helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java
New file
@@ -0,0 +1,27 @@
package com.xxl.job.admin.core.trigger;
import com.xxl.job.admin.core.util.I18nUtil;
/**
 * trigger type enum
 *
 * @author xuxueli 2018-09-16 04:56:41
 */
public enum TriggerTypeEnum {
    MANUAL(I18nUtil.getString("jobconf_trigger_type_manual")),
    CRON(I18nUtil.getString("jobconf_trigger_type_cron")),
    RETRY(I18nUtil.getString("jobconf_trigger_type_retry")),
    PARENT(I18nUtil.getString("jobconf_trigger_type_parent")),
    API(I18nUtil.getString("jobconf_trigger_type_api")),
    MISFIRE(I18nUtil.getString("jobconf_trigger_type_misfire"));
    private TriggerTypeEnum(String title){
        this.title = title;
    }
    private String title;
    public String getTitle() {
        return title;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java
New file
@@ -0,0 +1,226 @@
package com.xxl.job.admin.core.trigger;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLog;
import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.core.biz.ExecutorBiz;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.biz.model.TriggerParam;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.util.IpUtil;
import com.xxl.job.core.util.ThrowableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
/**
 * xxl-job trigger
 * Created by xuxueli on 17/7/13.
 */
public class XxlJobTrigger {
    private static Logger logger = LoggerFactory.getLogger(XxlJobTrigger.class);
    /**
     * trigger job
     *
     * @param jobId
     * @param triggerType
     * @param failRetryCount
     *             >=0: use this param
     *             <0: use param from job info config
     * @param executorShardingParam
     * @param executorParam
     *          null: use job param
     *          not null: cover job param
     * @param addressList
     *          null: use executor addressList
     *          not null: cover
     */
    public static void trigger(int jobId,
                               TriggerTypeEnum triggerType,
                               int failRetryCount,
                               String executorShardingParam,
                               String executorParam,
                               String addressList) {
        // load data
        XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId);
        if (jobInfo == null) {
            logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
            return;
        }
        if (executorParam != null) {
            jobInfo.setExecutorParam(executorParam);
        }
        int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount();
        XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup());
        // cover addressList
        if (addressList!=null && addressList.trim().length()>0) {
            group.setAddressType(1);
            group.setAddressList(addressList.trim());
        }
        // sharding param
        int[] shardingParam = null;
        if (executorShardingParam!=null){
            String[] shardingArr = executorShardingParam.split("/");
            if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) {
                shardingParam = new int[2];
                shardingParam[0] = Integer.valueOf(shardingArr[0]);
                shardingParam[1] = Integer.valueOf(shardingArr[1]);
            }
        }
        if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
                && group.getRegistryList()!=null && !group.getRegistryList().isEmpty()
                && shardingParam==null) {
            for (int i = 0; i < group.getRegistryList().size(); i++) {
                processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
            }
        } else {
            if (shardingParam == null) {
                shardingParam = new int[]{0, 1};
            }
            processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
        }
    }
    private static boolean isNumeric(String str){
        try {
            int result = Integer.valueOf(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    /**
     * @param group                     job group, registry list may be empty
     * @param jobInfo
     * @param finalFailRetryCount
     * @param triggerType
     * @param index                     sharding index
     * @param total                     sharding index
     */
    private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){
        // param
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION);  // block strategy
        ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null);    // route strategy
        String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null;
        // 1、save log-id
        XxlJobLog jobLog = new XxlJobLog();
        jobLog.setJobGroup(jobInfo.getJobGroup());
        jobLog.setJobId(jobInfo.getId());
        jobLog.setTriggerTime(new Date());
        XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog);
        logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());
        // 2、init trigger-param
        TriggerParam triggerParam = new TriggerParam();
        triggerParam.setJobId(jobInfo.getId());
        triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
        triggerParam.setExecutorParams(jobInfo.getExecutorParam());
        triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
        triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
        triggerParam.setLogId(jobLog.getId());
        triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime());
        triggerParam.setGlueType(jobInfo.getGlueType());
        triggerParam.setGlueSource(jobInfo.getGlueSource());
        triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
        triggerParam.setBroadcastIndex(index);
        triggerParam.setBroadcastTotal(total);
        // 3、init address
        String address = null;
        ReturnT<String> routeAddressResult = null;
        if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
            if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
                if (index < group.getRegistryList().size()) {
                    address = group.getRegistryList().get(index);
                } else {
                    address = group.getRegistryList().get(0);
                }
            } else {
                routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
                if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
                    address = routeAddressResult.getContent();
                }
            }
        } else {
            routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
        }
        // 4、trigger remote executor
        ReturnT<String> triggerResult = null;
        if (address != null) {
            triggerResult = runExecutor(triggerParam, address);
        } else {
            triggerResult = new ReturnT<String>(ReturnT.FAIL_CODE, null);
        }
        // 5、collection trigger info
        StringBuffer triggerMsgSb = new StringBuffer();
        triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
                .append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle());
        if (shardingParam != null) {
            triggerMsgSb.append("("+shardingParam+")");
        }
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount);
        triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>")
                .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"<br><br>":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():"");
        // 6、save log trigger-info
        jobLog.setExecutorAddress(address);
        jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
        jobLog.setExecutorParam(jobInfo.getExecutorParam());
        jobLog.setExecutorShardingParam(shardingParam);
        jobLog.setExecutorFailRetryCount(finalFailRetryCount);
        //jobLog.setTriggerTime();
        jobLog.setTriggerCode(triggerResult.getCode());
        jobLog.setTriggerMsg(triggerMsgSb.toString());
        XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog);
        logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());
    }
    /**
     * run executor
     * @param triggerParam
     * @param address
     * @return
     */
    public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
        ReturnT<String> runResult = null;
        try {
            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
            runResult = executorBiz.run(triggerParam);
        } catch (Exception e) {
            logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
            runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
        }
        StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
        runResultSB.append("<br>address:").append(address);
        runResultSB.append("<br>code:").append(runResult.getCode());
        runResultSB.append("<br>msg:").append(runResult.getMsg());
        runResult.setMsg(runResultSB.toString());
        return runResult;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java
New file
@@ -0,0 +1,98 @@
package com.xxl.job.admin.core.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * Cookie.Util
 *
 * @author xuxueli 2015-12-12 18:01:06
 */
public class CookieUtil {
    // 默认缓存时间,单位/秒, 2H
    private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE;
    // 保存路径,根路径
    private static final String COOKIE_PATH = "/";
    /**
     * 保存
     *
     * @param response
     * @param key
     * @param value
     * @param ifRemember
     */
    public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) {
        int age = ifRemember?COOKIE_MAX_AGE:-1;
        set(response, key, value, null, COOKIE_PATH, age, true);
    }
    /**
     * 保存
     *
     * @param response
     * @param key
     * @param value
     * @param maxAge
     */
    private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) {
        Cookie cookie = new Cookie(key, value);
        if (domain != null) {
            cookie.setDomain(domain);
        }
        cookie.setPath(path);
        cookie.setMaxAge(maxAge);
        cookie.setHttpOnly(isHttpOnly);
        response.addCookie(cookie);
    }
    /**
     * 查询value
     *
     * @param request
     * @param key
     * @return
     */
    public static String getValue(HttpServletRequest request, String key) {
        Cookie cookie = get(request, key);
        if (cookie != null) {
            return cookie.getValue();
        }
        return null;
    }
    /**
     * 查询Cookie
     *
     * @param request
     * @param key
     */
    private static Cookie get(HttpServletRequest request, String key) {
        Cookie[] arr_cookie = request.getCookies();
        if (arr_cookie != null && arr_cookie.length > 0) {
            for (Cookie cookie : arr_cookie) {
                if (cookie.getName().equals(key)) {
                    return cookie;
                }
            }
        }
        return null;
    }
    /**
     * 删除Cookie
     *
     * @param request
     * @param response
     * @param key
     */
    public static void remove(HttpServletRequest request, HttpServletResponse response, String key) {
        Cookie cookie = get(request, key);
        if (cookie != null) {
            set(response, key, "", null, COOKIE_PATH, 0, true);
        }
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java
New file
@@ -0,0 +1,31 @@
package com.xxl.job.admin.core.util;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.template.Configuration;
import freemarker.template.TemplateHashModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * ftl util
 *
 * @author xuxueli 2018-01-17 20:37:48
 */
public class FtlUtil {
    private static Logger logger = LoggerFactory.getLogger(FtlUtil.class);
    private static BeansWrapper wrapper = new BeansWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build();     //BeansWrapper.getDefaultInstance();
    public static TemplateHashModel generateStaticModel(String packageName) {
        try {
            TemplateHashModel staticModels = wrapper.getStaticModels();
            TemplateHashModel fileStatics = (TemplateHashModel) staticModels.get(packageName);
            return fileStatics;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java
New file
@@ -0,0 +1,79 @@
package com.xxl.job.admin.core.util;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
 * i18n util
 *
 * @author xuxueli 2018-01-17 20:39:06
 */
public class I18nUtil {
    private static Logger logger = LoggerFactory.getLogger(I18nUtil.class);
    private static Properties prop = null;
    public static Properties loadI18nProp(){
        if (prop != null) {
            return prop;
        }
        try {
            // build i18n prop
            String i18n = XxlJobAdminConfig.getAdminConfig().getI18n();
            String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n);
            // load prop
            Resource resource = new ClassPathResource(i18nFile);
            EncodedResource encodedResource = new EncodedResource(resource,"UTF-8");
            prop = PropertiesLoaderUtils.loadProperties(encodedResource);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
        return prop;
    }
    /**
     * get val of i18n key
     *
     * @param key
     * @return
     */
    public static String getString(String key) {
        return loadI18nProp().getProperty(key);
    }
    /**
     * get mult val of i18n mult key, as json
     *
     * @param keys
     * @return
     */
    public static String getMultString(String... keys) {
        Map<String, String> map = new HashMap<String, String>();
        Properties prop = loadI18nProp();
        if (keys!=null && keys.length>0) {
            for (String key: keys) {
                map.put(key, prop.getProperty(key));
            }
        } else {
            for (String key: prop.stringPropertyNames()) {
                map.put(key, prop.getProperty(key));
            }
        }
        String json = JacksonUtil.writeValueAsString(map);
        return json;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java
New file
@@ -0,0 +1,92 @@
package com.xxl.job.admin.core.util;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
 * Jackson util
 *
 * 1、obj need private and set/get;
 * 2、do not support inner class;
 *
 * @author xuxueli 2015-9-25 18:02:56
 */
public class JacksonUtil {
    private static Logger logger = LoggerFactory.getLogger(JacksonUtil.class);
    private final static ObjectMapper objectMapper = new ObjectMapper();
    public static ObjectMapper getInstance() {
        return objectMapper;
    }
    /**
     * bean、array、List、Map --> json
     *
     * @param obj
     * @return json string
     * @throws Exception
     */
    public static String writeValueAsString(Object obj) {
        try {
            return getInstance().writeValueAsString(obj);
        } catch (JsonGenerationException e) {
            logger.error(e.getMessage(), e);
        } catch (JsonMappingException e) {
            logger.error(e.getMessage(), e);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }
    /**
     * string --> bean、Map、List(array)
     *
     * @param jsonStr
     * @param clazz
     * @return obj
     * @throws Exception
     */
    public static <T> T readValue(String jsonStr, Class<T> clazz) {
        try {
            return getInstance().readValue(jsonStr, clazz);
        } catch (JsonParseException e) {
            logger.error(e.getMessage(), e);
        } catch (JsonMappingException e) {
            logger.error(e.getMessage(), e);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }
    /**
     * string --> List<Bean>...
     *
     * @param jsonStr
     * @param parametrized
     * @param parameterClasses
     * @param <T>
     * @return
     */
    public static <T> T readValue(String jsonStr, Class<?> parametrized, Class<?>... parameterClasses) {
        try {
            JavaType javaType = getInstance().getTypeFactory().constructParametricType(parametrized, parameterClasses);
            return getInstance().readValue(jsonStr, javaType);
        } catch (JsonParseException e) {
            logger.error(e.getMessage(), e);
        } catch (JsonMappingException e) {
            logger.error(e.getMessage(), e);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java
New file
@@ -0,0 +1,133 @@
package com.xxl.job.admin.core.util;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
 * local cache tool
 *
 * @author xuxueli 2018-01-22 21:37:34
 */
public class LocalCacheUtil {
    private static ConcurrentMap<String, LocalCacheData> cacheRepository = new ConcurrentHashMap<String, LocalCacheData>();   // 类型建议用抽象父类,兼容性更好;
    private static class LocalCacheData{
        private String key;
        private Object val;
        private long timeoutTime;
        public LocalCacheData() {
        }
        public LocalCacheData(String key, Object val, long timeoutTime) {
            this.key = key;
            this.val = val;
            this.timeoutTime = timeoutTime;
        }
        public String getKey() {
            return key;
        }
        public void setKey(String key) {
            this.key = key;
        }
        public Object getVal() {
            return val;
        }
        public void setVal(Object val) {
            this.val = val;
        }
        public long getTimeoutTime() {
            return timeoutTime;
        }
        public void setTimeoutTime(long timeoutTime) {
            this.timeoutTime = timeoutTime;
        }
    }
    /**
     * set cache
     *
     * @param key
     * @param val
     * @param cacheTime
     * @return
     */
    public static boolean set(String key, Object val, long cacheTime){
        // clean timeout cache, before set new cache (avoid cache too much)
        cleanTimeoutCache();
        // set new cache
        if (key==null || key.trim().length()==0) {
            return false;
        }
        if (val == null) {
            remove(key);
        }
        if (cacheTime <= 0) {
            remove(key);
        }
        long timeoutTime = System.currentTimeMillis() + cacheTime;
        LocalCacheData localCacheData = new LocalCacheData(key, val, timeoutTime);
        cacheRepository.put(localCacheData.getKey(), localCacheData);
        return true;
    }
    /**
     * remove cache
     *
     * @param key
     * @return
     */
    public static boolean remove(String key){
        if (key==null || key.trim().length()==0) {
            return false;
        }
        cacheRepository.remove(key);
        return true;
    }
    /**
     * get cache
     *
     * @param key
     * @return
     */
    public static Object get(String key){
        if (key==null || key.trim().length()==0) {
            return null;
        }
        LocalCacheData localCacheData = cacheRepository.get(key);
        if (localCacheData!=null && System.currentTimeMillis()<localCacheData.getTimeoutTime()) {
            return localCacheData.getVal();
        } else {
            remove(key);
            return null;
        }
    }
    /**
     * clean timeout cache
     *
     * @return
     */
    public static boolean cleanTimeoutCache(){
        if (!cacheRepository.keySet().isEmpty()) {
            for (String key: cacheRepository.keySet()) {
                LocalCacheData localCacheData = cacheRepository.get(key);
                if (localCacheData!=null && System.currentTimeMillis()>=localCacheData.getTimeoutTime()) {
                    cacheRepository.remove(key);
                }
            }
        }
        return true;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java
New file
@@ -0,0 +1,37 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobGroup;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * Created by xuxueli on 16/9/30.
 */
@Mapper
public interface XxlJobGroupDao {
    public List<XxlJobGroup> findAll();
    public List<XxlJobGroup> findByAddressType(@Param("addressType") int addressType);
    public int save(XxlJobGroup xxlJobGroup);
    public int update(XxlJobGroup xxlJobGroup);
    public int remove(@Param("id") int id);
    public XxlJobGroup load(@Param("id") int id);
    public List<XxlJobGroup> pageList(@Param("offset") int offset,
                                      @Param("pagesize") int pagesize,
                                      @Param("appname") String appname,
                                      @Param("title") String title);
    public int pageListCount(@Param("offset") int offset,
                             @Param("pagesize") int pagesize,
                             @Param("appname") String appname,
                             @Param("title") String title);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java
New file
@@ -0,0 +1,49 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * job info
 * @author xuxueli 2016-1-12 18:03:45
 */
@Mapper
public interface XxlJobInfoDao {
    public List<XxlJobInfo> pageList(@Param("offset") int offset,
                                     @Param("pagesize") int pagesize,
                                     @Param("jobGroup") int jobGroup,
                                     @Param("triggerStatus") int triggerStatus,
                                     @Param("jobDesc") String jobDesc,
                                     @Param("executorHandler") String executorHandler,
                                     @Param("author") String author);
    public int pageListCount(@Param("offset") int offset,
                             @Param("pagesize") int pagesize,
                             @Param("jobGroup") int jobGroup,
                             @Param("triggerStatus") int triggerStatus,
                             @Param("jobDesc") String jobDesc,
                             @Param("executorHandler") String executorHandler,
                             @Param("author") String author);
    public int save(XxlJobInfo info);
    public XxlJobInfo loadById(@Param("id") int id);
    public int update(XxlJobInfo xxlJobInfo);
    public int delete(@Param("id") long id);
    public List<XxlJobInfo> getJobsByGroup(@Param("jobGroup") int jobGroup);
    public int findAllCount();
    public List<XxlJobInfo> scheduleJobQuery(@Param("maxNextTime") long maxNextTime, @Param("pagesize") int pagesize );
    public int scheduleUpdate(XxlJobInfo xxlJobInfo);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java
New file
@@ -0,0 +1,62 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
 * job log
 * @author xuxueli 2016-1-12 18:03:06
 */
@Mapper
public interface XxlJobLogDao {
    // exist jobId not use jobGroup, not exist use jobGroup
    public List<XxlJobLog> pageList(@Param("offset") int offset,
                                    @Param("pagesize") int pagesize,
                                    @Param("jobGroup") int jobGroup,
                                    @Param("jobId") int jobId,
                                    @Param("triggerTimeStart") Date triggerTimeStart,
                                    @Param("triggerTimeEnd") Date triggerTimeEnd,
                                    @Param("logStatus") int logStatus);
    public int pageListCount(@Param("offset") int offset,
                             @Param("pagesize") int pagesize,
                             @Param("jobGroup") int jobGroup,
                             @Param("jobId") int jobId,
                             @Param("triggerTimeStart") Date triggerTimeStart,
                             @Param("triggerTimeEnd") Date triggerTimeEnd,
                             @Param("logStatus") int logStatus);
    public XxlJobLog load(@Param("id") long id);
    public long save(XxlJobLog xxlJobLog);
    public int updateTriggerInfo(XxlJobLog xxlJobLog);
    public int updateHandleInfo(XxlJobLog xxlJobLog);
    public int delete(@Param("jobId") int jobId);
    public Map<String, Object> findLogReport(@Param("from") Date from,
                                             @Param("to") Date to);
    public List<Long> findClearLogIds(@Param("jobGroup") int jobGroup,
                                      @Param("jobId") int jobId,
                                      @Param("clearBeforeTime") Date clearBeforeTime,
                                      @Param("clearBeforeNum") int clearBeforeNum,
                                      @Param("pagesize") int pagesize);
    public int clearLog(@Param("logIds") List<Long> logIds);
    public List<Long> findFailJobLogIds(@Param("pagesize") int pagesize);
    public int updateAlarmStatus(@Param("logId") long logId,
                                 @Param("oldAlarmStatus") int oldAlarmStatus,
                                 @Param("newAlarmStatus") int newAlarmStatus);
    public List<Long> findLostJobIds(@Param("losedTime") Date losedTime);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java
New file
@@ -0,0 +1,24 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobLogGlue;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * job log for glue
 * @author xuxueli 2016-5-19 18:04:56
 */
@Mapper
public interface XxlJobLogGlueDao {
    public int save(XxlJobLogGlue xxlJobLogGlue);
    public List<XxlJobLogGlue> findByJobId(@Param("jobId") int jobId);
    public int removeOld(@Param("jobId") int jobId, @Param("limit") int limit);
    public int deleteByJobId(@Param("jobId") int jobId);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java
New file
@@ -0,0 +1,26 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobLogReport;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
 * job log
 * @author xuxueli 2019-11-22
 */
@Mapper
public interface XxlJobLogReportDao {
    public int save(XxlJobLogReport xxlJobLogReport);
    public int update(XxlJobLogReport xxlJobLogReport);
    public List<XxlJobLogReport> queryLogReport(@Param("triggerDayFrom") Date triggerDayFrom,
                                                @Param("triggerDayTo") Date triggerDayTo);
    public XxlJobLogReport queryLogReportTotal();
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java
New file
@@ -0,0 +1,38 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobRegistry;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
 * Created by xuxueli on 16/9/30.
 */
@Mapper
public interface XxlJobRegistryDao {
    public List<Integer> findDead(@Param("timeout") int timeout,
                                  @Param("nowTime") Date nowTime);
    public int removeDead(@Param("ids") List<Integer> ids);
    public List<XxlJobRegistry> findAll(@Param("timeout") int timeout,
                                        @Param("nowTime") Date nowTime);
    public int registryUpdate(@Param("registryGroup") String registryGroup,
                              @Param("registryKey") String registryKey,
                              @Param("registryValue") String registryValue,
                              @Param("updateTime") Date updateTime);
    public int registrySave(@Param("registryGroup") String registryGroup,
                            @Param("registryKey") String registryKey,
                            @Param("registryValue") String registryValue,
                            @Param("updateTime") Date updateTime);
    public int registryDelete(@Param("registryGroup") String registryGroup,
                          @Param("registryKey") String registryKey,
                          @Param("registryValue") String registryValue);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java
New file
@@ -0,0 +1,31 @@
package com.xxl.job.admin.dao;
import com.xxl.job.admin.core.model.XxlJobUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * @author xuxueli 2019-05-04 16:44:59
 */
@Mapper
public interface XxlJobUserDao {
    public List<XxlJobUser> pageList(@Param("offset") int offset,
                                     @Param("pagesize") int pagesize,
                                     @Param("username") String username,
                                     @Param("role") int role);
    public int pageListCount(@Param("offset") int offset,
                             @Param("pagesize") int pagesize,
                             @Param("username") String username,
                             @Param("role") int role);
    public XxlJobUser loadByUserName(@Param("username") String username);
    public int save(XxlJobUser xxlJobUser);
    public int update(XxlJobUser xxlJobUser);
    public int delete(@Param("id") int id);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java
New file
@@ -0,0 +1,107 @@
package com.xxl.job.admin.service;
import com.xxl.job.admin.core.model.XxlJobUser;
import com.xxl.job.admin.core.util.CookieUtil;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.core.util.JacksonUtil;
import com.xxl.job.admin.dao.XxlJobUserDao;
import com.xxl.job.core.biz.model.ReturnT;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.DigestUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigInteger;
/**
 * @author xuxueli 2019-05-04 22:13:264
 */
@Configuration
public class LoginService {
    public static final String LOGIN_IDENTITY_KEY = "XXL_JOB_LOGIN_IDENTITY";
    @Resource
    private XxlJobUserDao xxlJobUserDao;
    private String makeToken(XxlJobUser xxlJobUser){
        String tokenJson = JacksonUtil.writeValueAsString(xxlJobUser);
        String tokenHex = new BigInteger(tokenJson.getBytes()).toString(16);
        return tokenHex;
    }
    private XxlJobUser parseToken(String tokenHex){
        XxlJobUser xxlJobUser = null;
        if (tokenHex != null) {
            String tokenJson = new String(new BigInteger(tokenHex, 16).toByteArray());      // username_password(md5)
            xxlJobUser = JacksonUtil.readValue(tokenJson, XxlJobUser.class);
        }
        return xxlJobUser;
    }
    public ReturnT<String> login(HttpServletRequest request, HttpServletResponse response, String username, String password, boolean ifRemember){
        // param
        if (username==null || username.trim().length()==0 || password==null || password.trim().length()==0){
            return new ReturnT<String>(500, I18nUtil.getString("login_param_empty"));
        }
        // valid passowrd
        XxlJobUser xxlJobUser = xxlJobUserDao.loadByUserName(username);
        if (xxlJobUser == null) {
            return new ReturnT<String>(500, I18nUtil.getString("login_param_unvalid"));
        }
        String passwordMd5 = DigestUtils.md5DigestAsHex(password.getBytes());
        if (!passwordMd5.equals(xxlJobUser.getPassword())) {
            return new ReturnT<String>(500, I18nUtil.getString("login_param_unvalid"));
        }
        String loginToken = makeToken(xxlJobUser);
        // do login
        CookieUtil.set(response, LOGIN_IDENTITY_KEY, loginToken, ifRemember);
        return ReturnT.SUCCESS;
    }
    /**
     * logout
     *
     * @param request
     * @param response
     */
    public ReturnT<String> logout(HttpServletRequest request, HttpServletResponse response){
        CookieUtil.remove(request, response, LOGIN_IDENTITY_KEY);
        return ReturnT.SUCCESS;
    }
    /**
     * logout
     *
     * @param request
     * @return
     */
    public XxlJobUser ifLogin(HttpServletRequest request, HttpServletResponse response){
        String cookieToken = CookieUtil.getValue(request, LOGIN_IDENTITY_KEY);
        if (cookieToken != null) {
            XxlJobUser cookieUser = null;
            try {
                cookieUser = parseToken(cookieToken);
            } catch (Exception e) {
                logout(request, response);
            }
            if (cookieUser != null) {
                XxlJobUser dbUser = xxlJobUserDao.loadByUserName(cookieUser.getUsername());
                if (dbUser != null) {
                    if (cookieUser.getPassword().equals(dbUser.getPassword())) {
                        return dbUser;
                    }
                }
            }
        }
        return null;
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java
New file
@@ -0,0 +1,86 @@
package com.xxl.job.admin.service;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.core.biz.model.ReturnT;
import java.util.Date;
import java.util.Map;
/**
 * core job action for xxl-job
 *
 * @author xuxueli 2016-5-28 15:30:33
 */
public interface XxlJobService {
    /**
     * page list
     *
     * @param start
     * @param length
     * @param jobGroup
     * @param jobDesc
     * @param executorHandler
     * @param author
     * @return
     */
    public Map<String, Object> pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author);
    /**
     * add job
     *
     * @param jobInfo
     * @return
     */
    public ReturnT<String> add(XxlJobInfo jobInfo);
    /**
     * update job
     *
     * @param jobInfo
     * @return
     */
    public ReturnT<String> update(XxlJobInfo jobInfo);
    /**
     * remove job
     *      *
     * @param id
     * @return
     */
    public ReturnT<String> remove(int id);
    /**
     * start job
     *
     * @param id
     * @return
     */
    public ReturnT<String> start(int id);
    /**
     * stop job
     *
     * @param id
     * @return
     */
    public ReturnT<String> stop(int id);
    /**
     * dashboard info
     *
     * @return
     */
    public Map<String,Object> dashboardInfo();
    /**
     * chart info
     *
     * @param startDate
     * @param endDate
     * @return
     */
    public ReturnT<Map<String,Object>> chartInfo(Date startDate, Date endDate);
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java
New file
@@ -0,0 +1,35 @@
package com.xxl.job.admin.service.impl;
import com.xxl.job.admin.core.thread.JobCompleteHelper;
import com.xxl.job.admin.core.thread.JobRegistryHelper;
import com.xxl.job.core.biz.AdminBiz;
import com.xxl.job.core.biz.model.HandleCallbackParam;
import com.xxl.job.core.biz.model.RegistryParam;
import com.xxl.job.core.biz.model.ReturnT;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * @author xuxueli 2017-07-27 21:54:20
 */
@Service
public class AdminBizImpl implements AdminBiz {
    @Override
    public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
        return JobCompleteHelper.getInstance().callback(callbackParamList);
    }
    @Override
    public ReturnT<String> registry(RegistryParam registryParam) {
        return JobRegistryHelper.getInstance().registry(registryParam);
    }
    @Override
    public ReturnT<String> registryRemove(RegistryParam registryParam) {
        return JobRegistryHelper.getInstance().registryRemove(registryParam);
    }
}
xxl-job/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java
New file
@@ -0,0 +1,434 @@
package com.xxl.job.admin.service.impl;
import com.xxl.job.admin.core.cron.CronExpression;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLogReport;
import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.thread.JobScheduleHelper;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.*;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.glue.GlueTypeEnum;
import com.xxl.job.core.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.*;
/**
 * core job action for xxl-job
 * @author xuxueli 2016-5-28 15:30:33
 */
@Service
public class XxlJobServiceImpl implements XxlJobService {
    private static Logger logger = LoggerFactory.getLogger(XxlJobServiceImpl.class);
    @Resource
    private XxlJobGroupDao xxlJobGroupDao;
    @Resource
    private XxlJobInfoDao xxlJobInfoDao;
    @Resource
    public XxlJobLogDao xxlJobLogDao;
    @Resource
    private XxlJobLogGlueDao xxlJobLogGlueDao;
    @Resource
    private XxlJobLogReportDao xxlJobLogReportDao;
    @Override
    public Map<String, Object> pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) {
        // page list
        List<XxlJobInfo> list = xxlJobInfoDao.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
        int list_count = xxlJobInfoDao.pageListCount(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
        // package result
        Map<String, Object> maps = new HashMap<String, Object>();
        maps.put("recordsTotal", list_count);        // 总记录数
        maps.put("recordsFiltered", list_count);    // 过滤后的总记录数
        maps.put("data", list);                      // 分页列表
        return maps;
    }
    @Override
    public ReturnT<String> add(XxlJobInfo jobInfo) {
        // valid base
        XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup());
        if (group == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")) );
        }
        if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) );
        }
        if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) );
        }
        // valid trigger
        ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
        if (scheduleTypeEnum == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
        }
        if (scheduleTypeEnum == ScheduleTypeEnum.CRON) {
            if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid"));
            }
        } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) {
            if (jobInfo.getScheduleConf() == null) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")) );
            }
            try {
                int fixSecond = Integer.valueOf(jobInfo.getScheduleConf());
                if (fixSecond < 1) {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
                }
            } catch (Exception e) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
            }
        }
        // valid job
        if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) );
        }
        if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+"JobHandler") );
        }
        // 》fix "\r" in shell
        if (GlueTypeEnum.GLUE_SHELL==GlueTypeEnum.match(jobInfo.getGlueType()) && jobInfo.getGlueSource()!=null) {
            jobInfo.setGlueSource(jobInfo.getGlueSource().replaceAll("\r", ""));
        }
        // valid advanced
        if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) );
        }
        if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) );
        }
        if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) );
        }
        // 》ChildJobId valid
        if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) {
            String[] childJobIds = jobInfo.getChildJobId().split(",");
            for (String childJobIdItem: childJobIds) {
                if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) {
                    XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem));
                    if (childJobInfo==null) {
                        return new ReturnT<String>(ReturnT.FAIL_CODE,
                                MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem));
                    }
                } else {
                    return new ReturnT<String>(ReturnT.FAIL_CODE,
                            MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem));
                }
            }
            // join , avoid "xxx,,"
            String temp = "";
            for (String item:childJobIds) {
                temp += item + ",";
            }
            temp = temp.substring(0, temp.length()-1);
            jobInfo.setChildJobId(temp);
        }
        // add in db
        jobInfo.setAddTime(new Date());
        jobInfo.setUpdateTime(new Date());
        jobInfo.setGlueUpdatetime(new Date());
        xxlJobInfoDao.save(jobInfo);
        if (jobInfo.getId() < 1) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) );
        }
        return new ReturnT<String>(String.valueOf(jobInfo.getId()));
    }
    private boolean isNumeric(String str){
        try {
            int result = Integer.valueOf(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    @Override
    public ReturnT<String> update(XxlJobInfo jobInfo) {
        // valid base
        if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) );
        }
        if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) );
        }
        // valid trigger
        ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
        if (scheduleTypeEnum == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
        }
        if (scheduleTypeEnum == ScheduleTypeEnum.CRON) {
            if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid") );
            }
        } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE /*|| scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) {
            if (jobInfo.getScheduleConf() == null) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
            }
            try {
                int fixSecond = Integer.valueOf(jobInfo.getScheduleConf());
                if (fixSecond < 1) {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
                }
            } catch (Exception e) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
            }
        }
        // valid advanced
        if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) );
        }
        if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) );
        }
        if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) );
        }
        // 》ChildJobId valid
        if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) {
            String[] childJobIds = jobInfo.getChildJobId().split(",");
            for (String childJobIdItem: childJobIds) {
                if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) {
                    XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem));
                    if (childJobInfo==null) {
                        return new ReturnT<String>(ReturnT.FAIL_CODE,
                                MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem));
                    }
                } else {
                    return new ReturnT<String>(ReturnT.FAIL_CODE,
                            MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem));
                }
            }
            // join , avoid "xxx,,"
            String temp = "";
            for (String item:childJobIds) {
                temp += item + ",";
            }
            temp = temp.substring(0, temp.length()-1);
            jobInfo.setChildJobId(temp);
        }
        // group valid
        XxlJobGroup jobGroup = xxlJobGroupDao.load(jobInfo.getJobGroup());
        if (jobGroup == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_jobgroup")+I18nUtil.getString("system_unvalid")) );
        }
        // stage job info
        XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId());
        if (exists_jobInfo == null) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) );
        }
        // next trigger time (5s后生效,避开预读周期)
        long nextTriggerTime = exists_jobInfo.getTriggerNextTime();
        boolean scheduleDataNotChanged = jobInfo.getScheduleType().equals(exists_jobInfo.getScheduleType()) && jobInfo.getScheduleConf().equals(exists_jobInfo.getScheduleConf());
        if (exists_jobInfo.getTriggerStatus() == 1 && !scheduleDataNotChanged) {
            try {
                Date nextValidTime = JobScheduleHelper.generateNextValidTime(jobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS));
                if (nextValidTime == null) {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
                }
                nextTriggerTime = nextValidTime.getTime();
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
            }
        }
        exists_jobInfo.setJobGroup(jobInfo.getJobGroup());
        exists_jobInfo.setJobDesc(jobInfo.getJobDesc());
        exists_jobInfo.setAuthor(jobInfo.getAuthor());
        exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail());
        exists_jobInfo.setScheduleType(jobInfo.getScheduleType());
        exists_jobInfo.setScheduleConf(jobInfo.getScheduleConf());
        exists_jobInfo.setMisfireStrategy(jobInfo.getMisfireStrategy());
        exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy());
        exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler());
        exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam());
        exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
        exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout());
        exists_jobInfo.setExecutorFailRetryCount(jobInfo.getExecutorFailRetryCount());
        exists_jobInfo.setChildJobId(jobInfo.getChildJobId());
        exists_jobInfo.setTriggerNextTime(nextTriggerTime);
        exists_jobInfo.setUpdateTime(new Date());
        xxlJobInfoDao.update(exists_jobInfo);
        return ReturnT.SUCCESS;
    }
    @Override
    public ReturnT<String> remove(int id) {
        XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
        if (xxlJobInfo == null) {
            return ReturnT.SUCCESS;
        }
        xxlJobInfoDao.delete(id);
        xxlJobLogDao.delete(id);
        xxlJobLogGlueDao.deleteByJobId(id);
        return ReturnT.SUCCESS;
    }
    @Override
    public ReturnT<String> start(int id) {
        XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
        // valid
        ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE);
        if (ScheduleTypeEnum.NONE == scheduleTypeEnum) {
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type_none_limit_start")) );
        }
        // next trigger time (5s后生效,避开预读周期)
        long nextTriggerTime = 0;
        try {
            Date nextValidTime = JobScheduleHelper.generateNextValidTime(xxlJobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS));
            if (nextValidTime == null) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
            }
            nextTriggerTime = nextValidTime.getTime();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
        }
        xxlJobInfo.setTriggerStatus(1);
        xxlJobInfo.setTriggerLastTime(0);
        xxlJobInfo.setTriggerNextTime(nextTriggerTime);
        xxlJobInfo.setUpdateTime(new Date());
        xxlJobInfoDao.update(xxlJobInfo);
        return ReturnT.SUCCESS;
    }
    @Override
    public ReturnT<String> stop(int id) {
        XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
        xxlJobInfo.setTriggerStatus(0);
        xxlJobInfo.setTriggerLastTime(0);
        xxlJobInfo.setTriggerNextTime(0);
        xxlJobInfo.setUpdateTime(new Date());
        xxlJobInfoDao.update(xxlJobInfo);
        return ReturnT.SUCCESS;
    }
    @Override
    public Map<String, Object> dashboardInfo() {
        int jobInfoCount = xxlJobInfoDao.findAllCount();
        int jobLogCount = 0;
        int jobLogSuccessCount = 0;
        XxlJobLogReport xxlJobLogReport = xxlJobLogReportDao.queryLogReportTotal();
        if (xxlJobLogReport != null) {
            jobLogCount = xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount();
            jobLogSuccessCount = xxlJobLogReport.getSucCount();
        }
        // executor count
        Set<String> executorAddressSet = new HashSet<String>();
        List<XxlJobGroup> groupList = xxlJobGroupDao.findAll();
        if (groupList!=null && !groupList.isEmpty()) {
            for (XxlJobGroup group: groupList) {
                if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
                    executorAddressSet.addAll(group.getRegistryList());
                }
            }
        }
        int executorCount = executorAddressSet.size();
        Map<String, Object> dashboardMap = new HashMap<String, Object>();
        dashboardMap.put("jobInfoCount", jobInfoCount);
        dashboardMap.put("jobLogCount", jobLogCount);
        dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount);
        dashboardMap.put("executorCount", executorCount);
        return dashboardMap;
    }
    @Override
    public ReturnT<Map<String, Object>> chartInfo(Date startDate, Date endDate) {
        // process
        List<String> triggerDayList = new ArrayList<String>();
        List<Integer> triggerDayCountRunningList = new ArrayList<Integer>();
        List<Integer> triggerDayCountSucList = new ArrayList<Integer>();
        List<Integer> triggerDayCountFailList = new ArrayList<Integer>();
        int triggerCountRunningTotal = 0;
        int triggerCountSucTotal = 0;
        int triggerCountFailTotal = 0;
        List<XxlJobLogReport> logReportList = xxlJobLogReportDao.queryLogReport(startDate, endDate);
        if (logReportList!=null && logReportList.size()>0) {
            for (XxlJobLogReport item: logReportList) {
                String day = DateUtil.formatDate(item.getTriggerDay());
                int triggerDayCountRunning = item.getRunningCount();
                int triggerDayCountSuc = item.getSucCount();
                int triggerDayCountFail = item.getFailCount();
                triggerDayList.add(day);
                triggerDayCountRunningList.add(triggerDayCountRunning);
                triggerDayCountSucList.add(triggerDayCountSuc);
                triggerDayCountFailList.add(triggerDayCountFail);
                triggerCountRunningTotal += triggerDayCountRunning;
                triggerCountSucTotal += triggerDayCountSuc;
                triggerCountFailTotal += triggerDayCountFail;
            }
        } else {
            for (int i = -6; i <= 0; i++) {
                triggerDayList.add(DateUtil.formatDate(DateUtil.addDays(new Date(), i)));
                triggerDayCountRunningList.add(0);
                triggerDayCountSucList.add(0);
                triggerDayCountFailList.add(0);
            }
        }
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("triggerDayList", triggerDayList);
        result.put("triggerDayCountRunningList", triggerDayCountRunningList);
        result.put("triggerDayCountSucList", triggerDayCountSucList);
        result.put("triggerDayCountFailList", triggerDayCountFailList);
        result.put("triggerCountRunningTotal", triggerCountRunningTotal);
        result.put("triggerCountSucTotal", triggerCountSucTotal);
        result.put("triggerCountFailTotal", triggerCountFailTotal);
        return new ReturnT<Map<String, Object>>(result);
    }
}
xxl-job/xxl-job-admin/src/main/resources/application.properties
New file
@@ -0,0 +1,65 @@
### web
server.port=8080
server.servlet.context-path=/xxl-job-admin
### actuator
management.server.servlet.context-path=/actuator
management.health.mail.enabled=false
### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########
### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
#mybatis.type-aliases-package=com.xxl.job.admin.core.model
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://rm-j6cwl6y72ln2918q2.mysql.rds.aliyuncs.com:3306/cdn1?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
spring.datasource.username=root
spring.datasource.password=!QAse4#@
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000
### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.from=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### xxl-job, access token
xxl.job.accessToken=default_token
### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
xxl.job.i18n=zh_CN
## xxl-job, triggerpool max size
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### xxl-job, log retention days
xxl.job.logretentiondays=30
xxl-job/xxl-job-admin/src/main/resources/i18n/message_en.properties
New file
@@ -0,0 +1,276 @@
admin_name=Scheduling Center
admin_name_full=Distributed Task Scheduling Platform XXL-JOB
admin_version=2.3.1
admin_i18n=en
## system
system_tips=System message
system_ok=Confirm
system_close=Close
system_save=Save
system_cancel=Cancel
system_search=Search
system_status=Status
system_opt=Operate
system_please_input=please input
system_please_choose=please choose
system_success=success
system_fail=fail
system_add_suc=add success
system_add_fail=add fail
system_update_suc=update success
system_update_fail=update fail
system_all=All
system_api_error=net error
system_show=Show
system_empty=Empty
system_opt_suc=operate success
system_opt_fail=operate fail
system_opt_edit=Edit
system_opt_del=Delete
system_opt_copy=Copy
system_unvalid=illegal
system_not_found=not exist
system_nav=Navigation
system_digits=digits
system_lengh_limit=Length limit
system_permission_limit=Permission limit
system_welcome=Welcome
## daterangepicker
daterangepicker_ranges_recent_hour=recent one hour
daterangepicker_ranges_today=today
daterangepicker_ranges_yesterday=yesterday
daterangepicker_ranges_this_month=this month
daterangepicker_ranges_last_month=last month
daterangepicker_ranges_recent_week=recent one week
daterangepicker_ranges_recent_month=recent one month
daterangepicker_custom_name=custom
daterangepicker_custom_starttime=start time
daterangepicker_custom_endtime=end time
daterangepicker_custom_daysofweek=Sun,Mon,Tue,Wed,Thu,Fri,Sat
daterangepicker_custom_monthnames=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec
## dataTable
dataTable_sProcessing=processing...
dataTable_sLengthMenu= _MENU_ records per page
dataTable_sZeroRecords=No matching results
dataTable_sInfo=page _PAGE_  ( Total _PAGES_ pages,_TOTAL_ records )
dataTable_sInfoEmpty=No Record
dataTable_sInfoFiltered=(Filtered by _MAX_ results)
dataTable_sSearch=Search
dataTable_sEmptyTable=Table data is empty
dataTable_sLoadingRecords=Loading...
dataTable_sFirst=FIRST PAGE
dataTable_sPrevious=Previous Page
dataTable_sNext=Next Page
dataTable_sLast=LAST PAGE
dataTable_sSortAscending=: Rank this column in ascending order
dataTable_sSortDescending=: Rank this column in descending order
## login
login_btn=Login
login_remember_me=Remember Me
login_username_placeholder=Please enter username
login_password_placeholder=Please enter password
login_username_empty=Please enter username
login_username_lt_4=Username length should not be less than 4
login_password_empty=Please enter password
login_password_lt_4=Password length should not be less than 4
login_success=Login success
login_fail=Login fail
login_param_empty=Username or password is empty
login_param_unvalid=Username or password error
## logout
logout_btn=Logout
logout_confirm=Confirm logout?
logout_success=Logout success
logout_fail=Logout fail
## change pwd
change_pwd=Change password
change_pwd_suc_to_logout=Change password successful, about to log out login
change_pwd_field_newpwd=new password
## dashboard
job_dashboard_name=Run report
job_dashboard_job_num=Job number
job_dashboard_job_num_tip=The number of tasks running in the scheduling center
job_dashboard_trigger_num=trigger number
job_dashboard_trigger_num_tip=The number of trigger record scheduled by the scheduling center
job_dashboard_jobgroup_num=Executor number
job_dashboard_jobgroup_num_tip=The number of online executor machines perceived by the scheduling center
job_dashboard_report=Scheduling report
job_dashboard_report_loaddata_fail=Scheduling report load data error
job_dashboard_date_report=Date distribution
job_dashboard_rate_report=Percentage distribution
## job info
jobinfo_name=Job Manage
jobinfo_job=Job
jobinfo_field_add=Add Job
jobinfo_field_update=Edit Job
jobinfo_field_id=Job ID
jobinfo_field_jobgroup=Executor
jobinfo_field_jobdesc=Job description
jobinfo_field_timeout=Job timeout period
jobinfo_field_gluetype=GLUE Type
jobinfo_field_executorparam=Param
jobinfo_field_author=Author
jobinfo_field_alarmemail=Alarm email
jobinfo_field_alarmemail_placeholder=Please enter alarm mail, if there are more than one comma separated
jobinfo_field_executorRouteStrategy=Route Strategy
jobinfo_field_childJobId=Child Job ID
jobinfo_field_childJobId_placeholder=Please enter the Child job ID, if there are more than one comma separated
jobinfo_field_executorBlockStrategy=Block Strategy
jobinfo_field_executorFailRetryCount=Fail Retry Count
jobinfo_field_executorFailRetryCount_placeholder=Fail Retry Count. effect if greater than zero
jobinfo_script_location=Script location
jobinfo_shard_index=Shard index
jobinfo_shard_total=Shard total
jobinfo_opt_stop=Stop
jobinfo_opt_start=Start
jobinfo_opt_log=Query Log
jobinfo_opt_run=Run Once
jobinfo_opt_run_tips=Please input the address for this trigger. Null will be obtained from the executor
jobinfo_opt_registryinfo=Registry Info
jobinfo_opt_next_time=Next trigger time
jobinfo_glue_remark=Resource Remark
jobinfo_glue_remark_limit=Resource Remark length is limited to 4~100
jobinfo_glue_rollback=Version Backtrack
jobinfo_glue_jobid_unvalid=Job ID is illegal
jobinfo_glue_gluetype_unvalid=The job is not GLUE Type
jobinfo_field_executorTimeout_placeholder=Job Timeout period,in seconds. effect if greater than zero
schedule_type=Schedule Type
schedule_type_none=None
schedule_type_cron=Cron
schedule_type_fix_rate=Fix rate
schedule_type_fix_delay=Fix delay
schedule_type_none_limit_start=The current schedule type disables startup
misfire_strategy=Misfire strategy
misfire_strategy_do_nothing=Do nothing
misfire_strategy_fire_once_now=Fire once now
jobinfo_conf_base=Base configuration
jobinfo_conf_schedule=Schedule configuration
jobinfo_conf_job=Job configuration
jobinfo_conf_advanced=Advanced configuration
## job log
joblog_name=Trigger Log
joblog_status=Status
joblog_status_all=All
joblog_status_suc=Success
joblog_status_fail=Fail
joblog_status_running=Running
joblog_field_triggerTime=Trigger Time
joblog_field_triggerCode=Trigger Result
joblog_field_triggerMsg=Trigger Msg
joblog_field_handleTime=Handle Time
joblog_field_handleCode=Handle Result
joblog_field_handleMsg=Trigger Msg
joblog_field_executorAddress=Executor Address
joblog_clean=Clean
joblog_clean_log=Clean Log
joblog_clean_type=Clean Type
joblog_clean_type_1=Clean up log data a month ago
joblog_clean_type_2=Clean up log data three month ago
joblog_clean_type_3=Clean up log data six month ago
joblog_clean_type_4=Clean up log data a year ago
joblog_clean_type_5=Clean up log data a thousand record ago
joblog_clean_type_6=Clean up log data ten thousand record ago
joblog_clean_type_7=Clean up log data thirty thousand record ago
joblog_clean_type_8=Clean up log data hundred thousand record ago
joblog_clean_type_9=Clean up all log data
joblog_clean_type_unvalid=Clean type is illegal
joblog_handleCode_200=Success
joblog_handleCode_500=Fail
joblog_handleCode_502=Timeout
joblog_kill_log=Kill Job
joblog_kill_log_limit=Trigger Fail, can not kill job
joblog_kill_log_byman=Manual operation, kill job
joblog_lost_fail=Job result lost, marked as failure
joblog_rolling_log=Rolling log
joblog_rolling_log_refresh=Refresh
joblog_rolling_log_triggerfail=The job trigger fail, can not view the rolling log
joblog_rolling_log_failoften=The request for the Rolling log is terminated, the number of failed requests exceeds the limit, Reload the log on the refresh page
joblog_logid_unvalid=Log ID is illegal
## job group
jobgroup_name=Executor Manage
jobgroup_list=Executor List
jobgroup_add=Add Executor
jobgroup_edit=Edit Executor
jobgroup_del=Delete Executor
jobgroup_field_title=Title
jobgroup_field_addressType=Registry Type
jobgroup_field_addressType_0=Automatic registration
jobgroup_field_addressType_1=Manual registration
jobgroup_field_addressType_limit=Manually registration type, the machine address must not be empty
jobgroup_field_registryList=machine address
jobgroup_field_registryList_unvalid=registry machine address is illegal
jobgroup_field_registryList_placeholder=Please enter the machine address, if there are more than one comma separated
jobgroup_field_appname_limit=Limit the beginning of a lowercase letter, consists of lowercase letters、number and hyphen.
jobgroup_field_appname_length=AppName length is limited to 4~64
jobgroup_field_title_length=Title length is limited to 4~12
jobgroup_field_order_digits=Please enter a positive integer
jobgroup_field_orderrange=Order is limited to 1~1000
jobgroup_del_limit_0=Refuse to delete, the executor is being used
jobgroup_del_limit_1=Refuses to delete, the system retains at least one executor
jobgroup_empty=There is no valid executor. Please contact the administrator
## job conf
jobconf_block_SERIAL_EXECUTION=Serial execution
jobconf_block_DISCARD_LATER=Discard Later
jobconf_block_COVER_EARLY=Cover Early
jobconf_route_first=First
jobconf_route_last=Last
jobconf_route_round=Round
jobconf_route_random=Random
jobconf_route_consistenthash=Consistent Hash
jobconf_route_lfu=Least Frequently Used
jobconf_route_lru=Least Recently Used
jobconf_route_failover=Failover
jobconf_route_busyover=Busyover
jobconf_route_shard=Sharding Broadcast
jobconf_idleBeat=Idle check
jobconf_beat=Heartbeats
jobconf_monitor=Task Scheduling Center monitor alarm
jobconf_monitor_detail=monitor alarm details
jobconf_monitor_alarm_title=Alarm Type
jobconf_monitor_alarm_type=Trigger Fail
jobconf_monitor_alarm_content=Alarm Content
jobconf_trigger_admin_adress=Trigger machine address
jobconf_trigger_exe_regtype=Execotor-Registry Type
jobconf_trigger_exe_regaddress=Execotor-Registry Address
jobconf_trigger_address_empty=Trigger Fail:registry address is empty
jobconf_trigger_run=Trigger Job
jobconf_trigger_child_run=Trigger child job
jobconf_callback_child_msg1={0}/{1} [Job ID={2}], Trigger {3}, Trigger msg: {4} <br>
jobconf_callback_child_msg2={0}/{1} [Job ID={2}], Trigger Fail, Trigger msg: Job ID is illegal <br>
jobconf_trigger_type=Job trigger type
jobconf_trigger_type_cron=Cron trigger
jobconf_trigger_type_manual=Manual trigger
jobconf_trigger_type_parent=Parent job trigger
jobconf_trigger_type_api=Api trigger
jobconf_trigger_type_retry=Fail retry trigger
jobconf_trigger_type_misfire=Misfire compensation trigger
## user
user_manage=User Manage
user_username=Username
user_password=Password
user_role=Role
user_role_admin=Admin User
user_role_normal=Normal User
user_permission=Permission
user_add=Add User
user_update=Edit User
user_username_repeat=Username Repeat
user_username_valid=Restrictions start with a lowercase letter and consist of lowercase letters and Numbers
user_password_update_placeholder=Please input password, empty means not update
user_update_loginuser_limit=Operation of current login account is not allowed
## help
job_help=Tutorial
job_help_document=Official Document
xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties
New file
@@ -0,0 +1,276 @@
admin_name=任务调度中心
admin_name_full=分布式任务调度平台XXL-JOB
admin_version=2.3.1
admin_i18n=
## system
system_tips=系统提示
system_ok=确定
system_close=关闭
system_save=保存
system_cancel=取消
system_search=搜索
system_status=状态
system_opt=操作
system_please_input=请输入
system_please_choose=请选择
system_success=成功
system_fail=失败
system_add_suc=新增成功
system_add_fail=新增失败
system_update_suc=更新成功
system_update_fail=更新失败
system_all=全部
system_api_error=接口异常
system_show=查看
system_empty=无
system_opt_suc=操作成功
system_opt_fail=操作失败
system_opt_edit=编辑
system_opt_del=删除
system_opt_copy=复制
system_unvalid=非法
system_not_found=不存在
system_nav=导航
system_digits=整数
system_lengh_limit=长度限制
system_permission_limit=权限拦截
system_welcome=欢迎
## daterangepicker
daterangepicker_ranges_recent_hour=最近一小时
daterangepicker_ranges_today=今日
daterangepicker_ranges_yesterday=昨日
daterangepicker_ranges_this_month=本月
daterangepicker_ranges_last_month=上个月
daterangepicker_ranges_recent_week=最近一周
daterangepicker_ranges_recent_month=最近一月
daterangepicker_custom_name=自定义
daterangepicker_custom_starttime=起始时间
daterangepicker_custom_endtime=结束时间
daterangepicker_custom_daysofweek=日,一,二,三,四,五,六
daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月
## dataTable
dataTable_sProcessing=处理中...
dataTable_sLengthMenu=每页 _MENU_ 条记录
dataTable_sZeroRecords=没有匹配结果
dataTable_sInfo=第 _PAGE_ 页 ( 总共 _PAGES_ 页,_TOTAL_ 条记录 )
dataTable_sInfoEmpty=无记录
dataTable_sInfoFiltered=(由 _MAX_ 项结果过滤)
dataTable_sSearch=搜索
dataTable_sEmptyTable=表中数据为空
dataTable_sLoadingRecords=载入中...
dataTable_sFirst=首页
dataTable_sPrevious=上页
dataTable_sNext=下页
dataTable_sLast=末页
dataTable_sSortAscending=: 以升序排列此列
dataTable_sSortDescending=: 以降序排列此列
## login
login_btn=登录
login_remember_me=记住密码
login_username_placeholder=请输入登录账号
login_password_placeholder=请输入登录密码
login_username_empty=请输入登录账号
login_username_lt_4=登录账号不应低于4位
login_password_empty=请输入登录密码
login_password_lt_4=登录密码不应低于4位
login_success=登录成功
login_fail=登录失败
login_param_empty=账号或密码为空
login_param_unvalid=账号或密码错误
## logout
logout_btn=注销
logout_confirm=确认注销登录?
logout_success=注销成功
logout_fail=注销失败
## change pwd
change_pwd=修改密码
change_pwd_suc_to_logout=修改密码成功,即将注销登陆
change_pwd_field_newpwd=新密码
## dashboard
job_dashboard_name=运行报表
job_dashboard_job_num=任务数量
job_dashboard_job_num_tip=调度中心运行的任务数量
job_dashboard_trigger_num=调度次数
job_dashboard_trigger_num_tip=调度中心触发的调度次数
job_dashboard_jobgroup_num=执行器数量
job_dashboard_jobgroup_num_tip=调度中心在线的执行器机器数量
job_dashboard_report=调度报表
job_dashboard_report_loaddata_fail=调度报表数据加载异常
job_dashboard_date_report=日期分布图
job_dashboard_rate_report=成功比例图
## job info
jobinfo_name=任务管理
jobinfo_job=任务
jobinfo_field_add=新增
jobinfo_field_update=更新任务
jobinfo_field_id=任务ID
jobinfo_field_jobgroup=执行器
jobinfo_field_jobdesc=任务描述
jobinfo_field_gluetype=运行模式
jobinfo_field_executorparam=任务参数
jobinfo_field_author=负责人
jobinfo_field_timeout=任务超时时间
jobinfo_field_alarmemail=报警邮件
jobinfo_field_alarmemail_placeholder=请输入报警邮件,多个邮件地址则逗号分隔
jobinfo_field_executorRouteStrategy=路由策略
jobinfo_field_childJobId=子任务ID
jobinfo_field_childJobId_placeholder=请输入子任务的任务ID,如存在多个则逗号分隔
jobinfo_field_executorBlockStrategy=阻塞处理策略
jobinfo_field_executorFailRetryCount=失败重试次数
jobinfo_field_executorFailRetryCount_placeholder=失败重试次数,大于零时生效
jobinfo_script_location=脚本位置
jobinfo_shard_index=分片序号
jobinfo_shard_total=分片总数
jobinfo_opt_stop=停止
jobinfo_opt_start=启动
jobinfo_opt_log=查询日志
jobinfo_opt_run=执行一次
jobinfo_opt_run_tips=请输入本次执行的机器地址,为空则从执行器获取
jobinfo_opt_registryinfo=注册节点
jobinfo_opt_next_time=下次执行时间
jobinfo_glue_remark=源码备注
jobinfo_glue_remark_limit=源码备注长度限制为4~100
jobinfo_glue_rollback=版本回溯
jobinfo_glue_jobid_unvalid=任务ID非法
jobinfo_glue_gluetype_unvalid=该任务非GLUE模式
jobinfo_field_executorTimeout_placeholder=任务超时时间,单位秒,大于零时生效
schedule_type=调度类型
schedule_type_none=无
schedule_type_cron=CRON
schedule_type_fix_rate=固定速度
schedule_type_fix_delay=固定延迟
schedule_type_none_limit_start=当前调度类型禁止启动
misfire_strategy=调度过期策略
misfire_strategy_do_nothing=忽略
misfire_strategy_fire_once_now=立即执行一次
jobinfo_conf_base=基础配置
jobinfo_conf_schedule=调度配置
jobinfo_conf_job=任务配置
jobinfo_conf_advanced=高级配置
## job log
joblog_name=调度日志
joblog_status=状态
joblog_status_all=全部
joblog_status_suc=成功
joblog_status_fail=失败
joblog_status_running=进行中
joblog_field_triggerTime=调度时间
joblog_field_triggerCode=调度结果
joblog_field_triggerMsg=调度备注
joblog_field_handleTime=执行时间
joblog_field_handleCode=执行结果
joblog_field_handleMsg=执行备注
joblog_field_executorAddress=执行器地址
joblog_clean=清理
joblog_clean_log=日志清理
joblog_clean_type=清理方式
joblog_clean_type_1=清理一个月之前日志数据
joblog_clean_type_2=清理三个月之前日志数据
joblog_clean_type_3=清理六个月之前日志数据
joblog_clean_type_4=清理一年之前日志数据
joblog_clean_type_5=清理一千条以前日志数据
joblog_clean_type_6=清理一万条以前日志数据
joblog_clean_type_7=清理三万条以前日志数据
joblog_clean_type_8=清理十万条以前日志数据
joblog_clean_type_9=清理所有日志数据
joblog_clean_type_unvalid=清理类型参数异常
joblog_handleCode_200=成功
joblog_handleCode_500=失败
joblog_handleCode_502=失败(超时)
joblog_kill_log=终止任务
joblog_kill_log_limit=调度失败,无法终止日志
joblog_kill_log_byman=人为操作,主动终止
joblog_lost_fail=任务结果丢失,标记失败
joblog_rolling_log=执行日志
joblog_rolling_log_refresh=刷新
joblog_rolling_log_triggerfail=任务发起调度失败,无法查看执行日志
joblog_rolling_log_failoften=终止请求Rolling日志,请求失败次数超上限,可刷新页面重新加载日志
joblog_logid_unvalid=日志ID非法
## job group
jobgroup_name=执行器管理
jobgroup_list=执行器列表
jobgroup_add=新增执行器
jobgroup_edit=编辑执行器
jobgroup_del=删除执行器
jobgroup_field_title=名称
jobgroup_field_addressType=注册方式
jobgroup_field_addressType_0=自动注册
jobgroup_field_addressType_1=手动录入
jobgroup_field_addressType_limit=手动录入注册方式,机器地址不可为空
jobgroup_field_registryList=机器地址
jobgroup_field_registryList_unvalid=机器地址格式非法
jobgroup_field_registryList_placeholder=请输入执行器地址列表,多地址逗号分隔
jobgroup_field_appname_limit=限制以小写字母开头,由小写字母、数字和中划线组成
jobgroup_field_appname_length=AppName长度限制为4~64
jobgroup_field_title_length=名称长度限制为4~12
jobgroup_field_order_digits=请输入整数
jobgroup_field_orderrange=取值范围为1~1000
jobgroup_del_limit_0=拒绝删除,该执行器使用中
jobgroup_del_limit_1=拒绝删除, 系统至少保留一个执行器
jobgroup_empty=不存在有效执行器,请联系管理员
## job conf
jobconf_block_SERIAL_EXECUTION=单机串行
jobconf_block_DISCARD_LATER=丢弃后续调度
jobconf_block_COVER_EARLY=覆盖之前调度
jobconf_route_first=第一个
jobconf_route_last=最后一个
jobconf_route_round=轮询
jobconf_route_random=随机
jobconf_route_consistenthash=一致性HASH
jobconf_route_lfu=最不经常使用
jobconf_route_lru=最近最久未使用
jobconf_route_failover=故障转移
jobconf_route_busyover=忙碌转移
jobconf_route_shard=分片广播
jobconf_idleBeat=空闲检测
jobconf_beat=心跳检测
jobconf_monitor=任务调度中心监控报警
jobconf_monitor_detail=监控告警明细
jobconf_monitor_alarm_title=告警类型
jobconf_monitor_alarm_type=调度失败
jobconf_monitor_alarm_content=告警内容
jobconf_trigger_admin_adress=调度机器
jobconf_trigger_exe_regtype=执行器-注册方式
jobconf_trigger_exe_regaddress=执行器-地址列表
jobconf_trigger_address_empty=调度失败:执行器地址为空
jobconf_trigger_run=触发调度
jobconf_trigger_child_run=触发子任务
jobconf_callback_child_msg1={0}/{1} [任务ID={2}], 触发{3}, 触发备注: {4} <br>
jobconf_callback_child_msg2={0}/{1} [任务ID={2}], 触发失败, 触发备注: 任务ID格式错误 <br>
jobconf_trigger_type=任务触发类型
jobconf_trigger_type_cron=Cron触发
jobconf_trigger_type_manual=手动触发
jobconf_trigger_type_parent=父任务触发
jobconf_trigger_type_api=API触发
jobconf_trigger_type_retry=失败重试触发
jobconf_trigger_type_misfire=调度过期补偿
## user
user_manage=用户管理
user_username=账号
user_password=密码
user_role=角色
user_role_admin=管理员
user_role_normal=普通用户
user_permission=权限
user_add=新增用户
user_update=更新用户
user_username_repeat=账号重复
user_username_valid=限制以小写字母开头,由小写字母、数字组成
user_password_update_placeholder=请输入新密码,为空则不更新密码
user_update_loginuser_limit=禁止操作当前登录账号
## help
job_help=使用教程
job_help_document=官方文档
xxl-job/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties
New file
@@ -0,0 +1,276 @@
admin_name=任務調度中心
admin_name_full=分布式任務調度平臺XXL-JOB
admin_version=2.3.1
admin_i18n=
## system
system_tips=系統提示
system_ok=確定
system_close=關閉
system_save=儲存
system_cancel=取消
system_search=搜尋
system_status=狀態
system_opt=操作
system_please_input=請輸入
system_please_choose=请選擇
system_success=成功
system_fail=失敗
system_add_suc=新增成功
system_add_fail=新增失敗
system_update_suc=更新成功
system_update_fail=更新失敗
system_all=全部
system_api_error=API錯誤
system_show=查看
system_empty=無
system_opt_suc=操作成功
system_opt_fail=操作失敗
system_opt_edit=編輯
system_opt_del=刪除
system_opt_copy=復制
system_unvalid=非法
system_not_found=不存在
system_nav=導航
system_digits=整數
system_lengh_limit=長度限制
system_permission_limit=權限控管
system_welcome=歡迎
## daterangepicker
daterangepicker_ranges_recent_hour=最近一小時
daterangepicker_ranges_today=今日
daterangepicker_ranges_yesterday=昨日
daterangepicker_ranges_this_month=本月
daterangepicker_ranges_last_month=上個月
daterangepicker_ranges_recent_week=最近一周
daterangepicker_ranges_recent_month=最近一月
daterangepicker_custom_name=自定義
daterangepicker_custom_starttime=起始時間
daterangepicker_custom_endtime=結束時間
daterangepicker_custom_daysofweek=日,一,二,三,四,五,六
daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月
## dataTable
dataTable_sProcessing=處理中...
dataTable_sLengthMenu=每頁 _MENU_ 條記錄
dataTable_sZeroRecords=沒有相符合記錄
dataTable_sInfo=第 _PAGE_ 頁 ( 總共 _PAGES_ 頁,_TOTAL_ 條記錄 )
dataTable_sInfoEmpty=無記錄
dataTable_sInfoFiltered=(由 _MAX_ 項結果過濾)
dataTable_sSearch=搜尋
dataTable_sEmptyTable=表中資料為空
dataTable_sLoadingRecords=載入中...
dataTable_sFirst=首頁
dataTable_sPrevious=上頁
dataTable_sNext=下頁
dataTable_sLast=末頁
dataTable_sSortAscending=: 以升幂排序此列
dataTable_sSortDescending=: 以降幂排序此列
## login
login_btn=登入
login_remember_me=記住密碼
login_username_placeholder=請輸入登入帳號
login_password_placeholder=請輸入登入密碼
login_username_empty=請輸入登入帳號
login_username_lt_4=登入帳號不應低於4位數
login_password_empty=請輸入登入密碼
login_password_lt_4=登入密碼不應低於4位數
login_success=登入成功
login_fail=登入失敗
login_param_empty=帳號或密碼為空值
login_param_unvalid=帳號或密碼錯誤
## logout
logout_btn=登出
logout_confirm=確認登出?
logout_success=登出成功
logout_fail=登出失敗
## change pwd
change_pwd=修改密碼
change_pwd_suc_to_logout=修改密碼成功,即將登出
change_pwd_field_newpwd=新密碼
## dashboard
job_dashboard_name=運行報表
job_dashboard_job_num=任務數量
job_dashboard_job_num_tip=調度中心運行的任務數量
job_dashboard_trigger_num=調度次數
job_dashboard_trigger_num_tip=調度中心觸發的調度次數
job_dashboard_jobgroup_num=執行器數量
job_dashboard_jobgroup_num_tip=調度中心在線的執行器機器數量
job_dashboard_report=調度報表
job_dashboard_report_loaddata_fail=調度報表資料加載異常
job_dashboard_date_report=日期分布圖
job_dashboard_rate_report=成功比例圖
## job info
jobinfo_name=任務管理
jobinfo_job=任務
jobinfo_field_add=新增
jobinfo_field_update=更新任務
jobinfo_field_id=任務ID
jobinfo_field_jobgroup=執行器
jobinfo_field_jobdesc=任務描述
jobinfo_field_gluetype=運行模式
jobinfo_field_executorparam=任務參數
jobinfo_field_author=負責人
jobinfo_field_timeout=任務超時秒數
jobinfo_field_alarmemail=告警郵件
jobinfo_field_alarmemail_placeholder=輸入多個告警郵件地址,請以逗號分隔
jobinfo_field_executorRouteStrategy=路由策略
jobinfo_field_childJobId=子任務ID
jobinfo_field_childJobId_placeholder=輸入子任務ID,如有多個請以逗號分隔
jobinfo_field_executorBlockStrategy=阻塞處理策略
jobinfo_field_executorFailRetryCount=失敗重試次數
jobinfo_field_executorFailRetryCount_placeholder=失敗重試次數,大於零時生效
jobinfo_script_location=腳本位置
jobinfo_shard_index=分片序號
jobinfo_shard_total=分片總數
jobinfo_opt_stop=停止
jobinfo_opt_start=啟動
jobinfo_opt_log=查詢日誌
jobinfo_opt_run=執行一次
jobinfo_opt_run_tips=請輸入本次執行的機器地址,為空則從執行器獲取
jobinfo_opt_registryinfo=注冊節點
jobinfo_opt_next_time=下次執行時間
jobinfo_glue_remark=源碼備註
jobinfo_glue_remark_limit=源碼備註長度限制為4~100
jobinfo_glue_rollback=版本回復
jobinfo_glue_jobid_unvalid=任務ID非法
jobinfo_glue_gluetype_unvalid=該任務非GLUE模式
jobinfo_field_executorTimeout_placeholder=任務超時時間,單位秒,大於零時生效
schedule_type=調度類型
schedule_type_none=無
schedule_type_cron=CRON
schedule_type_fix_rate=固定速度
schedule_type_fix_delay=固定延遲
schedule_type_none_limit_start=當前調度類型禁止啟動
misfire_strategy=調度過期策略
misfire_strategy_do_nothing=忽略
misfire_strategy_fire_once_now=立即執行壹次
jobinfo_conf_base=基礎配置
jobinfo_conf_schedule=調度配置
jobinfo_conf_job=任務配置
jobinfo_conf_advanced=高級配置
## job log
joblog_name=調度日誌
joblog_status=狀態
joblog_status_all=全部
joblog_status_suc=成功
joblog_status_fail=失敗
joblog_status_running=進行中
joblog_field_triggerTime=調度時間
joblog_field_triggerCode=調度結果
joblog_field_triggerMsg=調度備註
joblog_field_handleTime=執行時間
joblog_field_handleCode=執行结果
joblog_field_handleMsg=執行備註
joblog_field_executorAddress=執行器地址
joblog_clean=清理
joblog_clean_log=日誌清理
joblog_clean_type=清理方式
joblog_clean_type_1=清理一個月之前日誌資料
joblog_clean_type_2=清理三個月之前日誌資料
joblog_clean_type_3=清理六個月之前日誌資料
joblog_clean_type_4=清理一年之前日誌資料
joblog_clean_type_5=清理一千條以前日誌資料
joblog_clean_type_6=清理一萬條以前日誌資料
joblog_clean_type_7=清理三萬條以前日誌資料
joblog_clean_type_8=清理十萬條以前日誌資料
joblog_clean_type_9=清理所有日誌資料
joblog_clean_type_unvalid=清理類型參数異常
joblog_handleCode_200=成功
joblog_handleCode_500=失敗
joblog_handleCode_502=失敗(超時)
joblog_kill_log=终止任務
joblog_kill_log_limit=調度失敗,無法终止日誌
joblog_kill_log_byman=人為操作,主動終止
joblog_lost_fail=任務結果丟失,標記失敗
joblog_rolling_log=執行日誌
joblog_rolling_log_refresh=更新
joblog_rolling_log_triggerfail=任務發起調度失敗,無法查看執行日誌
joblog_rolling_log_failoften=終止請求Rolling日誌,請求失敗次數超上限,可刷新頁面重新加載日誌
joblog_logid_unvalid=日誌ID非法
## job group
jobgroup_name=執行器管理
jobgroup_list=執行器列表
jobgroup_add=新增執行器
jobgroup_edit=編輯執行器
jobgroup_del=刪除執行器
jobgroup_field_title=名稱
jobgroup_field_addressType=注冊方式
jobgroup_field_addressType_0=自動注冊
jobgroup_field_addressType_1=手動登錄
jobgroup_field_addressType_limit=手動登錄注冊方式,機器地址不可為空
jobgroup_field_registryList=機器地址
jobgroup_field_registryList_unvalid=機器地址格式非法
jobgroup_field_registryList_placeholder=請輸入執行器地址列表,多個地址請以逗號分隔
jobgroup_field_appname_limit=限制以小寫字母開頭,由小寫字母、數字和中划線組成
jobgroup_field_appname_length=AppName長度限制為4~64
jobgroup_field_title_length=名稱長度限制為4~12
jobgroup_field_order_digits=請輸入整數
jobgroup_field_orderrange=取值範圍為1~1000
jobgroup_del_limit_0=拒絕刪除,該執行器使用中
jobgroup_del_limit_1=拒絕删除,系统至少保留一個執行器
jobgroup_empty=不存在有效執行器,請聯絡系統管理員
## job conf
jobconf_block_SERIAL_EXECUTION=單機串行
jobconf_block_DISCARD_LATER=丢棄后續調度
jobconf_block_COVER_EARLY=覆蓋之前調度
jobconf_route_first=第一個
jobconf_route_last=最後一個
jobconf_route_round=輪詢
jobconf_route_random=隨機
jobconf_route_consistenthash=一致性HASH
jobconf_route_lfu=最不經常使用
jobconf_route_lru=最近最久未使用
jobconf_route_failover=故障轉移
jobconf_route_busyover=忙碌轉移
jobconf_route_shard=分片廣播
jobconf_idleBeat=空閒檢測
jobconf_beat=心跳檢測
jobconf_monitor=任務調度中心監控告警
jobconf_monitor_detail=監控告警明细
jobconf_monitor_alarm_title=告警類型
jobconf_monitor_alarm_type=調度失敗
jobconf_monitor_alarm_content=告警内容
jobconf_trigger_admin_adress=調度機器
jobconf_trigger_exe_regtype=執行器-注冊方式
jobconf_trigger_exe_regaddress=執行器-地址列表
jobconf_trigger_address_empty=調度失敗:執行器地址為空
jobconf_trigger_run=觸發調度
jobconf_trigger_child_run=觸發子任務
jobconf_callback_child_msg1={0}/{1} [任務ID={2}], 觸發{3}, 觸發備註: {4} <br>
jobconf_callback_child_msg2={0}/{1} [任務ID={2}], 觸發失败, 觸發備註: 任務ID格式錯誤 <br>
jobconf_trigger_type=任務觸發類型
jobconf_trigger_type_cron=Cron觸發
jobconf_trigger_type_manual=手動觸發
jobconf_trigger_type_parent=父任務觸發
jobconf_trigger_type_api=API觸發
jobconf_trigger_type_retry=失敗重試觸發
jobconf_trigger_type_misfire=調度過期補償
## user
user_manage=用户管理
user_username=帳號
user_password=密碼
user_role=角色
user_role_admin=管理員
user_role_normal=普通用戶
user_permission=權限
user_add=新增用戶
user_update=更新用戶
user_username_repeat=帳號重複
user_username_valid=限制以小寫字母開頭,由小寫字母、數字組成
user_password_update_placeholder=請輸入新密碼,為空則不更新密碼
user_update_loginuser_limit=禁止操作當前登入帳號
## help
job_help=使用教程
job_help_document=官方文件
xxl-job/xxl-job-admin/src/main/resources/logback.xml
New file
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
    <contextName>logback</contextName>
    <property name="log.path" value="/data/applogs/xxl-job/xxl-job-admin.log"/>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
            </pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>
</configuration>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml
New file
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobGroupDao">
    <resultMap id="XxlJobGroup" type="com.xxl.job.admin.core.model.XxlJobGroup" >
        <result column="id" property="id" />
        <result column="app_name" property="appname" />
        <result column="title" property="title" />
        <result column="address_type" property="addressType" />
        <result column="address_list" property="addressList" />
        <result column="update_time" property="updateTime" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.app_name,
        t.title,
        t.address_type,
        t.address_list,
        t.update_time
    </sql>
    <select id="findAll" resultMap="XxlJobGroup">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_group AS t
        ORDER BY t.app_name, t.title, t.id ASC
    </select>
    <select id="findByAddressType" parameterType="java.lang.Integer" resultMap="XxlJobGroup">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_group AS t
        WHERE t.address_type = #{addressType}
        ORDER BY t.app_name, t.title, t.id ASC
    </select>
    <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobGroup" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO xxl_job_group ( `app_name`, `title`, `address_type`, `address_list`, `update_time`)
        values ( #{appname}, #{title}, #{addressType}, #{addressList}, #{updateTime} );
    </insert>
    <update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobGroup" >
        UPDATE xxl_job_group
        SET `app_name` = #{appname},
            `title` = #{title},
            `address_type` = #{addressType},
            `address_list` = #{addressList},
            `update_time` = #{updateTime}
        WHERE id = #{id}
    </update>
    <delete id="remove" parameterType="java.lang.Integer" >
        DELETE FROM xxl_job_group
        WHERE id = #{id}
    </delete>
    <select id="load" parameterType="java.lang.Integer" resultMap="XxlJobGroup">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_group AS t
        WHERE t.id = #{id}
    </select>
    <select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobGroup">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_group AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="appname != null and appname != ''">
                AND t.app_name like CONCAT(CONCAT('%', #{appname}), '%')
            </if>
            <if test="title != null and title != ''">
                AND t.title like CONCAT(CONCAT('%', #{title}), '%')
            </if>
        </trim>
        ORDER BY t.app_name, t.title, t.id ASC
        LIMIT #{offset}, #{pagesize}
    </select>
    <select id="pageListCount" parameterType="java.util.HashMap" resultType="int">
        SELECT count(1)
        FROM xxl_job_group AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="appname != null and appname != ''">
                AND t.app_name like CONCAT(CONCAT('%', #{appname}), '%')
            </if>
            <if test="title != null and title != ''">
                AND t.title like CONCAT(CONCAT('%', #{title}), '%')
            </if>
        </trim>
    </select>
</mapper>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml
New file
@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobInfoDao">
    <resultMap id="XxlJobInfo" type="com.xxl.job.admin.core.model.XxlJobInfo" >
        <result column="id" property="id" />
        <result column="job_group" property="jobGroup" />
        <result column="job_desc" property="jobDesc" />
        <result column="add_time" property="addTime" />
        <result column="update_time" property="updateTime" />
        <result column="author" property="author" />
        <result column="alarm_email" property="alarmEmail" />
        <result column="schedule_type" property="scheduleType" />
        <result column="schedule_conf" property="scheduleConf" />
        <result column="misfire_strategy" property="misfireStrategy" />
        <result column="executor_route_strategy" property="executorRouteStrategy" />
        <result column="executor_handler" property="executorHandler" />
        <result column="executor_param" property="executorParam" />
        <result column="executor_block_strategy" property="executorBlockStrategy" />
        <result column="executor_timeout" property="executorTimeout" />
        <result column="executor_fail_retry_count" property="executorFailRetryCount" />
        <result column="glue_type" property="glueType" />
        <result column="glue_source" property="glueSource" />
        <result column="glue_remark" property="glueRemark" />
        <result column="glue_updatetime" property="glueUpdatetime" />
        <result column="child_jobid" property="childJobId" />
        <result column="trigger_status" property="triggerStatus" />
        <result column="trigger_last_time" property="triggerLastTime" />
        <result column="trigger_next_time" property="triggerNextTime" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.job_group,
        t.job_desc,
        t.add_time,
        t.update_time,
        t.author,
        t.alarm_email,
        t.schedule_type,
        t.schedule_conf,
        t.misfire_strategy,
        t.executor_route_strategy,
        t.executor_handler,
        t.executor_param,
        t.executor_block_strategy,
        t.executor_timeout,
        t.executor_fail_retry_count,
        t.glue_type,
        t.glue_source,
        t.glue_remark,
        t.glue_updatetime,
        t.child_jobid,
        t.trigger_status,
        t.trigger_last_time,
        t.trigger_next_time
    </sql>
    <select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobInfo">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_info AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="jobGroup gt 0">
                AND t.job_group = #{jobGroup}
            </if>
            <if test="triggerStatus gte 0">
                AND t.trigger_status = #{triggerStatus}
            </if>
            <if test="jobDesc != null and jobDesc != ''">
                AND t.job_desc like CONCAT(CONCAT('%', #{jobDesc}), '%')
            </if>
            <if test="executorHandler != null and executorHandler != ''">
                AND t.executor_handler like CONCAT(CONCAT('%', #{executorHandler}), '%')
            </if>
            <if test="author != null and author != ''">
                AND t.author like CONCAT(CONCAT('%', #{author}), '%')
            </if>
        </trim>
        ORDER BY id DESC
        LIMIT #{offset}, #{pagesize}
    </select>
    <select id="pageListCount" parameterType="java.util.HashMap" resultType="int">
        SELECT count(1)
        FROM xxl_job_info AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="jobGroup gt 0">
                AND t.job_group = #{jobGroup}
            </if>
            <if test="triggerStatus gte 0">
                AND t.trigger_status = #{triggerStatus}
            </if>
            <if test="jobDesc != null and jobDesc != ''">
                AND t.job_desc like CONCAT(CONCAT('%', #{jobDesc}), '%')
            </if>
            <if test="executorHandler != null and executorHandler != ''">
                AND t.executor_handler like CONCAT(CONCAT('%', #{executorHandler}), '%')
            </if>
            <if test="author != null and author != ''">
                AND t.author like CONCAT(CONCAT('%', #{author}), '%')
            </if>
        </trim>
    </select>
    <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobInfo" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO xxl_job_info (
            job_group,
            job_desc,
            add_time,
            update_time,
            author,
            alarm_email,
            schedule_type,
            schedule_conf,
            misfire_strategy,
            executor_route_strategy,
            executor_handler,
            executor_param,
            executor_block_strategy,
            executor_timeout,
            executor_fail_retry_count,
            glue_type,
            glue_source,
            glue_remark,
            glue_updatetime,
            child_jobid,
            trigger_status,
            trigger_last_time,
            trigger_next_time
        ) VALUES (
            #{jobGroup},
            #{jobDesc},
            #{addTime},
            #{updateTime},
            #{author},
            #{alarmEmail},
            #{scheduleType},
            #{scheduleConf},
            #{misfireStrategy},
            #{executorRouteStrategy},
            #{executorHandler},
            #{executorParam},
            #{executorBlockStrategy},
            #{executorTimeout},
            #{executorFailRetryCount},
            #{glueType},
            #{glueSource},
            #{glueRemark},
            #{glueUpdatetime},
            #{childJobId},
            #{triggerStatus},
            #{triggerLastTime},
            #{triggerNextTime}
        );
        <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id">
            SELECT LAST_INSERT_ID()
            /*SELECT @@IDENTITY AS id*/
        </selectKey>-->
    </insert>
    <select id="loadById" parameterType="java.util.HashMap" resultMap="XxlJobInfo">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_info AS t
        WHERE t.id = #{id}
    </select>
    <update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobInfo" >
        UPDATE xxl_job_info
        SET
            job_group = #{jobGroup},
            job_desc = #{jobDesc},
            update_time = #{updateTime},
            author = #{author},
            alarm_email = #{alarmEmail},
            schedule_type = #{scheduleType},
            schedule_conf = #{scheduleConf},
            misfire_strategy = #{misfireStrategy},
            executor_route_strategy = #{executorRouteStrategy},
            executor_handler = #{executorHandler},
            executor_param = #{executorParam},
            executor_block_strategy = #{executorBlockStrategy},
            executor_timeout = ${executorTimeout},
            executor_fail_retry_count = ${executorFailRetryCount},
            glue_type = #{glueType},
            glue_source = #{glueSource},
            glue_remark = #{glueRemark},
            glue_updatetime = #{glueUpdatetime},
            child_jobid = #{childJobId},
            trigger_status = #{triggerStatus},
            trigger_last_time = #{triggerLastTime},
            trigger_next_time = #{triggerNextTime}
        WHERE id = #{id}
    </update>
    <delete id="delete" parameterType="java.util.HashMap">
        DELETE
        FROM xxl_job_info
        WHERE id = #{id}
    </delete>
    <select id="getJobsByGroup" parameterType="java.util.HashMap" resultMap="XxlJobInfo">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_info AS t
        WHERE t.job_group = #{jobGroup}
    </select>
    <select id="findAllCount" resultType="int">
        SELECT count(1)
        FROM xxl_job_info
    </select>
    <select id="scheduleJobQuery" parameterType="java.util.HashMap" resultMap="XxlJobInfo">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_info AS t
        WHERE t.trigger_status = 1
            and t.trigger_next_time <![CDATA[ <= ]]> #{maxNextTime}
        ORDER BY id ASC
        LIMIT #{pagesize}
    </select>
    <update id="scheduleUpdate" parameterType="com.xxl.job.admin.core.model.XxlJobInfo"  >
        UPDATE xxl_job_info
        SET
            trigger_last_time = #{triggerLastTime},
            trigger_next_time = #{triggerNextTime},
            trigger_status = #{triggerStatus}
        WHERE id = #{id}
    </update>
</mapper>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml
New file
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobLogGlueDao">
    <resultMap id="XxlJobLogGlue" type="com.xxl.job.admin.core.model.XxlJobLogGlue" >
        <result column="id" property="id" />
        <result column="job_id" property="jobId" />
        <result column="glue_type" property="glueType" />
        <result column="glue_source" property="glueSource" />
        <result column="glue_remark" property="glueRemark" />
        <result column="add_time" property="addTime" />
        <result column="update_time" property="updateTime" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.job_id,
        t.glue_type,
        t.glue_source,
        t.glue_remark,
        t.add_time,
        t.update_time
    </sql>
    <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobLogGlue" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO xxl_job_logglue (
            `job_id`,
            `glue_type`,
            `glue_source`,
            `glue_remark`,
            `add_time`,
            `update_time`
        ) VALUES (
            #{jobId},
            #{glueType},
            #{glueSource},
            #{glueRemark},
            #{addTime},
            #{updateTime}
        );
        <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id">
            SELECT LAST_INSERT_ID()
        </selectKey>-->
    </insert>
    <select id="findByJobId" parameterType="java.lang.Integer" resultMap="XxlJobLogGlue">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_logglue AS t
        WHERE t.job_id = #{jobId}
        ORDER BY id DESC
    </select>
    <delete id="removeOld" >
        DELETE FROM xxl_job_logglue
        WHERE id NOT in(
            SELECT id FROM(
                SELECT id FROM xxl_job_logglue
                WHERE `job_id` = #{jobId}
                ORDER BY update_time desc
                LIMIT 0, #{limit}
            ) t1
        ) AND `job_id` = #{jobId}
    </delete>
    <delete id="deleteByJobId" parameterType="java.lang.Integer" >
        DELETE FROM xxl_job_logglue
        WHERE `job_id` = #{jobId}
    </delete>
</mapper>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml
New file
@@ -0,0 +1,273 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobLogDao">
    <resultMap id="XxlJobLog" type="com.xxl.job.admin.core.model.XxlJobLog" >
        <result column="id" property="id" />
        <result column="job_group" property="jobGroup" />
        <result column="job_id" property="jobId" />
        <result column="executor_address" property="executorAddress" />
        <result column="executor_handler" property="executorHandler" />
        <result column="executor_param" property="executorParam" />
        <result column="executor_sharding_param" property="executorShardingParam" />
        <result column="executor_fail_retry_count" property="executorFailRetryCount" />
        <result column="trigger_time" property="triggerTime" />
        <result column="trigger_code" property="triggerCode" />
        <result column="trigger_msg" property="triggerMsg" />
        <result column="handle_time" property="handleTime" />
        <result column="handle_code" property="handleCode" />
        <result column="handle_msg" property="handleMsg" />
        <result column="alarm_status" property="alarmStatus" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.job_group,
        t.job_id,
        t.executor_address,
        t.executor_handler,
        t.executor_param,
        t.executor_sharding_param,
        t.executor_fail_retry_count,
        t.trigger_time,
        t.trigger_code,
        t.trigger_msg,
        t.handle_time,
        t.handle_code,
        t.handle_msg,
        t.alarm_status
    </sql>
    <select id="pageList" resultMap="XxlJobLog">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_log AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="jobId==0 and jobGroup gt 0">
                AND t.job_group = #{jobGroup}
            </if>
            <if test="jobId gt 0">
                AND t.job_id = #{jobId}
            </if>
            <if test="triggerTimeStart != null">
                AND t.trigger_time <![CDATA[ >= ]]> #{triggerTimeStart}
            </if>
            <if test="triggerTimeEnd != null">
                AND t.trigger_time <![CDATA[ <= ]]> #{triggerTimeEnd}
            </if>
            <if test="logStatus == 1" >
                AND t.handle_code = 200
            </if>
            <if test="logStatus == 2" >
                AND (
                    t.trigger_code NOT IN (0, 200) OR
                    t.handle_code NOT IN (0, 200)
                )
            </if>
            <if test="logStatus == 3" >
                AND t.trigger_code = 200
                AND t.handle_code = 0
            </if>
        </trim>
        ORDER BY t.trigger_time DESC
        LIMIT #{offset}, #{pagesize}
    </select>
    <select id="pageListCount" resultType="int">
        SELECT count(1)
        FROM xxl_job_log AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="jobId==0 and jobGroup gt 0">
                AND t.job_group = #{jobGroup}
            </if>
            <if test="jobId gt 0">
                AND t.job_id = #{jobId}
            </if>
            <if test="triggerTimeStart != null">
                AND t.trigger_time <![CDATA[ >= ]]> #{triggerTimeStart}
            </if>
            <if test="triggerTimeEnd != null">
                AND t.trigger_time <![CDATA[ <= ]]> #{triggerTimeEnd}
            </if>
            <if test="logStatus == 1" >
                AND t.handle_code = 200
            </if>
            <if test="logStatus == 2" >
                AND (
                    t.trigger_code NOT IN (0, 200) OR
                    t.handle_code NOT IN (0, 200)
                )
            </if>
            <if test="logStatus == 3" >
                AND t.trigger_code = 200
                AND t.handle_code = 0
            </if>
        </trim>
    </select>
    <select id="load" parameterType="java.lang.Long" resultMap="XxlJobLog">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_log AS t
        WHERE t.id = #{id}
    </select>
    <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobLog" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO xxl_job_log (
            `job_group`,
            `job_id`,
            `trigger_time`,
            `trigger_code`,
            `handle_code`
        ) VALUES (
            #{jobGroup},
            #{jobId},
            #{triggerTime},
            #{triggerCode},
            #{handleCode}
        );
        <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id">
            SELECT LAST_INSERT_ID()
        </selectKey>-->
    </insert>
    <update id="updateTriggerInfo" >
        UPDATE xxl_job_log
        SET
            `trigger_time`= #{triggerTime},
            `trigger_code`= #{triggerCode},
            `trigger_msg`= #{triggerMsg},
            `executor_address`= #{executorAddress},
            `executor_handler`=#{executorHandler},
            `executor_param`= #{executorParam},
            `executor_sharding_param`= #{executorShardingParam},
            `executor_fail_retry_count`= #{executorFailRetryCount}
        WHERE `id`= #{id}
    </update>
    <update id="updateHandleInfo">
        UPDATE xxl_job_log
        SET
            `handle_time`= #{handleTime},
            `handle_code`= #{handleCode},
            `handle_msg`= #{handleMsg}
        WHERE `id`= #{id}
    </update>
    <delete id="delete" >
        delete from xxl_job_log
        WHERE job_id = #{jobId}
    </delete>
    <!--<select id="triggerCountByDay" resultType="java.util.Map" >
        SELECT
            DATE_FORMAT(trigger_time,'%Y-%m-%d') triggerDay,
            COUNT(handle_code) triggerDayCount,
            SUM(CASE WHEN (trigger_code in (0, 200) and handle_code = 0) then 1 else 0 end) as triggerDayCountRunning,
            SUM(CASE WHEN handle_code = 200 then 1 else 0 end) as triggerDayCountSuc
        FROM xxl_job_log
        WHERE trigger_time BETWEEN #{from} and #{to}
        GROUP BY triggerDay
        ORDER BY triggerDay
    </select>-->
    <select id="findLogReport" resultType="java.util.Map" >
        SELECT
            COUNT(handle_code) triggerDayCount,
            SUM(CASE WHEN (trigger_code in (0, 200) and handle_code = 0) then 1 else 0 end) as triggerDayCountRunning,
            SUM(CASE WHEN handle_code = 200 then 1 else 0 end) as triggerDayCountSuc
        FROM xxl_job_log
        WHERE trigger_time BETWEEN #{from} and #{to}
    </select>
    <select id="findClearLogIds" resultType="long" >
        SELECT id FROM xxl_job_log
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="jobGroup gt 0">
                AND job_group = #{jobGroup}
            </if>
            <if test="jobId gt 0">
                AND job_id = #{jobId}
            </if>
            <if test="clearBeforeTime != null">
                AND trigger_time <![CDATA[ <= ]]> #{clearBeforeTime}
            </if>
            <if test="clearBeforeNum gt 0">
                AND id NOT in(
                SELECT id FROM(
                SELECT id FROM xxl_job_log AS t
                <trim prefix="WHERE" prefixOverrides="AND | OR" >
                    <if test="jobGroup gt 0">
                        AND t.job_group = #{jobGroup}
                    </if>
                    <if test="jobId gt 0">
                        AND t.job_id = #{jobId}
                    </if>
                </trim>
                ORDER BY t.trigger_time desc
                LIMIT 0, #{clearBeforeNum}
                ) t1
                )
            </if>
        </trim>
        order by id asc
        LIMIT #{pagesize}
    </select>
    <delete id="clearLog" >
        delete from xxl_job_log
        WHERE id in
        <foreach collection="logIds" item="item" open="(" close=")" separator="," >
            #{item}
        </foreach>
    </delete>
    <select id="findFailJobLogIds" resultType="long" >
        SELECT id FROM `xxl_job_log`
        WHERE !(
            (trigger_code in (0, 200) and handle_code = 0)
            OR
            (handle_code = 200)
        )
        AND `alarm_status` = 0
        ORDER BY id ASC
        LIMIT #{pagesize}
    </select>
    <update id="updateAlarmStatus" >
        UPDATE xxl_job_log
        SET
            `alarm_status` = #{newAlarmStatus}
        WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus}
    </update>
    <select id="findLostJobIds" resultType="long" >
        SELECT
            t.id
        FROM
            xxl_job_log t
            LEFT JOIN xxl_job_registry t2 ON t.executor_address = t2.registry_value
        WHERE
            t.trigger_code = 200
                AND t.handle_code = 0
                AND t.trigger_time <![CDATA[ <= ]]> #{losedTime}
                AND t2.id IS NULL;
    </select>
    <!--
    SELECT t.id
    FROM xxl_job_log AS t
    WHERE t.trigger_code = 200
        and t.handle_code = 0
        and t.trigger_time <![CDATA[ <= ]]> #{losedTime}
        and t.executor_address not in (
            SELECT t2.registry_value
            FROM xxl_job_registry AS t2
        )
    -->
</mapper>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml
New file
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobLogReportDao">
    <resultMap id="XxlJobLogReport" type="com.xxl.job.admin.core.model.XxlJobLogReport" >
        <result column="id" property="id" />
        <result column="trigger_day" property="triggerDay" />
        <result column="running_count" property="runningCount" />
        <result column="suc_count" property="sucCount" />
        <result column="fail_count" property="failCount" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.trigger_day,
        t.running_count,
        t.suc_count,
        t.fail_count
    </sql>
    <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobLogReport" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO xxl_job_log_report (
            `trigger_day`,
            `running_count`,
            `suc_count`,
            `fail_count`
        ) VALUES (
            #{triggerDay},
            #{runningCount},
            #{sucCount},
            #{failCount}
        );
        <!--<selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id">
            SELECT LAST_INSERT_ID()
        </selectKey>-->
    </insert>
    <update id="update" >
        UPDATE xxl_job_log_report
        SET `running_count` = #{runningCount},
            `suc_count` = #{sucCount},
            `fail_count` = #{failCount}
        WHERE `trigger_day` = #{triggerDay}
    </update>
    <select id="queryLogReport" resultMap="XxlJobLogReport">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_log_report AS t
        WHERE t.trigger_day between #{triggerDayFrom} and #{triggerDayTo}
        ORDER BY t.trigger_day ASC
    </select>
    <select id="queryLogReportTotal" resultMap="XxlJobLogReport">
        SELECT
            SUM(running_count) running_count,
            SUM(suc_count) suc_count,
            SUM(fail_count) fail_count
        FROM xxl_job_log_report AS t
    </select>
</mapper>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml
New file
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobRegistryDao">
    <resultMap id="XxlJobRegistry" type="com.xxl.job.admin.core.model.XxlJobRegistry" >
        <result column="id" property="id" />
        <result column="registry_group" property="registryGroup" />
        <result column="registry_key" property="registryKey" />
        <result column="registry_value" property="registryValue" />
        <result column="update_time" property="updateTime" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.registry_group,
        t.registry_key,
        t.registry_value,
        t.update_time
    </sql>
    <select id="findDead" parameterType="java.util.HashMap" resultType="java.lang.Integer" >
        SELECT t.id
        FROM xxl_job_registry AS t
        WHERE t.update_time <![CDATA[ < ]]> DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND)
    </select>
    <delete id="removeDead" parameterType="java.lang.Integer" >
        DELETE FROM xxl_job_registry
        WHERE id in
        <foreach collection="ids" item="item" open="(" close=")" separator="," >
            #{item}
        </foreach>
    </delete>
    <select id="findAll" parameterType="java.util.HashMap" resultMap="XxlJobRegistry">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_registry AS t
        WHERE t.update_time <![CDATA[ > ]]> DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND)
    </select>
    <update id="registryUpdate" >
        UPDATE xxl_job_registry
        SET `update_time` = #{updateTime}
        WHERE `registry_group` = #{registryGroup}
          AND `registry_key` = #{registryKey}
          AND `registry_value` = #{registryValue}
    </update>
    <insert id="registrySave" >
        INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`)
        VALUES( #{registryGroup}  , #{registryKey} , #{registryValue}, #{updateTime})
    </insert>
    <delete id="registryDelete" >
        DELETE FROM xxl_job_registry
        WHERE registry_group = #{registryGroup}
            AND registry_key = #{registryKey}
            AND registry_value = #{registryValue}
    </delete>
</mapper>
xxl-job/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml
New file
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.job.admin.dao.XxlJobUserDao">
    <resultMap id="XxlJobUser" type="com.xxl.job.admin.core.model.XxlJobUser" >
        <result column="id" property="id" />
        <result column="username" property="username" />
        <result column="password" property="password" />
        <result column="role" property="role" />
        <result column="permission" property="permission" />
    </resultMap>
    <sql id="Base_Column_List">
        t.id,
        t.username,
        t.password,
        t.role,
        t.permission
    </sql>
    <select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobUser">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_user AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="username != null and username != ''">
                AND t.username like CONCAT(CONCAT('%', #{username}), '%')
            </if>
            <if test="role gt -1">
                AND t.role = #{role}
            </if>
        </trim>
        ORDER BY username ASC
        LIMIT #{offset}, #{pagesize}
    </select>
    <select id="pageListCount" parameterType="java.util.HashMap" resultType="int">
        SELECT count(1)
        FROM xxl_job_user AS t
        <trim prefix="WHERE" prefixOverrides="AND | OR" >
            <if test="username != null and username != ''">
                AND t.username like CONCAT(CONCAT('%', #{username}), '%')
            </if>
            <if test="role gt -1">
                AND t.role = #{role}
            </if>
        </trim>
    </select>
    <select id="loadByUserName" parameterType="java.util.HashMap" resultMap="XxlJobUser">
        SELECT <include refid="Base_Column_List" />
        FROM xxl_job_user AS t
        WHERE t.username = #{username}
    </select>
    <insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobUser" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO xxl_job_user (
            username,
            password,
            role,
            permission
        ) VALUES (
            #{username},
            #{password},
            #{role},
            #{permission}
        );
    </insert>
    <update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobUser" >
        UPDATE xxl_job_user
        SET
            <if test="password != null and password != ''">
                password = #{password},
            </if>
            role = #{role},
            permission = #{permission}
        WHERE id = #{id}
    </update>
    <delete id="delete" parameterType="java.util.HashMap">
        DELETE
        FROM xxl_job_user
        WHERE id = #{id}
    </delete>
</mapper>
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/icheck.min.js
New file
@@ -0,0 +1,10 @@
/*! iCheck v1.0.1 by Damir Sultanov, http://git.io/arlzeA, MIT Licensed */
(function(h){function F(a,b,d){var c=a[0],e=/er/.test(d)?m:/bl/.test(d)?s:l,f=d==H?{checked:c[l],disabled:c[s],indeterminate:"true"==a.attr(m)||"false"==a.attr(w)}:c[e];if(/^(ch|di|in)/.test(d)&&!f)D(a,e);else if(/^(un|en|de)/.test(d)&&f)t(a,e);else if(d==H)for(e in f)f[e]?D(a,e,!0):t(a,e,!0);else if(!b||"toggle"==d){if(!b)a[p]("ifClicked");f?c[n]!==u&&t(a,e):D(a,e)}}function D(a,b,d){var c=a[0],e=a.parent(),f=b==l,A=b==m,B=b==s,K=A?w:f?E:"enabled",p=k(a,K+x(c[n])),N=k(a,b+x(c[n]));if(!0!==c[b]){if(!d&&
b==l&&c[n]==u&&c.name){var C=a.closest("form"),r='input[name="'+c.name+'"]',r=C.length?C.find(r):h(r);r.each(function(){this!==c&&h(this).data(q)&&t(h(this),b)})}A?(c[b]=!0,c[l]&&t(a,l,"force")):(d||(c[b]=!0),f&&c[m]&&t(a,m,!1));L(a,f,b,d)}c[s]&&k(a,y,!0)&&e.find("."+I).css(y,"default");e[v](N||k(a,b)||"");B?e.attr("aria-disabled","true"):e.attr("aria-checked",A?"mixed":"true");e[z](p||k(a,K)||"")}function t(a,b,d){var c=a[0],e=a.parent(),f=b==l,h=b==m,q=b==s,p=h?w:f?E:"enabled",t=k(a,p+x(c[n])),
u=k(a,b+x(c[n]));if(!1!==c[b]){if(h||!d||"force"==d)c[b]=!1;L(a,f,p,d)}!c[s]&&k(a,y,!0)&&e.find("."+I).css(y,"pointer");e[z](u||k(a,b)||"");q?e.attr("aria-disabled","false"):e.attr("aria-checked","false");e[v](t||k(a,p)||"")}function M(a,b){if(a.data(q)){a.parent().html(a.attr("style",a.data(q).s||""));if(b)a[p](b);a.off(".i").unwrap();h(G+'[for="'+a[0].id+'"]').add(a.closest(G)).off(".i")}}function k(a,b,d){if(a.data(q))return a.data(q).o[b+(d?"":"Class")]}function x(a){return a.charAt(0).toUpperCase()+
a.slice(1)}function L(a,b,d,c){if(!c){if(b)a[p]("ifToggled");a[p]("ifChanged")[p]("if"+x(d))}}var q="iCheck",I=q+"-helper",u="radio",l="checked",E="un"+l,s="disabled",w="determinate",m="in"+w,H="update",n="type",v="addClass",z="removeClass",p="trigger",G="label",y="cursor",J=/ipad|iphone|ipod|android|blackberry|windows phone|opera mini|silk/i.test(navigator.userAgent);h.fn[q]=function(a,b){var d='input[type="checkbox"], input[type="'+u+'"]',c=h(),e=function(a){a.each(function(){var a=h(this);c=a.is(d)?
c.add(a):c.add(a.find(d))})};if(/^(check|uncheck|toggle|indeterminate|determinate|disable|enable|update|destroy)$/i.test(a))return a=a.toLowerCase(),e(this),c.each(function(){var c=h(this);"destroy"==a?M(c,"ifDestroyed"):F(c,!0,a);h.isFunction(b)&&b()});if("object"!=typeof a&&a)return this;var f=h.extend({checkedClass:l,disabledClass:s,indeterminateClass:m,labelHover:!0,aria:!1},a),k=f.handle,B=f.hoverClass||"hover",x=f.focusClass||"focus",w=f.activeClass||"active",y=!!f.labelHover,C=f.labelHoverClass||
"hover",r=(""+f.increaseArea).replace("%","")|0;if("checkbox"==k||k==u)d='input[type="'+k+'"]';-50>r&&(r=-50);e(this);return c.each(function(){var a=h(this);M(a);var c=this,b=c.id,e=-r+"%",d=100+2*r+"%",d={position:"absolute",top:e,left:e,display:"block",width:d,height:d,margin:0,padding:0,background:"#fff",border:0,opacity:0},e=J?{position:"absolute",visibility:"hidden"}:r?d:{position:"absolute",opacity:0},k="checkbox"==c[n]?f.checkboxClass||"icheckbox":f.radioClass||"i"+u,m=h(G+'[for="'+b+'"]').add(a.closest(G)),
A=!!f.aria,E=q+"-"+Math.random().toString(36).replace("0.",""),g='<div class="'+k+'" '+(A?'role="'+c[n]+'" ':"");m.length&&A&&m.each(function(){g+='aria-labelledby="';this.id?g+=this.id:(this.id=E,g+=E);g+='"'});g=a.wrap(g+"/>")[p]("ifCreated").parent().append(f.insert);d=h('<ins class="'+I+'"/>').css(d).appendTo(g);a.data(q,{o:f,s:a.attr("style")}).css(e);f.inheritClass&&g[v](c.className||"");f.inheritID&&b&&g.attr("id",q+"-"+b);"static"==g.css("position")&&g.css("position","relative");F(a,!0,H);
if(m.length)m.on("click.i mouseover.i mouseout.i touchbegin.i touchend.i",function(b){var d=b[n],e=h(this);if(!c[s]){if("click"==d){if(h(b.target).is("a"))return;F(a,!1,!0)}else y&&(/ut|nd/.test(d)?(g[z](B),e[z](C)):(g[v](B),e[v](C)));if(J)b.stopPropagation();else return!1}});a.on("click.i focus.i blur.i keyup.i keydown.i keypress.i",function(b){var d=b[n];b=b.keyCode;if("click"==d)return!1;if("keydown"==d&&32==b)return c[n]==u&&c[l]||(c[l]?t(a,l):D(a,l)),!1;if("keyup"==d&&c[n]==u)!c[l]&&D(a,l);else if(/us|ur/.test(d))g["blur"==
d?z:v](x)});d.on("click mousedown mouseup mouseover mouseout touchbegin.i touchend.i",function(b){var d=b[n],e=/wn|up/.test(d)?w:B;if(!c[s]){if("click"==d)F(a,!1,!0);else{if(/wn|er|in/.test(d))g[v](e);else g[z](e+" "+w);if(m.length&&y&&e==B)m[/ut|nd/.test(d)?z:v](C)}if(J)b.stopPropagation();else return!1}})})}})(window.jQuery||window.Zepto);
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.css
New file
@@ -0,0 +1,62 @@
/* iCheck plugin Square skin, blue
----------------------------------- */
.icheckbox_square-blue,
.iradio_square-blue {
    display: inline-block;
    *display: inline;
    vertical-align: middle;
    margin: 0;
    padding: 0;
    width: 22px;
    height: 22px;
    background: url(blue.png) no-repeat;
    border: none;
    cursor: pointer;
}
.icheckbox_square-blue {
    background-position: 0 0;
}
    .icheckbox_square-blue.hover {
        background-position: -24px 0;
    }
    .icheckbox_square-blue.checked {
        background-position: -48px 0;
    }
    .icheckbox_square-blue.disabled {
        background-position: -72px 0;
        cursor: default;
    }
    .icheckbox_square-blue.checked.disabled {
        background-position: -96px 0;
    }
.iradio_square-blue {
    background-position: -120px 0;
}
    .iradio_square-blue.hover {
        background-position: -144px 0;
    }
    .iradio_square-blue.checked {
        background-position: -168px 0;
    }
    .iradio_square-blue.disabled {
        background-position: -192px 0;
        cursor: default;
    }
    .iradio_square-blue.checked.disabled {
        background-position: -216px 0;
    }
/* Retina support */
@media only screen and (-webkit-min-device-pixel-ratio: 1.5),
       only screen and (-moz-min-device-pixel-ratio: 1.5),
       only screen and (-o-min-device-pixel-ratio: 3/2),
       only screen and (min-device-pixel-ratio: 1.5) {
    .icheckbox_square-blue,
    .iradio_square-blue {
        background-image: url(blue@2x.png);
        -webkit-background-size: 240px 24px;
        background-size: 240px 24px;
    }
}
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue.png
xxl-job/xxl-job-admin/src/main/resources/static/adminlte/plugins/iCheck/square/blue@2x.png
xxl-job/xxl-job-admin/src/main/resources/static/favicon.ico
xxl-job/xxl-job-admin/src/main/resources/static/js/common.1.js
New file
@@ -0,0 +1,156 @@
$(function(){
    // logout
    $("#logoutBtn").click(function(){
        layer.confirm( I18n.logout_confirm , {
            icon: 3,
            title: I18n.system_tips ,
            btn: [ I18n.system_ok, I18n.system_cancel ]
        }, function(index){
            layer.close(index);
            $.post(base_url + "/logout", function(data, status) {
                if (data.code == "200") {
                    layer.msg( I18n.logout_success );
                    setTimeout(function(){
                        window.location.href = base_url + "/";
                    }, 500);
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.logout_fail),
                        icon: '2'
                    });
                }
            });
        });
    });
    // slideToTop
    var slideToTop = $("<div />");
    slideToTop.html('<i class="fa fa-chevron-up"></i>');
    slideToTop.css({
        position: 'fixed',
        bottom: '20px',
        right: '25px',
        width: '40px',
        height: '40px',
        color: '#eee',
        'font-size': '',
        'line-height': '40px',
        'text-align': 'center',
        'background-color': '#222d32',
        cursor: 'pointer',
        'border-radius': '5px',
        'z-index': '99999',
        opacity: '.7',
        'display': 'none'
    });
    slideToTop.on('mouseenter', function () {
        $(this).css('opacity', '1');
    });
    slideToTop.on('mouseout', function () {
        $(this).css('opacity', '.7');
    });
    $('.wrapper').append(slideToTop);
    $(window).scroll(function () {
        if ($(window).scrollTop() >= 150) {
            if (!$(slideToTop).is(':visible')) {
                $(slideToTop).fadeIn(500);
            }
        } else {
            $(slideToTop).fadeOut(500);
        }
    });
    $(slideToTop).click(function () {
        $("html,body").animate({        // firefox ie not support body, chrome support body. but found that new version chrome not support body too.
            scrollTop: 0
        }, 100);
    });
    // left menu status v: js + server + cookie
    $('.sidebar-toggle').click(function(){
        var xxljob_adminlte_settings = $.cookie('xxljob_adminlte_settings');    // on=open,off=close
        if ('off' == xxljob_adminlte_settings) {
            xxljob_adminlte_settings = 'on';
        } else {
            xxljob_adminlte_settings = 'off';
        }
        $.cookie('xxljob_adminlte_settings', xxljob_adminlte_settings, { expires: 7 });    //$.cookie('the_cookie', '', { expires: -1 });
    });
    // left menu status v1: js + cookie
    /*
     var xxljob_adminlte_settings = $.cookie('xxljob_adminlte_settings');
     if (xxljob_adminlte_settings == 'off') {
         $('body').addClass('sidebar-collapse');
     }
     */
    // update pwd
    $('#updatePwd').on('click', function(){
        $('#updatePwdModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var updatePwdModalValidate = $("#updatePwdModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            password : {
                required : true ,
                rangelength:[4,50]
            }
        },
        messages : {
            password : {
                required : '请输入密码'  ,
                rangelength : "密码长度限制为4~50"
            }
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            $.post(base_url + "/user/updatePwd",  $("#updatePwdModal .form").serialize(), function(data, status) {
                if (data.code == 200) {
                    $('#updatePwdModal').modal('hide');
                    layer.msg( I18n.change_pwd_suc_to_logout );
                    setTimeout(function(){
                        $.post(base_url + "/logout", function(data, status) {
                            if (data.code == 200) {
                                window.location.href = base_url + "/";
                            } else {
                                layer.open({
                                    icon: '2',
                                    content: (data.msg|| I18n.logout_fail)
                                });
                            }
                        });
                    }, 500);
                } else {
                    layer.open({
                        icon: '2',
                        content: (data.msg|| I18n.change_pwd + I18n.system_fail )
                    });
                }
            });
        }
    });
    $("#updatePwdModal").on('hide.bs.modal', function () {
        $("#updatePwdModal .form")[0].reset();
        updatePwdModalValidate.resetForm();
        $("#updatePwdModal .form .form-group").removeClass("has-error");
    });
});
xxl-job/xxl-job-admin/src/main/resources/static/js/index.js
New file
@@ -0,0 +1,207 @@
/**
 * Created by xuxueli on 17/4/24.
 */
$(function () {
    // filter Time
    var rangesConf = {};
    rangesConf[I18n.daterangepicker_ranges_today] = [moment().startOf('day'), moment().endOf('day')];
    rangesConf[I18n.daterangepicker_ranges_yesterday] = [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')];
    rangesConf[I18n.daterangepicker_ranges_this_month] = [moment().startOf('month'), moment().endOf('month')];
    rangesConf[I18n.daterangepicker_ranges_last_month] = [moment().subtract(1, 'months').startOf('month'), moment().subtract(1, 'months').endOf('month')];
    rangesConf[I18n.daterangepicker_ranges_recent_week] = [moment().subtract(1, 'weeks').startOf('day'), moment().endOf('day')];
    rangesConf[I18n.daterangepicker_ranges_recent_month] = [moment().subtract(1, 'months').startOf('day'), moment().endOf('day')];
    $('#filterTime').daterangepicker({
        autoApply:false,
        singleDatePicker:false,
        showDropdowns:false,        // 是否显示年月选择条件
        timePicker: true,             // 是否显示小时和分钟选择条件
        timePickerIncrement: 10,     // 时间的增量,单位为分钟
        timePicker24Hour : true,
        opens : 'left', //日期选择框的弹出位置
        ranges: rangesConf,
        locale : {
            format: 'YYYY-MM-DD HH:mm:ss',
            separator : ' - ',
            customRangeLabel : I18n.daterangepicker_custom_name ,
            applyLabel : I18n.system_ok ,
            cancelLabel : I18n.system_cancel ,
            fromLabel : I18n.daterangepicker_custom_starttime ,
            toLabel : I18n.daterangepicker_custom_endtime ,
            daysOfWeek : I18n.daterangepicker_custom_daysofweek.split(',') ,        // '日', '一', '二', '三', '四', '五', '六'
            monthNames : I18n.daterangepicker_custom_monthnames.split(',') ,        // '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'
            firstDay : 1
        },
        startDate: rangesConf[I18n.daterangepicker_ranges_recent_week][0] ,
        endDate: rangesConf[I18n.daterangepicker_ranges_recent_week][1]
    }, function (start, end, label) {
        freshChartDate(start, end);
    });
    freshChartDate(rangesConf[I18n.daterangepicker_ranges_recent_week][0], rangesConf[I18n.daterangepicker_ranges_recent_week][1]);
    /**
     * fresh Chart Date
     *
     * @param startDate
     * @param endDate
     */
    function freshChartDate(startDate, endDate) {
        $.ajax({
            type : 'POST',
            url : base_url + '/chartInfo',
            data : {
                'startDate':startDate.format('YYYY-MM-DD HH:mm:ss'),
                'endDate':endDate.format('YYYY-MM-DD HH:mm:ss')
            },
            dataType : "json",
            success : function(data){
                if (data.code == 200) {
                    lineChartInit(data)
                    pieChartInit(data);
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.job_dashboard_report_loaddata_fail ),
                        icon: '2'
                    });
                }
            }
        });
    }
    /**
     * line Chart Init
     */
    function lineChartInit(data) {
        var option = {
               title: {
                   text: I18n.job_dashboard_date_report
               },
               tooltip : {
                   trigger: 'axis',
                   axisPointer: {
                       type: 'cross',
                       label: {
                           backgroundColor: '#6a7985'
                       }
                   }
               },
               legend: {
                   data:[I18n.joblog_status_suc, I18n.joblog_status_fail, I18n.joblog_status_running]
               },
               toolbox: {
                   feature: {
                       /*saveAsImage: {}*/
                   }
               },
               grid: {
                   left: '3%',
                   right: '4%',
                   bottom: '3%',
                   containLabel: true
               },
               xAxis : [
                   {
                       type : 'category',
                       boundaryGap : false,
                       data : data.content.triggerDayList
                   }
               ],
               yAxis : [
                   {
                       type : 'value'
                   }
               ],
               series : [
                   {
                       name:I18n.joblog_status_suc,
                       type:'line',
                       stack: 'Total',
                       areaStyle: {normal: {}},
                       data: data.content.triggerDayCountSucList
                   },
                   {
                       name:I18n.joblog_status_fail,
                       type:'line',
                       stack: 'Total',
                       label: {
                           normal: {
                               show: true,
                               position: 'top'
                           }
                       },
                       areaStyle: {normal: {}},
                       data: data.content.triggerDayCountFailList
                   },
                   {
                       name:I18n.joblog_status_running,
                       type:'line',
                       stack: 'Total',
                       areaStyle: {normal: {}},
                       data: data.content.triggerDayCountRunningList
                   }
               ],
                color:['#00A65A', '#c23632', '#F39C12']
        };
        var lineChart = echarts.init(document.getElementById('lineChart'));
        lineChart.setOption(option);
    }
    /**
     * pie Chart Init
     */
    function pieChartInit(data) {
        var option = {
            title : {
                text: I18n.job_dashboard_rate_report ,
                /*subtext: 'subtext',*/
                x:'center'
            },
            tooltip : {
                trigger: 'item',
                formatter: "{b} : {c} ({d}%)"
            },
            legend: {
                orient: 'vertical',
                left: 'left',
                data: [I18n.joblog_status_suc, I18n.joblog_status_fail, I18n.joblog_status_running ]
            },
            series : [
                {
                    //name: '分布比例',
                    type: 'pie',
                    radius : '55%',
                    center: ['50%', '60%'],
                    data:[
                        {
                            name:I18n.joblog_status_suc,
                            value:data.content.triggerCountSucTotal
                        },
                        {
                            name:I18n.joblog_status_fail,
                            value:data.content.triggerCountFailTotal
                        },
                        {
                            name:I18n.joblog_status_running,
                            value:data.content.triggerCountRunningTotal
                        }
                    ],
                    itemStyle: {
                        emphasis: {
                            shadowBlur: 10,
                            shadowOffsetX: 0,
                            shadowColor: 'rgba(0, 0, 0, 0.5)'
                        }
                    }
                }
            ],
            color:['#00A65A', '#c23632', '#F39C12']
        };
        var pieChart = echarts.init(document.getElementById('pieChart'));
        pieChart.setOption(option);
    }
});
xxl-job/xxl-job-admin/src/main/resources/static/js/jobcode.index.1.js
New file
@@ -0,0 +1,97 @@
$(function() {
    // init code editor
    var codeEditor;
    function initIde(glueSource) {
        if (codeEditor == null) {
            codeEditor = CodeMirror(document.getElementById("ideWindow"), {
                mode : ideMode,
                lineNumbers : true,
                matchBrackets : true,
                value: glueSource
            });
        } else {
            codeEditor.setValue(glueSource);
        }
    }
    initIde($("#version_now").val());
    // code change
    $(".source_version").click(function(){
        var sourceId = $(this).attr('version');
        var temp = $( "#" + sourceId ).val();
        //codeEditor.setValue('');
        initIde(temp);
    });
    // code source save
    $("#save").click(function() {
        $('#saveModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    $("#saveModal .ok").click(function() {
        var glueSource = codeEditor.getValue();
        var glueRemark = $("#glueRemark").val();
        if (!glueRemark) {
            layer.open({
                title: I18n.system_tips,
                btn: [ I18n.system_ok],
                content: I18n.system_please_input + I18n.jobinfo_glue_remark ,
                icon: '2'
            });
            return;
        }
        if (glueRemark.length <4 || glueRemark.length > 100) {
            layer.open({
                title: I18n.system_tips ,
                btn: [ I18n.system_ok ],
                content: I18n.jobinfo_glue_remark_limit ,
                icon: '2'
            });
            return;
        }
        $.ajax({
            type : 'POST',
            url : base_url + '/jobcode/save',
            data : {
                'id' : id,
                'glueSource' : glueSource,
                'glueRemark' : glueRemark
            },
            dataType : "json",
            success : function(data){
                if (data.code == 200) {
                    layer.open({
                        title: I18n.system_tips,
                        btn: [ I18n.system_ok ],
                        content: (I18n.system_save + I18n.system_success) ,
                        icon: '1',
                        end: function(layero, index){
                            //$(window).unbind('beforeunload');
                            window.location.reload();
                        }
                    });
                } else {
                    layer.open({
                        title: I18n.system_tips,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || (I18n.system_save + I18n.system_fail) ),
                        icon: '2'
                    });
                }
            }
        });
    });
    // before upload
    /*$(window).bind('beforeunload',function(){
        return 'Glue尚未保存,确定离开Glue编辑器?';
    });*/
});
xxl-job/xxl-job-admin/src/main/resources/static/js/jobgroup.index.1.js
New file
@@ -0,0 +1,359 @@
$(function() {
    // init date tables
    var jobGroupTable = $("#jobgroup_list").dataTable({
        "deferRender": true,
        "processing" : true,
        "serverSide": true,
        "ajax": {
            url: base_url + "/jobgroup/pageList",
            type:"post",
            data : function ( d ) {
                var obj = {};
                obj.appname = $('#appname').val();
                obj.title = $('#title').val();
                obj.start = d.start;
                obj.length = d.length;
                return obj;
            }
        },
        "searching": false,
        "ordering": false,
        //"scrollX": true,    // scroll x,close self-adaption
        "columns": [
            {
                "data": 'id',
                "visible" : false
            },
            {
                "data": 'appname',
                "visible" : true,
                "width":'30%'
            },
            {
                "data": 'title',
                "visible" : true,
                "width":'30%'
            },
            {
                "data": 'addressType',
                "width":'10%',
                "visible" : true,
                "render": function ( data, type, row ) {
                    if (row.addressType == 0) {
                        return I18n.jobgroup_field_addressType_0;
                    } else {
                        return I18n.jobgroup_field_addressType_1;
                    }
                }
            },
            {
                "data": 'registryList',
                "width":'15%',
                "visible" : true,
                "render": function ( data, type, row ) {
                    return row.registryList
                        ?'<a class="show_registryList" href="javascript:;" _id="'+ row.id +'" >'
                            + I18n.system_show +' ( ' + row.registryList.length+ ' )</a>'
                        :I18n.system_empty;
                }
            },
            {
                "data": I18n.system_opt ,
                "width":'15%',
                "render": function ( data, type, row ) {
                    return function(){
                        // data
                        tableData['key'+row.id] = row;
                        // opt
                        var html = '<div class="btn-group">\n' +
                            '     <button type="button" class="btn btn-primary btn-sm">'+ I18n.system_opt +'</button>\n' +
                            '     <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">\n' +
                            '       <span class="caret"></span>\n' +
                            '       <span class="sr-only">Toggle Dropdown</span>\n' +
                            '     </button>\n' +
                            '     <ul class="dropdown-menu" role="menu" _id="'+ row.id +'" >\n' +
                            '       <li><a href="javascript:void(0);" class="opt_edit" >'+ I18n.system_opt_edit +'</a></li>\n' +
                            '       <li><a href="javascript:void(0);" class="opt_del" >'+ I18n.system_opt_del +'</a></li>\n' +
                            '     </ul>\n' +
                            '   </div>';
                        return html;
                    };
                }
            }
        ],
        "language" : {
            "sProcessing" : I18n.dataTable_sProcessing ,
            "sLengthMenu" : I18n.dataTable_sLengthMenu ,
            "sZeroRecords" : I18n.dataTable_sZeroRecords ,
            "sInfo" : I18n.dataTable_sInfo ,
            "sInfoEmpty" : I18n.dataTable_sInfoEmpty ,
            "sInfoFiltered" : I18n.dataTable_sInfoFiltered ,
            "sInfoPostFix" : "",
            "sSearch" : I18n.dataTable_sSearch ,
            "sUrl" : "",
            "sEmptyTable" : I18n.dataTable_sEmptyTable ,
            "sLoadingRecords" : I18n.dataTable_sLoadingRecords ,
            "sInfoThousands" : ",",
            "oPaginate" : {
                "sFirst" : I18n.dataTable_sFirst ,
                "sPrevious" : I18n.dataTable_sPrevious ,
                "sNext" : I18n.dataTable_sNext ,
                "sLast" : I18n.dataTable_sLast
            },
            "oAria" : {
                "sSortAscending" : I18n.dataTable_sSortAscending ,
                "sSortDescending" : I18n.dataTable_sSortDescending
            }
        }
    });
    // table data
    var tableData = {};
    // search btn
    $('#searchBtn').on('click', function(){
        jobGroupTable.fnDraw();
    });
    // job registryinfo
    $("#jobgroup_list").on('click', '.show_registryList',function() {
        var id = $(this).attr("_id");
        var row = tableData['key'+id];
        var html = '<div>';
        if (row.registryList) {
            for (var index in row.registryList) {
                html += (parseInt(index)+1) + '. <span class="badge bg-green" >' + row.registryList[index] + '</span><br>';
            }
        }
        html += '</div>';
        layer.open({
            title: I18n.jobinfo_opt_registryinfo ,
            btn: [ I18n.system_ok ],
            content: html
        });
    });
    // opt_del
    $("#jobgroup_list").on('click', '.opt_del',function() {
        var id = $(this).parents('ul').attr("_id");
        layer.confirm( (I18n.system_ok + I18n.jobgroup_del + '?') , {
            icon: 3,
            title: I18n.system_tips ,
            btn: [ I18n.system_ok, I18n.system_cancel ]
        }, function(index){
            layer.close(index);
            $.ajax({
                type : 'POST',
                url : base_url + '/jobgroup/remove',
                data : {"id":id},
                dataType : "json",
                success : function(data){
                    if (data.code == 200) {
                        layer.open({
                            title: I18n.system_tips ,
                            btn: [ I18n.system_ok ],
                            content: (I18n.jobgroup_del + I18n.system_success),
                            icon: '1',
                            end: function(layero, index){
                                jobGroupTable.fnDraw();
                            }
                        });
                    } else {
                        layer.open({
                            title: I18n.system_tips,
                            btn: [ I18n.system_ok ],
                            content: (data.msg || (I18n.jobgroup_del + I18n.system_fail)),
                            icon: '2'
                        });
                    }
                },
            });
        });
    });
    // jquery.validate “low letters start, limit contants、 letters、numbers and line-through.”
    jQuery.validator.addMethod("myValid01", function(value, element) {
        var length = value.length;
        var valid = /^[a-z][a-zA-Z0-9-]*$/;
        return this.optional(element) || valid.test(value);
    }, I18n.jobgroup_field_appname_limit );
    $('.add').on('click', function(){
        $('#addModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var addModalValidate = $("#addModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            appname : {
                required : true,
                rangelength:[4,64],
                myValid01 : true
            },
            title : {
                required : true,
                rangelength:[4, 12]
            }
        },
        messages : {
            appname : {
                required : I18n.system_please_input+"AppName",
                rangelength: I18n.jobgroup_field_appname_length ,
                myValid01: I18n.jobgroup_field_appname_limit
            },
            title : {
                required : I18n.system_please_input + I18n.jobgroup_field_title ,
                rangelength: I18n.jobgroup_field_title_length
            }
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            $.post(base_url + "/jobgroup/save",  $("#addModal .form").serialize(), function(data, status) {
                if (data.code == "200") {
                    $('#addModal').modal('hide');
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: I18n.system_add_suc ,
                        icon: '1',
                        end: function(layero, index){
                            jobGroupTable.fnDraw();
                        }
                    });
                } else {
                    layer.open({
                        title: I18n.system_tips,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_add_fail  ),
                        icon: '2'
                    });
                }
            });
        }
    });
    $("#addModal").on('hide.bs.modal', function () {
        $("#addModal .form")[0].reset();
        addModalValidate.resetForm();
        $("#addModal .form .form-group").removeClass("has-error");
    });
    // addressType change
    $("#addModal input[name=addressType], #updateModal input[name=addressType]").click(function(){
        var addressType = $(this).val();
        var $addressList = $(this).parents("form").find("textarea[name=addressList]");
        if (addressType == 0) {
            $addressList.css("background-color", "#eee");    // 自动注册
            $addressList.attr("readonly","readonly");
            $addressList.val("");
        } else {
            $addressList.css("background-color", "white");
            $addressList.removeAttr("readonly");
        }
    });
    // opt_edit
    $("#jobgroup_list").on('click', '.opt_edit',function() {
        var id = $(this).parents('ul').attr("_id");
        var row = tableData['key'+id];
        $("#updateModal .form input[name='id']").val( row.id );
        $("#updateModal .form input[name='appname']").val( row.appname );
        $("#updateModal .form input[name='title']").val( row.title );
        // 注册方式
        $("#updateModal .form input[name='addressType']").removeAttr('checked');
        $("#updateModal .form input[name='addressType'][value='"+ row.addressType +"']").click();
        // 机器地址
        $("#updateModal .form textarea[name='addressList']").val( row.addressList );
        $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var updateModalValidate = $("#updateModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            appname : {
                required : true,
                rangelength:[4,64],
                myValid01 : true
            },
            title : {
                required : true,
                rangelength:[4, 12]
            }
        },
        messages : {
            appname : {
                required : I18n.system_please_input+"AppName",
                rangelength: I18n.jobgroup_field_appname_length ,
                myValid01: I18n.jobgroup_field_appname_limit
            },
            title : {
                required : I18n.system_please_input + I18n.jobgroup_field_title ,
                rangelength: I18n.jobgroup_field_title_length
            }
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            $.post(base_url + "/jobgroup/update",  $("#updateModal .form").serialize(), function(data, status) {
                if (data.code == "200") {
                    $('#updateModal').modal('hide');
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: I18n.system_update_suc ,
                        icon: '1',
                        end: function(layero, index){
                            jobGroupTable.fnDraw();
                        }
                    });
                } else {
                    layer.open({
                        title: I18n.system_tips,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_update_fail  ),
                        icon: '2'
                    });
                }
            });
        }
    });
    $("#updateModal").on('hide.bs.modal', function () {
        $("#updateModal .form")[0].reset();
        addModalValidate.resetForm();
        $("#updateModal .form .form-group").removeClass("has-error");
    });
});
xxl-job/xxl-job-admin/src/main/resources/static/js/jobinfo.index.1.js
New file
@@ -0,0 +1,739 @@
$(function() {
    // init date tables
    var jobTable = $("#job_list").dataTable({
        "deferRender": true,
        "processing" : true,
        "serverSide": true,
        "ajax": {
            url: base_url + "/jobinfo/pageList",
            type:"post",
            data : function ( d ) {
                var obj = {};
                obj.jobGroup = $('#jobGroup').val();
                obj.triggerStatus = $('#triggerStatus').val();
                obj.jobDesc = $('#jobDesc').val();
                obj.executorHandler = $('#executorHandler').val();
                obj.author = $('#author').val();
                obj.start = d.start;
                obj.length = d.length;
                return obj;
            }
        },
        "searching": false,
        "ordering": false,
        //"scrollX": true,    // scroll x,close self-adaption
        "columns": [
                    {
                        "data": 'id',
                        "bSortable": false,
                        "visible" : true,
                        "width":'7%'
                    },
                    {
                        "data": 'jobGroup',
                        "visible" : false,
                        "render": function ( data, type, row ) {
                            var groupMenu = $("#jobGroup").find("option");
                            for ( var index in $("#jobGroup").find("option")) {
                                if ($(groupMenu[index]).attr('value') == data) {
                                    return $(groupMenu[index]).html();
                                }
                            }
                            return data;
                        }
                    },
                    {
                        "data": 'jobDesc',
                        "visible" : true,
                        "width":'25%'
                    },
                    {
                        "data": 'scheduleType',
                        "visible" : true,
                        "width":'13%',
                        "render": function ( data, type, row ) {
                            if (row.scheduleConf) {
                                return row.scheduleType + ':'+ row.scheduleConf;
                            } else {
                                return row.scheduleType;
                            }
                        }
                    },
                    {
                        "data": 'glueType',
                        "width":'25%',
                        "visible" : true,
                        "render": function ( data, type, row ) {
                            var glueTypeTitle = findGlueTypeTitle(row.glueType);
                            if (row.executorHandler) {
                                return glueTypeTitle +":" + row.executorHandler;
                            } else {
                                return glueTypeTitle;
                            }
                        }
                    },
                    { "data": 'executorParam', "visible" : false},
                    {
                        "data": 'addTime',
                        "visible" : false,
                        "render": function ( data, type, row ) {
                            return data?moment(new Date(data)).format("YYYY-MM-DD HH:mm:ss"):"";
                        }
                    },
                    {
                        "data": 'updateTime',
                        "visible" : false,
                        "render": function ( data, type, row ) {
                            return data?moment(new Date(data)).format("YYYY-MM-DD HH:mm:ss"):"";
                        }
                    },
                    { "data": 'author', "visible" : true, "width":'10%'},
                    { "data": 'alarmEmail', "visible" : false},
                    {
                        "data": 'triggerStatus',
                        "width":'10%',
                        "visible" : true,
                        "render": function ( data, type, row ) {
                            // status
                            if (1 == data) {
                                return '<small class="label label-success" >RUNNING</small>';
                            } else {
                                return '<small class="label label-default" >STOP</small>';
                            }
                            return data;
                        }
                    },
                    {
                        "data": I18n.system_opt ,
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            return function(){
                                // status
                                var start_stop_div = "";
                                if (1 == row.triggerStatus ) {
                                    start_stop_div = '<li><a href="javascript:void(0);" class="job_operate" _type="job_pause" >'+ I18n.jobinfo_opt_stop +'</a></li>\n';
                                } else {
                                    start_stop_div = '<li><a href="javascript:void(0);" class="job_operate" _type="job_resume" >'+ I18n.jobinfo_opt_start +'</a></li>\n';
                                }
                                // job_next_time_html
                                var job_next_time_html = '';
                                if (row.scheduleType == 'CRON' || row.scheduleType == 'FIX_RATE') {
                                    job_next_time_html = '<li><a href="javascript:void(0);" class="job_next_time" >' + I18n.jobinfo_opt_next_time + '</a></li>\n';
                                }
                                // log url
                                var logHref = base_url +'/joblog?jobId='+ row.id;
                                // code url
                                var codeBtn = "";
                                if ('BEAN' != row.glueType) {
                                    var codeUrl = base_url +'/jobcode?jobId='+ row.id;
                                    codeBtn = '<li><a href="'+ codeUrl +'" target="_blank" >GLUE IDE</a></li>\n';
                                    codeBtn += '<li class="divider"></li>\n';
                                }
                                // data
                                tableData['key'+row.id] = row;
                                // opt
                                var html = '<div class="btn-group">\n' +
                                    '     <button type="button" class="btn btn-primary btn-sm">'+ I18n.system_opt +'</button>\n' +
                                    '     <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">\n' +
                                    '       <span class="caret"></span>\n' +
                                    '       <span class="sr-only">Toggle Dropdown</span>\n' +
                                    '     </button>\n' +
                                    '     <ul class="dropdown-menu" role="menu" _id="'+ row.id +'" >\n' +
                                    '       <li><a href="javascript:void(0);" class="job_trigger" >'+ I18n.jobinfo_opt_run +'</a></li>\n' +
                                    '       <li><a href="'+ logHref +'">'+ I18n.jobinfo_opt_log +'</a></li>\n' +
                                    '       <li><a href="javascript:void(0);" class="job_registryinfo" >' + I18n.jobinfo_opt_registryinfo + '</a></li>\n' +
                                    job_next_time_html +
                                    '       <li class="divider"></li>\n' +
                                    codeBtn +
                                    start_stop_div +
                                    '       <li><a href="javascript:void(0);" class="update" >'+ I18n.system_opt_edit +'</a></li>\n' +
                                    '       <li><a href="javascript:void(0);" class="job_operate" _type="job_del" >'+ I18n.system_opt_del +'</a></li>\n' +
                                    '       <li><a href="javascript:void(0);" class="job_copy" >'+ I18n.system_opt_copy +'</a></li>\n' +
                                    '     </ul>\n' +
                                    '   </div>';
                                return html;
                            };
                        }
                    }
                ],
        "language" : {
            "sProcessing" : I18n.dataTable_sProcessing ,
            "sLengthMenu" : I18n.dataTable_sLengthMenu ,
            "sZeroRecords" : I18n.dataTable_sZeroRecords ,
            "sInfo" : I18n.dataTable_sInfo ,
            "sInfoEmpty" : I18n.dataTable_sInfoEmpty ,
            "sInfoFiltered" : I18n.dataTable_sInfoFiltered ,
            "sInfoPostFix" : "",
            "sSearch" : I18n.dataTable_sSearch ,
            "sUrl" : "",
            "sEmptyTable" : I18n.dataTable_sEmptyTable ,
            "sLoadingRecords" : I18n.dataTable_sLoadingRecords ,
            "sInfoThousands" : ",",
            "oPaginate" : {
                "sFirst" : I18n.dataTable_sFirst ,
                "sPrevious" : I18n.dataTable_sPrevious ,
                "sNext" : I18n.dataTable_sNext ,
                "sLast" : I18n.dataTable_sLast
            },
            "oAria" : {
                "sSortAscending" : I18n.dataTable_sSortAscending ,
                "sSortDescending" : I18n.dataTable_sSortDescending
            }
        }
    });
    // table data
    var tableData = {};
    // search btn
    $('#searchBtn').on('click', function(){
        jobTable.fnDraw();
    });
    // jobGroup change
    $('#jobGroup').on('change', function(){
        //reload
        var jobGroup = $('#jobGroup').val();
        window.location.href = base_url + "/jobinfo?jobGroup=" + jobGroup;
    });
    // job operate
    $("#job_list").on('click', '.job_operate',function() {
        var typeName;
        var url;
        var needFresh = false;
        var type = $(this).attr("_type");
        if ("job_pause" == type) {
            typeName = I18n.jobinfo_opt_stop ;
            url = base_url + "/jobinfo/stop";
            needFresh = true;
        } else if ("job_resume" == type) {
            typeName = I18n.jobinfo_opt_start ;
            url = base_url + "/jobinfo/start";
            needFresh = true;
        } else if ("job_del" == type) {
            typeName = I18n.system_opt_del ;
            url = base_url + "/jobinfo/remove";
            needFresh = true;
        } else {
            return;
        }
        var id = $(this).parents('ul').attr("_id");
        layer.confirm( I18n.system_ok + typeName + '?', {
            icon: 3,
            title: I18n.system_tips ,
            btn: [ I18n.system_ok, I18n.system_cancel ]
        }, function(index){
            layer.close(index);
            $.ajax({
                type : 'POST',
                url : url,
                data : {
                    "id" : id
                },
                dataType : "json",
                success : function(data){
                    if (data.code == 200) {
                        layer.msg( typeName + I18n.system_success );
                        if (needFresh) {
                            //window.location.reload();
                            jobTable.fnDraw(false);
                        }
                    } else {
                        layer.msg( data.msg || typeName + I18n.system_fail );
                    }
                }
            });
        });
    });
    // job trigger
    $("#job_list").on('click', '.job_trigger',function() {
        var id = $(this).parents('ul').attr("_id");
        var row = tableData['key'+id];
        $("#jobTriggerModal .form input[name='id']").val( row.id );
        $("#jobTriggerModal .form textarea[name='executorParam']").val( row.executorParam );
        $('#jobTriggerModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    $("#jobTriggerModal .ok").on('click',function() {
        $.ajax({
            type : 'POST',
            url : base_url + "/jobinfo/trigger",
            data : {
                "id" : $("#jobTriggerModal .form input[name='id']").val(),
                "executorParam" : $("#jobTriggerModal .textarea[name='executorParam']").val(),
                "addressList" : $("#jobTriggerModal .textarea[name='addressList']").val()
            },
            dataType : "json",
            success : function(data){
                if (data.code == 200) {
                    $('#jobTriggerModal').modal('hide');
                    layer.msg( I18n.jobinfo_opt_run + I18n.system_success );
                } else {
                    layer.msg( data.msg || I18n.jobinfo_opt_run + I18n.system_fail );
                }
            }
        });
    });
    $("#jobTriggerModal").on('hide.bs.modal', function () {
        $("#jobTriggerModal .form")[0].reset();
    });
    // job registryinfo
    $("#job_list").on('click', '.job_registryinfo',function() {
        var id = $(this).parents('ul').attr("_id");
        var row = tableData['key'+id];
        var jobGroup = row.jobGroup;
        $.ajax({
            type : 'POST',
            url : base_url + "/jobgroup/loadById",
            data : {
                "id" : jobGroup
            },
            dataType : "json",
            success : function(data){
                var html = '<div>';
                if (data.code == 200 && data.content.registryList) {
                    for (var index in data.content.registryList) {
                        html += (parseInt(index)+1) + '. <span class="badge bg-green" >' + data.content.registryList[index] + '</span><br>';
                    }
                }
                html += '</div>';
                layer.open({
                    title: I18n.jobinfo_opt_registryinfo ,
                    btn: [ I18n.system_ok ],
                    content: html
                });
            }
        });
    });
    // job_next_time
    $("#job_list").on('click', '.job_next_time',function() {
        var id = $(this).parents('ul').attr("_id");
        var row = tableData['key'+id];
        $.ajax({
            type : 'POST',
            url : base_url + "/jobinfo/nextTriggerTime",
            data : {
                "scheduleType" : row.scheduleType,
                "scheduleConf" : row.scheduleConf
            },
            dataType : "json",
            success : function(data){
                if (data.code != 200) {
                    layer.open({
                        title: I18n.jobinfo_opt_next_time ,
                        btn: [ I18n.system_ok ],
                        content: data.msg
                    });
                } else {
                    var html = '<center>';
                    if (data.code == 200 && data.content) {
                        for (var index in data.content) {
                            html += '<span>' + data.content[index] + '</span><br>';
                        }
                    }
                    html += '</center>';
                    layer.open({
                        title: I18n.jobinfo_opt_next_time ,
                        btn: [ I18n.system_ok ],
                        content: html
                    });
                }
            }
        });
    });
    // add
    $(".add").click(function(){
        // init-cronGen
        $("#addModal .form input[name='schedule_conf_CRON']").show().siblings().remove();
        $("#addModal .form input[name='schedule_conf_CRON']").cronGen({});
        // 》init scheduleType
        $("#updateModal .form select[name=scheduleType]").change();
        // 》init glueType
        $("#updateModal .form select[name=glueType]").change();
        $('#addModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var addModalValidate = $("#addModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            jobDesc : {
                required : true,
                maxlength: 50
            },
            author : {
                required : true
            }/*,
            executorTimeout : {
                digits:true
            },
            executorFailRetryCount : {
                digits:true
            }*/
        },
        messages : {
            jobDesc : {
                required : I18n.system_please_input + I18n.jobinfo_field_jobdesc
            },
            author : {
                required : I18n.system_please_input + I18n.jobinfo_field_author
            }/*,
            executorTimeout : {
                digits: I18n.system_please_input + I18n.system_digits
            },
            executorFailRetryCount : {
                digits: I18n.system_please_input + I18n.system_digits
            }*/
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            // process executorTimeout+executorFailRetryCount
            var executorTimeout = $("#addModal .form input[name='executorTimeout']").val();
            if(!/^\d+$/.test(executorTimeout)) {
                executorTimeout = 0;
            }
            $("#addModal .form input[name='executorTimeout']").val(executorTimeout);
            var executorFailRetryCount = $("#addModal .form input[name='executorFailRetryCount']").val();
            if(!/^\d+$/.test(executorFailRetryCount)) {
                executorFailRetryCount = 0;
            }
            $("#addModal .form input[name='executorFailRetryCount']").val(executorFailRetryCount);
            // process schedule_conf
            var scheduleType = $("#addModal .form select[name='scheduleType']").val();
            var scheduleConf;
            if (scheduleType == 'CRON') {
                scheduleConf = $("#addModal .form input[name='cronGen_display']").val();
            } else if (scheduleType == 'FIX_RATE') {
                scheduleConf = $("#addModal .form input[name='schedule_conf_FIX_RATE']").val();
            } else if (scheduleType == 'FIX_DELAY') {
                scheduleConf = $("#addModal .form input[name='schedule_conf_FIX_DELAY']").val();
            }
            $("#addModal .form input[name='scheduleConf']").val( scheduleConf );
            $.post(base_url + "/jobinfo/add",  $("#addModal .form").serialize(), function(data, status) {
                if (data.code == "200") {
                    $('#addModal').modal('hide');
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: I18n.system_add_suc ,
                        icon: '1',
                        end: function(layero, index){
                            jobTable.fnDraw();
                            //window.location.reload();
                        }
                    });
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_add_fail),
                        icon: '2'
                    });
                }
            });
        }
    });
    $("#addModal").on('hide.bs.modal', function () {
        addModalValidate.resetForm();
        $("#addModal .form")[0].reset();
        $("#addModal .form .form-group").removeClass("has-error");
        $(".remote_panel").show();    // remote
        $("#addModal .form input[name='executorHandler']").removeAttr("readonly");
    });
    // scheduleType change
    $(".scheduleType").change(function(){
        var scheduleType = $(this).val();
        $(this).parents("form").find(".schedule_conf").hide();
        $(this).parents("form").find(".schedule_conf_" + scheduleType).show();
    });
    // glueType change
    $(".glueType").change(function(){
        // executorHandler
        var $executorHandler = $(this).parents("form").find("input[name='executorHandler']");
        var glueType = $(this).val();
        if ('BEAN' != glueType) {
            $executorHandler.val("");
            $executorHandler.attr("readonly","readonly");
        } else {
            $executorHandler.removeAttr("readonly");
        }
    });
    $("#addModal .glueType").change(function(){
        // glueSource
        var glueType = $(this).val();
        if ('GLUE_GROOVY'==glueType){
            $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_java").val() );
        } else if ('GLUE_SHELL'==glueType){
            $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_shell").val() );
        } else if ('GLUE_PYTHON'==glueType){
            $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_python").val() );
        } else if ('GLUE_PHP'==glueType){
            $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_php").val() );
        } else if ('GLUE_NODEJS'==glueType){
            $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_nodejs").val() );
        } else if ('GLUE_POWERSHELL'==glueType){
            $("#addModal .form textarea[name='glueSource']").val( $("#addModal .form .glueSource_powershell").val() );
        } else {
            $("#addModal .form textarea[name='glueSource']").val("");
        }
    });
    // update
    $("#job_list").on('click', '.update',function() {
        var id = $(this).parents('ul').attr("_id");
        var row = tableData['key'+id];
        // fill base
        $("#updateModal .form input[name='id']").val( row.id );
        $('#updateModal .form select[name=jobGroup] option[value='+ row.jobGroup +']').prop('selected', true);
        $("#updateModal .form input[name='jobDesc']").val( row.jobDesc );
        $("#updateModal .form input[name='author']").val( row.author );
        $("#updateModal .form input[name='alarmEmail']").val( row.alarmEmail );
        // fill trigger
        $('#updateModal .form select[name=scheduleType] option[value='+ row.scheduleType +']').prop('selected', true);
        $("#updateModal .form input[name='scheduleConf']").val( row.scheduleConf );
        if (row.scheduleType == 'CRON') {
            $("#updateModal .form input[name='schedule_conf_CRON']").val( row.scheduleConf );
        } else if (row.scheduleType == 'FIX_RATE') {
            $("#updateModal .form input[name='schedule_conf_FIX_RATE']").val( row.scheduleConf );
        } else if (row.scheduleType == 'FIX_DELAY') {
            $("#updateModal .form input[name='schedule_conf_FIX_DELAY']").val( row.scheduleConf );
        }
        // 》init scheduleType
        $("#updateModal .form select[name=scheduleType]").change();
        // fill job
        $('#updateModal .form select[name=glueType] option[value='+ row.glueType +']').prop('selected', true);
        $("#updateModal .form input[name='executorHandler']").val( row.executorHandler );
        $("#updateModal .form textarea[name='executorParam']").val( row.executorParam );
        // 》init glueType
        $("#updateModal .form select[name=glueType]").change();
        // 》init-cronGen
        $("#updateModal .form input[name='schedule_conf_CRON']").show().siblings().remove();
        $("#updateModal .form input[name='schedule_conf_CRON']").cronGen({});
        // fill advanced
        $('#updateModal .form select[name=executorRouteStrategy] option[value='+ row.executorRouteStrategy +']').prop('selected', true);
        $("#updateModal .form input[name='childJobId']").val( row.childJobId );
        $('#updateModal .form select[name=misfireStrategy] option[value='+ row.misfireStrategy +']').prop('selected', true);
        $('#updateModal .form select[name=executorBlockStrategy] option[value='+ row.executorBlockStrategy +']').prop('selected', true);
        $("#updateModal .form input[name='executorTimeout']").val( row.executorTimeout );
        $("#updateModal .form input[name='executorFailRetryCount']").val( row.executorFailRetryCount );
        // show
        $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var updateModalValidate = $("#updateModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            jobDesc : {
                required : true,
                maxlength: 50
            },
            author : {
                required : true
            }
        },
        messages : {
            jobDesc : {
                required : I18n.system_please_input + I18n.jobinfo_field_jobdesc
            },
            author : {
                required : I18n.system_please_input + I18n.jobinfo_field_author
            }
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            // process executorTimeout + executorFailRetryCount
            var executorTimeout = $("#updateModal .form input[name='executorTimeout']").val();
            if(!/^\d+$/.test(executorTimeout)) {
                executorTimeout = 0;
            }
            $("#updateModal .form input[name='executorTimeout']").val(executorTimeout);
            var executorFailRetryCount = $("#updateModal .form input[name='executorFailRetryCount']").val();
            if(!/^\d+$/.test(executorFailRetryCount)) {
                executorFailRetryCount = 0;
            }
            $("#updateModal .form input[name='executorFailRetryCount']").val(executorFailRetryCount);
            // process schedule_conf
            var scheduleType = $("#updateModal .form select[name='scheduleType']").val();
            var scheduleConf;
            if (scheduleType == 'CRON') {
                scheduleConf = $("#updateModal .form input[name='cronGen_display']").val();
            } else if (scheduleType == 'FIX_RATE') {
                scheduleConf = $("#updateModal .form input[name='schedule_conf_FIX_RATE']").val();
            } else if (scheduleType == 'FIX_DELAY') {
                scheduleConf = $("#updateModal .form input[name='schedule_conf_FIX_DELAY']").val();
            }
            $("#updateModal .form input[name='scheduleConf']").val( scheduleConf );
            // post
            $.post(base_url + "/jobinfo/update", $("#updateModal .form").serialize(), function(data, status) {
                if (data.code == "200") {
                    $('#updateModal').modal('hide');
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: I18n.system_update_suc ,
                        icon: '1',
                        end: function(layero, index){
                            //window.location.reload();
                            jobTable.fnDraw();
                        }
                    });
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_update_fail ),
                        icon: '2'
                    });
                }
            });
        }
    });
    $("#updateModal").on('hide.bs.modal', function () {
        updateModalValidate.resetForm();
        $("#updateModal .form")[0].reset();
        $("#updateModal .form .form-group").removeClass("has-error");
    });
    /**
     * find title by name, GlueType
     */
    function findGlueTypeTitle(glueType) {
        var glueTypeTitle;
        $("#addModal .form select[name=glueType] option").each(function () {
            var name = $(this).val();
            var title = $(this).text();
            if (glueType == name) {
                glueTypeTitle = title;
                return false
            }
        });
        return glueTypeTitle;
    }
    // job_copy
    $("#job_list").on('click', '.job_copy',function() {
        var id = $(this).parents('ul').attr("_id");
        var row = tableData['key'+id];
        // fill base
        $('#addModal .form select[name=jobGroup] option[value='+ row.jobGroup +']').prop('selected', true);
        $("#addModal .form input[name='jobDesc']").val( row.jobDesc );
        $("#addModal .form input[name='author']").val( row.author );
        $("#addModal .form input[name='alarmEmail']").val( row.alarmEmail );
        // fill trigger
        $('#addModal .form select[name=scheduleType] option[value='+ row.scheduleType +']').prop('selected', true);
        $("#addModal .form input[name='scheduleConf']").val( row.scheduleConf );
        if (row.scheduleType == 'CRON') {
            $("#addModal .form input[name='schedule_conf_CRON']").val( row.scheduleConf );
        } else if (row.scheduleType == 'FIX_RATE') {
            $("#addModal .form input[name='schedule_conf_FIX_RATE']").val( row.scheduleConf );
        } else if (row.scheduleType == 'FIX_DELAY') {
            $("#addModal .form input[name='schedule_conf_FIX_DELAY']").val( row.scheduleConf );
        }
        // 》init scheduleType
        $("#addModal .form select[name=scheduleType]").change();
        // fill job
        $('#addModal .form select[name=glueType] option[value='+ row.glueType +']').prop('selected', true);
        $("#addModal .form input[name='executorHandler']").val( row.executorHandler );
        $("#addModal .form textarea[name='executorParam']").val( row.executorParam );
        // 》init glueType
        $("#addModal .form select[name=glueType]").change();
        // 》init-cronGen
        $("#addModal .form input[name='schedule_conf_CRON']").show().siblings().remove();
        $("#addModal .form input[name='schedule_conf_CRON']").cronGen({});
        // fill advanced
        $('#addModal .form select[name=executorRouteStrategy] option[value='+ row.executorRouteStrategy +']').prop('selected', true);
        $("#addModal .form input[name='childJobId']").val( row.childJobId );
        $('#addModal .form select[name=misfireStrategy] option[value='+ row.misfireStrategy +']').prop('selected', true);
        $('#addModal .form select[name=executorBlockStrategy] option[value='+ row.executorBlockStrategy +']').prop('selected', true);
        $("#addModal .form input[name='executorTimeout']").val( row.executorTimeout );
        $("#addModal .form input[name='executorFailRetryCount']").val( row.executorFailRetryCount );
        // show
        $('#addModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
});
xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.detail.1.js
New file
@@ -0,0 +1,91 @@
$(function() {
    // trigger fail, end
    if ( !(triggerCode == 200 || handleCode != 0) ) {
        $('#logConsoleRunning').hide();
        $('#logConsole').append('<span style="color: red;">'+ I18n.joblog_rolling_log_triggerfail +'</span>');
        return;
    }
    // pull log
    var fromLineNum = 1;    // [from, to], start as 1
    var pullFailCount = 0;
    function pullLog() {
        // pullFailCount, max=20
        if (pullFailCount++ > 20) {
            logRunStop('<span style="color: red;">'+ I18n.joblog_rolling_log_failoften +'</span>');
            return;
        }
        // load
        console.log("pullLog, fromLineNum:" + fromLineNum);
        $.ajax({
            type : 'POST',
            async: false,   // sync, make log ordered
            url : base_url + '/joblog/logDetailCat',
            data : {
                "executorAddress":executorAddress,
                "triggerTime":triggerTime,
                "logId":logId,
                "fromLineNum":fromLineNum
            },
            dataType : "json",
            success : function(data){
                if (data.code == 200) {
                    if (!data.content) {
                        console.log('pullLog fail');
                        return;
                    }
                    if (fromLineNum != data.content.fromLineNum) {
                        console.log('pullLog fromLineNum not match');
                        return;
                    }
                    if (fromLineNum > data.content.toLineNum ) {
                        console.log('pullLog already line-end');
                        // valid end
                        if (data.content.end) {
                            logRunStop('<br><span style="color: green;">[Rolling Log Finish]</span>');
                            return;
                        }
                        return;
                    }
                    // append content
                    fromLineNum = data.content.toLineNum + 1;
                    $('#logConsole').append(data.content.logContent);
                    pullFailCount = 0;
                    // scroll to bottom
                    scrollTo(0, document.body.scrollHeight);        // $('#logConsolePre').scrollTop( document.body.scrollHeight + 300 );
                } else {
                    console.log('pullLog fail:'+data.msg);
                }
            }
        });
    }
    // pull first page
    pullLog();
    // handler already callback, end
    if (handleCode > 0) {
        logRunStop('<br><span style="color: green;">[Load Log Finish]</span>');
        return;
    }
    // round until end
    var logRun = setInterval(function () {
        pullLog()
    }, 3000);
    function logRunStop(content){
        $('#logConsoleRunning').hide();
        logRun = window.clearInterval(logRun);
        $('#logConsole').append(content);
    }
});
xxl-job/xxl-job-admin/src/main/resources/static/js/joblog.index.1.js
New file
@@ -0,0 +1,396 @@
$(function() {
    // jobGroup change, job list init and select
    $("#jobGroup").on("change", function () {
        var jobGroup = $(this).children('option:selected').val();
        $.ajax({
            type : 'POST',
            async: false,   // async, avoid js invoke pagelist before jobId data init
            url : base_url + '/joblog/getJobsByGroup',
            data : {"jobGroup":jobGroup},
            dataType : "json",
            success : function(data){
                if (data.code == 200) {
                    $("#jobId").html( '<option value="0" >'+ I18n.system_all +'</option>' );
                    $.each(data.content, function (n, value) {
                        $("#jobId").append('<option value="' + value.id + '" >' + value.jobDesc + '</option>');
                    });
                    if ($("#jobId").attr("paramVal")){
                        $("#jobId").find("option[value='" + $("#jobId").attr("paramVal") + "']").attr("selected",true);
                    }
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_api_error ),
                        icon: '2'
                    });
                }
            },
        });
    });
    if ($("#jobGroup").attr("paramVal")){
        $("#jobGroup").find("option[value='" + $("#jobGroup").attr("paramVal") + "']").attr("selected",true);
        $("#jobGroup").change();
    }
    // filter Time
    var rangesConf = {};
    rangesConf[I18n.daterangepicker_ranges_recent_hour] = [moment().subtract(1, 'hours'), moment()];
    rangesConf[I18n.daterangepicker_ranges_today] = [moment().startOf('day'), moment().endOf('day')];
    rangesConf[I18n.daterangepicker_ranges_yesterday] = [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')];
    rangesConf[I18n.daterangepicker_ranges_this_month] = [moment().startOf('month'), moment().endOf('month')];
    rangesConf[I18n.daterangepicker_ranges_last_month] = [moment().subtract(1, 'months').startOf('month'), moment().subtract(1, 'months').endOf('month')];
    rangesConf[I18n.daterangepicker_ranges_recent_week] = [moment().subtract(1, 'weeks').startOf('day'), moment().endOf('day')];
    rangesConf[I18n.daterangepicker_ranges_recent_month] = [moment().subtract(1, 'months').startOf('day'), moment().endOf('day')];
    $('#filterTime').daterangepicker({
        autoApply:false,
        singleDatePicker:false,
        showDropdowns:false,        // 是否显示年月选择条件
        timePicker: true,             // 是否显示小时和分钟选择条件
        timePickerIncrement: 10,     // 时间的增量,单位为分钟
        timePicker24Hour : true,
        opens : 'left', //日期选择框的弹出位置
        ranges: rangesConf,
        locale : {
            format: 'YYYY-MM-DD HH:mm:ss',
            separator : ' - ',
            customRangeLabel : I18n.daterangepicker_custom_name ,
            applyLabel : I18n.system_ok ,
            cancelLabel : I18n.system_cancel ,
            fromLabel : I18n.daterangepicker_custom_starttime ,
            toLabel : I18n.daterangepicker_custom_endtime ,
            daysOfWeek : I18n.daterangepicker_custom_daysofweek.split(',') ,        // '日', '一', '二', '三', '四', '五', '六'
            monthNames : I18n.daterangepicker_custom_monthnames.split(',') ,        // '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'
            firstDay : 1
        },
        startDate: rangesConf[I18n.daterangepicker_ranges_today][0],
        endDate: rangesConf[I18n.daterangepicker_ranges_today][1]
    });
    // init date tables
    var logTable = $("#joblog_list").dataTable({
        "deferRender": true,
        "processing" : true,
        "serverSide": true,
        "ajax": {
            url: base_url + "/joblog/pageList" ,
            type:"post",
            data : function ( d ) {
                var obj = {};
                obj.jobGroup = $('#jobGroup').val();
                obj.jobId = $('#jobId').val();
                obj.logStatus = $('#logStatus').val();
                obj.filterTime = $('#filterTime').val();
                obj.start = d.start;
                obj.length = d.length;
                return obj;
            }
        },
        "searching": false,
        "ordering": false,
        //"scrollX": false,
        "columns": [
                    {
                        "data": 'jobId',
                        "visible" : true,
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            var jobhandler = '';
                            if (row.executorHandler) {
                                jobhandler = "<br>JobHandler:" + row.executorHandler;
                            }
                            var temp = '';
                            temp += I18n.joblog_field_executorAddress + ':' + (row.executorAddress?row.executorAddress:'');
                            temp += jobhandler;
                            temp += '<br>'+ I18n.jobinfo_field_executorparam +':' + row.executorParam;
                            return '<a class="logTips" href="javascript:;" >'+ row.jobId +'<span style="display:none;">'+ temp +'</span></a>';
                        }
                    },
                    { "data": 'jobGroup', "visible" : false},
                    {
                        "data": 'triggerTime',
                        "width":'20%',
                        "render": function ( data, type, row ) {
                            return data?moment(data).format("YYYY-MM-DD HH:mm:ss"):"";
                        }
                    },
                    {
                        "data": 'triggerCode',
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            var html = data;
                            if (data == 200) {
                                html = '<span style="color: green">'+ I18n.system_success +'</span>';
                            } else if (data == 500) {
                                html = '<span style="color: red">'+ I18n.system_fail +'</span>';
                            } else if (data == 0) {
                                html = '';
                            }
                            return html;
                        }
                    },
                    {
                        "data": 'triggerMsg',
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            return data?'<a class="logTips" href="javascript:;" >'+ I18n.system_show +'<span style="display:none;">'+ data +'</span></a>':I18n.system_empty;
                        }
                    },
                    {
                        "data": 'handleTime',
                        "width":'20%',
                        "render": function ( data, type, row ) {
                            return data?moment(data).format("YYYY-MM-DD HH:mm:ss"):"";
                        }
                    },
                    {
                        "data": 'handleCode',
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            var html = data;
                            if (data == 200) {
                                html = '<span style="color: green">'+ I18n.joblog_handleCode_200 +'</span>';
                            } else if (data == 500) {
                                html = '<span style="color: red">'+ I18n.joblog_handleCode_500 +'</span>';
                            } else if (data == 502) {
                                html = '<span style="color: red">'+ I18n.joblog_handleCode_502 +'</span>';
                            } else if (data == 0) {
                                html = '';
                            }
                            return html;
                        }
                    },
                    {
                        "data": 'handleMsg',
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            return data?'<a class="logTips" href="javascript:;" >'+ I18n.system_show +'<span style="display:none;">'+ data +'</span></a>':I18n.system_empty;
                        }
                    },
                    {
                        "data": 'handleMsg' ,
                        "bSortable": false,
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            // better support expression or string, not function
                            return function () {
                                if (row.triggerCode == 200 || row.handleCode != 0){
                                    /*var temp = '<a href="javascript:;" class="logDetail" _id="'+ row.id +'">'+ I18n.joblog_rolling_log +'</a>';
                                    if(row.handleCode == 0){
                                        temp += '<br><a href="javascript:;" class="logKill" _id="'+ row.id +'" style="color: red;" >'+ I18n.joblog_kill_log +'</a>';
                                    }*/
                                    //return temp;
                                    var logKillDiv = '';
                                    if(row.handleCode == 0){
                                        logKillDiv = '       <li class="divider"></li>\n' +
                                            '       <li><a href="javascript:void(0);" class="logKill" _id="'+ row.id +'" >'+ I18n.joblog_kill_log +'</a></li>\n';
                                    }
                                    var html = '<div class="btn-group">\n' +
                                        '     <button type="button" class="btn btn-primary btn-sm">'+ I18n.system_opt +'</button>\n' +
                                        '     <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">\n' +
                                        '       <span class="caret"></span>\n' +
                                        '       <span class="sr-only">Toggle Dropdown</span>\n' +
                                        '     </button>\n' +
                                        '     <ul class="dropdown-menu" role="menu" _id="'+ row.id +'" >\n' +
                                        '       <li><a href="javascript:void(0);" class="logDetail" _id="'+ row.id +'" >'+ I18n.joblog_rolling_log +'</a></li>\n' +
                                        logKillDiv +
                                        '     </ul>\n' +
                                        '   </div>';
                                    return html;
                                }
                                return null;
                            }
                        }
                    }
                ],
        "language" : {
            "sProcessing" : I18n.dataTable_sProcessing ,
            "sLengthMenu" : I18n.dataTable_sLengthMenu ,
            "sZeroRecords" : I18n.dataTable_sZeroRecords ,
            "sInfo" : I18n.dataTable_sInfo ,
            "sInfoEmpty" : I18n.dataTable_sInfoEmpty ,
            "sInfoFiltered" : I18n.dataTable_sInfoFiltered ,
            "sInfoPostFix" : "",
            "sSearch" : I18n.dataTable_sSearch ,
            "sUrl" : "",
            "sEmptyTable" : I18n.dataTable_sEmptyTable ,
            "sLoadingRecords" : I18n.dataTable_sLoadingRecords ,
            "sInfoThousands" : ",",
            "oPaginate" : {
                "sFirst" : I18n.dataTable_sFirst ,
                "sPrevious" : I18n.dataTable_sPrevious ,
                "sNext" : I18n.dataTable_sNext ,
                "sLast" : I18n.dataTable_sLast
            },
            "oAria" : {
                "sSortAscending" : I18n.dataTable_sSortAscending ,
                "sSortDescending" : I18n.dataTable_sSortDescending
            }
        }
    });
    logTable.on('xhr.dt',function(e, settings, json, xhr) {
        if (json.code && json.code != 200) {
            layer.msg( json.msg || I18n.system_api_error );
        }
    });
    // logTips alert
    $('#joblog_list').on('click', '.logTips', function(){
        var msg = $(this).find('span').html();
        ComAlertTec.show(msg);
    });
    // search Btn
    $('#searchBtn').on('click', function(){
        logTable.fnDraw();
    });
    // logDetail look
    $('#joblog_list').on('click', '.logDetail', function(){
        var _id = $(this).attr('_id');
        window.open(base_url + '/joblog/logDetailPage?id=' + _id);
        return;
    });
    /**
     * log Kill
     */
    $('#joblog_list').on('click', '.logKill', function(){
        var _id = $(this).attr('_id');
        layer.confirm( (I18n.system_ok + I18n.joblog_kill_log + '?'), {
            icon: 3,
            title: I18n.system_tips ,
            btn: [ I18n.system_ok, I18n.system_cancel ]
        }, function(index){
            layer.close(index);
            $.ajax({
                type : 'POST',
                url : base_url + '/joblog/logKill',
                data : {"id":_id},
                dataType : "json",
                success : function(data){
                    if (data.code == 200) {
                        layer.open({
                            title: I18n.system_tips,
                            btn: [ I18n.system_ok ],
                            content: I18n.system_opt_suc ,
                            icon: '1',
                            end: function(layero, index){
                                logTable.fnDraw();
                            }
                        });
                    } else {
                        layer.open({
                            title: I18n.system_tips,
                            btn: [ I18n.system_ok ],
                            content: (data.msg || I18n.system_opt_fail ),
                            icon: '2'
                        });
                    }
                },
            });
        });
    });
    /**
     * clear Log
     */
    $('#clearLog').on('click', function(){
        var jobGroup = $('#jobGroup').val();
        var jobId = $('#jobId').val();
        var jobGroupText = $("#jobGroup").find("option:selected").text();
        var jobIdText = $("#jobId").find("option:selected").text();
        $('#clearLogModal input[name=jobGroup]').val(jobGroup);
        $('#clearLogModal input[name=jobId]').val(jobId);
        $('#clearLogModal .jobGroupText').val(jobGroupText);
        $('#clearLogModal .jobIdText').val(jobIdText);
        $('#clearLogModal').modal('show');
    });
    $("#clearLogModal .ok").on('click', function(){
        $.post(base_url + "/joblog/clearLog",  $("#clearLogModal .form").serialize(), function(data, status) {
            if (data.code == "200") {
                $('#clearLogModal').modal('hide');
                layer.open({
                    title: I18n.system_tips ,
                    btn: [ I18n.system_ok ],
                    content: (I18n.joblog_clean_log + I18n.system_success) ,
                    icon: '1',
                    end: function(layero, index){
                        logTable.fnDraw();
                    }
                });
            } else {
                layer.open({
                    title: I18n.system_tips ,
                    btn: [ I18n.system_ok ],
                    content: (data.msg || (I18n.joblog_clean_log + I18n.system_fail) ),
                    icon: '2'
                });
            }
        });
    });
    $("#clearLogModal").on('hide.bs.modal', function () {
        $("#clearLogModal .form")[0].reset();
    });
});
// Com Alert by Tec theme
var ComAlertTec = {
    html:function(){
        var html =
            '<div class="modal fade" id="ComAlertTec" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">' +
            '    <div class="modal-dialog modal-lg-">' +
            '        <div class="modal-content-tec">' +
            '            <div class="modal-body">' +
            '                <div class="alert" style="color:#fff;word-wrap: break-word;">' +
            '                </div>' +
            '            </div>' +
            '                <div class="modal-footer">' +
            '                <div class="text-center" >' +
            '                    <button type="button" class="btn btn-info ok" data-dismiss="modal" >'+ I18n.system_ok +'</button>' +
            '                </div>' +
            '            </div>' +
            '        </div>' +
            '    </div>' +
            '</div>';
        return html;
    },
    show:function(msg, callback){
        // dom init
        if ($('#ComAlertTec').length == 0){
            $('body').append(ComAlertTec.html());
        }
        // init com alert
        $('#ComAlertTec .alert').html(msg);
        $('#ComAlertTec').modal('show');
        $('#ComAlertTec .ok').click(function(){
            $('#ComAlertTec').modal('hide');
            if(typeof callback == 'function') {
                callback();
            }
        });
    }
};
xxl-job/xxl-job-admin/src/main/resources/static/js/login.1.js
New file
@@ -0,0 +1,66 @@
$(function(){
    // input iCheck
    $('input').iCheck({
      checkboxClass: 'icheckbox_square-blue',
      radioClass: 'iradio_square-blue',
      increaseArea: '20%' // optional
    });
    // login Form Valid
    var loginFormValid = $("#loginForm").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            userName : {
                required : true ,
                minlength: 4,
                maxlength: 18
            },
            password : {
                required : true ,
                minlength: 4,
                maxlength: 18
            }
        },
        messages : {
            userName : {
                required  : I18n.login_username_empty,
                minlength : I18n.login_username_lt_4
            },
            password : {
                required  : I18n.login_password_empty  ,
                minlength : I18n.login_password_lt_4
                /*,maxlength:"登录密码不应超过18位"*/
            }
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            $.post(base_url + "/login", $("#loginForm").serialize(), function(data, status) {
                if (data.code == "200") {
                    layer.msg( I18n.login_success );
                    setTimeout(function(){
                        window.location.href = base_url + "/";
                    }, 500);
                } else {
                    layer.open({
                        title: I18n.system_tips,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.login_fail ),
                        icon: '2'
                    });
                }
            });
        }
    });
});
xxl-job/xxl-job-admin/src/main/resources/static/js/user.index.1.js
New file
@@ -0,0 +1,328 @@
$(function() {
    // init date tables
    var userListTable = $("#user_list").dataTable({
        "deferRender": true,
        "processing" : true,
        "serverSide": true,
        "ajax": {
            url: base_url + "/user/pageList",
            type:"post",
            data : function ( d ) {
                var obj = {};
                obj.username = $('#username').val();
                obj.role = $('#role').val();
                obj.start = d.start;
                obj.length = d.length;
                return obj;
            }
        },
        "searching": false,
        "ordering": false,
        //"scrollX": true,    // scroll x,close self-adaption
        "columns": [
                    {
                        "data": 'id',
                        "visible" : false,
                        "width":'10%'
                    },
                    {
                        "data": 'username',
                        "visible" : true,
                        "width":'20%'
                    },
                    {
                        "data": 'password',
                        "visible" : false,
                        "width":'20%',
                        "render": function ( data, type, row ) {
                            return '*********';
                        }
                    },
                    {
                        "data": 'role',
                        "visible" : true,
                        "width":'10%',
                        "render": function ( data, type, row ) {
                            if (data == 1) {
                                return I18n.user_role_admin
                            } else {
                                return I18n.user_role_normal
                            }
                        }
                    },
                    {
                        "data": 'permission',
                        "width":'10%',
                        "visible" : false
                    },
                    {
                        "data": I18n.system_opt ,
                        "width":'15%',
                        "render": function ( data, type, row ) {
                            return function(){
                                // html
                                tableData['key'+row.id] = row;
                                var html = '<p id="'+ row.id +'" >'+
                                    '<button class="btn btn-warning btn-xs update" type="button">'+ I18n.system_opt_edit +'</button>  '+
                                    '<button class="btn btn-danger btn-xs delete" type="button">'+ I18n.system_opt_del +'</button>  '+
                                    '</p>';
                                return html;
                            };
                        }
                    }
                ],
        "language" : {
            "sProcessing" : I18n.dataTable_sProcessing ,
            "sLengthMenu" : I18n.dataTable_sLengthMenu ,
            "sZeroRecords" : I18n.dataTable_sZeroRecords ,
            "sInfo" : I18n.dataTable_sInfo ,
            "sInfoEmpty" : I18n.dataTable_sInfoEmpty ,
            "sInfoFiltered" : I18n.dataTable_sInfoFiltered ,
            "sInfoPostFix" : "",
            "sSearch" : I18n.dataTable_sSearch ,
            "sUrl" : "",
            "sEmptyTable" : I18n.dataTable_sEmptyTable ,
            "sLoadingRecords" : I18n.dataTable_sLoadingRecords ,
            "sInfoThousands" : ",",
            "oPaginate" : {
                "sFirst" : I18n.dataTable_sFirst ,
                "sPrevious" : I18n.dataTable_sPrevious ,
                "sNext" : I18n.dataTable_sNext ,
                "sLast" : I18n.dataTable_sLast
            },
            "oAria" : {
                "sSortAscending" : I18n.dataTable_sSortAscending ,
                "sSortDescending" : I18n.dataTable_sSortDescending
            }
        }
    });
    // table data
    var tableData = {};
    // search btn
    $('#searchBtn').on('click', function(){
        userListTable.fnDraw();
    });
    // job operate
    $("#user_list").on('click', '.delete',function() {
        var id = $(this).parent('p').attr("id");
        layer.confirm( I18n.system_ok + I18n.system_opt_del + '?', {
            icon: 3,
            title: I18n.system_tips ,
            btn: [ I18n.system_ok, I18n.system_cancel ]
        }, function(index){
            layer.close(index);
            $.ajax({
                type : 'POST',
                url : base_url + "/user/remove",
                data : {
                    "id" : id
                },
                dataType : "json",
                success : function(data){
                    if (data.code == 200) {
                        layer.msg( I18n.system_success );
                        userListTable.fnDraw(false);
                    } else {
                        layer.msg( data.msg || I18n.system_opt_del + I18n.system_fail );
                    }
                }
            });
        });
    });
    // add role
    $("#addModal .form input[name=role]").change(function () {
        var role = $(this).val();
        if (role == 1) {
            $("#addModal .form input[name=permission]").parents('.form-group').hide();
        } else {
            $("#addModal .form input[name=permission]").parents('.form-group').show();
        }
        $("#addModal .form input[name='permission']").prop("checked",false);
    });
    jQuery.validator.addMethod("myValid01", function(value, element) {
        var length = value.length;
        var valid = /^[a-z][a-z0-9]*$/;
        return this.optional(element) || valid.test(value);
    }, I18n.user_username_valid );
    // add
    $(".add").click(function(){
        $('#addModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var addModalValidate = $("#addModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        rules : {
            username : {
                required : true,
                rangelength:[4, 20],
                myValid01: true
            },
            password : {
                required : true,
                rangelength:[4, 20]
            }
        },
        messages : {
            username : {
                required : I18n.system_please_input + I18n.user_username,
                rangelength: I18n.system_lengh_limit + "[4-20]"
            },
            password : {
                required : I18n.system_please_input + I18n.user_password,
                rangelength: I18n.system_lengh_limit + "[4-20]"
            }
        },
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            var permissionArr = [];
            $("#addModal .form input[name=permission]:checked").each(function(){
                permissionArr.push($(this).val());
            });
            var paramData = {
                "username": $("#addModal .form input[name=username]").val(),
                "password": $("#addModal .form input[name=password]").val(),
                "role": $("#addModal .form input[name=role]:checked").val(),
                "permission": permissionArr.join(',')
            };
            $.post(base_url + "/user/add", paramData, function(data, status) {
                if (data.code == "200") {
                    $('#addModal').modal('hide');
                    layer.msg( I18n.system_add_suc );
                    userListTable.fnDraw();
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_add_fail),
                        icon: '2'
                    });
                }
            });
        }
    });
    $("#addModal").on('hide.bs.modal', function () {
        $("#addModal .form")[0].reset();
        addModalValidate.resetForm();
        $("#addModal .form .form-group").removeClass("has-error");
        $(".remote_panel").show();    // remote
        $("#addModal .form input[name=permission]").parents('.form-group').show();
    });
    // update role
    $("#updateModal .form input[name=role]").change(function () {
        var role = $(this).val();
        if (role == 1) {
            $("#updateModal .form input[name=permission]").parents('.form-group').hide();
        } else {
            $("#updateModal .form input[name=permission]").parents('.form-group').show();
        }
        $("#updateModal .form input[name='permission']").prop("checked",false);
    });
    // update
    $("#user_list").on('click', '.update',function() {
        var id = $(this).parent('p').attr("id");
        var row = tableData['key'+id];
        // base data
        $("#updateModal .form input[name='id']").val( row.id );
        $("#updateModal .form input[name='username']").val( row.username );
        $("#updateModal .form input[name='password']").val( '' );
        $("#updateModal .form input[name='role'][value='"+ row.role +"']").click();
        var permissionArr = [];
        if (row.permission) {
            permissionArr = row.permission.split(",");
        }
        $("#updateModal .form input[name='permission']").each(function () {
            if($.inArray($(this).val(), permissionArr) > -1) {
                $(this).prop("checked",true);
            } else {
                $(this).prop("checked",false);
            }
        });
        // show
        $('#updateModal').modal({backdrop: false, keyboard: false}).modal('show');
    });
    var updateModalValidate = $("#updateModal .form").validate({
        errorElement : 'span',
        errorClass : 'help-block',
        focusInvalid : true,
        highlight : function(element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        success : function(label) {
            label.closest('.form-group').removeClass('has-error');
            label.remove();
        },
        errorPlacement : function(error, element) {
            element.parent('div').append(error);
        },
        submitHandler : function(form) {
            var permissionArr =[];
            $("#updateModal .form input[name=permission]:checked").each(function(){
                permissionArr.push($(this).val());
            });
            var paramData = {
                "id": $("#updateModal .form input[name=id]").val(),
                "username": $("#updateModal .form input[name=username]").val(),
                "password": $("#updateModal .form input[name=password]").val(),
                "role": $("#updateModal .form input[name=role]:checked").val(),
                "permission": permissionArr.join(',')
            };
            $.post(base_url + "/user/update", paramData, function(data, status) {
                if (data.code == "200") {
                    $('#updateModal').modal('hide');
                    layer.msg( I18n.system_update_suc );
                    userListTable.fnDraw();
                } else {
                    layer.open({
                        title: I18n.system_tips ,
                        btn: [ I18n.system_ok ],
                        content: (data.msg || I18n.system_update_fail),
                        icon: '2'
                    });
                }
            });
        }
    });
    $("#updateModal").on('hide.bs.modal', function () {
        $("#updateModal .form")[0].reset();
        updateModalValidate.resetForm();
        $("#updateModal .form .form-group").removeClass("has-error");
        $(".remote_panel").show();    // remote
        $("#updateModal .form input[name=permission]").parents('.form-group').show();
    });
});
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/anyword-hint.js
New file
@@ -0,0 +1,41 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    mod(require("../../lib/codemirror"));
  else if (typeof define == "function" && define.amd) // AMD
    define(["../../lib/codemirror"], mod);
  else // Plain browser env
    mod(CodeMirror);
})(function(CodeMirror) {
  "use strict";
  var WORD = /[\w$]+/, RANGE = 500;
  CodeMirror.registerHelper("hint", "anyword", function(editor, options) {
    var word = options && options.word || WORD;
    var range = options && options.range || RANGE;
    var cur = editor.getCursor(), curLine = editor.getLine(cur.line);
    var end = cur.ch, start = end;
    while (start && word.test(curLine.charAt(start - 1))) --start;
    var curWord = start != end && curLine.slice(start, end);
    var list = options && options.list || [], seen = {};
    var re = new RegExp(word.source, "g");
    for (var dir = -1; dir <= 1; dir += 2) {
      var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;
      for (; line != endLine; line += dir) {
        var text = editor.getLine(line), m;
        while (m = re.exec(text)) {
          if (line == cur.line && m[0] === curWord) continue;
          if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
            seen[m[0]] = true;
            list.push(m[0]);
          }
        }
      }
    }
    return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)};
  });
});
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.css
New file
@@ -0,0 +1,36 @@
.CodeMirror-hints {
  position: absolute;
  z-index: 10;
  overflow: hidden;
  list-style: none;
  margin: 0;
  padding: 2px;
  -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
  -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
  box-shadow: 2px 3px 5px rgba(0,0,0,.2);
  border-radius: 3px;
  border: 1px solid silver;
  background: white;
  font-size: 90%;
  font-family: monospace;
  max-height: 20em;
  overflow-y: auto;
}
.CodeMirror-hint {
  margin: 0;
  padding: 0 4px;
  border-radius: 2px;
  white-space: pre;
  color: black;
  cursor: pointer;
}
li.CodeMirror-hint-active {
  background: #08f;
  color: white;
}
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/addon/hint/show-hint.js
New file
@@ -0,0 +1,434 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    mod(require("../../lib/codemirror"));
  else if (typeof define == "function" && define.amd) // AMD
    define(["../../lib/codemirror"], mod);
  else // Plain browser env
    mod(CodeMirror);
})(function(CodeMirror) {
  "use strict";
  var HINT_ELEMENT_CLASS        = "CodeMirror-hint";
  var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
  // This is the old interface, kept around for now to stay
  // backwards-compatible.
  CodeMirror.showHint = function(cm, getHints, options) {
    if (!getHints) return cm.showHint(options);
    if (options && options.async) getHints.async = true;
    var newOpts = {hint: getHints};
    if (options) for (var prop in options) newOpts[prop] = options[prop];
    return cm.showHint(newOpts);
  };
  CodeMirror.defineExtension("showHint", function(options) {
    options = parseOptions(this, this.getCursor("start"), options);
    var selections = this.listSelections()
    if (selections.length > 1) return;
    // By default, don't allow completion when something is selected.
    // A hint function can have a `supportsSelection` property to
    // indicate that it can handle selections.
    if (this.somethingSelected()) {
      if (!options.hint.supportsSelection) return;
      // Don't try with cross-line selections
      for (var i = 0; i < selections.length; i++)
        if (selections[i].head.line != selections[i].anchor.line) return;
    }
    if (this.state.completionActive) this.state.completionActive.close();
    var completion = this.state.completionActive = new Completion(this, options);
    if (!completion.options.hint) return;
    CodeMirror.signal(this, "startCompletion", this);
    completion.update(true);
  });
  function Completion(cm, options) {
    this.cm = cm;
    this.options = options;
    this.widget = null;
    this.debounce = 0;
    this.tick = 0;
    this.startPos = this.cm.getCursor("start");
    this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
    var self = this;
    cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
  }
  var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
    return setTimeout(fn, 1000/60);
  };
  var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
  Completion.prototype = {
    close: function() {
      if (!this.active()) return;
      this.cm.state.completionActive = null;
      this.tick = null;
      this.cm.off("cursorActivity", this.activityFunc);
      if (this.widget && this.data) CodeMirror.signal(this.data, "close");
      if (this.widget) this.widget.close();
      CodeMirror.signal(this.cm, "endCompletion", this.cm);
    },
    active: function() {
      return this.cm.state.completionActive == this;
    },
    pick: function(data, i) {
      var completion = data.list[i];
      if (completion.hint) completion.hint(this.cm, data, completion);
      else this.cm.replaceRange(getText(completion), completion.from || data.from,
                                completion.to || data.to, "complete");
      CodeMirror.signal(data, "pick", completion);
      this.close();
    },
    cursorActivity: function() {
      if (this.debounce) {
        cancelAnimationFrame(this.debounce);
        this.debounce = 0;
      }
      var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
      if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
          pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
          (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
        this.close();
      } else {
        var self = this;
        this.debounce = requestAnimationFrame(function() {self.update();});
        if (this.widget) this.widget.disable();
      }
    },
    update: function(first) {
      if (this.tick == null) return
      var self = this, myTick = ++this.tick
      fetchHints(this.options.hint, this.cm, this.options, function(data) {
        if (self.tick == myTick) self.finishUpdate(data, first)
      })
    },
    finishUpdate: function(data, first) {
      if (this.data) CodeMirror.signal(this.data, "update");
      var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
      if (this.widget) this.widget.close();
      this.data = data;
      if (data && data.list.length) {
        if (picked && data.list.length == 1) {
          this.pick(data, 0);
        } else {
          this.widget = new Widget(this, data);
          CodeMirror.signal(data, "shown");
        }
      }
    }
  };
  function parseOptions(cm, pos, options) {
    var editor = cm.options.hintOptions;
    var out = {};
    for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
    if (editor) for (var prop in editor)
      if (editor[prop] !== undefined) out[prop] = editor[prop];
    if (options) for (var prop in options)
      if (options[prop] !== undefined) out[prop] = options[prop];
    if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
    return out;
  }
  function getText(completion) {
    if (typeof completion == "string") return completion;
    else return completion.text;
  }
  function buildKeyMap(completion, handle) {
    var baseMap = {
      Up: function() {handle.moveFocus(-1);},
      Down: function() {handle.moveFocus(1);},
      PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
      PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
      Home: function() {handle.setFocus(0);},
      End: function() {handle.setFocus(handle.length - 1);},
      Enter: handle.pick,
      Tab: handle.pick,
      Esc: handle.close
    };
    var custom = completion.options.customKeys;
    var ourMap = custom ? {} : baseMap;
    function addBinding(key, val) {
      var bound;
      if (typeof val != "string")
        bound = function(cm) { return val(cm, handle); };
      // This mechanism is deprecated
      else if (baseMap.hasOwnProperty(val))
        bound = baseMap[val];
      else
        bound = val;
      ourMap[key] = bound;
    }
    if (custom)
      for (var key in custom) if (custom.hasOwnProperty(key))
        addBinding(key, custom[key]);
    var extra = completion.options.extraKeys;
    if (extra)
      for (var key in extra) if (extra.hasOwnProperty(key))
        addBinding(key, extra[key]);
    return ourMap;
  }
  function getHintElement(hintsElement, el) {
    while (el && el != hintsElement) {
      if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
      el = el.parentNode;
    }
  }
  function Widget(completion, data) {
    this.completion = completion;
    this.data = data;
    this.picked = false;
    var widget = this, cm = completion.cm;
    var hints = this.hints = document.createElement("ul");
    var theme = completion.cm.options.theme;
    hints.className = "CodeMirror-hints " + theme;
    this.selectedHint = data.selectedHint || 0;
    var completions = data.list;
    for (var i = 0; i < completions.length; ++i) {
      var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
      var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
      if (cur.className != null) className = cur.className + " " + className;
      elt.className = className;
      if (cur.render) cur.render(elt, data, cur);
      else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
      elt.hintId = i;
    }
    var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
    var left = pos.left, top = pos.bottom, below = true;
    hints.style.left = left + "px";
    hints.style.top = top + "px";
    // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
    var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
    var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
    (completion.options.container || document.body).appendChild(hints);
    var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
    var scrolls = hints.scrollHeight > hints.clientHeight + 1
    var startScroll = cm.getScrollInfo();
    if (overlapY > 0) {
      var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
      if (curTop - height > 0) { // Fits above cursor
        hints.style.top = (top = pos.top - height) + "px";
        below = false;
      } else if (height > winH) {
        hints.style.height = (winH - 5) + "px";
        hints.style.top = (top = pos.bottom - box.top) + "px";
        var cursor = cm.getCursor();
        if (data.from.ch != cursor.ch) {
          pos = cm.cursorCoords(cursor);
          hints.style.left = (left = pos.left) + "px";
          box = hints.getBoundingClientRect();
        }
      }
    }
    var overlapX = box.right - winW;
    if (overlapX > 0) {
      if (box.right - box.left > winW) {
        hints.style.width = (winW - 5) + "px";
        overlapX -= (box.right - box.left) - winW;
      }
      hints.style.left = (left = pos.left - overlapX) + "px";
    }
    if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
      node.style.paddingRight = cm.display.nativeBarWidth + "px"
    cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
      moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
      setFocus: function(n) { widget.changeActive(n); },
      menuSize: function() { return widget.screenAmount(); },
      length: completions.length,
      close: function() { completion.close(); },
      pick: function() { widget.pick(); },
      data: data
    }));
    if (completion.options.closeOnUnfocus) {
      var closingOnBlur;
      cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
      cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
    }
    cm.on("scroll", this.onScroll = function() {
      var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
      var newTop = top + startScroll.top - curScroll.top;
      var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
      if (!below) point += hints.offsetHeight;
      if (point <= editor.top || point >= editor.bottom) return completion.close();
      hints.style.top = newTop + "px";
      hints.style.left = (left + startScroll.left - curScroll.left) + "px";
    });
    CodeMirror.on(hints, "dblclick", function(e) {
      var t = getHintElement(hints, e.target || e.srcElement);
      if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
    });
    CodeMirror.on(hints, "click", function(e) {
      var t = getHintElement(hints, e.target || e.srcElement);
      if (t && t.hintId != null) {
        widget.changeActive(t.hintId);
        if (completion.options.completeOnSingleClick) widget.pick();
      }
    });
    CodeMirror.on(hints, "mousedown", function() {
      setTimeout(function(){cm.focus();}, 20);
    });
    CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
    return true;
  }
  Widget.prototype = {
    close: function() {
      if (this.completion.widget != this) return;
      this.completion.widget = null;
      this.hints.parentNode.removeChild(this.hints);
      this.completion.cm.removeKeyMap(this.keyMap);
      var cm = this.completion.cm;
      if (this.completion.options.closeOnUnfocus) {
        cm.off("blur", this.onBlur);
        cm.off("focus", this.onFocus);
      }
      cm.off("scroll", this.onScroll);
    },
    disable: function() {
      this.completion.cm.removeKeyMap(this.keyMap);
      var widget = this;
      this.keyMap = {Enter: function() { widget.picked = true; }};
      this.completion.cm.addKeyMap(this.keyMap);
    },
    pick: function() {
      this.completion.pick(this.data, this.selectedHint);
    },
    changeActive: function(i, avoidWrap) {
      if (i >= this.data.list.length)
        i = avoidWrap ? this.data.list.length - 1 : 0;
      else if (i < 0)
        i = avoidWrap ? 0  : this.data.list.length - 1;
      if (this.selectedHint == i) return;
      var node = this.hints.childNodes[this.selectedHint];
      if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
      node = this.hints.childNodes[this.selectedHint = i];
      node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
      if (node.offsetTop < this.hints.scrollTop)
        this.hints.scrollTop = node.offsetTop - 3;
      else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
        this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
      CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
    },
    screenAmount: function() {
      return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
    }
  };
  function applicableHelpers(cm, helpers) {
    if (!cm.somethingSelected()) return helpers
    var result = []
    for (var i = 0; i < helpers.length; i++)
      if (helpers[i].supportsSelection) result.push(helpers[i])
    return result
  }
  function fetchHints(hint, cm, options, callback) {
    if (hint.async) {
      hint(cm, callback, options)
    } else {
      var result = hint(cm, options)
      if (result && result.then) result.then(callback)
      else callback(result)
    }
  }
  function resolveAutoHints(cm, pos) {
    var helpers = cm.getHelpers(pos, "hint"), words
    if (helpers.length) {
      var resolved = function(cm, callback, options) {
        var app = applicableHelpers(cm, helpers);
        function run(i) {
          if (i == app.length) return callback(null)
          fetchHints(app[i], cm, options, function(result) {
            if (result && result.list.length > 0) callback(result)
            else run(i + 1)
          })
        }
        run(0)
      }
      resolved.async = true
      resolved.supportsSelection = true
      return resolved
    } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
      return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
    } else if (CodeMirror.hint.anyword) {
      return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
    } else {
      return function() {}
    }
  }
  CodeMirror.registerHelper("hint", "auto", {
    resolve: resolveAutoHints
  });
  CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
    var cur = cm.getCursor(), token = cm.getTokenAt(cur)
    var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
    if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
      term = token.string.substr(0, cur.ch - token.start)
    } else {
      term = ""
      from = cur
    }
    var found = [];
    for (var i = 0; i < options.words.length; i++) {
      var word = options.words[i];
      if (word.slice(0, term.length) == term)
        found.push(word);
    }
    if (found.length) return {list: found, from: from, to: to};
  });
  CodeMirror.commands.autocomplete = CodeMirror.showHint;
  var defaultOptions = {
    hint: CodeMirror.hint.auto,
    completeSingle: true,
    alignWithWord: true,
    closeCharacters: /[\s()\[\]{};:>,]/,
    closeOnUnfocus: true,
    completeOnSingleClick: true,
    container: null,
    customKeys: null,
    extraKeys: null
  };
  CodeMirror.defineOption("hintOptions", null);
});
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.css
New file
@@ -0,0 +1,346 @@
/* BASICS */
.CodeMirror {
  /* Set height, width, borders, and global font properties here */
  font-family: monospace;
  height: 300px;
  color: black;
  direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
  padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
  padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
  background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
  border-right: 1px solid #ddd;
  background-color: #f7f7f7;
  white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
  padding: 0 3px 0 5px;
  min-width: 20px;
  text-align: right;
  color: #999;
  white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
  border-left: 1px solid black;
  border-right: none;
  width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
  border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
  width: auto;
  border: 0 !important;
  background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
  z-index: 1;
}
.cm-fat-cursor-mark {
  background-color: rgba(20, 255, 20, 0.5);
  -webkit-animation: blink 1.06s steps(1) infinite;
  -moz-animation: blink 1.06s steps(1) infinite;
  animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
  width: auto;
  border: 0;
  -webkit-animation: blink 1.06s steps(1) infinite;
  -moz-animation: blink 1.06s steps(1) infinite;
  animation: blink 1.06s steps(1) infinite;
  background-color: #7e7;
}
@-moz-keyframes blink {
  0% {}
  50% { background-color: transparent; }
  100% {}
}
@-webkit-keyframes blink {
  0% {}
  50% { background-color: transparent; }
  100% {}
}
@keyframes blink {
  0% {}
  50% { background-color: transparent; }
  100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
  position: absolute;
  left: 0; right: 0; top: -50px; bottom: -20px;
  overflow: hidden;
}
.CodeMirror-ruler {
  border-left: 1px solid #ccc;
  top: 0; bottom: 0;
  position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
   the editor. You probably shouldn't touch them. */
.CodeMirror {
  position: relative;
  overflow: hidden;
  background: white;
}
.CodeMirror-scroll {
  overflow: scroll !important; /* Things will break if this is overridden */
  /* 30px is the magic margin used to hide the element's real scrollbars */
  /* See overflow: hidden in .CodeMirror */
  margin-bottom: -30px; margin-right: -30px;
  padding-bottom: 30px;
  height: 100%;
  outline: none; /* Prevent dragging from highlighting the element */
  position: relative;
}
.CodeMirror-sizer {
  position: relative;
  border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
   before actual scrolling happens, thus preventing shaking and
   flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
  position: absolute;
  z-index: 6;
  display: none;
}
.CodeMirror-vscrollbar {
  right: 0; top: 0;
  overflow-x: hidden;
  overflow-y: scroll;
}
.CodeMirror-hscrollbar {
  bottom: 0; left: 0;
  overflow-y: hidden;
  overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
  right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
  left: 0; bottom: 0;
}
.CodeMirror-gutters {
  position: absolute; left: 0; top: 0;
  min-height: 100%;
  z-index: 3;
}
.CodeMirror-gutter {
  white-space: normal;
  height: 100%;
  display: inline-block;
  vertical-align: top;
  margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
  position: absolute;
  z-index: 4;
  background: none !important;
  border: none !important;
}
.CodeMirror-gutter-background {
  position: absolute;
  top: 0; bottom: 0;
  z-index: 4;
}
.CodeMirror-gutter-elt {
  position: absolute;
  cursor: default;
  z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
  cursor: text;
  min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
  /* Reset some styles that the rest of the page might have set */
  -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
  border-width: 0;
  background: transparent;
  font-family: inherit;
  font-size: inherit;
  margin: 0;
  white-space: pre;
  word-wrap: normal;
  line-height: inherit;
  color: inherit;
  z-index: 2;
  position: relative;
  overflow: visible;
  -webkit-tap-highlight-color: transparent;
  -webkit-font-variant-ligatures: contextual;
  font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
  word-wrap: break-word;
  white-space: pre-wrap;
  word-break: normal;
}
.CodeMirror-linebackground {
  position: absolute;
  left: 0; right: 0; top: 0; bottom: 0;
  z-index: 0;
}
.CodeMirror-linewidget {
  position: relative;
  z-index: 2;
  padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
  outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
  -moz-box-sizing: content-box;
  box-sizing: content-box;
}
.CodeMirror-measure {
  position: absolute;
  width: 100%;
  height: 0;
  overflow: hidden;
  visibility: hidden;
}
.CodeMirror-cursor {
  position: absolute;
  pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
  visibility: hidden;
  position: relative;
  z-index: 3;
}
div.CodeMirror-dragcursors {
  visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
  visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
  background-color: #ffa;
  background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
  /* Hide the cursor when printing */
  .CodeMirror div.CodeMirror-cursors {
    visibility: hidden;
  }
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/lib/codemirror.js
New file
Diff too large
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/clike/clike.js
New file
@@ -0,0 +1,879 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    mod(require("../../lib/codemirror"));
  else if (typeof define == "function" && define.amd) // AMD
    define(["../../lib/codemirror"], mod);
  else // Plain browser env
    mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
function Context(indented, column, type, info, align, prev) {
  this.indented = indented;
  this.column = column;
  this.type = type;
  this.info = info;
  this.align = align;
  this.prev = prev;
}
function pushContext(state, col, type, info) {
  var indent = state.indented;
  if (state.context && state.context.type == "statement" && type != "statement")
    indent = state.context.indented;
  return state.context = new Context(indent, col, type, info, null, state.context);
}
function popContext(state) {
  var t = state.context.type;
  if (t == ")" || t == "]" || t == "}")
    state.indented = state.context.indented;
  return state.context = state.context.prev;
}
function typeBefore(stream, state, pos) {
  if (state.prevToken == "variable" || state.prevToken == "type") return true;
  if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true;
  if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true;
}
function isTopScope(context) {
  for (;;) {
    if (!context || context.type == "top") return true;
    if (context.type == "}" && context.prev.info != "namespace") return false;
    context = context.prev;
  }
}
CodeMirror.defineMode("clike", function(config, parserConfig) {
  var indentUnit = config.indentUnit,
      statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
      dontAlignCalls = parserConfig.dontAlignCalls,
      keywords = parserConfig.keywords || {},
      types = parserConfig.types || {},
      builtin = parserConfig.builtin || {},
      blockKeywords = parserConfig.blockKeywords || {},
      defKeywords = parserConfig.defKeywords || {},
      atoms = parserConfig.atoms || {},
      hooks = parserConfig.hooks || {},
      multiLineStrings = parserConfig.multiLineStrings,
      indentStatements = parserConfig.indentStatements !== false,
      indentSwitch = parserConfig.indentSwitch !== false,
      namespaceSeparator = parserConfig.namespaceSeparator,
      isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/,
      numberStart = parserConfig.numberStart || /[\d\.]/,
      number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,
      isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/,
      isIdentifierChar = parserConfig.isIdentifierChar || /[\w\$_\xa1-\uffff]/,
      // An optional function that takes a {string} token and returns true if it
      // should be treated as a builtin.
      isReservedIdentifier = parserConfig.isReservedIdentifier || false;
  var curPunc, isDefKeyword;
  function tokenBase(stream, state) {
    var ch = stream.next();
    if (hooks[ch]) {
      var result = hooks[ch](stream, state);
      if (result !== false) return result;
    }
    if (ch == '"' || ch == "'") {
      state.tokenize = tokenString(ch);
      return state.tokenize(stream, state);
    }
    if (isPunctuationChar.test(ch)) {
      curPunc = ch;
      return null;
    }
    if (numberStart.test(ch)) {
      stream.backUp(1)
      if (stream.match(number)) return "number"
      stream.next()
    }
    if (ch == "/") {
      if (stream.eat("*")) {
        state.tokenize = tokenComment;
        return tokenComment(stream, state);
      }
      if (stream.eat("/")) {
        stream.skipToEnd();
        return "comment";
      }
    }
    if (isOperatorChar.test(ch)) {
      while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {}
      return "operator";
    }
    stream.eatWhile(isIdentifierChar);
    if (namespaceSeparator) while (stream.match(namespaceSeparator))
      stream.eatWhile(isIdentifierChar);
    var cur = stream.current();
    if (contains(keywords, cur)) {
      if (contains(blockKeywords, cur)) curPunc = "newstatement";
      if (contains(defKeywords, cur)) isDefKeyword = true;
      return "keyword";
    }
    if (contains(types, cur)) return "type";
    if (contains(builtin, cur)
        || (isReservedIdentifier && isReservedIdentifier(cur))) {
      if (contains(blockKeywords, cur)) curPunc = "newstatement";
      return "builtin";
    }
    if (contains(atoms, cur)) return "atom";
    return "variable";
  }
  function tokenString(quote) {
    return function(stream, state) {
      var escaped = false, next, end = false;
      while ((next = stream.next()) != null) {
        if (next == quote && !escaped) {end = true; break;}
        escaped = !escaped && next == "\\";
      }
      if (end || !(escaped || multiLineStrings))
        state.tokenize = null;
      return "string";
    };
  }
  function tokenComment(stream, state) {
    var maybeEnd = false, ch;
    while (ch = stream.next()) {
      if (ch == "/" && maybeEnd) {
        state.tokenize = null;
        break;
      }
      maybeEnd = (ch == "*");
    }
    return "comment";
  }
  function maybeEOL(stream, state) {
    if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context))
      state.typeAtEndOfLine = typeBefore(stream, state, stream.pos)
  }
  // Interface
  return {
    startState: function(basecolumn) {
      return {
        tokenize: null,
        context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false),
        indented: 0,
        startOfLine: true,
        prevToken: null
      };
    },
    token: function(stream, state) {
      var ctx = state.context;
      if (stream.sol()) {
        if (ctx.align == null) ctx.align = false;
        state.indented = stream.indentation();
        state.startOfLine = true;
      }
      if (stream.eatSpace()) { maybeEOL(stream, state); return null; }
      curPunc = isDefKeyword = null;
      var style = (state.tokenize || tokenBase)(stream, state);
      if (style == "comment" || style == "meta") return style;
      if (ctx.align == null) ctx.align = true;
      if (curPunc == ";" || curPunc == ":" || (curPunc == "," && stream.match(/^\s*(?:\/\/.*)?$/, false)))
        while (state.context.type == "statement") popContext(state);
      else if (curPunc == "{") pushContext(state, stream.column(), "}");
      else if (curPunc == "[") pushContext(state, stream.column(), "]");
      else if (curPunc == "(") pushContext(state, stream.column(), ")");
      else if (curPunc == "}") {
        while (ctx.type == "statement") ctx = popContext(state);
        if (ctx.type == "}") ctx = popContext(state);
        while (ctx.type == "statement") ctx = popContext(state);
      }
      else if (curPunc == ctx.type) popContext(state);
      else if (indentStatements &&
               (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") ||
                (ctx.type == "statement" && curPunc == "newstatement"))) {
        pushContext(state, stream.column(), "statement", stream.current());
      }
      if (style == "variable" &&
          ((state.prevToken == "def" ||
            (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) &&
             isTopScope(state.context) && stream.match(/^\s*\(/, false)))))
        style = "def";
      if (hooks.token) {
        var result = hooks.token(stream, state, style);
        if (result !== undefined) style = result;
      }
      if (style == "def" && parserConfig.styleDefs === false) style = "variable";
      state.startOfLine = false;
      state.prevToken = isDefKeyword ? "def" : style || curPunc;
      maybeEOL(stream, state);
      return style;
    },
    indent: function(state, textAfter) {
      if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
      var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
      var closing = firstChar == ctx.type;
      if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
      if (parserConfig.dontIndentStatements)
        while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
          ctx = ctx.prev
      if (hooks.indent) {
        var hook = hooks.indent(state, ctx, textAfter, indentUnit);
        if (typeof hook == "number") return hook
      }
      var switchBlock = ctx.prev && ctx.prev.info == "switch";
      if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
        while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
        return ctx.indented
      }
      if (ctx.type == "statement")
        return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
      if (ctx.align && (!dontAlignCalls || ctx.type != ")"))
        return ctx.column + (closing ? 0 : 1);
      if (ctx.type == ")" && !closing)
        return ctx.indented + statementIndentUnit;
      return ctx.indented + (closing ? 0 : indentUnit) +
        (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0);
    },
    electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/,
    blockCommentStart: "/*",
    blockCommentEnd: "*/",
    blockCommentContinue: " * ",
    lineComment: "//",
    fold: "brace"
  };
});
  function words(str) {
    var obj = {}, words = str.split(" ");
    for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
    return obj;
  }
  function contains(words, word) {
    if (typeof words === "function") {
      return words(word);
    } else {
      return words.propertyIsEnumerable(word);
    }
  }
  var cKeywords = "auto if break case register continue return default do sizeof " +
    "static else struct switch extern typedef union for goto while enum const " +
    "volatile inline restrict asm fortran";
  // Do not use this. Use the cTypes function below. This is global just to avoid
  // excessive calls when cTypes is being called multiple times during a parse.
  var basicCTypes = words("int long char short double float unsigned signed " +
    "void bool");
  // Do not use this. Use the objCTypes function below. This is global just to avoid
  // excessive calls when objCTypes is being called multiple times during a parse.
  var basicObjCTypes = words("SEL instancetype id Class Protocol BOOL");
  // Returns true if identifier is a "C" type.
  // C type is defined as those that are reserved by the compiler (basicTypes),
  // and those that end in _t (Reserved by POSIX for types)
  // http://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html
  function cTypes(identifier) {
    return contains(basicCTypes, identifier) || /.+_t/.test(identifier);
  }
  // Returns true if identifier is a "Objective C" type.
  function objCTypes(identifier) {
    return cTypes(identifier) || contains(basicObjCTypes, identifier);
  }
  var cBlockKeywords = "case do else for if switch while struct enum union";
  var cDefKeywords = "struct enum union";
  function cppHook(stream, state) {
    if (!state.startOfLine) return false
    for (var ch, next = null; ch = stream.peek();) {
      if (ch == "\\" && stream.match(/^.$/)) {
        next = cppHook
        break
      } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) {
        break
      }
      stream.next()
    }
    state.tokenize = next
    return "meta"
  }
  function pointerHook(_stream, state) {
    if (state.prevToken == "type") return "type";
    return false;
  }
  // For C and C++ (and ObjC): identifiers starting with __
  // or _ followed by a capital letter are reserved for the compiler.
  function cIsReservedIdentifier(token) {
    if (!token || token.length < 2) return false;
    if (token[0] != '_') return false;
    return (token[1] == '_') || (token[1] !== token[1].toLowerCase());
  }
  function cpp14Literal(stream) {
    stream.eatWhile(/[\w\.']/);
    return "number";
  }
  function cpp11StringHook(stream, state) {
    stream.backUp(1);
    // Raw strings.
    if (stream.match(/(R|u8R|uR|UR|LR)/)) {
      var match = stream.match(/"([^\s\\()]{0,16})\(/);
      if (!match) {
        return false;
      }
      state.cpp11RawStringDelim = match[1];
      state.tokenize = tokenRawString;
      return tokenRawString(stream, state);
    }
    // Unicode strings/chars.
    if (stream.match(/(u8|u|U|L)/)) {
      if (stream.match(/["']/, /* eat */ false)) {
        return "string";
      }
      return false;
    }
    // Ignore this hook.
    stream.next();
    return false;
  }
  function cppLooksLikeConstructor(word) {
    var lastTwo = /(\w+)::~?(\w+)$/.exec(word);
    return lastTwo && lastTwo[1] == lastTwo[2];
  }
  // C#-style strings where "" escapes a quote.
  function tokenAtString(stream, state) {
    var next;
    while ((next = stream.next()) != null) {
      if (next == '"' && !stream.eat('"')) {
        state.tokenize = null;
        break;
      }
    }
    return "string";
  }
  // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where
  // <delim> can be a string up to 16 characters long.
  function tokenRawString(stream, state) {
    // Escape characters that have special regex meanings.
    var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&');
    var match = stream.match(new RegExp(".*?\\)" + delim + '"'));
    if (match)
      state.tokenize = null;
    else
      stream.skipToEnd();
    return "string";
  }
  function def(mimes, mode) {
    if (typeof mimes == "string") mimes = [mimes];
    var words = [];
    function add(obj) {
      if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
        words.push(prop);
    }
    add(mode.keywords);
    add(mode.types);
    add(mode.builtin);
    add(mode.atoms);
    if (words.length) {
      mode.helperType = mimes[0];
      CodeMirror.registerHelper("hintWords", mimes[0], words);
    }
    for (var i = 0; i < mimes.length; ++i)
      CodeMirror.defineMIME(mimes[i], mode);
  }
  def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
    name: "clike",
    keywords: words(cKeywords),
    types: cTypes,
    blockKeywords: words(cBlockKeywords),
    defKeywords: words(cDefKeywords),
    typeFirstDefinitions: true,
    atoms: words("NULL true false"),
    isReservedIdentifier: cIsReservedIdentifier,
    hooks: {
      "#": cppHook,
      "*": pointerHook,
    },
    modeProps: {fold: ["brace", "include"]}
  });
  def(["text/x-c++src", "text/x-c++hdr"], {
    name: "clike",
    keywords: words(cKeywords + " dynamic_cast namespace reinterpret_cast try explicit new " +
                    "static_cast typeid catch operator template typename class friend private " +
                    "this using const_cast public throw virtual delete mutable protected " +
                    "alignas alignof constexpr decltype nullptr noexcept thread_local final " +
                    "static_assert override"),
    types: cTypes,
    blockKeywords: words(cBlockKeywords +" class try catch finally"),
    defKeywords: words(cDefKeywords + " class namespace"),
    typeFirstDefinitions: true,
    atoms: words("true false NULL"),
    dontIndentStatements: /^template$/,
    isIdentifierChar: /[\w\$_~\xa1-\uffff]/,
    isReservedIdentifier: cIsReservedIdentifier,
    hooks: {
      "#": cppHook,
      "*": pointerHook,
      "u": cpp11StringHook,
      "U": cpp11StringHook,
      "L": cpp11StringHook,
      "R": cpp11StringHook,
      "0": cpp14Literal,
      "1": cpp14Literal,
      "2": cpp14Literal,
      "3": cpp14Literal,
      "4": cpp14Literal,
      "5": cpp14Literal,
      "6": cpp14Literal,
      "7": cpp14Literal,
      "8": cpp14Literal,
      "9": cpp14Literal,
      token: function(stream, state, style) {
        if (style == "variable" && stream.peek() == "(" &&
            (state.prevToken == ";" || state.prevToken == null ||
             state.prevToken == "}") &&
            cppLooksLikeConstructor(stream.current()))
          return "def";
      }
    },
    namespaceSeparator: "::",
    modeProps: {fold: ["brace", "include"]}
  });
  def("text/x-java", {
    name: "clike",
    keywords: words("abstract assert break case catch class const continue default " +
                    "do else enum extends final finally float for goto if implements import " +
                    "instanceof interface native new package private protected public " +
                    "return static strictfp super switch synchronized this throw throws transient " +
                    "try volatile while @interface"),
    types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " +
                 "Integer Long Number Object Short String StringBuffer StringBuilder Void"),
    blockKeywords: words("catch class do else finally for if switch try while"),
    defKeywords: words("class interface enum @interface"),
    typeFirstDefinitions: true,
    atoms: words("true false null"),
    number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
    hooks: {
      "@": function(stream) {
        // Don't match the @interface keyword.
        if (stream.match('interface', false)) return false;
        stream.eatWhile(/[\w\$_]/);
        return "meta";
      }
    },
    modeProps: {fold: ["brace", "import"]}
  });
  def("text/x-csharp", {
    name: "clike",
    keywords: words("abstract as async await base break case catch checked class const continue" +
                    " default delegate do else enum event explicit extern finally fixed for" +
                    " foreach goto if implicit in interface internal is lock namespace new" +
                    " operator out override params private protected public readonly ref return sealed" +
                    " sizeof stackalloc static struct switch this throw try typeof unchecked" +
                    " unsafe using virtual void volatile while add alias ascending descending dynamic from get" +
                    " global group into join let orderby partial remove select set value var yield"),
    types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" +
                 " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" +
                 " UInt64 bool byte char decimal double short int long object"  +
                 " sbyte float string ushort uint ulong"),
    blockKeywords: words("catch class do else finally for foreach if struct switch try while"),
    defKeywords: words("class interface namespace struct var"),
    typeFirstDefinitions: true,
    atoms: words("true false null"),
    hooks: {
      "@": function(stream, state) {
        if (stream.eat('"')) {
          state.tokenize = tokenAtString;
          return tokenAtString(stream, state);
        }
        stream.eatWhile(/[\w\$_]/);
        return "meta";
      }
    }
  });
  function tokenTripleString(stream, state) {
    var escaped = false;
    while (!stream.eol()) {
      if (!escaped && stream.match('"""')) {
        state.tokenize = null;
        break;
      }
      escaped = stream.next() == "\\" && !escaped;
    }
    return "string";
  }
  function tokenNestedComment(depth) {
    return function (stream, state) {
      var ch
      while (ch = stream.next()) {
        if (ch == "*" && stream.eat("/")) {
          if (depth == 1) {
            state.tokenize = null
            break
          } else {
            state.tokenize = tokenNestedComment(depth - 1)
            return state.tokenize(stream, state)
          }
        } else if (ch == "/" && stream.eat("*")) {
          state.tokenize = tokenNestedComment(depth + 1)
          return state.tokenize(stream, state)
        }
      }
      return "comment"
    }
  }
  def("text/x-scala", {
    name: "clike",
    keywords: words(
      /* scala */
      "abstract case catch class def do else extends final finally for forSome if " +
      "implicit import lazy match new null object override package private protected return " +
      "sealed super this throw trait try type val var while with yield _ " +
      /* package scala */
      "assert assume require print println printf readLine readBoolean readByte readShort " +
      "readChar readInt readLong readFloat readDouble"
    ),
    types: words(
      "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " +
      "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " +
      "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " +
      "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " +
      "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " +
      /* package java.lang */
      "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
      "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
      "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
      "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
    ),
    multiLineStrings: true,
    blockKeywords: words("catch class enum do else finally for forSome if match switch try while"),
    defKeywords: words("class enum def object package trait type val var"),
    atoms: words("true false null"),
    indentStatements: false,
    indentSwitch: false,
    isOperatorChar: /[+\-*&%=<>!?|\/#:@]/,
    hooks: {
      "@": function(stream) {
        stream.eatWhile(/[\w\$_]/);
        return "meta";
      },
      '"': function(stream, state) {
        if (!stream.match('""')) return false;
        state.tokenize = tokenTripleString;
        return state.tokenize(stream, state);
      },
      "'": function(stream) {
        stream.eatWhile(/[\w\$_\xa1-\uffff]/);
        return "atom";
      },
      "=": function(stream, state) {
        var cx = state.context
        if (cx.type == "}" && cx.align && stream.eat(">")) {
          state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev)
          return "operator"
        } else {
          return false
        }
      },
      "/": function(stream, state) {
        if (!stream.eat("*")) return false
        state.tokenize = tokenNestedComment(1)
        return state.tokenize(stream, state)
      }
    },
    modeProps: {closeBrackets: {pairs: '()[]{}""', triples: '"'}}
  });
  function tokenKotlinString(tripleString){
    return function (stream, state) {
      var escaped = false, next, end = false;
      while (!stream.eol()) {
        if (!tripleString && !escaped && stream.match('"') ) {end = true; break;}
        if (tripleString && stream.match('"""')) {end = true; break;}
        next = stream.next();
        if(!escaped && next == "$" && stream.match('{'))
          stream.skipTo("}");
        escaped = !escaped && next == "\\" && !tripleString;
      }
      if (end || !tripleString)
        state.tokenize = null;
      return "string";
    }
  }
  def("text/x-kotlin", {
    name: "clike",
    keywords: words(
      /*keywords*/
      "package as typealias class interface this super val operator " +
      "var fun for is in This throw return annotation " +
      "break continue object if else while do try when !in !is as? " +
      /*soft keywords*/
      "file import where by get set abstract enum open inner override private public internal " +
      "protected catch finally out final vararg reified dynamic companion constructor init " +
      "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
      "external annotation crossinline const operator infix suspend actual expect setparam"
    ),
    types: words(
      /* package java.lang */
      "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
      "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
      "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
      "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void Annotation Any BooleanArray " +
      "ByteArray Char CharArray DeprecationLevel DoubleArray Enum FloatArray Function Int IntArray Lazy " +
      "LazyThreadSafetyMode LongArray Nothing ShortArray Unit"
    ),
    intendSwitch: false,
    indentStatements: false,
    multiLineStrings: true,
    number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+(\.\d+)?|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
    blockKeywords: words("catch class do else finally for if where try while enum"),
    defKeywords: words("class val var object interface fun"),
    atoms: words("true false null this"),
    hooks: {
      "@": function(stream) {
        stream.eatWhile(/[\w\$_]/);
        return "meta";
      },
      '"': function(stream, state) {
        state.tokenize = tokenKotlinString(stream.match('""'));
        return state.tokenize(stream, state);
      },
      indent: function(state, ctx, textAfter, indentUnit) {
        var firstChar = textAfter && textAfter.charAt(0);
        if ((state.prevToken == "}" || state.prevToken == ")") && textAfter == "")
          return state.indented;
        if (state.prevToken == "operator" && textAfter != "}" ||
          state.prevToken == "variable" && firstChar == "." ||
          (state.prevToken == "}" || state.prevToken == ")") && firstChar == ".")
          return indentUnit * 2 + ctx.indented;
        if (ctx.align && ctx.type == "}")
          return ctx.indented + (state.context.type == (textAfter || "").charAt(0) ? 0 : indentUnit);
      }
    },
    modeProps: {closeBrackets: {triples: '"'}}
  });
  def(["x-shader/x-vertex", "x-shader/x-fragment"], {
    name: "clike",
    keywords: words("sampler1D sampler2D sampler3D samplerCube " +
                    "sampler1DShadow sampler2DShadow " +
                    "const attribute uniform varying " +
                    "break continue discard return " +
                    "for while do if else struct " +
                    "in out inout"),
    types: words("float int bool void " +
                 "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
                 "mat2 mat3 mat4"),
    blockKeywords: words("for while do if else struct"),
    builtin: words("radians degrees sin cos tan asin acos atan " +
                    "pow exp log exp2 sqrt inversesqrt " +
                    "abs sign floor ceil fract mod min max clamp mix step smoothstep " +
                    "length distance dot cross normalize ftransform faceforward " +
                    "reflect refract matrixCompMult " +
                    "lessThan lessThanEqual greaterThan greaterThanEqual " +
                    "equal notEqual any all not " +
                    "texture1D texture1DProj texture1DLod texture1DProjLod " +
                    "texture2D texture2DProj texture2DLod texture2DProjLod " +
                    "texture3D texture3DProj texture3DLod texture3DProjLod " +
                    "textureCube textureCubeLod " +
                    "shadow1D shadow2D shadow1DProj shadow2DProj " +
                    "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " +
                    "dFdx dFdy fwidth " +
                    "noise1 noise2 noise3 noise4"),
    atoms: words("true false " +
                "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " +
                "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " +
                "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " +
                "gl_FogCoord gl_PointCoord " +
                "gl_Position gl_PointSize gl_ClipVertex " +
                "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " +
                "gl_TexCoord gl_FogFragCoord " +
                "gl_FragCoord gl_FrontFacing " +
                "gl_FragData gl_FragDepth " +
                "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " +
                "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " +
                "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " +
                "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " +
                "gl_ProjectionMatrixInverseTranspose " +
                "gl_ModelViewProjectionMatrixInverseTranspose " +
                "gl_TextureMatrixInverseTranspose " +
                "gl_NormalScale gl_DepthRange gl_ClipPlane " +
                "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " +
                "gl_FrontLightModelProduct gl_BackLightModelProduct " +
                "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " +
                "gl_FogParameters " +
                "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " +
                "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " +
                "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
                "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
                "gl_MaxDrawBuffers"),
    indentSwitch: false,
    hooks: {"#": cppHook},
    modeProps: {fold: ["brace", "include"]}
  });
  def("text/x-nesc", {
    name: "clike",
    keywords: words(cKeywords + " as atomic async call command component components configuration event generic " +
                    "implementation includes interface module new norace nx_struct nx_union post provides " +
                    "signal task uses abstract extends"),
    types: cTypes,
    blockKeywords: words(cBlockKeywords),
    atoms: words("null true false"),
    hooks: {"#": cppHook},
    modeProps: {fold: ["brace", "include"]}
  });
  def("text/x-objectivec", {
    name: "clike",
    keywords: words(cKeywords + " bycopy byref in inout oneway out self super atomic nonatomic retain copy " +
                    "readwrite readonly strong weak assign typeof nullable nonnull null_resettable _cmd " +
                    "@interface @implementation @end @protocol @encode @property @synthesize @dynamic @class " +
                    "@public @package @private @protected @required @optional @try @catch @finally @import " +
                    "@selector @encode @defs @synchronized @autoreleasepool @compatibility_alias @available"),
    types: objCTypes,
    builtin: words("FOUNDATION_EXPORT FOUNDATION_EXTERN NS_INLINE NS_FORMAT_FUNCTION NS_RETURNS_RETAINED " +
                   "NS_ERROR_ENUM NS_RETURNS_NOT_RETAINED NS_RETURNS_INNER_POINTER NS_DESIGNATED_INITIALIZER " +
                   "NS_ENUM NS_OPTIONS NS_REQUIRES_NIL_TERMINATION NS_ASSUME_NONNULL_BEGIN " +
                   "NS_ASSUME_NONNULL_END NS_SWIFT_NAME NS_REFINED_FOR_SWIFT"),
    blockKeywords: words(cBlockKeywords + " @synthesize @try @catch @finally @autoreleasepool @synchronized"),
    defKeywords: words(cDefKeywords + " @interface @implementation @protocol @class"),
    dontIndentStatements: /^@.*$/,
    typeFirstDefinitions: true,
    atoms: words("YES NO NULL Nil nil true false nullptr"),
    isReservedIdentifier: cIsReservedIdentifier,
    hooks: {
      "#": cppHook,
      "*": pointerHook,
    },
    modeProps: {fold: ["brace", "include"]}
  });
  def("text/x-squirrel", {
    name: "clike",
    keywords: words("base break clone continue const default delete enum extends function in class" +
                    " foreach local resume return this throw typeof yield constructor instanceof static"),
    types: cTypes,
    blockKeywords: words("case catch class else for foreach if switch try while"),
    defKeywords: words("function local class"),
    typeFirstDefinitions: true,
    atoms: words("true false null"),
    hooks: {"#": cppHook},
    modeProps: {fold: ["brace", "include"]}
  });
  // Ceylon Strings need to deal with interpolation
  var stringTokenizer = null;
  function tokenCeylonString(type) {
    return function(stream, state) {
      var escaped = false, next, end = false;
      while (!stream.eol()) {
        if (!escaped && stream.match('"') &&
              (type == "single" || stream.match('""'))) {
          end = true;
          break;
        }
        if (!escaped && stream.match('``')) {
          stringTokenizer = tokenCeylonString(type);
          end = true;
          break;
        }
        next = stream.next();
        escaped = type == "single" && !escaped && next == "\\";
      }
      if (end)
          state.tokenize = null;
      return "string";
    }
  }
  def("text/x-ceylon", {
    name: "clike",
    keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" +
                    " exists extends finally for function given if import in interface is let module new" +
                    " nonempty object of out outer package return satisfies super switch then this throw" +
                    " try value void while"),
    types: function(word) {
        // In Ceylon all identifiers that start with an uppercase are types
        var first = word.charAt(0);
        return (first === first.toUpperCase() && first !== first.toLowerCase());
    },
    blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"),
    defKeywords: words("class dynamic function interface module object package value"),
    builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" +
                   " native optional sealed see serializable shared suppressWarnings tagged throws variable"),
    isPunctuationChar: /[\[\]{}\(\),;\:\.`]/,
    isOperatorChar: /[+\-*&%=<>!?|^~:\/]/,
    numberStart: /[\d#$]/,
    number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,
    multiLineStrings: true,
    typeFirstDefinitions: true,
    atoms: words("true false null larger smaller equal empty finished"),
    indentSwitch: false,
    styleDefs: false,
    hooks: {
      "@": function(stream) {
        stream.eatWhile(/[\w\$_]/);
        return "meta";
      },
      '"': function(stream, state) {
          state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single");
          return state.tokenize(stream, state);
        },
      '`': function(stream, state) {
          if (!stringTokenizer || !stream.match('`')) return false;
          state.tokenize = stringTokenizer;
          stringTokenizer = null;
          return state.tokenize(stream, state);
        },
      "'": function(stream) {
        stream.eatWhile(/[\w\$_\xa1-\uffff]/);
        return "atom";
      },
      token: function(_stream, state, style) {
          if ((style == "variable" || style == "type") &&
              state.prevToken == ".") {
            return "variable-2";
          }
        }
    },
    modeProps: {
        fold: ["brace", "import"],
        closeBrackets: {triples: '"'}
    }
  });
});
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/javascript/javascript.js
New file
@@ -0,0 +1,899 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    mod(require("../../lib/codemirror"));
  else if (typeof define == "function" && define.amd) // AMD
    define(["../../lib/codemirror"], mod);
  else // Plain browser env
    mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("javascript", function(config, parserConfig) {
  var indentUnit = config.indentUnit;
  var statementIndent = parserConfig.statementIndent;
  var jsonldMode = parserConfig.jsonld;
  var jsonMode = parserConfig.json || jsonldMode;
  var isTS = parserConfig.typescript;
  var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
  // Tokenizer
  var keywords = function(){
    function kw(type) {return {type: type, style: "keyword"};}
    var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
    var operator = kw("operator"), atom = {type: "atom", style: "atom"};
    return {
      "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
      "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
      "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
      "function": kw("function"), "catch": kw("catch"),
      "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
      "in": operator, "typeof": operator, "instanceof": operator,
      "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
      "this": kw("this"), "class": kw("class"), "super": kw("atom"),
      "yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
      "await": C
    };
  }();
  var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
  var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
  function readRegexp(stream) {
    var escaped = false, next, inSet = false;
    while ((next = stream.next()) != null) {
      if (!escaped) {
        if (next == "/" && !inSet) return;
        if (next == "[") inSet = true;
        else if (inSet && next == "]") inSet = false;
      }
      escaped = !escaped && next == "\\";
    }
  }
  // Used as scratch variables to communicate multiple values without
  // consing up tons of objects.
  var type, content;
  function ret(tp, style, cont) {
    type = tp; content = cont;
    return style;
  }
  function tokenBase(stream, state) {
    var ch = stream.next();
    if (ch == '"' || ch == "'") {
      state.tokenize = tokenString(ch);
      return state.tokenize(stream, state);
    } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
      return ret("number", "number");
    } else if (ch == "." && stream.match("..")) {
      return ret("spread", "meta");
    } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
      return ret(ch);
    } else if (ch == "=" && stream.eat(">")) {
      return ret("=>", "operator");
    } else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) {
      return ret("number", "number");
    } else if (/\d/.test(ch)) {
      stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/);
      return ret("number", "number");
    } else if (ch == "/") {
      if (stream.eat("*")) {
        state.tokenize = tokenComment;
        return tokenComment(stream, state);
      } else if (stream.eat("/")) {
        stream.skipToEnd();
        return ret("comment", "comment");
      } else if (expressionAllowed(stream, state, 1)) {
        readRegexp(stream);
        stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
        return ret("regexp", "string-2");
      } else {
        stream.eat("=");
        return ret("operator", "operator", stream.current());
      }
    } else if (ch == "`") {
      state.tokenize = tokenQuasi;
      return tokenQuasi(stream, state);
    } else if (ch == "#") {
      stream.skipToEnd();
      return ret("error", "error");
    } else if (isOperatorChar.test(ch)) {
      if (ch != ">" || !state.lexical || state.lexical.type != ">") {
        if (stream.eat("=")) {
          if (ch == "!" || ch == "=") stream.eat("=")
        } else if (/[<>*+\-]/.test(ch)) {
          stream.eat(ch)
          if (ch == ">") stream.eat(ch)
        }
      }
      return ret("operator", "operator", stream.current());
    } else if (wordRE.test(ch)) {
      stream.eatWhile(wordRE);
      var word = stream.current()
      if (state.lastType != ".") {
        if (keywords.propertyIsEnumerable(word)) {
          var kw = keywords[word]
          return ret(kw.type, kw.style, word)
        }
        if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false))
          return ret("async", "keyword", word)
      }
      return ret("variable", "variable", word)
    }
  }
  function tokenString(quote) {
    return function(stream, state) {
      var escaped = false, next;
      if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
        state.tokenize = tokenBase;
        return ret("jsonld-keyword", "meta");
      }
      while ((next = stream.next()) != null) {
        if (next == quote && !escaped) break;
        escaped = !escaped && next == "\\";
      }
      if (!escaped) state.tokenize = tokenBase;
      return ret("string", "string");
    };
  }
  function tokenComment(stream, state) {
    var maybeEnd = false, ch;
    while (ch = stream.next()) {
      if (ch == "/" && maybeEnd) {
        state.tokenize = tokenBase;
        break;
      }
      maybeEnd = (ch == "*");
    }
    return ret("comment", "comment");
  }
  function tokenQuasi(stream, state) {
    var escaped = false, next;
    while ((next = stream.next()) != null) {
      if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
        state.tokenize = tokenBase;
        break;
      }
      escaped = !escaped && next == "\\";
    }
    return ret("quasi", "string-2", stream.current());
  }
  var brackets = "([{}])";
  // This is a crude lookahead trick to try and notice that we're
  // parsing the argument patterns for a fat-arrow function before we
  // actually hit the arrow token. It only works if the arrow is on
  // the same line as the arguments and there's no strange noise
  // (comments) in between. Fallback is to only notice when we hit the
  // arrow, and not declare the arguments as locals for the arrow
  // body.
  function findFatArrow(stream, state) {
    if (state.fatArrowAt) state.fatArrowAt = null;
    var arrow = stream.string.indexOf("=>", stream.start);
    if (arrow < 0) return;
    if (isTS) { // Try to skip TypeScript return type declarations after the arguments
      var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
      if (m) arrow = m.index
    }
    var depth = 0, sawSomething = false;
    for (var pos = arrow - 1; pos >= 0; --pos) {
      var ch = stream.string.charAt(pos);
      var bracket = brackets.indexOf(ch);
      if (bracket >= 0 && bracket < 3) {
        if (!depth) { ++pos; break; }
        if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
      } else if (bracket >= 3 && bracket < 6) {
        ++depth;
      } else if (wordRE.test(ch)) {
        sawSomething = true;
      } else if (/["'\/]/.test(ch)) {
        return;
      } else if (sawSomething && !depth) {
        ++pos;
        break;
      }
    }
    if (sawSomething && !depth) state.fatArrowAt = pos;
  }
  // Parser
  var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true};
  function JSLexical(indented, column, type, align, prev, info) {
    this.indented = indented;
    this.column = column;
    this.type = type;
    this.prev = prev;
    this.info = info;
    if (align != null) this.align = align;
  }
  function inScope(state, varname) {
    for (var v = state.localVars; v; v = v.next)
      if (v.name == varname) return true;
    for (var cx = state.context; cx; cx = cx.prev) {
      for (var v = cx.vars; v; v = v.next)
        if (v.name == varname) return true;
    }
  }
  function parseJS(state, style, type, content, stream) {
    var cc = state.cc;
    // Communicate our context to the combinators.
    // (Less wasteful than consing up a hundred closures on every call.)
    cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
    if (!state.lexical.hasOwnProperty("align"))
      state.lexical.align = true;
    while(true) {
      var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
      if (combinator(type, content)) {
        while(cc.length && cc[cc.length - 1].lex)
          cc.pop()();
        if (cx.marked) return cx.marked;
        if (type == "variable" && inScope(state, content)) return "variable-2";
        return style;
      }
    }
  }
  // Combinator utils
  var cx = {state: null, column: null, marked: null, cc: null};
  function pass() {
    for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
  }
  function cont() {
    pass.apply(null, arguments);
    return true;
  }
  function inList(name, list) {
    for (var v = list; v; v = v.next) if (v.name == name) return true
    return false;
  }
  function register(varname) {
    var state = cx.state;
    cx.marked = "def";
    if (state.context) {
      if (state.lexical.info == "var" && state.context && state.context.block) {
        // FIXME function decls are also not block scoped
        var newContext = registerVarScoped(varname, state.context)
        if (newContext != null) {
          state.context = newContext
          return
        }
      } else if (!inList(varname, state.localVars)) {
        state.localVars = new Var(varname, state.localVars)
        return
      }
    }
    // Fall through means this is global
    if (parserConfig.globalVars && !inList(varname, state.globalVars))
      state.globalVars = new Var(varname, state.globalVars)
  }
  function registerVarScoped(varname, context) {
    if (!context) {
      return null
    } else if (context.block) {
      var inner = registerVarScoped(varname, context.prev)
      if (!inner) return null
      if (inner == context.prev) return context
      return new Context(inner, context.vars, true)
    } else if (inList(varname, context.vars)) {
      return context
    } else {
      return new Context(context.prev, new Var(varname, context.vars), false)
    }
  }
  function isModifier(name) {
    return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
  }
  // Combinators
  function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
  function Var(name, next) { this.name = name; this.next = next }
  var defaultVars = new Var("this", new Var("arguments", null))
  function pushcontext() {
    cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
    cx.state.localVars = defaultVars
  }
  function pushblockcontext() {
    cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
    cx.state.localVars = null
  }
  function popcontext() {
    cx.state.localVars = cx.state.context.vars
    cx.state.context = cx.state.context.prev
  }
  popcontext.lex = true
  function pushlex(type, info) {
    var result = function() {
      var state = cx.state, indent = state.indented;
      if (state.lexical.type == "stat") indent = state.lexical.indented;
      else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
        indent = outer.indented;
      state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
    };
    result.lex = true;
    return result;
  }
  function poplex() {
    var state = cx.state;
    if (state.lexical.prev) {
      if (state.lexical.type == ")")
        state.indented = state.lexical.indented;
      state.lexical = state.lexical.prev;
    }
  }
  poplex.lex = true;
  function expect(wanted) {
    function exp(type) {
      if (type == wanted) return cont();
      else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
      else return cont(exp);
    };
    return exp;
  }
  function statement(type, value) {
    if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
    if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
    if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
    if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
    if (type == "debugger") return cont(expect(";"));
    if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
    if (type == ";") return cont();
    if (type == "if") {
      if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
        cx.state.cc.pop()();
      return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
    }
    if (type == "function") return cont(functiondef);
    if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
    if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), className, poplex); }
    if (type == "variable") {
      if (isTS && value == "declare") {
        cx.marked = "keyword"
        return cont(statement)
      } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
        cx.marked = "keyword"
        if (value == "enum") return cont(enumdef);
        else if (value == "type") return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
        else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
      } else if (isTS && value == "namespace") {
        cx.marked = "keyword"
        return cont(pushlex("form"), expression, block, poplex)
      } else if (isTS && value == "abstract") {
        cx.marked = "keyword"
        return cont(statement)
      } else {
        return cont(pushlex("stat"), maybelabel);
      }
    }
    if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
                                      block, poplex, poplex, popcontext);
    if (type == "case") return cont(expression, expect(":"));
    if (type == "default") return cont(expect(":"));
    if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
    if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
    if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
    if (type == "async") return cont(statement)
    if (value == "@") return cont(expression, statement)
    return pass(pushlex("stat"), expression, expect(";"), poplex);
  }
  function maybeCatchBinding(type) {
    if (type == "(") return cont(funarg, expect(")"))
  }
  function expression(type, value) {
    return expressionInner(type, value, false);
  }
  function expressionNoComma(type, value) {
    return expressionInner(type, value, true);
  }
  function parenExpr(type) {
    if (type != "(") return pass()
    return cont(pushlex(")"), expression, expect(")"), poplex)
  }
  function expressionInner(type, value, noComma) {
    if (cx.state.fatArrowAt == cx.stream.start) {
      var body = noComma ? arrowBodyNoComma : arrowBody;
      if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
      else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
    }
    var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
    if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
    if (type == "function") return cont(functiondef, maybeop);
    if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
    if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
    if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
    if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
    if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
    if (type == "{") return contCommasep(objprop, "}", null, maybeop);
    if (type == "quasi") return pass(quasi, maybeop);
    if (type == "new") return cont(maybeTarget(noComma));
    if (type == "import") return cont(expression);
    return cont();
  }
  function maybeexpression(type) {
    if (type.match(/[;\}\)\],]/)) return pass();
    return pass(expression);
  }
  function maybeoperatorComma(type, value) {
    if (type == ",") return cont(expression);
    return maybeoperatorNoComma(type, value, false);
  }
  function maybeoperatorNoComma(type, value, noComma) {
    var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
    var expr = noComma == false ? expression : expressionNoComma;
    if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
    if (type == "operator") {
      if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
      if (isTS && value == "<" && cx.stream.match(/^([^>]|<.*?>)*>\s*\(/, false))
        return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
      if (value == "?") return cont(expression, expect(":"), expr);
      return cont(expr);
    }
    if (type == "quasi") { return pass(quasi, me); }
    if (type == ";") return;
    if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
    if (type == ".") return cont(property, me);
    if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
    if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
    if (type == "regexp") {
      cx.state.lastType = cx.marked = "operator"
      cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
      return cont(expr)
    }
  }
  function quasi(type, value) {
    if (type != "quasi") return pass();
    if (value.slice(value.length - 2) != "${") return cont(quasi);
    return cont(expression, continueQuasi);
  }
  function continueQuasi(type) {
    if (type == "}") {
      cx.marked = "string-2";
      cx.state.tokenize = tokenQuasi;
      return cont(quasi);
    }
  }
  function arrowBody(type) {
    findFatArrow(cx.stream, cx.state);
    return pass(type == "{" ? statement : expression);
  }
  function arrowBodyNoComma(type) {
    findFatArrow(cx.stream, cx.state);
    return pass(type == "{" ? statement : expressionNoComma);
  }
  function maybeTarget(noComma) {
    return function(type) {
      if (type == ".") return cont(noComma ? targetNoComma : target);
      else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
      else return pass(noComma ? expressionNoComma : expression);
    };
  }
  function target(_, value) {
    if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
  }
  function targetNoComma(_, value) {
    if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
  }
  function maybelabel(type) {
    if (type == ":") return cont(poplex, statement);
    return pass(maybeoperatorComma, expect(";"), poplex);
  }
  function property(type) {
    if (type == "variable") {cx.marked = "property"; return cont();}
  }
  function objprop(type, value) {
    if (type == "async") {
      cx.marked = "property";
      return cont(objprop);
    } else if (type == "variable" || cx.style == "keyword") {
      cx.marked = "property";
      if (value == "get" || value == "set") return cont(getterSetter);
      var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
      if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
        cx.state.fatArrowAt = cx.stream.pos + m[0].length
      return cont(afterprop);
    } else if (type == "number" || type == "string") {
      cx.marked = jsonldMode ? "property" : (cx.style + " property");
      return cont(afterprop);
    } else if (type == "jsonld-keyword") {
      return cont(afterprop);
    } else if (isTS && isModifier(value)) {
      cx.marked = "keyword"
      return cont(objprop)
    } else if (type == "[") {
      return cont(expression, maybetype, expect("]"), afterprop);
    } else if (type == "spread") {
      return cont(expressionNoComma, afterprop);
    } else if (value == "*") {
      cx.marked = "keyword";
      return cont(objprop);
    } else if (type == ":") {
      return pass(afterprop)
    }
  }
  function getterSetter(type) {
    if (type != "variable") return pass(afterprop);
    cx.marked = "property";
    return cont(functiondef);
  }
  function afterprop(type) {
    if (type == ":") return cont(expressionNoComma);
    if (type == "(") return pass(functiondef);
  }
  function commasep(what, end, sep) {
    function proceed(type, value) {
      if (sep ? sep.indexOf(type) > -1 : type == ",") {
        var lex = cx.state.lexical;
        if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
        return cont(function(type, value) {
          if (type == end || value == end) return pass()
          return pass(what)
        }, proceed);
      }
      if (type == end || value == end) return cont();
      return cont(expect(end));
    }
    return function(type, value) {
      if (type == end || value == end) return cont();
      return pass(what, proceed);
    };
  }
  function contCommasep(what, end, info) {
    for (var i = 3; i < arguments.length; i++)
      cx.cc.push(arguments[i]);
    return cont(pushlex(end, info), commasep(what, end), poplex);
  }
  function block(type) {
    if (type == "}") return cont();
    return pass(statement, block);
  }
  function maybetype(type, value) {
    if (isTS) {
      if (type == ":") return cont(typeexpr);
      if (value == "?") return cont(maybetype);
    }
  }
  function mayberettype(type) {
    if (isTS && type == ":") {
      if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
      else return cont(typeexpr)
    }
  }
  function isKW(_, value) {
    if (value == "is") {
      cx.marked = "keyword"
      return cont()
    }
  }
  function typeexpr(type, value) {
    if (value == "keyof" || value == "typeof") {
      cx.marked = "keyword"
      return cont(value == "keyof" ? typeexpr : expressionNoComma)
    }
    if (type == "variable" || value == "void") {
      cx.marked = "type"
      return cont(afterType)
    }
    if (type == "string" || type == "number" || type == "atom") return cont(afterType);
    if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
    if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType)
    if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType)
    if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
  }
  function maybeReturnType(type) {
    if (type == "=>") return cont(typeexpr)
  }
  function typeprop(type, value) {
    if (type == "variable" || cx.style == "keyword") {
      cx.marked = "property"
      return cont(typeprop)
    } else if (value == "?") {
      return cont(typeprop)
    } else if (type == ":") {
      return cont(typeexpr)
    } else if (type == "[") {
      return cont(expression, maybetype, expect("]"), typeprop)
    }
  }
  function typearg(type, value) {
    if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
    if (type == ":") return cont(typeexpr)
    return pass(typeexpr)
  }
  function afterType(type, value) {
    if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
    if (value == "|" || type == "." || value == "&") return cont(typeexpr)
    if (type == "[") return cont(expect("]"), afterType)
    if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
  }
  function maybeTypeArgs(_, value) {
    if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
  }
  function typeparam() {
    return pass(typeexpr, maybeTypeDefault)
  }
  function maybeTypeDefault(_, value) {
    if (value == "=") return cont(typeexpr)
  }
  function vardef(_, value) {
    if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
    return pass(pattern, maybetype, maybeAssign, vardefCont);
  }
  function pattern(type, value) {
    if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
    if (type == "variable") { register(value); return cont(); }
    if (type == "spread") return cont(pattern);
    if (type == "[") return contCommasep(eltpattern, "]");
    if (type == "{") return contCommasep(proppattern, "}");
  }
  function proppattern(type, value) {
    if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
      register(value);
      return cont(maybeAssign);
    }
    if (type == "variable") cx.marked = "property";
    if (type == "spread") return cont(pattern);
    if (type == "}") return pass();
    return cont(expect(":"), pattern, maybeAssign);
  }
  function eltpattern() {
    return pass(pattern, maybeAssign)
  }
  function maybeAssign(_type, value) {
    if (value == "=") return cont(expressionNoComma);
  }
  function vardefCont(type) {
    if (type == ",") return cont(vardef);
  }
  function maybeelse(type, value) {
    if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
  }
  function forspec(type, value) {
    if (value == "await") return cont(forspec);
    if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
  }
  function forspec1(type) {
    if (type == "var") return cont(vardef, expect(";"), forspec2);
    if (type == ";") return cont(forspec2);
    if (type == "variable") return cont(formaybeinof);
    return pass(expression, expect(";"), forspec2);
  }
  function formaybeinof(_type, value) {
    if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
    return cont(maybeoperatorComma, forspec2);
  }
  function forspec2(type, value) {
    if (type == ";") return cont(forspec3);
    if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
    return pass(expression, expect(";"), forspec3);
  }
  function forspec3(type) {
    if (type != ")") cont(expression);
  }
  function functiondef(type, value) {
    if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
    if (type == "variable") {register(value); return cont(functiondef);}
    if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
    if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
  }
  function funarg(type, value) {
    if (value == "@") cont(expression, funarg)
    if (type == "spread") return cont(funarg);
    if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
    return pass(pattern, maybetype, maybeAssign);
  }
  function classExpression(type, value) {
    // Class expressions may have an optional name.
    if (type == "variable") return className(type, value);
    return classNameAfter(type, value);
  }
  function className(type, value) {
    if (type == "variable") {register(value); return cont(classNameAfter);}
  }
  function classNameAfter(type, value) {
    if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
    if (value == "extends" || value == "implements" || (isTS && type == ",")) {
      if (value == "implements") cx.marked = "keyword";
      return cont(isTS ? typeexpr : expression, classNameAfter);
    }
    if (type == "{") return cont(pushlex("}"), classBody, poplex);
  }
  function classBody(type, value) {
    if (type == "async" ||
        (type == "variable" &&
         (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
         cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) {
      cx.marked = "keyword";
      return cont(classBody);
    }
    if (type == "variable" || cx.style == "keyword") {
      cx.marked = "property";
      return cont(isTS ? classfield : functiondef, classBody);
    }
    if (type == "[")
      return cont(expression, maybetype, expect("]"), isTS ? classfield : functiondef, classBody)
    if (value == "*") {
      cx.marked = "keyword";
      return cont(classBody);
    }
    if (type == ";") return cont(classBody);
    if (type == "}") return cont();
    if (value == "@") return cont(expression, classBody)
  }
  function classfield(type, value) {
    if (value == "?") return cont(classfield)
    if (type == ":") return cont(typeexpr, maybeAssign)
    if (value == "=") return cont(expressionNoComma)
    return pass(functiondef)
  }
  function afterExport(type, value) {
    if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
    if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
    if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
    return pass(statement);
  }
  function exportField(type, value) {
    if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
    if (type == "variable") return pass(expressionNoComma, exportField);
  }
  function afterImport(type) {
    if (type == "string") return cont();
    if (type == "(") return pass(expression);
    return pass(importSpec, maybeMoreImports, maybeFrom);
  }
  function importSpec(type, value) {
    if (type == "{") return contCommasep(importSpec, "}");
    if (type == "variable") register(value);
    if (value == "*") cx.marked = "keyword";
    return cont(maybeAs);
  }
  function maybeMoreImports(type) {
    if (type == ",") return cont(importSpec, maybeMoreImports)
  }
  function maybeAs(_type, value) {
    if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
  }
  function maybeFrom(_type, value) {
    if (value == "from") { cx.marked = "keyword"; return cont(expression); }
  }
  function arrayLiteral(type) {
    if (type == "]") return cont();
    return pass(commasep(expressionNoComma, "]"));
  }
  function enumdef() {
    return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
  }
  function enummember() {
    return pass(pattern, maybeAssign);
  }
  function isContinuedStatement(state, textAfter) {
    return state.lastType == "operator" || state.lastType == "," ||
      isOperatorChar.test(textAfter.charAt(0)) ||
      /[,.]/.test(textAfter.charAt(0));
  }
  function expressionAllowed(stream, state, backUp) {
    return state.tokenize == tokenBase &&
      /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
      (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
  }
  // Interface
  return {
    startState: function(basecolumn) {
      var state = {
        tokenize: tokenBase,
        lastType: "sof",
        cc: [],
        lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
        localVars: parserConfig.localVars,
        context: parserConfig.localVars && new Context(null, null, false),
        indented: basecolumn || 0
      };
      if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
        state.globalVars = parserConfig.globalVars;
      return state;
    },
    token: function(stream, state) {
      if (stream.sol()) {
        if (!state.lexical.hasOwnProperty("align"))
          state.lexical.align = false;
        state.indented = stream.indentation();
        findFatArrow(stream, state);
      }
      if (state.tokenize != tokenComment && stream.eatSpace()) return null;
      var style = state.tokenize(stream, state);
      if (type == "comment") return style;
      state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
      return parseJS(state, style, type, content, stream);
    },
    indent: function(state, textAfter) {
      if (state.tokenize == tokenComment) return CodeMirror.Pass;
      if (state.tokenize != tokenBase) return 0;
      var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
      // Kludge to prevent 'maybelse' from blocking lexical scope pops
      if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
        var c = state.cc[i];
        if (c == poplex) lexical = lexical.prev;
        else if (c != maybeelse) break;
      }
      while ((lexical.type == "stat" || lexical.type == "form") &&
             (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
                                   (top == maybeoperatorComma || top == maybeoperatorNoComma) &&
                                   !/^[,\.=+\-*:?[\(]/.test(textAfter))))
        lexical = lexical.prev;
      if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
        lexical = lexical.prev;
      var type = lexical.type, closing = firstChar == type;
      if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
      else if (type == "form" && firstChar == "{") return lexical.indented;
      else if (type == "form") return lexical.indented + indentUnit;
      else if (type == "stat")
        return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
      else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
        return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
      else if (lexical.align) return lexical.column + (closing ? 0 : 1);
      else return lexical.indented + (closing ? 0 : indentUnit);
    },
    electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
    blockCommentStart: jsonMode ? null : "/*",
    blockCommentEnd: jsonMode ? null : "*/",
    blockCommentContinue: jsonMode ? null : " * ",
    lineComment: jsonMode ? null : "//",
    fold: "brace",
    closeBrackets: "()[]{}''\"\"``",
    helperType: jsonMode ? "json" : "javascript",
    jsonldMode: jsonldMode,
    jsonMode: jsonMode,
    expressionAllowed: expressionAllowed,
    skipExpression: function(state) {
      var top = state.cc[state.cc.length - 1]
      if (top == expression || top == expressionNoComma) state.cc.pop()
    }
  };
});
CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/x-javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true});
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
});
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/php/php.js
New file
@@ -0,0 +1,234 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../clike/clike"));
  else if (typeof define == "function" && define.amd) // AMD
    define(["../../lib/codemirror", "../htmlmixed/htmlmixed", "../clike/clike"], mod);
  else // Plain browser env
    mod(CodeMirror);
})(function(CodeMirror) {
  "use strict";
  function keywords(str) {
    var obj = {}, words = str.split(" ");
    for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
    return obj;
  }
  // Helper for phpString
  function matchSequence(list, end, escapes) {
    if (list.length == 0) return phpString(end);
    return function (stream, state) {
      var patterns = list[0];
      for (var i = 0; i < patterns.length; i++) if (stream.match(patterns[i][0])) {
        state.tokenize = matchSequence(list.slice(1), end);
        return patterns[i][1];
      }
      state.tokenize = phpString(end, escapes);
      return "string";
    };
  }
  function phpString(closing, escapes) {
    return function(stream, state) { return phpString_(stream, state, closing, escapes); };
  }
  function phpString_(stream, state, closing, escapes) {
    // "Complex" syntax
    if (escapes !== false && stream.match("${", false) || stream.match("{$", false)) {
      state.tokenize = null;
      return "string";
    }
    // Simple syntax
    if (escapes !== false && stream.match(/^\$[a-zA-Z_][a-zA-Z0-9_]*/)) {
      // After the variable name there may appear array or object operator.
      if (stream.match("[", false)) {
        // Match array operator
        state.tokenize = matchSequence([
          [["[", null]],
          [[/\d[\w\.]*/, "number"],
           [/\$[a-zA-Z_][a-zA-Z0-9_]*/, "variable-2"],
           [/[\w\$]+/, "variable"]],
          [["]", null]]
        ], closing, escapes);
      }
      if (stream.match(/\-\>\w/, false)) {
        // Match object operator
        state.tokenize = matchSequence([
          [["->", null]],
          [[/[\w]+/, "variable"]]
        ], closing, escapes);
      }
      return "variable-2";
    }
    var escaped = false;
    // Normal string
    while (!stream.eol() &&
           (escaped || escapes === false ||
            (!stream.match("{$", false) &&
             !stream.match(/^(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{)/, false)))) {
      if (!escaped && stream.match(closing)) {
        state.tokenize = null;
        state.tokStack.pop(); state.tokStack.pop();
        break;
      }
      escaped = stream.next() == "\\" && !escaped;
    }
    return "string";
  }
  var phpKeywords = "abstract and array as break case catch class clone const continue declare default " +
    "do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final " +
    "for foreach function global goto if implements interface instanceof namespace " +
    "new or private protected public static switch throw trait try use var while xor " +
    "die echo empty exit eval include include_once isset list require require_once return " +
    "print unset __halt_compiler self static parent yield insteadof finally";
  var phpAtoms = "true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__";
  var phpBuiltin = "func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists array_intersect_key array_combine array_column pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count";
  CodeMirror.registerHelper("hintWords", "php", [phpKeywords, phpAtoms, phpBuiltin].join(" ").split(" "));
  CodeMirror.registerHelper("wordChars", "php", /[\w$]/);
  var phpConfig = {
    name: "clike",
    helperType: "php",
    keywords: keywords(phpKeywords),
    blockKeywords: keywords("catch do else elseif for foreach if switch try while finally"),
    defKeywords: keywords("class function interface namespace trait"),
    atoms: keywords(phpAtoms),
    builtin: keywords(phpBuiltin),
    multiLineStrings: true,
    hooks: {
      "$": function(stream) {
        stream.eatWhile(/[\w\$_]/);
        return "variable-2";
      },
      "<": function(stream, state) {
        var before;
        if (before = stream.match(/<<\s*/)) {
          var quoted = stream.eat(/['"]/);
          stream.eatWhile(/[\w\.]/);
          var delim = stream.current().slice(before[0].length + (quoted ? 2 : 1));
          if (quoted) stream.eat(quoted);
          if (delim) {
            (state.tokStack || (state.tokStack = [])).push(delim, 0);
            state.tokenize = phpString(delim, quoted != "'");
            return "string";
          }
        }
        return false;
      },
      "#": function(stream) {
        while (!stream.eol() && !stream.match("?>", false)) stream.next();
        return "comment";
      },
      "/": function(stream) {
        if (stream.eat("/")) {
          while (!stream.eol() && !stream.match("?>", false)) stream.next();
          return "comment";
        }
        return false;
      },
      '"': function(_stream, state) {
        (state.tokStack || (state.tokStack = [])).push('"', 0);
        state.tokenize = phpString('"');
        return "string";
      },
      "{": function(_stream, state) {
        if (state.tokStack && state.tokStack.length)
          state.tokStack[state.tokStack.length - 1]++;
        return false;
      },
      "}": function(_stream, state) {
        if (state.tokStack && state.tokStack.length > 0 &&
            !--state.tokStack[state.tokStack.length - 1]) {
          state.tokenize = phpString(state.tokStack[state.tokStack.length - 2]);
        }
        return false;
      }
    }
  };
  CodeMirror.defineMode("php", function(config, parserConfig) {
    var htmlMode = CodeMirror.getMode(config, (parserConfig && parserConfig.htmlMode) || "text/html");
    var phpMode = CodeMirror.getMode(config, phpConfig);
    function dispatch(stream, state) {
      var isPHP = state.curMode == phpMode;
      if (stream.sol() && state.pending && state.pending != '"' && state.pending != "'") state.pending = null;
      if (!isPHP) {
        if (stream.match(/^<\?\w*/)) {
          state.curMode = phpMode;
          if (!state.php) state.php = CodeMirror.startState(phpMode, htmlMode.indent(state.html, ""))
          state.curState = state.php;
          return "meta";
        }
        if (state.pending == '"' || state.pending == "'") {
          while (!stream.eol() && stream.next() != state.pending) {}
          var style = "string";
        } else if (state.pending && stream.pos < state.pending.end) {
          stream.pos = state.pending.end;
          var style = state.pending.style;
        } else {
          var style = htmlMode.token(stream, state.curState);
        }
        if (state.pending) state.pending = null;
        var cur = stream.current(), openPHP = cur.search(/<\?/), m;
        if (openPHP != -1) {
          if (style == "string" && (m = cur.match(/[\'\"]$/)) && !/\?>/.test(cur)) state.pending = m[0];
          else state.pending = {end: stream.pos, style: style};
          stream.backUp(cur.length - openPHP);
        }
        return style;
      } else if (isPHP && state.php.tokenize == null && stream.match("?>")) {
        state.curMode = htmlMode;
        state.curState = state.html;
        if (!state.php.context.prev) state.php = null;
        return "meta";
      } else {
        return phpMode.token(stream, state.curState);
      }
    }
    return {
      startState: function() {
        var html = CodeMirror.startState(htmlMode)
        var php = parserConfig.startOpen ? CodeMirror.startState(phpMode) : null
        return {html: html,
                php: php,
                curMode: parserConfig.startOpen ? phpMode : htmlMode,
                curState: parserConfig.startOpen ? php : html,
                pending: null};
      },
      copyState: function(state) {
        var html = state.html, htmlNew = CodeMirror.copyState(htmlMode, html),
            php = state.php, phpNew = php && CodeMirror.copyState(phpMode, php), cur;
        if (state.curMode == htmlMode) cur = htmlNew;
        else cur = phpNew;
        return {html: htmlNew, php: phpNew, curMode: state.curMode, curState: cur,
                pending: state.pending};
      },
      token: dispatch,
      indent: function(state, textAfter) {
        if ((state.curMode != phpMode && /^\s*<\//.test(textAfter)) ||
            (state.curMode == phpMode && /^\?>/.test(textAfter)))
          return htmlMode.indent(state.html, textAfter);
        return state.curMode.indent(state.curState, textAfter);
      },
      blockCommentStart: "/*",
      blockCommentEnd: "*/",
      lineComment: "//",
      innerMode: function(state) { return {state: state.curState, mode: state.curMode}; }
    };
  }, "htmlmixed", "clike");
  CodeMirror.defineMIME("application/x-httpd-php", "php");
  CodeMirror.defineMIME("application/x-httpd-php-open", {name: "php", startOpen: true});
  CodeMirror.defineMIME("text/x-php", phpConfig);
});
Diff truncated after the above file
xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/powershell/powershell.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/python/python.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/codemirror/mode/shell/shell.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/echarts/echarts.common.min.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.cookie.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/jquery/jquery.validate.min.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/layer.js xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon-ext.png xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/icon.png xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/layer.css xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-0.gif xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-1.gif xxl-job/xxl-job-admin/src/main/resources/static/plugins/layer/theme/default/loading-2.gif xxl-job/xxl-job-admin/src/main/resources/templates/common/common.exception.ftl xxl-job/xxl-job-admin/src/main/resources/templates/common/common.macro.ftl xxl-job/xxl-job-admin/src/main/resources/templates/help.ftl xxl-job/xxl-job-admin/src/main/resources/templates/index.ftl xxl-job/xxl-job-admin/src/main/resources/templates/jobcode/jobcode.index.ftl xxl-job/xxl-job-admin/src/main/resources/templates/jobgroup/jobgroup.index.ftl xxl-job/xxl-job-admin/src/main/resources/templates/jobinfo/jobinfo.index.ftl xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.detail.ftl xxl-job/xxl-job-admin/src/main/resources/templates/joblog/joblog.index.ftl xxl-job/xxl-job-admin/src/main/resources/templates/login.ftl xxl-job/xxl-job-admin/src/main/resources/templates/user/user.index.ftl xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/AbstractSpringMvcTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/controller/JobInfoControllerTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/JacksonUtilTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobGroupDaoTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobInfoDaoTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogDaoTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobLogGlueDaoTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/dao/XxlJobRegistryDaoTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/admin/util/I18nUtilTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/adminbiz/AdminBizTest.java xxl-job/xxl-job-admin/src/test/java/com/xxl/job/executorbiz/ExecutorBizTest.java xxl-job/xxl-job-core/pom.xml xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/AdminBiz.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/ExecutorBiz.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/AdminBizClient.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/client/ExecutorBizClient.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/impl/ExecutorBizImpl.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/HandleCallbackParam.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/IdleBeatParam.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/KillParam.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogParam.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/LogResult.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/RegistryParam.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/ReturnT.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/biz/model/TriggerParam.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobContext.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/context/XxlJobHelper.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/ExecutorBlockStrategyEnum.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/enums/RegistryConfig.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSimpleExecutor.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/executor/impl/XxlJobSpringExecutor.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueTypeEnum.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/glue/impl/SpringGlueFactory.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/IJobHandler.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/JobHandler.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/annotation/XxlJob.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/GlueJobHandler.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/MethodJobHandler.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/handler/impl/ScriptJobHandler.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/log/XxlJobFileAppender.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/ExecutorRegistryThread.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobLogFileCleanThread.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/JobThread.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/thread/TriggerCallbackThread.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/DateUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/FileUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/GsonTool.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/IpUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/JdkSerializeTool.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/NetUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ScriptUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ShardingUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/ThrowableUtil.java xxl-job/xxl-job-core/src/main/java/com/xxl/job/core/util/XxlJobRemotingUtil.java xxl-job/xxl-job-executor-samples/pom.xml xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/FramelessApplication.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/config/FrameLessXxlJobConfig.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xxl/job/executor/sample/frameless/jobhandler/SampleXxlJob.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/test/java/com/xxl/job/executor/sample/frameless/test/FramelessApplicationTest.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/Dockerfile xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/XxlJobExecutorApplication.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/mvc/controller/IndexController.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/logback.xml xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/test/java/com/xxl/job/executor/test/XxlJobExecutorExampleBootApplicationTests.java yami-shop-api/Dockerfile yami-shop-api/Dockerfile-test yami-shop-api/pom.xml yami-shop-api/src/main/java/com/yami/shop/api/ApiApplication.java yami-shop-api/src/main/java/com/yami/shop/api/config/OrderThreadConfig.java yami-shop-api/src/main/java/com/yami/shop/api/config/SwaggerConfiguration.java yami-shop-api/src/main/java/com/yami/shop/api/config/ThreadPoolConfigProperties.java yami-shop-api/src/main/java/com/yami/shop/api/config/ValidationsConfiguration.java yami-shop-api/src/main/java/com/yami/shop/api/controller/AddrController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/AgentController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/AreaController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/C2cRealNameController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/CategoryController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/CompanyAuditingController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/CpsController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/DeliveryController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/DiplomaController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/FileController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/FlowLogController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/FlowLogLoginController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/IndexImgController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/LuckyCoinController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsHandCardController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsSalesController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsTranLeveController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsTranOrderController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MarsWantBuyController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MyNoticeController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MyOrderController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/MyShopDetailController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/NoticeController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/NotifyLogController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/OrderController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/OrderInvoiceController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/OrderRefundController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/PayBalanceController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/PayController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/PayLevelController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/PayNoticeController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdBrowseLogController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdCommController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdRecommendController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ProdTagController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/QrcodeTicketController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/SearchController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopBankCardController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopCartController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopCompanyController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopDetailController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopManagementController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopRenovationController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/ShopUserRegisterController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/SkuController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/SmsController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/TaskRuleController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/TestsController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/UserCollectionController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/UserCollectionShopController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/UserController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/UserRegisterController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/WebConfigController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnFlowController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnIncomeBangController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnIndexController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnMyWalletController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnQrCodeController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnRealNameController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/cdn/CdnUserWalletController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/im/LuckImFriendController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/im/LuckImGroupController.java yami-shop-api/src/main/java/com/yami/shop/api/controller/im/LuckImMsgController.java yami-shop-api/src/main/java/com/yami/shop/api/listener/SubmitOrderListener.java yami-shop-api/src/main/java/com/yami/shop/api/manager/SubmitOrderImplManager.java yami-shop-api/src/main/java/com/yami/shop/api/task/V1BoxTask.java yami-shop-api/src/main/resources/application-dev.yml yami-shop-api/src/main/resources/application-docker.yml yami-shop-api/src/main/resources/application-prod.yml yami-shop-api/src/main/resources/application-test.yml yami-shop-api/src/main/resources/application.yml yami-shop-api/src/main/resources/banner.txt yami-shop-api/src/main/resources/cert/alipayCertPublicKey_RSA2.crt yami-shop-api/src/main/resources/cert/alipayRootCert.crt yami-shop-api/src/main/resources/cert/appCertPublicKey_2021004183677087.crt yami-shop-api/src/main/resources/cert/test/appPublicCert.crt yami-shop-api/src/main/resources/i18n/validations.properties yami-shop-api/src/main/resources/i18n/validations_en.properties yami-shop-api/src/main/resources/i18n/validations_zh.properties yami-shop-api/src/main/resources/logback-spring-test.xml yami-shop-api/src/main/resources/logback-spring.xml yami-shop-api/src/main/resources/logback/logback-dev.xml yami-shop-api/src/main/resources/logback/logback-docker.xml yami-shop-api/src/main/resources/logback/logback-prod.xml