<search>
    
     <entry>
        <title>XxlJob09：路由策略</title>
        <url>https://www.szlinkroutes.com/post/xxljob09%E8%B7%AF%E7%94%B1%E7%AD%96%E7%95%A5/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
在前面配置xxl-job任务时，提到过xxl-job的路由策略，本文将来分析这些策略。
在任务的编辑页面，可以配置路由策略：
所谓的路由策略，是指在多个executor存在时，选择哪一个executor来执行任务的策略。xxl-job支持的路由策略有如下几种：
第一个：选择第一个executor 最后一个：选择最后一个executor 轮询：依次选择executor 随机：随机选择一个executor 一致性hash：使用一致性hash算法来选择executor 最不经常使用：选择最不经常使用的executor 最近最久未使用：选择最久未使用的executor 故障转移：判断当前选择的executor是否可用，若不可用，则选择另一个executor 忙碌转移：判断当前选择的executor是否处于忙碌状态，若处于，则选择另一executor 分片广播：每一个executor都会执行任务 路由策略的选择 路由策略的选择在XxlJobTrigger#processTrigger，相关代码如下：
// 获取路由策略枚举类，jobInfo.getExecutorRouteStrategy() 来自于数据库 ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); .... // 路由策略的处理，区分分片广播与其他策略 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { if (index &amp;lt; 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(); } } 在任务的配置时，任务的路由策略会保存在数据库中，在XxlJobTrigger#processTrigger方法中，会把路由策略拿出来以便获取路由策略的枚举类，jobInfo.getExecutorRouteStrategy()就是数据库中保存的策略。
路由枚举类ExecutorRouteStrategyEnum的代码如下：
public enum ExecutorRouteStrategyEnum { /** 第一个 */ FIRST(I18nUtil.getString(&amp;#34;jobconf_route_first&amp;#34;), new ExecutorRouteFirst()), /** 最后一个 */ LAST(I18nUtil.getString(&amp;#34;jobconf_route_last&amp;#34;), new ExecutorRouteLast()), /** 轮询 */ ROUND(I18nUtil.getString(&amp;#34;jobconf_route_round&amp;#34;), new ExecutorRouteRound()), /** 随机 */ RANDOM(I18nUtil.getString(&amp;#34;jobconf_route_random&amp;#34;), new ExecutorRouteRandom()), /** 一致性hash */ CONSISTENT_HASH(I18nUtil.getString(&amp;#34;jobconf_route_consistenthash&amp;#34;), new ExecutorRouteConsistentHash()), /** 最不经常使用 */ LEAST_FREQUENTLY_USED(I18nUtil.getString(&amp;#34;jobconf_route_lfu&amp;#34;), new ExecutorRouteLFU()), /** 最近最久未使用 */ LEAST_RECENTLY_USED(I18nUtil.getString(&amp;#34;jobconf_route_lru&amp;#34;), new ExecutorRouteLRU()), /** 故障转移 */ FAILOVER(I18nUtil.getString(&amp;#34;jobconf_route_failover&amp;#34;), new ExecutorRouteFailover()), /** 忙碌转移 */ BUSYOVER(I18nUtil.getString(&amp;#34;jobconf_route_busyover&amp;#34;), new ExecutorRouteBusyover()), /** 分片广播 */ SHARDING_BROADCAST(I18nUtil.getString(&amp;#34;jobconf_route_shard&amp;#34;), null); ... 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; } } ExecutorRouteStrategyEnum的枚举值中包含了该策略的处理类，获取到ExecutorRouteStrategyEnum时，同时也就获取到了该策略的处理类了。
策略处理的接口为ExecutorRouter，代码如下：
public abstract class ExecutorRouter { protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class); /** * route address * * @param addressList * @return ReturnT.content=address */ public abstract ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList); } 接口中只有一个方法route(xxx)，它的返回值就是要执行任务的executor的地址。以上10种策略，除了分片广播外，其他每种策略都有一个对应的实现类：
接下来我们就来逐一分析这些策略.
分片广播 之所以把分片广播策略放在最前面，是因为没有一个策略类是用来处理分片广播策略的，它的实现是xxl-job对它做了额外处理。介绍xxl-job对分片广播策略的额外处理前，需要先回顾下任务的触发流程，直接上图：
以上流程是《任务执行流程（二）之触发器揭秘》中介绍的内容，跟随着触发流程，我们到XxlJobTrigger#trigger方法：
public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { ... // 额外处理分片广播策略，这一行是关键 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST== ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) &amp;amp;&amp;amp; group.getRegistryList()!=null &amp;amp;&amp;amp; !group.getRegistryList().isEmpty() &amp;amp;&amp;amp; shardingParam==null) { // 依次广播到每台机器 for (int i = 0; i &amp;lt; group.getRegistryList().size(); i&#43;&#43;) { processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size()); } } ... 以上方法删减了其他代码，重点保留了分片广播的处理，如果是分片广播策略，就遍历executor列表，对每个executor都执行processTrigger方法。
继续，进入XxlJobTrigger#processTrigger方法：
private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){ .... // 路由策略的处理，区分分片广播与其他策略 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { if (index &amp;lt; group.getRegistryList().size()) { address = group.getRegistryList().get(index); } else { address = group.getRegistryList().get(0); } } ... } 方法中的index，就是XxlJobTrigger#trigger传入的executor列表的下标id，对分片广播策略来说，这个index就是执行任务的executor了。
第一个 处理第一个路由策略的类是ExecutorRouteFirst，代码如下：
public ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList) { return new ReturnT&amp;lt;String&amp;gt;(addressList.get(0)); } 这里就是获取exceutor地址列表中的第一个exceutor的地址。
最后一个 处理最后一个路由策略的类是ExecutorRouteFirst，代码如下：
public ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList) { return new ReturnT&amp;lt;String&amp;gt;(addressList.get(addressList.size()-1)); } 与第一个策略相对应，这里就是获取exceutor地址列表中的最后一个exceutor的地址。
轮询 处理轮询策略的类是ExecutorRouteRound，代码如下：
/** 执行次数，key 为 jobId，value 为次数 */ private static ConcurrentMap&amp;lt;Integer, AtomicInteger&amp;gt; routeCountEachJob = new ConcurrentHashMap&amp;lt;&amp;gt;(); private static long CACHE_VALID_TIME = 0; /** * 计数操作 * * 如果首次执行或任务执行次数大于1000000了，那么执行次数重置为100以内的整数 * 否则，每次请求时，count次数都加1 */ private static int count(int jobId) { // cache clear if (System.currentTimeMillis() &amp;gt; CACHE_VALID_TIME) { routeCountEachJob.clear(); CACHE_VALID_TIME = System.currentTimeMillis() &#43; 1000*60*60*24; } AtomicInteger count = routeCountEachJob.get(jobId); if (count == null || count.get() &amp;gt; 1000000) { // 初始化时主动Random一次，缓解首次压力 count = new AtomicInteger(new Random().nextInt(100)); } else { // count&#43;&#43; count.addAndGet(1); } routeCountEachJob.put(jobId, count); return count.get(); } @Override public ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList) { // 求余，计算executor的索引 String address = addressList.get( count(triggerParam.getJobId())%addressList.size()); return new ReturnT&amp;lt;String&amp;gt;(address); } 在 ExecutorRouteRound 中，有一个ConcurrentMap，用来统计任务的执行次数（key 为 jobId，value 为次数）。
ExecutorRouteRound中，count(xxx)方法的核心逻辑就两句话：如果首次执行或任务执行次数大于1000000了，那么执行次数重置为100以内的整数；否则，每次请求时，count次数都加1。
最后来看看route(xxx)方法，这个方法最核心的代码就一行：
count(triggerParam.getJobId())%addressList.size() 即获取任务总的执行次数，然后对executor总实例数进行求余操作，得到的结果就是executor列表的下标索引了，再调用addressList.get(xxx)就能得到具体的executor地址了。
随机 处理随机策略的类是ExecutorRouteRandom，代码如下：
private static Random localRandom = new Random(); public ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList) { String address = addressList.get(localRandom.nextInt(addressList.size())); return new ReturnT&amp;lt;String&amp;gt;(address); } 这个实现很简单，就是随机生成一个0~executor实例总数之间的整数，这个整数就是执行任务的executor了。
一致性hash 接着我们来介绍一个有意思的策略：一致性hash。关于一致性算法的相关介绍，这里就不过多介绍了，我们重点来看xxl-job关于它的实现，进入ExecutorRouteConsistentHash：
/** 虚拟节点的数量 */ private static int VIRTUAL_NODE_NUM = 100; /** * 处理address的获取 */ public String hashJob(int jobId, List&amp;lt;String&amp;gt; addressList) { // 构建 addressRing TreeMap&amp;lt;Long, String&amp;gt; addressRing = new TreeMap&amp;lt;Long, String&amp;gt;(); for (String address: addressList) { for (int i = 0; i &amp;lt; VIRTUAL_NODE_NUM; i&#43;&#43;) { long addressHash = hash(&amp;#34;SHARD-&amp;#34; &#43; address &#43; &amp;#34;-NODE-&amp;#34; &#43; i); addressRing.put(addressHash, address); } } // 根据 jobId 的 hash 值 long jobHash = hash(String.valueOf(jobId)); SortedMap&amp;lt;Long, String&amp;gt; lastRing = addressRing.tailMap(jobHash); if (!lastRing.isEmpty()) { return lastRing.get(lastRing.firstKey()); } return addressRing.firstEntry().getValue(); } 方法的一开始，使用了TreeMap来保存节点，key是节点生成的hash值，value是address。
我们都知道TreeMap是一个有序的结构，它会根据key指定的排序规则来排序，这里的key是Long类型，因此会按从小到的顺序排序。
从for (int i = 0; i &amp;lt; VIRTUAL_NODE_NUM; i&#43;&#43;)来看，它会为每个address生成100个虚拟节点(VIRTUAL_NODE_NUM值为100)，节点生成的规则为hash(&amp;quot;SHARD-&amp;quot; &#43; address &#43; &amp;quot;-NODE-&amp;quot; &#43; i)，其中i取值0~99。规则中的hash(xxxx)方法是ExecutorRouteConsistentHash的私有方法，它的作用就是将传入字符串进行md5计算，然后将得到的值截取前4个Byte转成Long类型。
举例说明，比如现在executor的address为a1、a2、a3、a4，经过上述虚拟化计算后，得到的TreeMap如下：
TreeMap中的key是Long类型且有序递增，value就是打散后的a1、a2、a3、a4了。需要注意的是，上述key是为了说明而设置的，真实计算得到的值可能并不是如此。
整个TreeMap中的元素最多400，不排除存在key相同从而导致覆盖的情况，比如hash(&amp;quot;a1-NODE-10&amp;quot;) 与 hash(&amp;quot;a4-NODE-32&amp;quot;) 得到的结果都是47，这样后放入的value会覆盖先放入的。
得到这个TreeMap后，接着就是根据jobId得到对应的address了，这里同样也对jobId进行hash求值，然后拿jobId的哈希值去切分TreeMap：
SortedMap&amp;lt;Long, String&amp;gt; lastRing = addressRing.tailMap(jobHash); 假设jobId的hash值是30，以上图为例，tailMap的操作后得到的结果如下：
对于一致性hash算法来说，得到的lastRing的首个元素的value值就是求得的address了。回到我们示例中，32对应的a2就是一致性hash算法计算得到的地址。
如果jobId的hash值是164，大于163了，大于key的最大值163，此时得到的lastRing里什么元素也没有，应该如何呢？此时直接取TreeMap中的第一个元素就可以了。回到我们示例中，即14对应的a3就是一致性hash算法计算得到的地址。
最不经常使用 处理随机策略的类是ExecutorRouteLFU，
/** * 保存任务的执行次数 * jobLfuMap 内容：&amp;lt;任务id, &amp;lt;executor地址, 任务在该executor上执行的次数&amp;gt;&amp;gt; */ private static ConcurrentMap&amp;lt;Integer, HashMap&amp;lt;String, Integer&amp;gt;&amp;gt; jobLfuMap = new ConcurrentHashMap&amp;lt;Integer, HashMap&amp;lt;String, Integer&amp;gt;&amp;gt;(); /** 任务执行次数的缓存时间 */ private static long CACHE_VALID_TIME = 0; /** * 计算使用哪个 executor */ public String route(int jobId, List&amp;lt;String&amp;gt; addressList) { // 1. 如果缓存到期，清除 jobLfuMap if (System.currentTimeMillis() &amp;gt; CACHE_VALID_TIME) { jobLfuMap.clear(); // 缓存一天 CACHE_VALID_TIME = System.currentTimeMillis() &#43; 1000*60*60*24; } // 2. 创建 lfuItemMap HashMap&amp;lt;String, Integer&amp;gt; lfuItemMap = jobLfuMap.get(jobId); if (lfuItemMap == null) { lfuItemMap = new HashMap&amp;lt;String, Integer&amp;gt;(); jobLfuMap.putIfAbsent(jobId, lfuItemMap); } // 3. 初始化 lfuItemMap for (String address: addressList) { if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) &amp;gt;1000000 ) { // 初始化时主动Random一次，缓解首次压力 lfuItemMap.put(address, new Random().nextInt(addressList.size())); } } // 4. 去重无用的地址 List&amp;lt;String&amp;gt; delKeys = new ArrayList&amp;lt;&amp;gt;(); for (String existKey: lfuItemMap.keySet()) { if (!addressList.contains(existKey)) { delKeys.add(existKey); } } if (delKeys.size() &amp;gt; 0) { for (String delKey: delKeys) { lfuItemMap.remove(delKey); } } // 5. 对执行次数排序 List&amp;lt;Map.Entry&amp;lt;String, Integer&amp;gt;&amp;gt; lfuItemList = new ArrayList&amp;lt;Map.Entry&amp;lt;String, Integer&amp;gt;&amp;gt;(lfuItemMap.entrySet()); Collections.sort(lfuItemList, new Comparator&amp;lt;Map.Entry&amp;lt;String, Integer&amp;gt;&amp;gt;() { @Override public int compare(Map.Entry&amp;lt;String, Integer&amp;gt; o1, Map.Entry&amp;lt;String, Integer&amp;gt; o2) { return o1.getValue().compareTo(o2.getValue()); } }); // 6. 获取执行次数最少的address Map.Entry&amp;lt;String, Integer&amp;gt; addressItem = lfuItemList.get(0); String minAddress = addressItem.getKey(); addressItem.setValue(addressItem.getValue() &#43; 1); return addressItem.getKey(); } ExecutorRouteLFU中有两个属性：
jobLfuMap：类型是ConcurrentMap，它保存的key-value为&amp;lt;任务id, &amp;lt;executor地址, 任务在该executor上执行的次数&amp;gt;&amp;gt;，嵌套型的ConcurrentMap，用表格示意大概是这样：
CACHE_VALID_TIME：类型是long，记录了缓存过期时间
计算使用哪个executor的关键在于route(int, List&amp;lt;String&amp;gt;)，下面我们来逐一分析它的功能。
1. 清除 jobLfuMap 每次执行方法时，都会判断缓存是否到期，如果到期，就清理jobLfuMap，相当于重新统计任务执行次数。目前缓存时间是1000*60*60*24毫秒，也就是1天。
2. 创建 lfuItemMap 如果jobId对应的lfuItemMap不存在，就新建一个，并放入jobLfuMap中。lfuItemMap的key是address，value是任务在该executor上执行的次数。
值得一提的，为了避免覆盖，代码中使用的是putIfAbsent(...)方法：
lfuItemMap = new HashMap&amp;lt;String, Integer&amp;gt;(); jobLfuMap.putIfAbsent(jobId, lfuItemMap); 关于putIfAbsent(...)方法，ConcurrentHasmMap的注释如下：
该方法的作用是，仅在指定的key不存在进才放入map中，这点想必大家都知道，而它的返回值就有意思了：在原来有值的情况下，返回原来的值，否则返回null。
实际上这里直接使用put(...)方法就可以了，在《执行器揭秘》一文的分析中可以得知，xxl-job传为每个jobId创建一个jobThread，即同一个jobId产生的任务由同一个jobThread来执行，不会产生并发问题。
3. 初始化 lfuItemMap 初始化方式就比较简单了，对于不在jobItemMap中的address，放入其中并且指定一个随机数值。
4. 去重无用的地址 如果运行途中有executor实例挂了，那executor的地址就不会出现在addressList中了，此时需要把这些非存活状态的address从jobItemMap中剔除出去。关于Map的遍历-删除操作，这些都是java基础内容，这里就不多说了。
结合3、4步，这里以一个实例来说明下操作流程：
假设 jobItemMap 中存在address为：
现阶段存活的addressList 为：
即 a1 跟 a2 挂了，新增了 a5, a6 两个实例
经过第3步后，jobItemMap 中存在address为：
即新增了 a5, a6 两个实例
经过第4步后，jobItemMap 中存在address为：
即剔除了 a1, a2 两个非存活状态的实例
这样之后，jobItemMap中的address就都是存活的executor实例了。
5. 对执行次数排序 xxl-job在这块处理的比较简单，流程如下：
使用lfuItemMap.entrySet()方法，得到lfuItemMap的entrySet 使用new ArrayList(entrySet)的方式，将第1步得到的entrySet转成List类型，List存放的元素为Map.Entry，这个key就是address，value就是该address执行任务的次数 使用Collections.sort对第2步得到的List排序，排序对象为Map.Entry的value，规则为Integer类型由小到大 6. 获取执行次数最少的address 在第5步排序完成之后，在lfuItemList中的第一个元素就是执行次数最小的executor了（即最不经常使用的executor），直接使用lfuItemList.get(0)获取就行了。
获取后，就表示该executor执行1次任务了，别忘了执行次数&#43;1的操作：
addressItem.setValue(addressItem.getValue() &#43; 1); 最近最久未使用 处理最近最久未使用的类为ExecutorRouteLRU，代码如下：
/** * 保存任务的执行地址 * jobLRUMap 内容：&amp;lt;任务id, &amp;lt;executor地址, executor地址&amp;gt;&amp;gt; */ private static ConcurrentMap&amp;lt;Integer, LinkedHashMap&amp;lt;String, String&amp;gt;&amp;gt; jobLRUMap = new ConcurrentHashMap&amp;lt;Integer, LinkedHashMap&amp;lt;String, String&amp;gt;&amp;gt;(); /** 任务执行次数的缓存时间 */ private static long CACHE_VALID_TIME = 0; /** * 计算使用哪个 executor */ public String route(int jobId, List&amp;lt;String&amp;gt; addressList) { // 1. 清理缓存 if (System.currentTimeMillis() &amp;gt; CACHE_VALID_TIME) { jobLRUMap.clear(); CACHE_VALID_TIME = System.currentTimeMillis() &#43; 1000*60*60*24; } // 2. 创建 lruItem LinkedHashMap&amp;lt;String, String&amp;gt; lruItem = jobLRUMap.get(jobId); if (lruItem == null) { lruItem = new LinkedHashMap&amp;lt;String, String&amp;gt;(16, 0.75f, true); jobLRUMap.putIfAbsent(jobId, lruItem); } // 3. 初始化 lruItem for (String address: addressList) { if (!lruItem.containsKey(address)) { lruItem.put(address, address); } } // 4. 移除无用的地址 List&amp;lt;String&amp;gt; delKeys = new ArrayList&amp;lt;&amp;gt;(); for (String existKey: lruItem.keySet()) { if (!addressList.contains(existKey)) { delKeys.add(existKey); } } if (delKeys.size() &amp;gt; 0) { for (String delKey: delKeys) { lruItem.remove(delKey); } } // 5. 获取最近最久未使用的地址 String eldestKey = lruItem.entrySet().iterator().next().getKey(); // 6. 通过 get(...) 方法访问元素 String eldestValue = lruItem.get(eldestKey); return eldestValue; } ExecutorRouteLRU中有两个属性：
jobLRUMap：类型是ConcurrentMap，它保存的key-value为&amp;lt;任务id, &amp;lt;executor地址, executor地址&amp;gt;&amp;gt;，嵌套型的Map，内部嵌套了一个LinkedHashMap，实现LRU的关键就是这个LinkedHashMap了。整个结构用表格示意大概是这样：
CACHE_VALID_TIME：类型是long，记录了缓存过期时间
计算使用哪个executor的关键在于route(int, List&amp;lt;String&amp;gt;)，它的步骤如下：
清理缓存 创建 lruItem 初始化 lruItem 移除无用的地址 获取最近最久未使用的地址 通过 get(&amp;hellip;) 方法访问元素 从代码形式上看，ExecutorRouteLRU的route(int, List&amp;lt;String&amp;gt;)方法与ExecutorRouteLFU的route(int, List&amp;lt;String&amp;gt;)方法非常相似，在本小节我们重点关注功能的实现。
最近最久未使用（LRU）的实现关键在于LinkedHashMap，注意它的实例化：
lruItem = new LinkedHashMap&amp;lt;String, String&amp;gt;(16, 0.75f, true); 该构造方法的注释如下：
重点关键accessOrder的值：true 表示访问顺序排序（get/put时排序），false表示插入顺序排序。
与HashMap不同，在LinkedHashMap中，除了数组&#43;链表（或数组&#43;红黑树）来保存元素外，还有一个双向链表来维护元素的顺序：
代码节选自 java.util.LinkedHashMap
/** * 双向链表的结点 */ static class Entry&amp;lt;K,V&amp;gt; extends HashMap.Node&amp;lt;K,V&amp;gt; { Entry&amp;lt;K,V&amp;gt; before, after; Entry(int hash, K key, V value, Node&amp;lt;K,V&amp;gt; next) { super(hash, key, value, next); } } /** 双向链表的头节点 */ transient LinkedHashMap.Entry&amp;lt;K,V&amp;gt; head; /** 双向链表的尾节点 */ transient LinkedHashMap.Entry&amp;lt;K,V&amp;gt; tail; 查看代码可以发现，在LinkedHashMap的put/get操作时，都会维护这个链表（在put操作时，维护链表的操作在newNode(...)方法中）。正是由于这个链表的存在，LinkedHashMap才能保证元素有序。
代码中传入的是true，表示按访问顺序排序，即调用完get/put后，即访问过的元素就会被移到链表的尾部，双向链表的第一个元素就是最久未使用的地址了。
有了LinkedHashMap的一些了解后，再来看route(int, List&amp;lt;String&amp;gt;)就比较清晰了，实际上前面的4步：
清理缓存 创建 lruItem 初始化 lruItem 移除无用的地址 都是在构建包含有效address的LinkedHashMap，此时的LinkedHashMap中首个元素就是最久未使用的address了，接着就是取首个元素操作：
String eldestKey = lruItem.entrySet().iterator().next().getKey(); 在 LinkedHashMap 中，key与value都是address，这里得到的eldestKey就是最久未使用的address了。不过到这里还没完，还要把这个address移到LinkedHashMap的最后位置，下次再获取首个元素时就又是最久未使用的address了，这就是get的操作：
String eldestValue = lruItem.get(eldestKey); 总的来说，最近最久未使用（LRU）的实现利用了LinkedHashMap的几个特点：
构造方法中accessOrder传true表示按访问元素排序 accessOrder=true的情况下，访问过的元素就会被移到链表的尾部，即首个元素就是最久未使用的元素 get的操作就是访问操作 故障转移 处理故障转移的类是ExecutorRouteFailover，代码如下：
@Override public ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList) { StringBuffer beatResultSB = new StringBuffer(); for (String address : addressList) { // beat ReturnT&amp;lt;String&amp;gt; beatResult = null; try { ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); // 判断是否存活 beatResult = executorBiz.beat(); } catch (Exception e) { logger.error(e.getMessage(), e); beatResult = new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;&amp;#34;&#43;e ); } // 结果信息 beatResultSB.append( (beatResultSB.length()&amp;gt;0)?&amp;#34;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;#34;:&amp;#34;&amp;#34;) .append(I18nUtil.getString(&amp;#34;jobconf_beat&amp;#34;) &#43; &amp;#34;：&amp;#34;) .append(&amp;#34;&amp;lt;br&amp;gt;address：&amp;#34;).append(address) .append(&amp;#34;&amp;lt;br&amp;gt;code：&amp;#34;).append(beatResult.getCode()) .append(&amp;#34;&amp;lt;br&amp;gt;msg：&amp;#34;).append(beatResult.getMsg()); // executor 存活，return if (beatResult.getCode() == ReturnT.SUCCESS_CODE) { beatResult.setMsg(beatResultSB.toString()); beatResult.setContent(address); return beatResult; } } return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, beatResultSB.toString()); } 整个方法的流程如下：
遍历所有的executor 对每一个executor，判断是否存活，如果存活就返回该executor，结束；否则就继续判断下一个executor是否存活 如果最终没有存活的executor，返回失败 方法流程比较简单，重点在于如果判断executor是否存活，代码中调用的是executorBiz.beat()，跟进去就到ExecutorBizClient#beat：
@Override public ReturnT&amp;lt;String&amp;gt; beat() { return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;beat&amp;#34;, accessToken, timeout, &amp;#34;&amp;#34;, String.class); } 注意到路由策略的执行都是在admin实例，在ExecutorBizClient#beat方法中会发送http请求到executor来探测该executor是否存活，admin到executor的通讯流程在《admin与executor通讯》一文中已经分析过了，这里我们进入executor中对应的处理方法ExecutorBizImpl#beat：
@Override public ReturnT&amp;lt;String&amp;gt; beat() { return ReturnT.SUCCESS; } 方法中只是返回了一个成功标识，除此之外并无其他逻辑。
忙碌转移 处理忙碌转移策略的类是ExecutorRouteBusyover，方法如下：
public ReturnT&amp;lt;String&amp;gt; route(TriggerParam triggerParam, List&amp;lt;String&amp;gt; addressList) { StringBuffer idleBeatResultSB = new StringBuffer(); for (String address : addressList) { // beat ReturnT&amp;lt;String&amp;gt; idleBeatResult = null; try { ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); // 判断当前executor是否忙碌 idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId())); } catch (Exception e) { logger.error(e.getMessage(), e); idleBeatResult = new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;&amp;#34;&#43;e ); } // 处理参数 idleBeatResultSB.append( (idleBeatResultSB.length()&amp;gt;0)?&amp;#34;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;#34;:&amp;#34;&amp;#34;) .append(I18nUtil.getString(&amp;#34;jobconf_idleBeat&amp;#34;) &#43; &amp;#34;：&amp;#34;) .append(&amp;#34;&amp;lt;br&amp;gt;address：&amp;#34;).append(address) .append(&amp;#34;&amp;lt;br&amp;gt;code：&amp;#34;).append(idleBeatResult.getCode()) .append(&amp;#34;&amp;lt;br&amp;gt;msg：&amp;#34;).append(idleBeatResult.getMsg()); // 返回成功标识，表示当前executor不忙碌，return if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) { idleBeatResult.setMsg(idleBeatResultSB.toString()); idleBeatResult.setContent(address); return idleBeatResult; } } return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, idleBeatResultSB.toString()); } 忙碌转移策略的代码形式与故障转移基本一样，处理流程如下： 整个方法的流程如下：
遍历所有的executor 对每一个executor，判断是否忙碌，如果不忙碌就返回该executor，结束；否则就继续判断下一个executor是否存活 如果最终没有空闲的executor，返回失败 检测executor忙碌的方法为executorBiz.idleBeat(xxx)，跟进该方法就到了ExecutorBizClient#idleBeat：
@Override public ReturnT&amp;lt;String&amp;gt; idleBeat(IdleBeatParam idleBeatParam){ return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;idleBeat&amp;#34;, accessToken, timeout, idleBeatParam, String.class); } 通故障转移中的ExecutorBizClient#beat一样，该方法也是通过http请求到executor上来检测当前executor是否处于空闲中。废话不多说，直接进入executor实例上对应的处理方法ExecutorBizImpl#idleBeat：
@Override public ReturnT&amp;lt;String&amp;gt; idleBeat(IdleBeatParam idleBeatParam) { boolean isRunningOrHasQueue = false; JobThread jobThread = XxlJobExecutor.loadJobThread(idleBeatParam.getJobId()); // 忙碌条件 if (jobThread != null &amp;amp;&amp;amp; jobThread.isRunningOrHasQueue()) { isRunningOrHasQueue = true; } if (isRunningOrHasQueue) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;job thread is running or has trigger queue.&amp;#34;); } return ReturnT.SUCCESS; } 代码中出现了JobThread这个类，在《执行器揭秘》一文中，对JobThread已经分析过，这里就不多作介绍了。
代码中，判断是否忙碌的处理为：
jobThread != null &amp;amp;&amp;amp; jobThread.isRunningOrHasQueue() 对jobThread != null条件，在jobThread不为null时，可能并没有任务在执行，jobThread只是在等待任务的到来；因此还需要进一步判断，且看jobThread.isRunningOrHasQueue()方法：
public boolean isRunningOrHasQueue() { return running || triggerQueue.size()&amp;gt;0; } 这个方法非常之简单，只是判断了running的值与triggerQueue的长度，两个值在哪里更改的呢？
running 我们来看看JobThread 的 run() 方法：
/** 是否执行任务的标识 */ private boolean running = false; @Override public void run() { // init try { handler.init(); } catch (Throwable e) { logger.error(e.getMessage(), e); } // execute while(!toStop){ running = false; idleTimes&#43;&#43;; TriggerParam triggerParam = null; try { triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS); if (triggerParam!=null) { // 任务在执行，变更 running 的值 running = true; ... } } ... } } 从代码来看，executor在执行该任务时，running就会变成true，表明此时不空闲了。
triggerQueue 关于triggerQueue，在《执行器揭秘》一文中已经分析过，这里总结如下：
在executor接收到执行该任务的请求时，并不是直接执行该任务，只是将任务放入jobThread中的triggerQueue中，等待jobThread执行。 如果triggerQueue的长度大于0，表示jobThread来不及执行而导致任务堆积了，此时表明executor处于忙碌中。 总结 路由策略，即选择使用哪个executor来执行任务的策略。本文分析了xxl-job中10大路由策略：
分片广播 第一个 最后一个 轮询 随机 一致性hash 最不经常使用 最近最久未使用 故障转移 忙碌转移 从代码的角度来分析了这些路由策略的实现过程，了解这些策略背后的实现原理后，在我们平时使用的xxl-job过程中，可以更好地选择合适的策略。
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob08：任务执行流程（三）之执行器揭秘</title>
        <url>https://www.szlinkroutes.com/post/xxljob08%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%89%E4%B9%8B%E6%89%A7%E8%A1%8C%E5%99%A8%E6%8F%AD%E7%A7%98/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
在上一篇文章中，要执行的任务终于通过ExecutorBiz.run(...)发往了executor，由此开始了executor上任务的执行操作。关于admin到executor之间的通讯，在《admin与executor通讯》一文中已详细分析过了，这里我们直接看executor进程中ExecutorBiz.run(...)方法的操作。
ExecutorBizImpl#run 获取 jobHandler 进入 ExecutorBizImpl#run 方法，内容如下：
public ReturnT&amp;lt;String&amp;gt; run(TriggerParam triggerParam) { // load old：jobHandler &#43; jobThread JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId()); IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null; String removeOldReason = null; // 先分析这么多，剩下的接下来再分析 ... 方法一开始，就从XxlJobExecutor中获取JobThread，然后从JobThread中获取IJobHandler。这简单的3行代码，一下子出现了3个类，下面一一来分析。
XxlJobExecutor.loadJobThread 关于XxlJobExecutor，从名称上来看，就是处理xxlJob任务执行逻辑的，在《执行器启动流程》一文中对该类的部分功能也做过分析，这里我们直接来看loadJobThread(...)相关方法的内容：
private static ConcurrentMap&amp;lt;Integer, JobThread&amp;gt; jobThreadRepository = new ConcurrentHashMap&amp;lt;Integer, JobThread&amp;gt;(); public static JobThread loadJobThread(int jobId){ JobThread jobThread = jobThreadRepository.get(jobId); return jobThread; } 在XxlJobExecutor中，有一个ConcurrentMap结构，key是jobId，value是jobThread，loadJobThread(xxx)方法就是从这个ConcurrentMap中获取jobThread的操作了。
关于JobThread放入jobThreadRepository中的操作，我们下面会分析到。
IJobHandler IJobHandler是一个接口，其中定义了3个方法：
/** * job handler * * @author xuxueli 2015-12-19 19:06:38 */ public abstract class IJobHandler { /** * 处理任务的执行，当executor收到调度请求时被调用 * execute handler, invoked when executor receives a scheduling request * * @throws Exception */ public abstract void execute() throws Exception; /*@Deprecated public abstract ReturnT&amp;lt;String&amp;gt; execute(String param) throws Exception;*/ /** * 初始化 handler，当 JobThread 初始化时被调用 * init handler, invoked when JobThread init */ public void init() throws Exception { // do something } /** * 销毁 handler，当 JobThread 销毁时被调用 * destroy handler, invoked when JobThread destroy */ public void destroy() throws Exception { // do something } } 每个方法的作用代码中已给出了注释，这个IJobHandler就是执行任务的组件了，它有3个子类：
在管理后台新增任务时，可以指定任务的运行模式：
上述IJobHandler的3个子类就是用来处理这些运行模式的，各运行模式与对应IJobHandler的关系如下：
本系列文章我们仅关注BEAN模式下任务的执行，后续我们重点关注MethodJobHandler。
JobThread JobThread的部分内容如下：
public class JobThread extends Thread { private static Logger logger = LoggerFactory.getLogger(JobThread.class); /** 任务id */ private int jobId; /** 处理该处理的 handler */ private IJobHandler handler; /** 队列，用于保存待执行的任务 */ private LinkedBlockingQueue&amp;lt;TriggerParam&amp;gt; triggerQueue; /** 任务日志id，用来去重 */ private Set&amp;lt;Long&amp;gt; triggerLogIdSet; ... } 上述代码仅展示了JobThread部分属性，对该类几点说明如下：
JobThread 继承了 Thread，它的run()用来处理任务的具体执行操作，这点我们后面再分析 JobThread 中有两个属性：jobId与handler，即每一个任务，都有一个对应的handler来处理该任务的执行 JobThread 中triggerQueue用来保存即将执行的任务，即任务是异步执行的，先放入triggerQueue中，再另一个线程（JobThread）从triggerQueue中取出并执行，这部分的内容我们后面会分析 注意到 JobThread 有一个属性：triggerLogIdSet，这是一个Set类型，用来保存jobLogId，可以避免任务重复执行。对于同一个任务（jobId），每次执行都会生成一条记录（jobLog）从而得到一个jobLogId，如果一个jobLogId在triggerLogIdSet中已存在，这就表明本次是重复执行。 GlueType 的处理 让我们回到ExecutorBizImpl#run继续往下：
// 获取 GlueTypeEnum GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType()); if (GlueTypeEnum.BEAN == glueTypeEnum) { // 加载 IJobHandler IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler( triggerParam.getExecutorHandler()); // 判断新的 JobHandler 与 jobThread 中的 JobHandler是否相同 if (jobThread!=null &amp;amp;&amp;amp; jobHandler != newJobHandler) { removeOldReason = &amp;#34;change jobhandler or glue type, &amp;#34; &#43; &amp;#34;and terminate the old job thread.&amp;#34;; // 不相同，置为 null jobThread = null; jobHandler = null; } // 使用新的 JobHandler，最终得到的 JobHandler 就是最新的了 if (jobHandler == null) { jobHandler = newJobHandler; if (jobHandler == null) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;job handler [&amp;#34; &#43; triggerParam.getExecutorHandler() &#43; &amp;#34;] not found.&amp;#34;); } } } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) { // 省略 ... } else if (glueTypeEnum!=null &amp;amp;&amp;amp; glueTypeEnum.isScript()) { // 省略 ... } else { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;glueType[&amp;#34; &#43; triggerParam.getGlueType() &#43; &amp;#34;] is not valid.&amp;#34;); } 这一步是根据GlueTypeEnum来获取对应的jobHandler，本系列文章重点关注BEAN运行模式，因此其他运行模式的代码进行了删减，我们也逐步来分析这块的操作。
GlueTypeEnum GlueTypeEnum内容如下：
public enum GlueTypeEnum { BEAN(&amp;#34;BEAN&amp;#34;, false, null, null), GLUE_GROOVY(&amp;#34;GLUE(Java)&amp;#34;, false, null, null), GLUE_SHELL(&amp;#34;GLUE(Shell)&amp;#34;, true, &amp;#34;bash&amp;#34;, &amp;#34;.sh&amp;#34;), GLUE_PYTHON(&amp;#34;GLUE(Python)&amp;#34;, true, &amp;#34;python&amp;#34;, &amp;#34;.py&amp;#34;), GLUE_PHP(&amp;#34;GLUE(PHP)&amp;#34;, true, &amp;#34;php&amp;#34;, &amp;#34;.php&amp;#34;), GLUE_NODEJS(&amp;#34;GLUE(Nodejs)&amp;#34;, true, &amp;#34;node&amp;#34;, &amp;#34;.js&amp;#34;), GLUE_POWERSHELL(&amp;#34;GLUE(PowerShell)&amp;#34;, true, &amp;#34;powershell&amp;#34;, &amp;#34;.ps1&amp;#34;); // 省略其他 ... } 这部分内容与界面配置的运行模式一致，就不多作分析了。
BEAN模式下获取IJobHandler 获取IJobHandler的代码为
IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler( triggerParam.getExecutorHandler()); 同样是XxlJobExecutor，前面分析了XxlJobExecutor.loadJobThread方法，这里我们再来分析XxlJobExecutor.loadJobHandler方法：
/** * 用来保存 executorHandler 对应的 IJobHandler * key: executorHandler * value: IJobHandler */ private static ConcurrentMap&amp;lt;String, IJobHandler&amp;gt; jobHandlerRepository = new ConcurrentHashMap&amp;lt;String, IJobHandler&amp;gt;(); /** * 从 jobHandlerRepository 中获取 IJobHandler */ public static IJobHandler loadJobHandler(String name){ return jobHandlerRepository.get(name); } 可以看到，在XxlJobExecutor中，有一个jobHandlerRepository属性用来保存executorHandler对应的IJobHandler.
那么何谓executorHandler呢？我们在编写任务时，是这个进行的：
@XxlJob(&amp;#34;demoJobHandler&amp;#34;) public void demoJobHandler() throws Exception { ... } @XxlJob注解中的demoJobHandler就是executorHandler了。
这个IJobHandler是什么时候放入jobHandlerRepository中的呢？在《执行器启动流程》一文中，我们分析过xxl-job对@XxlJob注解的处理，IJobHandler就是在这个阶段放入jobHandlerRepository中的，具体参考XxlJobSpringExecutor#initJobHandlerMethodRepository方法。
阻塞策略 回到 ExecutorBizImpl#run 方法，继续：
// 处理阻塞策略 if (jobThread != null) { ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match( triggerParam.getExecutorBlockStrategy(), null); if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) { // 丢弃后续调度 if (jobThread.isRunningOrHasQueue()) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;block strategy effect：&amp;#34; &#43; ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle()); } } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) { // 覆盖之前调度 if (jobThread.isRunningOrHasQueue()) { removeOldReason = &amp;#34;block strategy effect：&amp;#34; &#43; ExecutorBlockStrategyEnum.COVER_EARLY.getTitle(); jobThread = null; } } else { // 默认策略：什么也不处理 } } if (jobThread == null) { // 注册线程，即一个job对应一个jobThread对象 jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason); } 这部分是来处理阻塞策略的，何谓阻塞策略呢？如果一个任务处于执行中，又收到了该任务的执行请求，这就造成了任务的阻塞。
在xxl-job中，支持如下阻塞策略：
阻塞策略的枚举类为ExecutorBlockStrategyEnum：
public enum ExecutorBlockStrategyEnum { /** 单机串行 */ SERIAL_EXECUTION(&amp;#34;Serial execution&amp;#34;), /** 丢弃后续调度 */ DISCARD_LATER(&amp;#34;Discard Later&amp;#34;), /** 覆盖之前调度 */ COVER_EARLY(&amp;#34;Cover Early&amp;#34;); // 省略其他代码 ... } 这块正是对应了管理后台任务编辑界面的策略选择了。接下来我们具体分析这3个阻塞策略。
单机串行 所谓的“单机串行”，指的是在每一个executor，任务都是串行执行。虽然该策略叫单机串行且是默认策略（位于else代码块），但是代码中并没有对该策略进行任何操作，那么为什么该策略会叫单机串行呢？
此时我们还未分析到jobThread执行任务的流程，但不妨碍我们先“剧透”下这块的流程：下面的操作中，我们可以看到任务会添加到jobThread的triggerQueue队列中，之后jobThread的线程会从triggerQueue一个一个地获取任务，然后执行。
这里需要注意两点：
任务是先放入JobThread#triggerQueue中再执行的 任务执行时，同一个jobId都是由同一个线程执行的 由这两点可以看出，任务确实是串行执行的。
丢弃后续调度 处理该策略的代码如下：
if (jobThread.isRunningOrHasQueue()) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;block strategy effect：&amp;#34; &#43; ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle()); } 如果jobThread中有任务在执行，就返回失败，即丢弃了该任务。从代码来看，丢弃后续调度可概括为丢弃当前任务的执行请求。
判断任务是否在执行的方法为jobThread.isRunningOrHasQueue()，我们来看看它做了什么，进入JobThread#isRunningOrHasQueue：
public boolean isRunningOrHasQueue() { return running || triggerQueue.size()&amp;gt;0; } 这里有两个判断条件：
running triggerQueue.size() 先来看看running，它的更新位于JobThread#run()方法中：
/** 是否执行任务的标识 */ private boolean running = false; @Override public void run() { // init try { handler.init(); } catch (Throwable e) { logger.error(e.getMessage(), e); } // execute while(!toStop){ running = false; idleTimes&#43;&#43;; TriggerParam triggerParam = null; try { triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS); if (triggerParam!=null) { // 任务在执行，变更 running 的值 running = true; ... } } ... } } 从代码来看，executor在执行该任务时，running就会变成true，表明正在执行任务。
再来说说triggerQueue，在前面分析JobThread时，提到JobThread中的triggerQueue属性用于存放待执行的任务的，因此当triggerQueue.size() &amp;gt; 0时，就表明待执行任务数大于0，即JobThread中有正在执行的任务，来不及从triggerQueue中取出任务来执行。
覆盖之前调度 处理该策略的方法如下：
if (jobThread.isRunningOrHasQueue()) { removeOldReason = &amp;#34;block strategy effect：&amp;#34; &#43; ExecutorBlockStrategyEnum.COVER_EARLY.getTitle(); jobThread = null; } 从代码来看，如果有任务在执行，将jobThread置为null就完了。
但事实并没有这么简单，当jobThread为null时，接下来就有这么一段代码：
if (jobThread == null) { // 注册线程，即一个job对应一个jobThread对象 jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason); } 从表面来看，这个方法是处理jobHandler操作，我们进入XxlJobExecutor.registJobThread方法：
public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason) { JobThread newJobThread = new JobThread(jobId, handler); // 启动线程 newJobThread.start(); logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job regist JobThread success, jobId:{}, handler:{}&amp;#34;, new Object[]{jobId, handler}); // put 操作会返回旧的值 JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);	if (oldJobThread != null) { // 更新jobThread，并且关闭旧的线程 oldJobThread.toStop(removeOldReason); oldJobThread.interrupt(); } return newJobThread; } 这块代码比较简单，就是创建线程，接着启动线程，然后将线程放入jobThreadRepository中。这里需要特别注意jobThreadRepository.put(...)操作，在Map中，put(...)的返回值为旧的值：
因此这里的oldJobThread，就是旧的jobThread，在该值不为null时，会进行停止操作：
oldJobThread.toStop(removeOldReason); oldJobThread.interrupt(); 关于线程停止操作，在xxl-job中都是基于一个套路，这里就不赘述了。
分析到这里，就可以看到，覆盖之前调度的策略会停止旧的执行线程，总结下该策略的执行操作：
将当前jobThread置为null 创建新的jobThread，并放入jobThreadRepository 停止jobThreadRepository中旧的jobThread 保存执行数据 继续分析ExecutorBizImpl#run方法：
ReturnT&amp;lt;String&amp;gt; pushResult = jobThread.pushTriggerQueue(triggerParam); return pushResult; } 这是该方法的收尾操作了，这块就是将任务添加到jobThread的triggerQueue队列的操作，进入JobThread#pushTriggerQueue 方法：
public ReturnT&amp;lt;String&amp;gt; pushTriggerQueue(TriggerParam triggerParam) { // 使用 triggerLogIdSet 进行去重操作 if (triggerLogIdSet.contains(triggerParam.getLogId())) { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; repeate trigger job, logId:{}&amp;#34;, triggerParam.getLogId()); return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;repeate trigger job, logId:&amp;#34; &#43; triggerParam.getLogId()); } // 两个结构中都要添加 triggerLogIdSet.add(triggerParam.getLogId()); triggerQueue.add(triggerParam); return ReturnT.SUCCESS; } 前面提到过JobThread的triggerLogIdSet属性，这里可以清晰地看到它的去重操作：对于某一个任务（jobId），每次执行都会生成一条记录（jobLog）从而得到一个jobLogId，如果一个jobLogId在triggerLogIdSet中已存在，这就表明本次是重复执行。
比如，jobId为2的任务执行了4次，生成了4条jobLogId:
如果jobLogId为3的记录已经存在，就表示jobLogId=3重复执行了。
再来看看添加操作，所谓的添加操作就是调用队列的add(...)方法，这里就不多说了。
到了这里，ExecutorBizImpl#run的调用就完结了，即任务调度完成，admin到executor的http请求已经结束了。
任务调度虽然完成，但任务的执行还远没结束，调度结果是成功还是失败admin并不知道，我们还需要继续往下分析。
任务的执行：JobThread#run 前面我们多次提到，任务的执行是在JobThread线程中，我们直接来看看JobThread#run方法：
public void run() { // 1. 执行初始化方法 try { handler.init(); } catch (Throwable e) { logger.error(e.getMessage(), e); } // 2. while 循环中执行 while(!toStop){ running = false; idleTimes&#43;&#43;; TriggerParam triggerParam = null; try { // 3. 从队列中获取待任务，注意到这是一个阻塞操作，时间为3s triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS); if (triggerParam!=null) { running = true; idleTimes = 0; triggerLogIdSet.remove(triggerParam.getLogId()); // 4. 执行前的准备 // 日志名称 String logFileName = XxlJobFileAppender.makeLogFileName( new Date(triggerParam.getLogDateTime()), triggerParam.getLogId()); // 上下文参数 XxlJobContext xxlJobContext = new XxlJobContext( triggerParam.getJobId(), triggerParam.getExecutorParams(), logFileName, triggerParam.getBroadcastIndex(), triggerParam.getBroadcastTotal()); // init job context XxlJobContext.setXxlJobContext(xxlJobContext); // execute XxlJobHelper.log(&amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job execute start &amp;#34; &#43; &amp;#34;-----------&amp;lt;br&amp;gt;----------- Param:&amp;#34; &#43; xxlJobContext.getJobParam()); // 5. 执行任务 if (triggerParam.getExecutorTimeout() &amp;gt; 0) { // 处理任务超时 Thread futureThread = null; try { FutureTask&amp;lt;Boolean&amp;gt; futureTask = new FutureTask&amp;lt;Boolean&amp;gt;( new Callable&amp;lt;Boolean&amp;gt;() { @Override public Boolean call() throws Exception { // init job context XxlJobContext.setXxlJobContext(xxlJobContext); // 这里执行了任务的具体内容 handler.execute(); return true; } }); futureThread = new Thread(futureTask); futureThread.start(); // 在 futureTask 中执行 Boolean tempResult = futureTask.get( triggerParam.getExecutorTimeout(), TimeUnit.SECONDS); } catch (TimeoutException e) { XxlJobHelper.log(&amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job execute timeout&amp;#34;); XxlJobHelper.log(e); // handle result XxlJobHelper.handleTimeout(&amp;#34;job execute timeout &amp;#34;); } finally { futureThread.interrupt(); } } else { // just execute // 这里执行了任务的具体内容 handler.execute(); } // 6. 处理执行结果 if (XxlJobContext.getXxlJobContext().getHandleCode() &amp;lt;= 0) { XxlJobHelper.handleFail(&amp;#34;job handle result lost.&amp;#34;); } else { String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg(); tempHandleMsg = (tempHandleMsg!=null&amp;amp;&amp;amp;tempHandleMsg.length()&amp;gt;50000) ?tempHandleMsg.substring(0, 50000).concat(&amp;#34;...&amp;#34;) :tempHandleMsg; XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg); } XxlJobHelper.log(&amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job execute end(finish) &amp;#34; &#43; &amp;#34;-----------&amp;lt;br&amp;gt;----------- Result: handleCode=&amp;#34; &#43; XxlJobContext.getXxlJobContext().getHandleCode() &#43; &amp;#34;, handleMsg = &amp;#34; &#43; XxlJobContext.getXxlJobContext().getHandleMsg() ); } else { // 轮空达到30次，且队列中无待执行的任务，清除当前线程 if (idleTimes &amp;gt; 30) { if(triggerQueue.size() == 0) { XxlJobExecutor.removeJobThread(jobId, &amp;#34;excutor idel times over limit.&amp;#34;); } } } } catch (Throwable e) { if (toStop) { XxlJobHelper.log(&amp;#34;&amp;lt;br&amp;gt;----------- JobThread toStop, stopReason:&amp;#34; &#43; stopReason); } // handle result StringWriter stringWriter = new StringWriter(); e.printStackTrace(new PrintWriter(stringWriter)); String errorMsg = stringWriter.toString(); XxlJobHelper.handleFail(errorMsg); XxlJobHelper.log(&amp;#34;&amp;lt;br&amp;gt;----------- JobThread Exception:&amp;#34; &#43; errorMsg &#43; &amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job execute end(error) -----------&amp;#34;); } finally { if(triggerParam != null) { // 8. 回调处理，即将执行结果传回 admin // callback handler info if (!toStop) { // 正常情况：从`triggerQueue`中获取到了任务并执行完成 // 所谓的 callback，所做的工作即将任务执行结果回传到 xxl-job-admin TriggerCallbackThread.pushCallBack(new HandleCallbackParam( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.getXxlJobContext().getHandleCode(), XxlJobContext.getXxlJobContext().getHandleMsg() ) ); } else { // 任务执行完但线程已停止：从`triggerQueue`中获取到任务并且执行完成，但 // 线程就执行了停止流程 TriggerCallbackThread.pushCallBack(new HandleCallbackParam( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.HANDLE_COCE_FAIL, stopReason &#43; &amp;#34; [job running, killed]&amp;#34; ) ); } } } } // 运行到这里，表示退出了循环，即 toStop = true // 线程停止但任务未执行：线程执行停止流程后，`triggerQueue`中还存在任务， // 跑个while循环，对这些任务都执行回调操作 while(triggerQueue !=null &amp;amp;&amp;amp; triggerQueue.size()&amp;gt;0){ TriggerParam triggerParam = triggerQueue.poll(); if (triggerParam!=null) { // is killed // 线程关闭后，队列中的任务放在 callBack 队列中 TriggerCallbackThread.pushCallBack(new HandleCallbackParam( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.HANDLE_COCE_FAIL, stopReason &#43; &amp;#34; [job not executed, in the job queue, killed.]&amp;#34;) ); } } // 9. 执行销毁方法 try { handler.destroy(); } catch (Throwable e) { logger.error(e.getMessage(), e); } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job JobThread stoped, hashCode:{}&amp;#34;, Thread.currentThread()); } 方法比较长，重点位置都已作了注释，接下来我们就一一分析该方法中的关键之处。
1. 执行初始化方法 执行初始化方法的代码如下：
try { handler.init(); } catch (Throwable e) { logger.error(e.getMessage(), e); } 执行的是handler的init()方法，这里以MethodJobHandler为例，在创建handler时，可以这样指定它的init与destroy方法：
@Component public class SampleXxlJob { /** * 生命周期任务示例：任务初始化与销毁时，支持自定义相关逻辑； */ @XxlJob(value = &amp;#34;demoJobHandler2&amp;#34;, init = &amp;#34;init&amp;#34;, destroy = &amp;#34;destroy&amp;#34;) public void demoJobHandler2() throws Exception { XxlJobHelper.log(&amp;#34;XXL-JOB, Hello World.&amp;#34;); } public void init(){ logger.info(&amp;#34;init&amp;#34;); } public void destroy(){ logger.info(&amp;#34;destory&amp;#34;); } } 在@XxlJob注解中，可以使用init与destroy属性来指定初始化与销毁方法。
handler.init()最终调用的是MethodJobHandler#init，代码如下：
public class MethodJobHandler extends IJobHandler { private final Object target; private Method initMethod; // 省略其他属性 ... /** * init 方法的执行，使用反射调用 */ @Override public void init() throws Exception { if(initMethod != null) { initMethod.invoke(target); } } // 省略其他方法 ... } 以上述SampleXxlJob为例，MethodJobHandler#init方法中出现的target就是SampleXxlJob的实例，initMethod就是SampleXxlJob#init方法，最终是通过反射进行调用。
2. while 循环中执行 回到JobThread#run，接着就是一段while循环：
while(!toStop){ running = false; idleTimes&#43;&#43;; // 任务参数 TriggerParam triggerParam = null; ... } 这里又是一段while循环，关于线程中跑while循环的操作，前面的文章中已经多次遇到过了，这块操作就不多说了。这里有3个属性值得注意下：
running：运行标识，用来判断当前线程是否正在执行任务，默认为false，接下来的代码会看到running变成true的情况 idleTimes：记录空闲次数，所谓的空闲，是指从线程中没有任务在执行的情况，接下来的代码也会看到idleTimes值的变化 triggerParam：任务参数 3. 从队列中获取待任务 获取任务的操作如下：
triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS); if (triggerParam!=null) { running = true; idleTimes = 0; triggerLogIdSet.remove(triggerParam.getLogId()); // 省略其他代码 ... } else { // 轮空达到30次，且队列中无待执行的任务，清除当前线程 if (idleTimes &amp;gt; 30) { if(triggerQueue.size() == 0) { XxlJobExecutor.removeJobThread(jobId, &amp;#34;excutor idel times over limit.&amp;#34;); } } } 前面多次提到，triggerQueue队列里存放的是待执行的任务，这里通过poll(...)方法从triggerQueue中获取待执行的任务，这是个阻塞操作，阻塞时间为3s，即：如果队列中有任务，立即返回，如果无任务就阻塞，直到有任务加入或达到阻塞时间（3s）。
接着，如果从triggerQueue中拿到了待执行任务，会进几个操作：
running = true：running字段改成true，即表示任务正在执行中 idleTimes = 0：空闲次数设置为0，表示空闲次数重新统计 triggerLogIdSet.remove(triggerParam.getLogId())：前面提到，triggerLogIdSet 用来存放jobLogId，用于判断任务是否重复执行，任务真正执行前，会从triggerLogIdSet中删除jobLogId，这相当于是保持triggerQueue与triggerLogIdSet的统一性。 如果从triggerQueue获取不到待执行的任务，就判断idleTimes的值，对于idleTimes &amp;gt; 30并且triggerQueue为空的情况，就移除当前线程。
4. 执行前的准备 执行前的准备工作如下：
// 日志名称 String logFileName = XxlJobFileAppender.makeLogFileName( new Date(triggerParam.getLogDateTime()), triggerParam.getLogId()); // 上下文参数 XxlJobContext xxlJobContext = new XxlJobContext( triggerParam.getJobId(), triggerParam.getExecutorParams(), logFileName, triggerParam.getBroadcastIndex(), triggerParam.getBroadcastTotal()); // init job context XxlJobContext.setXxlJobContext(xxlJobContext); 1. 处理日志文件名 xxl-job会为每一次任务的执行生成一个日志文件，日志文件会保存在executor所在的服务器上。日志文件名格式：
${logBasePath}/yyyy-MM-dd/${jobLogId}.log 看个本人机器鲜活的例子：
2. 处理任务上下文参数 这块有个关键类：XxlJobContext：
public class XxlJobContext { /** 任务执行的结果码，200-成功，500-失败，502-超时 */ public static final int HANDLE_COCE_SUCCESS = 200; public static final int HANDLE_COCE_FAIL = 500; public static final int HANDLE_COCE_TIMEOUT = 502; // ---------------------- base info ---------------------- /** * job id * 任务id */ private final long jobId; /** * job param * 任务参数 */ private final String jobParam; // ---------------------- for log ---------------------- /** * job log filename * 任务日志名称 */ private final String jobLogFileName; // ---------------------- for shard ---------------------- /** * shard index * 分片索引 */ private final int shardIndex; /** * shard total * 分片结点数 */ private final int shardTotal; // ---------------------- for handle ---------------------- /** * handleCode：The result status of job execution * * 200 : success * 500 : fail * 502 : timeout * 任务执行的结果码 */ private int handleCode; /** * 任务执行结果的说明 * handleMsg：The simple log msg of job execution */ private String handleMsg; // 省略其他内容 ... } 这个类中有一系列的属性，用来保存任务的执行参数，以上都是一个简单的bean操作。
回到JobThread#run，得到xxlJobContext实例后，接着进行了一个设置操作：
XxlJobContext.setXxlJobContext(xxlJobContext); 调用的也是XxlJobContext提供的方法：
public class XxlJobContext { // 省略其他 ... /** * 使用 InheritableThreadLocal * 它与 ThreadLocal 的最大区别是，变量可与子线程共享 */ private static InheritableThreadLocal&amp;lt;XxlJobContext&amp;gt; contextHolder = new InheritableThreadLocal&amp;lt;XxlJobContext&amp;gt;(); /** * 设置操作 */ public static void setXxlJobContext(XxlJobContext xxlJobContext){ contextHolder.set(xxlJobContext); } /** * 获取操作 */ public static XxlJobContext getXxlJobContext(){ return contextHolder.get(); } } 这块操作使用的是InheritableThreadLocal，将xxlJobContext与当前线程及其子线程绑定，这样在当前线程及其子线程中都可通过XxlJobContext.getXxlJobContext() 得到 这个xxlJobContext实例。
5. 执行任务 接着就是执行任务了，代码如下：
if (triggerParam.getExecutorTimeout() &amp;gt; 0) { // 处理任务超时 Thread futureThread = null; try { FutureTask&amp;lt;Boolean&amp;gt; futureTask = new FutureTask&amp;lt;Boolean&amp;gt;( new Callable&amp;lt;Boolean&amp;gt;() { @Override public Boolean call() throws Exception { // init job context XxlJobContext.setXxlJobContext(xxlJobContext); // 这里执行了任务的具体内容 handler.execute(); return true; } }); futureThread = new Thread(futureTask); futureThread.start(); // 在 futureTask 中执行 Boolean tempResult = futureTask.get( triggerParam.getExecutorTimeout(), TimeUnit.SECONDS); } catch (TimeoutException e) { XxlJobHelper.log(&amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job execute timeout&amp;#34;); XxlJobHelper.log(e); // handle result XxlJobHelper.handleTimeout(&amp;#34;job execute timeout &amp;#34;); } finally { futureThread.interrupt(); } } else { // just execute // 这里执行了任务的具体内容 handler.execute(); } 从代码来看，任务并不是直接执行的，而是区分有没有设置超时参数，这个参数可以在管理后台界面设置，默认为0：
对于超时时间大于0的情况，需要把任务放到FutureTask中执行，futureTask.get(xxx)可以指定执行阻塞时间，即在指定时间内未获得结果就会抛出异常，这块是java并发编程相关知识，就不展开了，这里我们目光聚集任务的执行方法handler.execute()。
不管有没有设置超时，最终执行的方法都是handler.execute()，这里我们重点来看MethodJobHandler#execute方法：
public void execute() throws Exception { Class&amp;lt;?&amp;gt;[] paramTypes = method.getParameterTypes(); if (paramTypes.length &amp;gt; 0) { method.invoke(target, new Object[paramTypes.length]); } else { method.invoke(target); } } 一个简单的反射调用。
等等，这个方法的执行是不是有问题，没处理传入的参数，也没处理方法的返回值，记得我们平时使用中@XxlJob方法是这么写的：
@XxlJob(&amp;#34;demoJobHandler3&amp;#34;) public ReturnT&amp;lt;String&amp;gt; demoJobHandler3(String param) throws Exception { XxlJobHelper.log(&amp;#34;job param:&amp;#34; &#43; param); return ReturnT.FAIL; } 印象中handler方法如上图所示，方法可传入参数，可接收管理后台传入的参数：
并且返回支持返回值，可根据返回值来判断任务是否执行成功，但从代码来看，并没有处理方法的参数与返回，这是怎么回事呢？
为了控制这个问题，我特地对比了上个版本（2.2.0）的代码：
从JobThread代码来看，旧版确实处理了参数与返回值，这块是新旧版本的最大区别。
既然新版本没有处理参数与返回，那我们如何实现与旧版相同的功能呢？我们继续往下看。
6. 处理执行结果 执行完任务后，接着就是处理任务的执行结果了：
if (XxlJobContext.getXxlJobContext().getHandleCode() &amp;lt;= 0) { XxlJobHelper.handleFail(&amp;#34;job handle result lost.&amp;#34;); } else { String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg(); tempHandleMsg = (tempHandleMsg!=null&amp;amp;&amp;amp;tempHandleMsg.length()&amp;gt;50000) ?tempHandleMsg.substring(0, 50000).concat(&amp;#34;...&amp;#34;) :tempHandleMsg; XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg); } 这里获取任务的结果是从XxlJobContext中获取的，任务的结果码与结果说明都保存在了XxlJobContext中。而handleCode的默认值为SUCCESS：
而从创建XxlJobContext到从XxlJobContext中获取执行结果，中间运行的是任务执行的代码，这说明我们可以在自己编写的handler方法中操作XxlJobContext，像这样：
@XxlJob(&amp;#34;demoJobHandler5&amp;#34;) public void demoJobHandler5() throws Exception { // 获取执行参数 String jobParam = XxlJobContext.getXxlJobContext().getJobParam(); XxlJobHelper.log(&amp;#34;job param:&amp;#34; &#43; jobParam); boolean result = xxx; if (result) { XxlJobContext.getXxlJobContext().setHandleCode( XxlJobContext.HANDLE_COCE_SUCCESS); XxlJobContext.getXxlJobContext().setHandleMsg(&amp;#34;success msg&amp;#34;); } else { // 错误的返回值 XxlJobContext.getXxlJobContext().setHandleCode( XxlJobContext.HANDLE_COCE_FAIL); XxlJobContext.getXxlJobContext().setHandleMsg(&amp;#34;fail msg&amp;#34;); } } 进一步研究发现，xxl-job将XxlJobContext的相关操作由XxlJobHelper做了一个封装，上述代码可简化如下：
@XxlJob(&amp;#34;demoJobHandler4&amp;#34;) public void demoJobHandler4() throws Exception { // 获取执行参数 String jobParam = XxlJobHelper.getJobParam(); XxlJobHelper.log(&amp;#34;job param:&amp;#34; &#43; jobParam); boolean result = xxx; if (result) { XxlJobHelper.handleSuccess(&amp;#34;success msg&amp;#34;); } else { // 错误的返回值 XxlJobHelper.handleFail(&amp;#34;fail msg&amp;#34;); } } demoJobHandler4()正是xxl-job推荐的写法，xxl-job文档中有提及：
7. 回调处理 所谓的回调，是指将任务的执行结果传回给admin，JobThread#run方法中回调处理的代码分的略散，主要是回调处理分为几种情况：
正常情况：从triggerQueue中获取到了任务并执行完成 任务执行完但线程已停止：从triggerQueue中获取到任务并且执行完成，但线程就执行了停止流程 线程停止但任务未执行：线程执行停止流程后，triggerQueue中还存在任务，需要对这些任务都执行回调操作 以上3种情况都会调用到TriggerCallbackThread.pushCallBack(xxx)方法，只不过只有正常情况是按任务的结果来处理，其他两种情况都是按任务失败来处理。处理回调的关键代码如下：
TriggerCallbackThread.pushCallBack(new HandleCallbackParam( triggerParam.getLogId(), triggerParam.getLogDateTime(), XxlJobContext.getXxlJobContext().getHandleCode(), XxlJobContext.getXxlJobContext().getHandleMsg() ) ); 这里引出了一个关键类：TriggerCallbackThread，从名字来看，它就是来处理回调操作的，TriggerCallbackThread.pushCallBack(xxx)方法代码如下：
public class TriggerCallbackThread { private static Logger logger = LoggerFactory.getLogger(TriggerCallbackThread.class); // 单例模式 private static TriggerCallbackThread instance = new TriggerCallbackThread(); public static TriggerCallbackThread getInstance(){ return instance; } /** * job results callback queue * 存入任务的执行结果 */ private LinkedBlockingQueue&amp;lt;HandleCallbackParam&amp;gt; callBackQueue = new LinkedBlockingQueue&amp;lt;HandleCallbackParam&amp;gt;(); /** * 添加操作 */ public static void pushCallBack(HandleCallbackParam callback){ // 将结果添加到队列中 getInstance().callBackQueue.add(callback); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, push callback request, logId:{}&amp;#34;, callback.getLogId()); } // 省略其他 ... } 这里我们仅关注添加操作，TriggerCallbackThread 采用了单例模式，getInstance()用于获取实例，这没啥好说的。
在TriggerCallbackThread中，有一个属性callBackQueue用来保存任务的执行结果，有了JobThread的triggerQueue，我们大概也能猜想到后面也会有线程中callBackQueue中获取元素执行操作，不过这块的分析我们后面再进行。
8. 执行销毁方法 执行销毁方法的代码如下：
try { handler.destroy(); } catch (Throwable e) { logger.error(e.getMessage(), e); } 这块操作与执行初始化方法基本一致，最终在MethodJobHandler#destroy方法中也是通过反射调用，这里就不多说了。
到这里为止，任务的执行操作也分析完了，但是整个执行流程依然没完，我们到目前为止只是看到了任务的执行，但任务的执行结果并没有提交给admin，我们还得继续前行。
执行结果的回调：TriggerCallbackThread 上一小节我们提到，任务执行完成后，会调用TriggerCallbackThread.pushCallBack(xxx)方法放入TriggerCallbackThread的callBackQueue队列中，之后回调的处理就由TriggerCallbackThread来处理了，接下来我们来分析执行结果放入callBackQueue后的操作。
在TriggerCallbackThread中有两个Thread属性：
public class TriggerCallbackThread { /** * 处理回调的线程 */ private Thread triggerCallbackThread; /** * 回调重试线程 */ private Thread triggerRetryCallbackThread; // 省略其他 ... } 两个线程的作用在注释中已指明，这里就不多说了。
线程的启动在 TriggerCallbackThread#start 方法中：
public void start() { // 校验操作 if (XxlJobExecutor.getAdminBizList() == null) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor callback config fail, &amp;#34; &#43; &amp;#34;adminAddresses is null.&amp;#34;); return; } // 回调线程的启动 triggerCallbackThread = new Thread(new Runnable() { @Override public void run() { ... } }); triggerCallbackThread.setDaemon(true); triggerCallbackThread.setName(&amp;#34;xxl-job, executor TriggerCallbackThread&amp;#34;); triggerCallbackThread.start(); // 回调重试线程的启动 triggerRetryCallbackThread = new Thread(new Runnable() { @Override public void run() { ... } }); triggerRetryCallbackThread.setDaemon(true); triggerRetryCallbackThread.start(); } 整个方法看下来，主要就是启动了这两个线程。以上代码省去了两个线程的run()方法，这两个run()方法将是我们分析的重点。
这个TriggerCallbackThread#start方法是在何时调用的呢？在《执行器启动流程》一文中，我们分析过XxlJobExecutor#start方法，在这个方法里就调用了TriggerCallbackThread#start：
triggerCallbackThread的run()方法 接下来我们来分析triggerCallbackThread的具体功能，进入它的run()方法：
@Override public void run() { // while 循环中进行 while(!toStop){ try { // 从 callBackQueue 中获取，注意 take() 是个阻塞操作 HandleCallbackParam callback = getInstance().callBackQueue.take(); if (callback != null) { // 运行到这里，就表示从callBackQueue拿到了参数 List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList = new ArrayList&amp;lt;HandleCallbackParam&amp;gt;(); // 将 callBackQueue 中的内容转移到 callbackParamList 中 int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); callbackParamList.add(callback); // 进行回调操作 if (callbackParamList!=null &amp;amp;&amp;amp; callbackParamList.size()&amp;gt;0) { doCallback(callbackParamList); } } } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } } // 跳出循环后，最后进行一次回调，套路跟上面类似 try { List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList = new ArrayList&amp;lt;HandleCallbackParam&amp;gt;(); // 将 callBackQueue 中的内容转移到 callbackParamList 中 int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); if (callbackParamList!=null &amp;amp;&amp;amp; callbackParamList.size()&amp;gt;0) { doCallback(callbackParamList); } } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor callback thread destory.&amp;#34;); } 这段代码总的操作就两个：
从callBackQueue获取元素 调用doCallback(...)方法处理回调操作 先来看看从callBackQueue获取元素的操作，代码一开始就调用了LinkedBlockingQueue提供的阻塞方法take()，它会移除队首元素并且将元素返回。当take()方法获得了返回值时，就表示队列中可能还存在其他元素，接着就使用drainTo(xxx)方法将队列中的元素都转移出来：
int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList); 关于 drainTo() 方法，注释如下：
Removes all available elements from this queue and adds them to the given collection. This operation may be more efficient than repeatedly polling this queue. A failure encountered while attempting to add elements to collection c may result in elements being in neither, either or both collections when the associated exception is thrown. Attempts to drain a queue to itself result in IllegalArgumentException. Further, the behavior of this operation is undefined if the specified collection is modified while the operation is in progress.
从此队列中删除所有可用元素并将它们添加到给定集合中。此操作可能比重复轮询此队列更有效。尝试将元素添加到集合 c 时遇到的失败可能会导致在引发相关异常时元素既不在集合中，又不属于任何一个集合或两个集合。尝试将队列排空到自身会导致 IllegalArgumentException。此外，如果在操作正在进行时修改了指定的集合，则此操作的行为是未定义的.
注释看着挺长，其实只要关注第一句就明白它是干什么的了：Removes all available elements from this queue and adds them to the given collection，运行完这句后，队列中已存在的元素就都到了callbackParamList中了。
处理完元素的获取操作后，接着就处理回调操作，也就doCallback(callbackParamList)方法：
private void doCallback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList){ boolean callbackRet = false; // 回调操作，使用 admin 客户端，请求到 admin 实例 for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { try { // 真正处理回调操作 ReturnT&amp;lt;String&amp;gt; callbackResult = adminBiz.callback(callbackParamList); if (callbackResult!=null &amp;amp;&amp;amp; ReturnT.SUCCESS_CODE == callbackResult.getCode()) { callbackLog(callbackParamList, &amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job callback finish.&amp;#34;); callbackRet = true; // 回调成功，就退出 for 循环，即只要在其中一个 adminBiz 回调成功就算成功了 break; } else { callbackLog(callbackParamList, &amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job callback fail, &amp;#34; &#43; &amp;#34;callbackResult:&amp;#34; &#43; callbackResult); } } catch (Exception e) { callbackLog(callbackParamList, &amp;#34;&amp;lt;br&amp;gt;----------- xxl-job job callback error, errorMsg:&amp;#34; &#43; e.getMessage()); } } // 回调失败，加入到错误日志中，等待重试 if (!callbackRet) { appendFailCallbackFile(callbackParamList); } } 这个方法的功能主要有两个：
处理回调操作 回调失败，加入错误日志 回调操作 我们先来看回调操作，同前面介绍的executor注册、executor摘除操作一样，回调操作也是在AdminBizList的for循环中进行的，并且只要在其中之一的adminBiz上回调成功，就算回调成功，就不再需要在剩下的adminBiz上再执行回调操作了。
adminBiz处理回调操作的方法为AdminBizClient#callback，代码如下：
@Override public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList) { return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;api/callback&amp;#34;, accessToken, timeout, callbackParamList, String.class); } 这是一个http请求，它会把回调数据通过http协议发往admin实例，关于这其中的通讯流程在《admin与executor通讯》一文已经分析过了，这里直接进入admin实例的处理方法AdminBizImpl#callback：
@Override public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList) { return JobCompleteHelper.getInstance().callback(callbackParamList); } 深入JobCompleteHelper#callback(xxx)方法：
public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList) { // 在线程池中执行 callbackThreadPool.execute(new Runnable() { @Override public void run() { // 循环传入的 callback 参数 for (HandleCallbackParam handleCallbackParam: callbackParamList) { // 调用 callback 方法处理 ReturnT&amp;lt;String&amp;gt; callbackResult = callback(handleCallbackParam); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; JobApiController.callback {}, &amp;#34; &#43; &amp;#34;handleCallbackParam={}, callbackResult={}&amp;#34;, (callbackResult.getCode()== ReturnT.SUCCESS_CODE?&amp;#34;success&amp;#34;:&amp;#34;fail&amp;#34;), handleCallbackParam, callbackResult); } } }); return ReturnT.SUCCESS; } 继续，最终发现处理回调操作的方法为JobCompleteHelper#callback(xxx)：
private ReturnT&amp;lt;String&amp;gt; callback(HandleCallbackParam handleCallbackParam) { // 日志任务执行日志，表：xxl_job_log XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .load(handleCallbackParam.getLogId()); if (log == null) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;log item not found.&amp;#34;); } if (log.getHandleCode() &amp;gt; 0) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;log repeate callback.&amp;#34;); } // 处理执行信息 StringBuffer handleMsg = new StringBuffer(); if (log.getHandleMsg()!=null) { handleMsg.append(log.getHandleMsg()).append(&amp;#34;&amp;lt;br&amp;gt;&amp;#34;); } if (handleCallbackParam.getHandleMsg() != null) { handleMsg.append(handleCallbackParam.getHandleMsg()); } // 填充执行结果 log.setHandleTime(new Date()); log.setHandleCode(handleCallbackParam.getHandleCode()); log.setHandleMsg(handleMsg.toString()); XxlJobCompleter.updateHandleInfoAndFinish(log); return ReturnT.SUCCESS; } 可以看到，这个方法会找到jobLogId对应的XxlJobLog记录，然后填充执行结果。到这里，整个回调流程就完成了，不过这只是正常的情况，下面我们再来看看异常的情况。
回调失败的处理 让我们再回到TriggerCallbackThread#doCallback方法，继续往下分析：
private void doCallback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList){ boolean callbackRet = false; // 回调操作，使用 admin 客户端，请求到 admin 实例 // 省略回调处理代码 ... // 回调失败，加入到错误日志中，等待重试 if (!callbackRet) { appendFailCallbackFile(callbackParamList); } } 如果回调失败了，会调用appendFailCallbackFile方法处理，进入该方法：
/** 回调失败文件的路径 */ private static String failCallbackFilePath = XxlJobFileAppender.getLogPath() .concat(File.separator).concat(&amp;#34;callbacklog&amp;#34;).concat(File.separator); /** 回调失败文件的名称 */ private static String failCallbackFileName = failCallbackFilePath .concat(&amp;#34;xxl-job-callback-{x}&amp;#34;).concat(&amp;#34;.log&amp;#34;); /** * 处理回调失败的文件 */ private void appendFailCallbackFile(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList){ // 参数校验 if (callbackParamList==null || callbackParamList.size()==0) { return; } // jdk序列化，将List中的内容序列化成byte数组 byte[] callbackParamList_bytes = JdkSerializeTool.serialize(callbackParamList); // 得到文件名 File callbackLogFile = new File(failCallbackFileName.replace(&amp;#34;{x}&amp;#34;, String.valueOf(System.currentTimeMillis()))); // 如果文件存在，使用当前时间（System.currentTimeMillis()）&#43; 1-100 以内的序列找到一个不存在的文件 if (callbackLogFile.exists()) { for (int i = 0; i &amp;lt; 100; i&#43;&#43;) { callbackLogFile = new File(failCallbackFileName.replace(&amp;#34;{x}&amp;#34;, String.valueOf(System.currentTimeMillis()).concat(&amp;#34;-&amp;#34;).concat(String.valueOf(i)) )); if (!callbackLogFile.exists()) { break; } } } // 将前面得到的byte数组写入文件 FileUtil.writeFileContent(callbackLogFile, callbackParamList_bytes); } 这个方法的主要作用就是将回调失败记录保存到文件中，要完成这个操作，包含以下3点：
文件路径：${logBasePath}/callbacklog/ 文件名：xxl-job-callback-${System.currentTimeMillis()}.log，如果文件名重复了，加个序号：xxl-job-callback-${System.currentTimeMillis()}-0.log、xxl-job-callback-${System.currentTimeMillis()}-1.log，序号最多可达到99 序列化方式：使用jdk提供的序列化方式 最终，回调失败的记录就保存在了${logBasePath}/callbacklog/xxl-job-callback-${System.currentTimeMillis()}-{index}.log文件中了。
triggerRetryCallbackThread的run()方法 我们再回到TriggerCallbackThread#start方法，上面分析的是triggerCallbackThread的run()方法，我们继续来分析triggerRetryCallbackThread的run()方法：
@Override public void run() { while(!toStop){ try { // 处理回调重试操作 retryFailCallbackFile(); } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } // 处理休眠操作，时间为 30 s，即每30s执行一次重试操作 try { TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); } catch (InterruptedException e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor retry callback thread destory.&amp;#34;); } 这个方法比较简单，就是调用了retryFailCallbackFile()方法来处理重试操作，我们进入该方法：
private void retryFailCallbackFile(){ // 校验文件路径是否存在 File callbackLogPath = new File(failCallbackFilePath); if (!callbackLogPath.exists()) { return; } if (callbackLogPath.isFile()) { callbackLogPath.delete(); } if (!(callbackLogPath.isDirectory() &amp;amp;&amp;amp; callbackLogPath.list()!=null &amp;amp;&amp;amp; callbackLogPath.list().length&amp;gt;0)) { return; } // 遍历路径下的文件 for (File callbaclLogFile: callbackLogPath.listFiles()) { // 读取文件内容，得到byte数组 byte[] callbackParamList_bytes = FileUtil.readFileContent(callbaclLogFile); // 保证文件内容不为空 if(callbackParamList_bytes == null || callbackParamList_bytes.length &amp;lt; 1){ callbaclLogFile.delete(); continue; } // 进行 jdk 反序列化 List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList = (List&amp;lt;HandleCallbackParam&amp;gt;) JdkSerializeTool .deserialize(callbackParamList_bytes, List.class); // 清除回调失败的文件，再次进行 callback 操作 callbaclLogFile.delete(); doCallback(callbackParamList); } } 在前面我们将失败的记录转成byte数组，然后采用jdk序列化操作将失败记录写入文件中，这个方法会先将文件中的内容读取为byte数组，然后使用jdk反序列化操作得到回调失败的记录，得到这些失败的记录后，就再次调用doCallback(xxx)方法进行回调了，回调重试操作就完成了。
总结 本文介绍了executor执行任务的流程，流程细节比较多，我总结了一张图如下：
整个执行流程类似于“接力式”操作，其中的LinkedBlockQueue与文件扮演了数据流转的载体。
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob07：任务执行流程（二）之触发器揭秘</title>
        <url>https://www.szlinkroutes.com/post/xxljob07%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%BA%8C%E4%B9%8B%E8%A7%A6%E5%8F%91%E5%99%A8%E6%8F%AD%E7%A7%98/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
上一篇文章中，任务调度完成后，最终会调用JobTriggerPoolHelper.trigger(...)方法进行触发操作，本文将来分析xxl-job任务触发流程。
JobTriggerPoolHelper#trigger 关于任务的触发，直观感受应该来自于管理后台界面：
点击执行一次后，调用的接口是/jobinfo/trigger，方法是JobInfoController#triggerJob代码如下：
@RequestMapping(&amp;#34;/trigger&amp;#34;) @ResponseBody public ReturnT&amp;lt;String&amp;gt; triggerJob(int id, String executorParam, String addressList) { if (executorParam == null) { executorParam = &amp;#34;&amp;#34;; } JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); return ReturnT.SUCCESS; } 同上一文触发操作一样，最终调用的也是JobTriggerPoolHelper.trigger(...)方法.
继续，看看JobTriggerPoolHelper.trigger(...)方法的内容：
public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); } 仅仅只是调用了JobTriggerPoolHelper#addTrigger方法，那我们继续下去，看看这个方法：
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); // 超时次数超过10次，使用 slowTriggerPool if (jobTimeoutCount!=null &amp;amp;&amp;amp; jobTimeoutCount.get() &amp;gt; 10) { 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 // 保证统计的是1分钟内的次数 long minTim_now = System.currentTimeMillis()/60000; if (minTim != minTim_now) { minTim = minTim_now; jobTimeoutCountMap.clear(); } // incr timeout-count-map // 任务的超时阈值为 500 ms，触发时间超过500ms，表示1次超时 long cost = System.currentTimeMillis()-start; if (cost &amp;gt; 500) { // ob-timeout threshold 500ms AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent( jobId, new AtomicInteger(1)); if (timeoutCount != null) { timeoutCount.incrementAndGet(); } } } } }); } 方法内容比较简单，关键部分已经添加了注释，代码一开始，先是选择triggerPool的操作。在触发器中，triggerPool分为两大类：
fastTriggerPool：默认使用 slowTriggerPool：超时触发次数在10次以上就使用该线程池，任务触发的超时阈值为 500 ms 为任务选定好triggerPool后，就是触发操作了，代码如下：
// trigger triggerPool_.execute(new Runnable() { @Override public void run() { try { // 处理触发操作 XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); } catch (Exception e) { // 省略了许多内容 ... } } 触发操作是在线程池中执行的，调用的是XxlJobTrigger.trigger(...)方法来处理。
XxlJobTrigger.trigger 继续进入XxlJobTrigger.trigger(...)方法，代码如下：
public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { // 1. 获取任务数据 XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig() .getXxlJobInfoDao().loadById(jobId); if (jobInfo == null) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; trigger fail, jobId invalid，jobId={}&amp;#34;, jobId); return; } if (executorParam != null) { jobInfo.setExecutorParam(executorParam); } // 2. 处理失败重试次数 int finalFailRetryCount = failRetryCount&amp;gt;=0 ? failRetryCount : jobInfo.getExecutorFailRetryCount(); XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao() .load(jobInfo.getJobGroup()); // 3. 是否指定了执行的机器 if (addressList!=null &amp;amp;&amp;amp; addressList.trim().length()&amp;gt;0) { group.setAddressType(1); group.setAddressList(addressList.trim()); } // 4. 分片参数 int[] shardingParam = null; if (executorShardingParam!=null){ String[] shardingArr = executorShardingParam.split(&amp;#34;/&amp;#34;); if (shardingArr.length==2 &amp;amp;&amp;amp; isNumeric(shardingArr[0]) &amp;amp;&amp;amp; isNumeric(shardingArr[1])) { shardingParam = new int[2]; shardingParam[0] = Integer.valueOf(shardingArr[0]); shardingParam[1] = Integer.valueOf(shardingArr[1]); } } // 5. 执行，分为广播模式与其他模式 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match( jobInfo.getExecutorRouteStrategy(), null) &amp;amp;&amp;amp; group.getRegistryList()!=null &amp;amp;&amp;amp; !group.getRegistryList().isEmpty() &amp;amp;&amp;amp; shardingParam==null) { // 依次广播到每台机器 for (int i = 0; i &amp;lt; group.getRegistryList().size(); i&#43;&#43;) { 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]); } } 咱们对该方法的功能逐一分析吧。
1. 获取任务数据 方法中传入的是jobId，这里需要获取任务的详细信息，执行的方法是
XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig() .getXxlJobInfoDao().loadById(jobId); 执行的就是一个简单的sql查询操作：
SELECT &amp;lt;include refid=&amp;#34;Base_Column_List&amp;#34; /&amp;gt; FROM xxl_job_info AS t WHERE t.id = #{id} 2. 计算失败重试次数 所谓的失败重试次数，是指任务执行失败了，最多重试多少次，次数来源于任务的配置：
3. 是否指定了执行的机器 该参数在执行一次时指定：
点击执行1次后，在弹出的界面中，可以指定机器地址：
4. 分片参数 分片参数有两个：
index：指定executor的下标索引 total：executor的总数量 该参数用于指定使用的executor。
5. 执行，分为广播模式与其他模式 这里会调用XxlJobTrigger#processTrigger方法处理执行操作，这里需要提一个概念：路由策略。所谓的路由策略，是指在多个executor时，选择哪个executor执行任务的策略，可以在任务编辑界面设置：
路由策略主要分为两大类：
分片广播：所谓的广播，就是将任务的执行请求发往每一个executor，该模式下，任务会在每个executor都会执行 其他策略：除了分片广播外的策略外，其他策略均只会选择一个executor来执行，选择逻辑由ExecutorRouter的子类来实现，关于具体的选择策略，在后面再详细展开。 XxlJobTrigger#processTrigger 在XxlJobTrigger.trigger(...)方法中，调用了processTrigger(...)来处理触发操作，我们继续进入深入该方法：
private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){ // 1. 根据是否为广播模式来处理分片参数 ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match( jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match( jobInfo.getExecutorRouteStrategy(), null); String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) ? String.valueOf(index).concat(&amp;#34;/&amp;#34;).concat(String.valueOf(total)) : null; // 2. 保存执行日志 XxlJobLog jobLog = new XxlJobLog(); jobLog.setJobGroup(jobInfo.getJobGroup()); jobLog.setJobId(jobInfo.getId()); jobLog.setTriggerTime(new Date()); XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job trigger start, jobId:{}&amp;#34;, jobLog.getId()); // 2、初始化触发参数 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、根据执行策略获取executor的地址 String address = null; ReturnT&amp;lt;String&amp;gt; routeAddressResult = null; if (group.getRegistryList()!=null &amp;amp;&amp;amp; !group.getRegistryList().isEmpty()) { // index 与 total 仅在广播模式中发挥作用 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { if (index &amp;lt; 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&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, I18nUtil.getString(&amp;#34;jobconf_trigger_address_empty&amp;#34;)); } // 4、触发操作 ReturnT&amp;lt;String&amp;gt; triggerResult = null; if (address != null) { // 执行 triggerResult = runExecutor(triggerParam, address); } else { triggerResult = new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, null); } // 5、构建触发信息 StringBuffer triggerMsgSb = new StringBuffer(); // 省略触发信息构建 ... // 6、更新触发日志 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(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job trigger end, jobId:{}&amp;#34;, jobLog.getId()); } 这个方法有点长，关键部分已经做了注释，对照着注释理解起来并不难，这里总结下该方法的逻辑内容：
根据是否为广播模式来处理分片参数 保存执行日志 根据执行策略获取executor的地址 触发操作 构建触发信息 更新触发日志 这里值得注意的是，在选择由哪个executor执行任务时，如果是广播模式，会使用传入的参数index与total计算得到具体使用的executor，否则就使用界面配置的策略来选择，这点在分析XxlJobTrigger.trigger方法时也提到过。
在这个方法中，调用了runExecutor(...)继续处理触发操作，看来我们还得往下走。
XxlJobTrigger#runExecutor 在XxlJobTrigger#processTrigger方法中，调用了runExecutor(...)处理触发操作，我们继续进入该方法：
public static ReturnT&amp;lt;String&amp;gt; runExecutor(TriggerParam triggerParam, String address){ ReturnT&amp;lt;String&amp;gt; runResult = null; try { // 执行，请求执行器 ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); runResult = executorBiz.run(triggerParam); } catch (Exception e) { logger.error(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job trigger error, please check if &amp;#34; &#43; &amp;#34;the executor[{}] is running.&amp;#34;, address, e); runResult = new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, ThrowableUtil.toString(e)); } // 组装结果参数 StringBuffer runResultSB = new StringBuffer( I18nUtil.getString(&amp;#34;jobconf_trigger_run&amp;#34;) &#43; &amp;#34;：&amp;#34;); runResultSB.append(&amp;#34;&amp;lt;br&amp;gt;address：&amp;#34;).append(address); runResultSB.append(&amp;#34;&amp;lt;br&amp;gt;code：&amp;#34;).append(runResult.getCode()); runResultSB.append(&amp;#34;&amp;lt;br&amp;gt;msg：&amp;#34;).append(runResult.getMsg()); runResult.setMsg(runResultSB.toString()); return runResult; } 这个方法调用的是ExecutorBiz.run(...)，继续深入：
public ReturnT&amp;lt;String&amp;gt; run(TriggerParam triggerParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;run&amp;#34;, accessToken, timeout, triggerParam, String.class); } 有没有对这个方法十分眼熟？在《admin与executor通讯》一文中，我们介绍了admin到executor的交互流程，这个方法run()就是当时介绍的操作之一。
总结 到这里，触发器的执行流程就分析完成了，相比于前面文章中的其他流程，这块流程还是很清晰直白的，以一张图来总结触发操作的方法执行情况：
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob06：任务执行流程（一）之调度器揭密</title>
        <url>https://www.szlinkroutes.com/post/xxljob06%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E4%B8%80%E4%B9%8B%E8%B0%83%E5%BA%A6%E5%99%A8%E6%8F%AD%E5%AF%86/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
从本文开始，我们将用三篇文章来介绍xxl-job最核心的功能——xxl-job任务执行流程。xxl-job任务的执行包含3个组件：调度器、触发器、执行器，本文将重点介绍调度器的流程。
xxl-job调度器位于admin进程，主要功能为精准获取将要执行的任务，处理调度器的方法为JobScheduleHelper#start，在《admin启动流程》一文中也提到过该方法，不过当时并未展开讨论，在本文中将详细分析该方法。
JobScheduleHelper#start 代码如下：
public void start(){ // schedule thread scheduleThread = new Thread(new Runnable() { public void run() { // 省略run()方法的内容，下面再展开 ... } }); scheduleThread.setDaemon(true); scheduleThread.setName(&amp;#34;xxl-job, admin JobScheduleHelper#scheduleThread&amp;#34;); scheduleThread.start(); // ring thread ringThread = new Thread(new Runnable() { public void run() { // 省略run()方法的内容，下面再展开 ... } }); ringThread.setDaemon(true); ringThread.setName(&amp;#34;xxl-job, admin JobScheduleHelper#ringThread&amp;#34;); ringThread.start(); } 可以看到，这个方法启动了两个线程：
scheduleThread：调度线程，用来获取需要执行的任务 ringThread：时间轮线程，用来精准控制任务的执行时间点 任务的调度就是依靠以上两个线程来完成的，我们继续。
scheduleThread 我们先来分析scheduleThread，查看其 run()方法：
@Override public void run() { // 休眠，表示线程启动5s后才去执行任务 try { TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; init xxl-job admin scheduler success.&amp;#34;); // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, // qps = 1000/50 = 20) int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() &#43; 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(); // 调整数据库连接的自动提交方式，为false表示手动提交，后面会看到 conn.commit() 的调用 conn.setAutoCommit(false); // sql 语句后加 for update，表示获取数据库的写锁（排他锁） preparedStatement = conn.prepareStatement( &amp;#34;select * from xxl_job_lock where lock_name = &amp;#39;schedule_lock&amp;#39; for update&amp;#34;); preparedStatement.execute(); // 能执行到这里，就表示获得了锁 // tx start // 1、pre read long nowTime = System.currentTimeMillis(); // 查出未来5s内需要执行的任务 List&amp;lt;XxlJobInfo&amp;gt; scheduleList = XxlJobAdminConfig.getAdminConfig() .getXxlJobInfoDao().scheduleJobQuery(nowTime &#43; PRE_READ_MS, preReadCount); if (scheduleList!=null &amp;amp;&amp;amp; scheduleList.size()&amp;gt;0) { // 2、push time-ring for (XxlJobInfo jobInfo: scheduleList) { // time-ring jump // 处理过期任务：执行触发时间超过了5s还没执行 if (nowTime &amp;gt; jobInfo.getTriggerNextTime() &#43; PRE_READ_MS) { // 2.1、trigger-expire &amp;gt; 5s：pass &amp;amp;&amp;amp; make next-trigger-time logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule misfire, jobId = &amp;#34; &#43; jobInfo.getId()); // 1、misfire match // 处理过期的任务，处理策略有 FIRE_ONCE_NOW（立即执行1次） // 与 DO_NOTHING（什么也不做），默认是 DO_NOTHING MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match( jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) { // FIRE_ONCE_NOW 》 trigger // 触发过期的任务，这里的 trigger(...) 就是触发任务的方法了 JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule push trigger :&amp;#34; &#43; &amp;#34; jobId = &amp;#34; &#43; jobInfo.getId() ); } // 2、fresh next // 更新最近一次执行时间以及下次执行时间，这里只是设置值，数据库的更新操作在下面执行 refreshNextValidTime(jobInfo, new Date()); } else if (nowTime &amp;gt; jobInfo.getTriggerNextTime()) { // 2.2、trigger-expire &amp;lt; 5s：direct-trigger &amp;amp;&amp;amp; make next-trigger-time // 处理立即执行的任务：过了执行时间，但执行时间与当前时间相差不足5s，立即执行1次 // 1、trigger JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule push trigger : jobId = &amp;#34; &#43; jobInfo.getId() ); // 2、fresh next refreshNextValidTime(jobInfo, new Date()); // next-trigger-time in 5s, pre-read again // 执行完之后，还需要再次执行，加入时间轮 if (jobInfo.getTriggerStatus()==1 &amp;amp;&amp;amp; nowTime &#43; PRE_READ_MS &amp;gt; jobInfo.getTriggerNextTime()) { // 1、make ring second // 执行时间的秒数，比如任务的执行时间为 11:02:05，这里得到的就是秒数 5 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 { // 未来5秒内执行的任务，加入时间轮 // 2.3、trigger-pre-read：time-ring trigger &amp;amp;&amp;amp; 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(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, JobScheduleHelper#scheduleThread error:{}&amp;#34;, 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 { // 在使用数据库连接池时，close()方法表示将连接归还到连接池中 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 &amp;lt; 1000) { // scan-overtime, not wait try { // pre-read period: success &amp;gt; scan each second; fail &amp;gt; skip this period; // 加载到了接下来的任务，就休眠1s，否则休眠5s TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, JobScheduleHelper#scheduleThread stop&amp;#34;); } 方法有点长，但实际并不复杂，对照着代码中的注释，大体上应该也能明白这个方法做了什么，不明白也没关系，接下来我们对这个方法具体分析。
1. 休眠，对齐执行时间 // 休眠，表示线程启动5s后才去执行任务 try { TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; init xxl-job admin scheduler success.&amp;#34;); 之所以不是一启动就去获取要执行的任务，主要是为了等待准备工作的完成（如触发线程的启动）。在设置休眠时间时，xxl-job是这么处理的：
5000 - System.currentTimeMillis()%1000 前面的5000表示5s这没问题，后面的System.currentTimeMillis()%1000是几个意思呢？
我们都知道，System.currentTimeMillis()得到的是一个精确到毫秒的时间戳，将其与1000求余，得到的就是当时时刻的毫秒数了。
那么5000 - 当前时刻的毫秒数究竟有什么意义呢？我们想一想，假设当前时间是2022-05-12 12:02:12.324，当前时刻的毫秒数是324，如果直接休眠5s，休眠后的时间是2022-05-12 12:02:17.324，而如果休眠时间是5000 - 324 = 4676 毫秒呢？这样休眠后的时间就是2022-05-12 12:02:17.000，可以看到，这样就凑成了整秒数。
这样我们就明白了，休眠时间设置为5000 - System.currentTimeMillis()%1000，是为了整秒执行线程里的内容。
2. 每次查询的任务数 int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() &#43; XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; 这个值设置的是每次获取的任务数，其实就是限制每次从数据库中获取的记录数，值为线程池的总线程数的20倍，之所以有getTriggerPoolFastMax()与getTriggerPoolSlowMax()，是因为xxl-job会根据任务触发时间，将任务分为“触发慢的任务”与“触发快的任务”，分别对应一个线程池来处理，这块是xxl-job触发流程的内容，我们在下一篇文章中再详细叙述。
getTriggerPoolFastMax()与getTriggerPoolSlowMax()的值是多少呢？在application.properties中这样的配置：
## xxl-job, triggerpool max size xxl.job.triggerpool.fast.max=200 xxl.job.triggerpool.slow.max=100 这就是用来配置这两个线程池的线程数的，这样算下来，preReadCount = （200 &#43; 100）* 20 = 6000，即每次查询从数据库中最多可获取6000个待执行的任务。
3. 在 while 循环中获取需要执行的任务 接下来的代码都在一个while循环体中，代码如下：
while (!scheduleThreadToStop) { long start = System.currentTimeMillis(); // 获取需要执行的任务 ... long cost = System.currentTimeMillis()-start; if (cost &amp;lt; 1000) { try { // 加载到了接下来的任务，就休眠1s，否则休眠5s // 加载到了任务时，preReadSuc为ture TimeUnit.MILLISECONDS.sleep((preReadSuc ? 1000 : PRE_READ_MS) - System.currentTimeMillis() % 1000); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } } } 为了能清晰看到代码的脉络，以上代码省略了任务获取逻辑。
对于代码中的while (!scheduleThreadToStop)，想必小伙伴已经很熟悉了，xxl-job中需要一直执行的线程都是这么个套路，scheduleThreadToStop的值又会是一个stop方法中被修改，这块我们就不展示了，有兴趣的小伙伴可自行查看JobScheduleHelper#toStop。
while循环体中的代码，代码运行到最后，依然是进行休眠操作，这里需要重点说明下他的休眠处理。
在休眠前，会先判断cost &amp;lt; 1000，这里的cost是“获取待执行任务”这个操作消耗的时间，当消耗的时间小于1000毫秒时才进入休眠操作。为何大于等于1000时，不用休眠呢？
xxl-job任务调度的最小精度是秒，因此理想情况下，调度线程应该每秒去数据库中获取下将要执行的任务，对于cost &amp;lt; 1000的情况就应该休眠下等到下一秒再去执行；对于cost &amp;gt;= 1000，已经过了1秒了，不用再休眠，直接执行就可以了。
再来看看休眠时间的处理：
(preReadSuc ? 1000 : PRE_READ_MS) - System.currentTimeMillis() % 1000 对上述代码中出现的两个变量做下说明：
preReadSuc：当从数据库中获取到任务时，该值会设置成true，否则为false PRE_READ_MS：常量，值为 5000 因此，从以上表达式中可以看出，当数据库中存在待执行的任务时，就休眠1s，否则就休眠5s.为何是5s呢？因为从数据库中获取任务时，会获取在未来5s内执行的任务，如果未获取到任务，就表示未来5s内都没有需要执行的任务，直接休眠5s就可以了。
对于减去System.currentTimeMillis() % 1000的操作，同前面讲述的一样，也是为了得到整秒数，就不多说了。
4. 分布式锁的实现 在任务的获取前，需要处理加锁操作，这点也好理解，在分布式时代，可能有多个admin实例在协作运行，如果不加分布式锁，在同一时刻，会有多个admin实例同时执行调度操作，最终造成任务的重复执行，这样明显不是我们所期望的，因此加锁是有必要的，那么分布式锁该如何加呢？
xxl-job加分布式锁的方案是使用数据库的读锁，相关代码如下：
try { conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); connAutoCommit = conn.getAutoCommit(); // 调整数据库连接的自动提交方式，为false表示手动提交，后面会看到 conn.commit() 的调用 conn.setAutoCommit(false); // sql 查询语句后加 for update，表示获取数据库的写锁（排他锁） preparedStatement = conn.prepareStatement( &amp;#34;select * from xxl_job_lock where lock_name = &amp;#39;schedule_lock&amp;#39; for update&amp;#34;); preparedStatement.execute(); // 能执行到这里，就表示获得了锁 // 省略执行的内容 ... } finally { // commit if (conn != null) { try { conn.commit(); } catch (SQLException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } try { // 还原 autoCommit 值 conn.setAutoCommit(connAutoCommit); } catch (SQLException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } try { // 在使用数据库连接池时，close()方法表示将连接归还到连接池中 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); } } } } 对代码中重要的地方都作了注释，使用数据库实现排他锁的关键就在于如下：
select * from xxl_job_lock where lock_name = &amp;#39;schedule_lock&amp;#39; for update 对一个select语句加上for update，就表示获取数据库的写锁，写锁是排他锁，当一个连接获取到写锁后，其他连接就只能等待写锁的释放了，这样也就实现了分布式锁。
关于以上代码获取连接、关闭自动提交、手动执行sql、重围自动提交状态、关闭连接等等操作，为何不使用spring-tx提供的功能呢？使用spring编程式事务或声明式事务（@Transactional注解）就能完成代码以上一大堆代码的功能了。
在获取锁时，如果一个连接一直无法获取锁，是不是会一直等待呢？在mysql中，获取锁是有超时时间的，达到超时时间后，会报错：
报完错之后，while循环还在继续，又进入下一轮的锁等待，直到获得锁或到达超时时间。因此，对于数据库实现的分布式锁来说，在多个线程竞争锁的情况下，至少有以下几个不足：
无法立即知道锁获取结果，也无法指定超时时间，成功与否完全取决于数据库，等待时间也是，自主性太差 获取不到锁的数据库连接会一直等待锁，这会造成数据库的压力会比较大 5. 任务的获取 继续，获取到锁后，接着就是从数据库查找要执行的任务了：
// 查出未来5s内需要执行的任务 List&amp;lt;XxlJobInfo&amp;gt; scheduleList = XxlJobAdminConfig.getAdminConfig() .getXxlJobInfoDao().scheduleJobQuery(nowTime &#43; PRE_READ_MS, preReadCount); 查找需要执行的任务的方法为XxlJobInfoDao#scheduleJobQuery，参数中的PRE_READ_MS值为5000，preReadCount值为6000，执行的sql如下：
SELECT &amp;lt;include refid=&amp;#34;Base_Column_List&amp;#34; /&amp;gt; FROM xxl_job_info AS t WHERE t.trigger_status = 1 and t.trigger_next_time &amp;lt;![CDATA[ &amp;lt;= ]]&amp;gt; #{maxNextTime} ORDER BY id ASC LIMIT #{pagesize} 注意到and t.trigger_next_time &amp;lt;= #{maxNextTime}，这样就得到的是下一次执行时间小于maxNextTime的任务，也包含了过期任务（即到了执行时间但但没执行的任务）。
6. 任务的处理 得到待执行的任务后，接下来就是对这些任务进行触发操作了。在xxl-job中，待执行的任务分为3类：
过期时间超时5s的任务：例如，当前时间是2022-05-12 12:00:00，任务A下一次的执行应该在2022-05-12 11:59:50，这就是到点但任务未执行的情况，而执行时间与当前时间相差超过了5s. 过期时间在5s内：例如，当前时间是2022-05-12 12:00:00，任务B下一次的执行应该在2022-05-12 11:59:56，这也是到点任务未执行的情况，不过执行时间与当前时间相差在5s内. 在未来5s内要执行的任务：例如，当前时间是2022-05-12 12:00:00，任务A下一次的执行应该在2022-05-12 12:00:03，该任务是未来5s内将要执行的. 由于查询时指定了PRE_READ_MS值为5000，因此得到的任务中不会出现在未来执行但与当前时间超过5s的任务。
对于以上3类任务，xxl-job分别做了不同的处理，我们继续往下分析。
1. 过期时间超过5s的任务 对于过期时间超过5s的任务，xxl-job处理的代码如下：
// 处理过期任务：执行触发时间超过了5s还没执行 if (nowTime &amp;gt; jobInfo.getTriggerNextTime() &#43; PRE_READ_MS) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule misfire, jobId = &amp;#34; &#43; jobInfo.getId()); // 处理过期的任务，处理策略有 FIRE_ONCE_NOW（立即执行1次） // 与 DO_NOTHING（什么也不做），默认是 DO_NOTHING MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match( jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) { // FIRE_ONCE_NOW 》 trigger // 触发过期的任务，这里的 trigger(...) 就是触发任务的方法了 JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule push trigger :&amp;#34; &#43; &amp;#34; jobId = &amp;#34; &#43; jobInfo.getId() ); } // 更新最近一次执行时间以及下次执行时间，这里只是设置值，数据库的更新操作在下面执行 refreshNextValidTime(jobInfo, new Date()); } 代码中，会先判断过期处理策略，对于FIRE_ONCE_NOW策略，会调用JobTriggerPoolHelper.trigger(...)方法，否则就什么也不做，之前就调用refreshNextValidTime(...)设置该任务下一次执行时间。
在xxl-job中，调度过期策略有两种：
FIRE_ONCE_NOW：立即执行一次 DO_NOTHING：什么也不做，默认策略 该策略可以在任务编辑界面设置：
代码中出现的JobTriggerPoolHelper.trigger(...)方法，就是任务的触发方法，也是任务调度的另一组件——触发器。关于它的内容我们在下一篇文章中再详细展开，这里知道它是负责将任务提交到executor的组件就行了。
代码的最后调用了refreshNextValidTime(...)方法，我们来看看这个方法做了什么：
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(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, refreshNextValidTime fail for job: jobId={}, &amp;#34; &#43; &amp;#34;scheduleType={}, scheduleConf={}&amp;#34;, jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf()); } } 从代码来看，以上代码就是得到任务下一次的执行时间，然后进行一个赋值操作，generateNextValidTime(jobInfo, fromTime)就是计算下一次执行时间的。注意到该方法仅是做了一个赋值，真正更新到数据库的操作还在接下来的代码中。
2. 过期5s内的任务 继续看来看过期时间在5s内的任务，代码如下：
else if (nowTime &amp;gt; jobInfo.getTriggerNextTime()) { // 处理立即执行的任务：过了执行时间，但执行时间与当前时间相差不足5s，立即执行1次 JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule push trigger : jobId = &amp;#34; &#43; jobInfo.getId() ); refreshNextValidTime(jobInfo, new Date()); // 执行完之后，还需要再次执行，加入时间轮 if (jobInfo.getTriggerStatus()==1 &amp;amp;&amp;amp; nowTime &#43; PRE_READ_MS &amp;gt; jobInfo.getTriggerNextTime()) { // 执行时间的秒数，比如任务的执行时间为 11:02:05，这里得到的就是秒数 5 int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); pushTimeRing(ringSecond, jobInfo.getId()); refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); } } 对于过期时间在5s内的任务，xxl-job的做法是立即触发一次，触发完成后，设置任务的下一次执行时间。处理完这两步操作后，如果这个任务在未来5s内还要再次执行，那要如何呢？这里分了3步：
计算执行时间的秒数，也就是jobInfo.getTriggerNextTime()/1000)%60，比如执行时间是2022-05-12 12:00:03，这样计算得到的秒数为3 根据秒数，将任务id放到时间轮指定的位置，也就是pushTimeRing(...)方法 刷新任务下一次的执行时间，也就是refreshNextValidTime(...)方法 实际上，在下一小节中，对未到执行时间的任务的处理，也是按以上3步操作来进行的。注意到以上有个方法pushTimeRing(...)，关于该方法的操作，在下面的篇幅中再展开。
3. 未到执行时间的任务 最后就是处理未到执行时间的任务了，代码如下：
else { // 未来5秒内执行的任务，加入时间轮 // 2.3、trigger-pre-read：time-ring trigger &amp;amp;&amp;amp; 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())); } 处理方式与过期5s内的任务中对未来要执行的任务处理一模一样，这里就不多说了。
7. 时间轮处理 对于需要在未来执行的任务，xxl-job的做法是调用pushTimeRing(...)方法，接下来我们就来看看这个方法做了什么，进入 JobScheduleHelper#pushTimeRing：
private void pushTimeRing(int ringSecond, int jobId){ // push async ring List&amp;lt;Integer&amp;gt; ringItemData = ringData.get(ringSecond); if (ringItemData == null) { ringItemData = new ArrayList&amp;lt;Integer&amp;gt;(); ringData.put(ringSecond, ringItemData); } ringItemData.add(jobId); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, schedule push time-ring : &amp;#34; &#43; ringSecond &#43; &amp;#34; = &amp;#34; &#43; Arrays.asList(ringItemData) ); } 对于时间轮，结构如下：
就一个秒盘一样，拥有 0-59 60个索引位，每个索引位都是一个List&amp;lt;Integer&amp;gt;结构，存放的是未来要执行任务的jobId。在调用pushTimeRing(...)前，会先通过jobInfo.getTriggerNextTime()/1000)%60计算执行时间的秒数，这个秒数就是时间轮的索引位。
将任务添加到时间轮后，任务的获取操作就算完成了，那么放入时间轮中的任务又在什么时候拿出来交给触发器呢？这个就是ringThread线程的任务了，在后面会继续深入。
8. 更新任务下一次执行时间 对于每类任务，在代码的最后，都会调用refreshNextValidTime(...)方法，前面已经提到过该方法是用来设置任务下一次执行任务的，但并未更新到数据库中。在处理完3类任务后，接下来就是数据库的更新操作了，代码如下：
for (XxlJobInfo jobInfo: scheduleList) { XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); } 这里才是真正地进行数据库的操作，更新任务下一次执行时间的方法为XxlJobInfoDao#scheduleUpdate，执行的sql如下：
UPDATE xxl_job_info SET trigger_last_time = #{triggerLastTime}, trigger_next_time = #{triggerNextTime}, trigger_status = #{triggerStatus} WHERE id = #{id} 介绍完调度线程执行的流程后，我们对该线程的执行流程梳理如下：
获取锁 加载过期的任务以及即将执行的任务 过期的任务：过期时间超过5s，按配置的规则处理（立即执行1次、什么也不做） 立即执行的任务：过期时间在5s内，立即执行1次；对于在未来5s内还需要执行的任务，放入时间轮，待到时触发 即将执行的任务：放入时间轮，待到时触发 更新任务下一次执行时间 释放锁、休眠操作 ringThread 我们再来看看在JobScheduleHelper#start中启动的另一个线程：ringThread。在介绍scheduleThread线程的流程时，对于未来5s内需要执行的线程，xxl-job是放了一个名为时间轮的结构中，而ringThread所做的工作就是从时间轮中获取到点任务，将其交给触发器的，直接进入ringThread的run()方法：
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&amp;lt;Integer&amp;gt; ringItemData = new ArrayList&amp;lt;&amp;gt;(); int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 这里的i仅取值 0、1，从准确性上讲，应该仅处理i=0刻度的任务就可以了， // 这里作者为了避免处理耗时太长，跨过刻度，因此向前校验一个刻度，即i=1 for (int i = 0; i &amp;lt; 2; i&#43;&#43;) { // 注意刻度的计算，为了防止出现负数，需要加60再取余，跟循环队列的处理类似 List&amp;lt;Integer&amp;gt; tmpData = ringData.remove( (nowSecond&#43;60-i)%60 ); if (tmpData != null) { ringItemData.addAll(tmpData); } } // ring trigger logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, time-ring beat : &amp;#34; &#43; nowSecond &#43; &amp;#34; = &amp;#34; &#43; Arrays.asList(ringItemData) ); if (ringItemData.size() &amp;gt; 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(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, JobScheduleHelper#ringThread error:{}&amp;#34;, e); } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, JobScheduleHelper#ringThread stop&amp;#34;); } 这个方法的内容还是比较简单的，对照着代码中的注释，基本上能明白其操作流程了，这里我们简单地分析下。
整个方法的主要操作是在while (!ringThreadToStop)循环中，关于while循环的套路以及ringThreadToStop值的更新，前面介绍得已经够多了，这里就不多说了。
代码一开始，就是一段休眠操作，休眠的时间为1000 - System.currentTimeMillis() % 1000毫秒，这是一个对齐整秒数的操作，在分析scheduleThread线程的流程时已经详细分析过这种操作，这里就不多分析了。
接下来就是从时间轮中获取jobId列表的操作，key为当前时间的秒数，需要注意的是，这里取了两个秒数：(nowSecond&#43;60-0)%60、(nowSecond&#43;60-1)%60，本来只取(nowSecond&#43;60-0)%60对应的jobId列表就可以了，这里作者为了避免处理耗时太长，跨过刻度，因此向前多取了一个刻度。
获取到jobId列表后，接着就是调用JobTriggerPoolHelper.trigger(...)进行触发操作，到这里任务的调度就算是完成了。
本小节的最后，我们也来总结下该线程的执行流程：
从时间轮中获取当前秒数对应的jobIdList 遍历jobIdList，对每一个jobId分别进行触发操作 总结 本文重点分析了xxl-job的调度器执行流程，调度器由两个线程相互协作完成：
scheduleThread：定时从数据库中获取待执行的任务，并将这些任务放入时间轮中 ringThread：定时从时间轮中获取将要执行的任务，进行触发操作 详细流程如下：
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob05：执行器注册流程</title>
        <url>https://www.szlinkroutes.com/post/xxljob05%E6%89%A7%E8%A1%8C%E5%99%A8%E6%B3%A8%E5%86%8C%E6%B5%81%E7%A8%8B/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
本文来分析执行器注册到admin的流程，也就是EmbedServer#startRegistry方法。
有了前面两篇文章的铺垫，我们终于可以分析executor的注册流程了，注册流程在executor启动流程中的位置如下：
我们直接进入ExecutorRegistryThread#start的方法：
public void start(final String appname, final String address){ // 判断参数的合法性 if (appname==null || appname.trim().length()==0) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor registry config fail, &amp;#34; &#43; &amp;#34;appname is null.&amp;#34;); return; } // 如果没有admin服务，就不注册了 if (XxlJobExecutor.getAdminBizList() == null) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor registry config fail, &amp;#34; &#43; &amp;#34;adminAddresses is null.&amp;#34;); return; } registryThread = new Thread(new Runnable() { @Override public void run() { ... } }); registryThread.setDaemon(true); registryThread.setName(&amp;#34;xxl-job, executor ExecutorRegistryThread&amp;#34;); registryThread.start(); } 这个方法主要的功能就是创建并启动了一个名为registryThread的线程，线程中执行的操作就是executor的注册操作，其run() 方法如下：
run()方法的功能还是挺简单的，主要做了两件事：
执行注册操作 执行摘除操作 接下来我们就来具体分析这两个操作。
执行注册操作 run()方法一开始，就是一段while循环：
while循环里，先是执行了注册操作，然后就是休眠30s，由于是在while循环中，休眠结束后，又会再次执行注册操作，反复进行下去。那么它会在何时停止注册呢？
从while的条件来看，就是toStop为true时注册操作才停止，而后往下执行executor摘除操作，改变toStop的代码如下：
public void toStop() { // 改变 toStop 的值 toStop = true; // 如果执行该方法时，线程正好处于休眠中，调用 interrupt() 打断休眠 if (registryThread != null) { registryThread.interrupt(); try { registryThread.join(); } catch (InterruptedException e) { logger.error(e.getMessage(), e); } } } 这个方法是不是看着很眼熟，在前面介绍admin启动流程时，已经介绍过xxl-job中线程关闭的套路，这里也是同样的配方。
再进一步探索toStop()的调用关系：
发现最终来自于XxlJobSpringExecutor#destroy方法：
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean { @Override public void destroy() { super.destroy(); } ... } 关于XxlJobSpringExecutor#destroy方法，在分析执行器启动流程时，他是由DisposableBean接口提供，在当前spring bean（也就是XxlJobSpringExecutor）销毁时执行，这也可以说，在项目停止时toStop()方法会调用到。
接着我们来看看注册操作，代码如下：
RegistryParam registryParam = new RegistryParam( RegistryConfig.RegistType.EXECUTOR.name(), appname, address); for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { try { // 注册 ReturnT&amp;lt;String&amp;gt; registryResult = adminBiz.registry(registryParam); if (registryResult!=null &amp;amp;&amp;amp; ReturnT.SUCCESS_CODE == registryResult.getCode()) { registryResult = ReturnT.SUCCESS; logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job registry success, &amp;#34; &#43; &amp;#34;registryParam:{}, registryResult:{}&amp;#34;, new Object[]{registryParam, registryResult}); // 其中之一注册成功即可，成功就break break; } else { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job registry fail, &amp;#34; &#43; &amp;#34;registryParam:{}, registryResult:{}&amp;#34;, new Object[]{registryParam, registryResult}); } } catch (Exception e) { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job registry error, &amp;#34; &#43; &amp;#34;registryParam:{}&amp;#34;, registryParam, e); } } 这块代码还是很清晰的，先是遍历XxlJobExecutor.getAdminBizList()，逐一进行注册，只要其中之一注册成功了，之后的adminBiz就不会再注册了，也就是每次注册时，只要有一个adminBiz注册成功就表示该executor注册成功了。
执行注册的方法为adminBiz.registry(...)，也就是AdminBizClient#registry，它的代码如下：
@Override public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;api/registry&amp;#34;, accessToken, timeout, registryParam, String.class); } 这是一个http请求，请求到admin来处理。
上一篇文章中，我们已经详细分析了executor与admin之间的通讯方式，这里我们直接来看admin的处理方法，进入AdminBizImpl#registry方法：
@Override public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam) { return JobRegistryHelper.getInstance().registry(registryParam); } 继续查看JobRegistryHelper#registry方法：
public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam) { // valid if (!StringUtils.hasText(registryParam.getRegistryGroup()) || !StringUtils.hasText(registryParam.getRegistryKey()) || !StringUtils.hasText(registryParam.getRegistryValue())) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;Illegal Argument.&amp;#34;); } // 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 &amp;lt; 1) { XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao() // 更新失败了，就注册执行器 .registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); // 这里是个空方法，没执行任何内容 freshGroupRegistryInfo(registryParam); } } }); return ReturnT.SUCCESS; } 在《admin启动流程》一文中，我们在分析JobRegistryHelper#start方法时，介绍到该方法创建了两个线程池：
registryOrRemoveThreadPool：注册或摘除executor registryMonitorThread：executor有效监测，及时去除挂了的executor 注册操作就是在registryOrRemoveThreadPool中处理的，注册操作主要是进行两个：
registryUpdate(...)：更新executor注册信息，sql 如下：
UPDATE xxl_job_registry SET `update_time` = #{updateTime} WHERE `registry_group` = #{registryGroup} AND `registry_key` = #{registryKey} AND `registry_value` = #{registryValue} registrySave(...)：保存executor注册信息
INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`) VALUES( #{registryGroup} , #{registryKey} , #{registryValue}, #{updateTime}) executor 注册成功后，xxl_job_registry表中就有最新的executor的注册记录了。
对于xxl_job_registry表中的记录，JobRegistryHelper 中的 registryMonitorThread线程会持续监测executor的注册信息，及时摘除长时未更新注册时间的executor，这一点在《admin启动流程》一文中已分析过了，这里就不展开了。
执行摘除操作 executor停止时，ExecutorRegistryThread#toStop方法会执行，进而registryThread#run开始executor的摘除操作，相关代码如下：
public void run() { while (!toStop) { // 省略注册操作 ... } // 跳出上面的 while 循环（即toStop=true，也就是服务停止）时才执行下面的代码 // registry remove try { RegistryParam registryParam = new RegistryParam( RegistryConfig.RegistType.EXECUTOR.name(), appname, address); for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) { try { // 执行删除操作 ReturnT&amp;lt;String&amp;gt; registryResult = adminBiz .registryRemove(registryParam); if (registryResult!=null &amp;amp;&amp;amp; ReturnT.SUCCESS_CODE == registryResult.getCode()) { registryResult = ReturnT.SUCCESS; logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job registry-remove success, &amp;#34; &#43; &amp;#34;registryParam:{}, registryResult:{}&amp;#34;, new Object[]{registryParam, registryResult}); // 其中之一注册成功即可，成功就break break; } else { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job registry-remove fail, &amp;#34; &#43; &amp;#34;registryParam:{}, registryResult:{}&amp;#34;, new Object[]{registryParam, registryResult}); } } catch (Exception e) { if (!toStop) { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job registry-remove error, &amp;#34; &#43; &amp;#34;registryParam:{}&amp;#34;, registryParam, e); } } } } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor registry thread destory.&amp;#34;); } 同注册操作一样，执行操作也是先遍历 XxlJobExecutor.getAdminBizList()，逐一进行摘除操作。同样地，只要有一个adminBiz.registryRemove(...)上执行成功了，就表示该executor摘除成功了，对后续的adminBiz就不再执行摘除操作了。
执行摘除操作的方法是adminBiz.registryRemove，也就是AdminBizClient#registryRemove，代码如下：
public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;api/registryRemove&amp;#34;, accessToken, timeout, registryParam, String.class); } 这个方法中只要是进行一个http请求，将数据发往admin，然后由admin处理摘除操作。对于executor与admin之间的通讯，在前面的文章已经分析过了，这里直接来看看admin是如何处理executor摘除的，进入JobRegistryHelper#registryRemove方法：
public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam) { // valid if (!StringUtils.hasText(registryParam.getRegistryGroup()) || !StringUtils.hasText(registryParam.getRegistryKey()) || !StringUtils.hasText(registryParam.getRegistryValue())) { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;Illegal Argument.&amp;#34;); } // async execute registryOrRemoveThreadPool.execute(new Runnable() { @Override public void run() { int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao() // 删除执行器 .registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue()); if (ret &amp;gt; 0) { // 这里是个空方法，没执行任何内容 freshGroupRegistryInfo(registryParam); } } }); return ReturnT.SUCCESS; } 以上方法最关键的就是XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete方法了，摘除服务器的操作就是在这里进行的，这个方法执行的的sql如下:
DELETE FROM xxl_job_registry WHERE registry_group = #{registryGroup} AND registry_key = #{registryKey} AND registry_value = #{registryValue} 从这里可以看到，所谓的executor摘除操作，就是把executor注册信息从xxl_job_registry表中删除，这样任务就不再调度到这个executor了。
关于执行器注册流程的介绍就到这里了，下一篇继续探索xxl-job其他流程。
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob04：admin与executor通讯</title>
        <url>https://www.szlinkroutes.com/post/xxljob04admin%E4%B8%8Eexecutor%E9%80%9A%E8%AE%AF/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
本文将分析执行器（executor）与admin之间的通讯。
在xxl-job中，executor与admin并不是相互独立工作的，他们之前会通过网络通讯相互协作完成任务的调度流程，比如，executor启动时，需要把自己的地址信息（ip:端口）信息告知admin；任务调度时，admin会把任务信息发送给executor，在executor上真正执行任务。这两类通讯中，前者是executor到admin的通讯，后者是admin到executor的通讯，本文接下来就分析这两类通讯。
executor到admin 处理类：AdminBiz admin对executor提供的服务定义在com.xxl.job.core.biz.AdminBiz接口中，内容如下：
public interface AdminBiz { /** * callback * * @param callbackParamList * @return */ public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList); /** * registry * * @param registryParam * @return */ public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam); /** * registry remove * * @param registryParam * @return */ public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam); } 这是一个接口，可以看到它共有3个方法：
callback：处理任务回调 registry：处理执行器(executor)的注册，即将executor注册到admin registryRemove：处理执行器(executor)的下线，即将executor从admin中删除 AdminBiz有2个实现类：
com.xxl.job.core.biz.client.AdminBizClient：从名称上看，它是一个客户端类，位于executor进程中，内容如下：
public class AdminBizClient implements AdminBiz { // 省略属性及构造方法 ... @Override public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList) { return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;api/callback&amp;#34;, accessToken, timeout, callbackParamList, String.class); } @Override public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;api/registry&amp;#34;, accessToken, timeout, registryParam, String.class); } @Override public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;api/registryRemove&amp;#34;, accessToken, timeout, registryParam, String.class); } } 这3个方法最终都调用了XxlJobRemotingUtil#postBody方法，而XxlJobRemotingUtil#postBody方法就是用来发起http请求的，即executor通过http协议将数据发送到admin。
com.xxl.job.admin.service.impl.AdminBizImpl：这个类就是具体的业务处理类了，它位于admin进程中，代码如下：
public class AdminBizImpl implements AdminBiz { @Override public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList) { return JobCompleteHelper.getInstance().callback(callbackParamList); } @Override public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam) { return JobRegistryHelper.getInstance().registry(registryParam); } @Override public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam) { return JobRegistryHelper.getInstance().registryRemove(registryParam); } } 从代码中可以看到，真正干活的是JobCompleteHelper与JobRegistryHelper类，这两个类的start()方法在介绍admin启动流程有介绍过，当时介绍的是启动，这里就是真正的使用了。关于这些方法的具体细节本文就不展开了，等到分析到具体功能时再详细阐述。
admin请求入口 到了这里，我们就明白了executor是通过http请求将数据发送到admin的，那么admin的请求入口又是在哪里呢？xxl-job-admin是一个springboot项目，请求入口是通过springmvc实现的，具体的的controller为com.xxl.job.admin.controller.JobApiController：
@Controller @RequestMapping(&amp;#34;/api&amp;#34;) public class JobApiController { @Resource private AdminBiz adminBiz; /** * api * * @param uri * @param data * @return */ @RequestMapping(&amp;#34;/{uri}&amp;#34;) @ResponseBody @PermissionLimit(limit=false) public ReturnT&amp;lt;String&amp;gt; api(HttpServletRequest request, @PathVariable(&amp;#34;uri&amp;#34;) String uri, @RequestBody(required = false) String data) { // 省略校验代码 ... // 服务映射 if (&amp;#34;callback&amp;#34;.equals(uri)) { List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class); return adminBiz.callback(callbackParamList); } else if (&amp;#34;registry&amp;#34;.equals(uri)) { RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); return adminBiz.registry(registryParam); } else if (&amp;#34;registryRemove&amp;#34;.equals(uri)) { RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); return adminBiz.registryRemove(registryParam); } else { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;invalid request, uri-mapping(&amp;#34;&#43; uri &#43;&amp;#34;) not found.&amp;#34;); } } } 它对外开放的入口是/api/{url}，在JobApiController#api方法中处理具体的请求路径，这些路径包括：
/api/callback：处理方法是AdminBizImpl#callback /api/registry：处理方法是AdminBizImpl#registry /api/registryRemove：处理方法是AdminBizImpl#registryRemove 对于这3个方法的作用，xxl-job文档已经说明得很清楚了：
想要了解的小伙伴可自动参考。
小结 最后以一幅图来总结下executor与admin之间的通讯流程：
admin到executor的通讯 处理类：ExecutorBiz executor对admin提供的服务定义在com.xxl.job.core.biz.ExecutorBiz接口中，内容如下：
public interface ExecutorBiz { /** * beat * @return */ public ReturnT&amp;lt;String&amp;gt; beat(); /** * idle beat * * @param idleBeatParam * @return */ public ReturnT&amp;lt;String&amp;gt; idleBeat(IdleBeatParam idleBeatParam); /** * run * @param triggerParam * @return */ public ReturnT&amp;lt;String&amp;gt; run(TriggerParam triggerParam); /** * kill * @param killParam * @return */ public ReturnT&amp;lt;String&amp;gt; kill(KillParam killParam); /** * log * @param logParam * @return */ public ReturnT&amp;lt;LogResult&amp;gt; log(LogParam logParam); } 对该接口内的方法说明如下：
beat()：存活检测，该方法仅是返回了一个SUCCESS，admin可通过该方法的返回值判断executor是否存活 idleBeat()：空闲检测，admin可通过该方法来判断executor是否处于空闲中 run()：任务执行方法，执行具体的任务 kill()：杀死正在执行中任务 log()：获取任务执行日志 ExecutorBiz接口也有两个实现类：
com.xxl.job.core.biz.client.ExecutorBizClient：请求发起类，位于admin进程中，代码如下：
public class ExecutorBizClient implements ExecutorBiz { // 省略构造方法及属性 ... @Override public ReturnT&amp;lt;String&amp;gt; beat() { return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;beat&amp;#34;, accessToken, timeout, &amp;#34;&amp;#34;, String.class); } @Override public ReturnT&amp;lt;String&amp;gt; idleBeat(IdleBeatParam idleBeatParam){ return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;idleBeat&amp;#34;, accessToken, timeout, idleBeatParam, String.class); } @Override public ReturnT&amp;lt;String&amp;gt; run(TriggerParam triggerParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;run&amp;#34;, accessToken, timeout, triggerParam, String.class); } @Override public ReturnT&amp;lt;String&amp;gt; kill(KillParam killParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;kill&amp;#34;, accessToken, timeout, killParam, String.class); } @Override public ReturnT&amp;lt;LogResult&amp;gt; log(LogParam logParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;log&amp;#34;, accessToken, timeout, logParam, LogResult.class); } } 同AdminBizClient一样，ExecutorBizClient也是通过http协议请求executor接口的，XxlJobRemotingUtil.postBody(...)中出现的addressUrl就是executor的地址。
com.xxl.job.core.biz.impl.ExecutorBizImpl：业务处理类，位于executor进程中，在这个类中才真正处理executor的各种操作。关于该类的具体内容，本文就不展开了，后面等到分析具体功能才详细阐述。
executor请求入口 admin通过springmvc对executor开放了请求入口，那executor是不是同样以springmvc对admin开放入口呢？
在上一篇介绍《xxl-job执行器启动流程》中，我们知道在XxlJobExecutor#initEmbedServer方法中启动了一个netty服务，而这个netty服务正是用来提供对admin的请求入口的！
关于netty的启动流程及相关知识的介绍并非本文重点，我们直接进入请求处理方法EmbedHttpServerHandler#channelRead0：
@Override protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { // http属性的处理，如请求参数、uri、请求方法、accessToken等 String requestData = msg.content().toString(CharsetUtil.UTF_8); String uri = msg.uri(); HttpMethod httpMethod = msg.method(); boolean keepAlive = HttpUtil.isKeepAlive(msg); String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN); // 在业务线程池中处理请求 bizThreadPool.execute(new Runnable() { @Override public void run() { // 在这里处理请求 Object responseObj = process(httpMethod, uri, requestData, accessTokenReq); // to json String responseJson = GsonTool.toJson(responseObj); // write response writeResponse(ctx, keepAlive, responseJson); } }); } 继续，进入EmbedHttpServerHandler#process方法：
private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) { // 省略参数校验 ... // 为每个uri分配特定的处理方法 try { if (&amp;#34;/beat&amp;#34;.equals(uri)) { return executorBiz.beat(); } else if (&amp;#34;/idleBeat&amp;#34;.equals(uri)) { IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class); return executorBiz.idleBeat(idleBeatParam); } else if (&amp;#34;/run&amp;#34;.equals(uri)) { TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class); return executorBiz.run(triggerParam); } else if (&amp;#34;/kill&amp;#34;.equals(uri)) { KillParam killParam = GsonTool.fromJson(requestData, KillParam.class); return executorBiz.kill(killParam); } else if (&amp;#34;/log&amp;#34;.equals(uri)) { LogParam logParam = GsonTool.fromJson(requestData, LogParam.class); return executorBiz.log(logParam); } else { return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;invalid request, uri-mapping(&amp;#34;&#43; uri &#43;&amp;#34;) not found.&amp;#34;); } } catch (Exception e) { logger.error(e.getMessage(), e); return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &amp;#34;request error:&amp;#34; &#43; ThrowableUtil.toString(e)); } } 可以看到，最近处理请求uri的方法就是EmbedHttpServerHandler#process了，而它最终调用的也是ExecutorBizImpl的方法。
关于 executor 对外接口，xxl-job也贴心地为我们提供了官方文档：
这里留个思考题：admin与executor之间的通讯，同样提供http请求入口，为何admin使用的是springmvc，而executor使用的却是netty呢？executor能不能也使用springmvc呢？
小结 最后以一幅图来总结下admin与executor之间的通讯流程：
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob03：执行器启动流程</title>
        <url>https://www.szlinkroutes.com/post/xxljob03%E6%89%A7%E8%A1%8C%E5%99%A8%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
本文将分析xxl-job执行器（executor）的启动流程。
执行器接入流程 在分析xxl-job执行器启动流程之前，我们先来看下如何让自己的项目变成xxl-job执行器，这里以springboot框架集成为例，示例项目为xxl-job-executor-sample-springboot。
1. 引入xxl-job-core包 要使用xxl-job的功能，第一步当然是引入其依赖包了，xxl-job-core的GAV坐标如下：
&amp;lt;!-- xxl-job-core --&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;com.xuxueli&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;xxl-job-core&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;${latest.version}&amp;lt;/version&amp;gt; &amp;lt;/dependency&amp;gt; 2. 新增配置类 这块是为了在项目中引入xxl-job的配置，配置类为 com.xxl.job.executor.core.config.XxlJobConfig:
@Configuration public class XxlJobConfig { private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); @Value(&amp;#34;${xxl.job.admin.addresses}&amp;#34;) private String adminAddresses; @Value(&amp;#34;${xxl.job.accessToken}&amp;#34;) private String accessToken; @Value(&amp;#34;${xxl.job.executor.appname}&amp;#34;) private String appname; @Value(&amp;#34;${xxl.job.executor.address}&amp;#34;) private String address; @Value(&amp;#34;${xxl.job.executor.ip}&amp;#34;) private String ip; @Value(&amp;#34;${xxl.job.executor.port}&amp;#34;) private int port; @Value(&amp;#34;${xxl.job.executor.logpath}&amp;#34;) private String logPath; @Value(&amp;#34;${xxl.job.executor.logretentiondays}&amp;#34;) private int logRetentionDays; @Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job config init.&amp;#34;); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor; } } 这是一个配置类，该类先是定义了一系列的属性，用于获取application.properties的配置，接着以@Bean注解的方式向spring容器中引入了xxlJobExecutor，该bean才是xxl-job执行器的关键，在本文的后面会详细分析。
3. application.properties中添加xxl-job相关配置 这块就是处理执行器的相关配置了，这些配置如下：
# xxl-job admin address 地址列表，多个使用,分开 xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin # 与admin通读的token，不配置表示不启用 xxl.job.accessToken= # 执行器名称，同一个执行器的不同实例，应该使用同一个名称 xxl.job.executor.appname=xxl-job-executor-sample # 执行器地址，可以指定，不指定时则使用ip:port的形式 xxl.job.executor.address= # 服务器ip与端口信息 ## 如果不指定ip，则自行获取服务器ip地址 xxl.job.executor.ip= ## 如果不指定端口，则先获取 9999 ~ 65535之间可用的端口，如果无 ## 可用端口，再获取9999~0中可用的端口 xxl.job.executor.port=9998 # 执行日志保存路径 xxl.job.executor.logpath=./data/applogs/xxl-job/jobhandler # 执行日志保存时长 xxl.job.executor.logretentiondays=30 4. 编写任务 任务的示例类为com.xxl.job.executor.service.jobhandler.SampleXxlJob，代码如下：
@Component public class SampleXxlJob { /** * 注意 @XxlJob 注解 */ @XxlJob(&amp;#34;demoJobHandler&amp;#34;) public void demoJobHandler() throws Exception { XxlJobHelper.log(&amp;#34;XXL-JOB, Hello World.&amp;#34;); for (int i = 0; i &amp;lt; 5; i&#43;&#43;) { XxlJobHelper.log(&amp;#34;beat at:&amp;#34; &#43; i); TimeUnit.SECONDS.sleep(2); } // default success } } 实际上，对于一个bean类型的任务来说，只需满足两个条件即可：
该类是一个 spring bean 中：SampleXxlJob由@Component注解标记 任务的执行方法由@XxlJob注解标记：demoJobHandler()由@XxlJob注解标记，并且指定了任务名称为demoJobHandler 5. 界面配置任务 然后是配置执行器，界面如下（注意名称与项目中配置的xxl.job.executor.appname一致）：
接着配置任务：
配置之后，就可以执行任务了。
配置类：XxlJobConfig 前面的介绍中，我们向代码中编写了一个配置类：com.xxl.job.executor.core.config.XxlJobConfig，而这个类也是启动xxl-job执行器组件的关键所在，它的xxlJobExecutor()方法如下：
@Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job config init.&amp;#34;); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appname); xxlJobSpringExecutor.setAddress(address); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor; } 该方法就是XxlJobConfig的核心所在，可以说，XxlJobConfig就是为了向spring容器中引入 类型为XxlJobSpringExecutor的bean，接下来我们就来探索XxlJobSpringExecutor的奥秘。
com.xxl.job.core.executor.impl.XxlJobSpringExecutor关键内容如下：
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean { @Override public void afterSingletonsInstantiated() { // 带 @XxlJob 注解的方法就是在这里扫描的 initJobHandlerMethodRepository(applicationContext); // refresh GlueFactory GlueFactory.refreshInstance(1); // super start try { super.start(); } catch (Exception e) { throw new RuntimeException(e); } } // destroy @Override public void destroy() { super.destroy(); } ... } 该类实现了3个类，我们重点分析其中两个：
SmartInitializingSingleton：由spring提供，其afterSingletonsInstantiated()的方法会在容器中所有的单例bean初始化完成后调用； DisposableBean：由spring提供，其destroy()方法会在当前的spring bean销毁时调用 接下来我们就只需重点关注afterSingletonsInstantiated()与destroy()方法了.
上面已经展示了XxlJobSpringExecutor的afterSingletonsInstantiated()方法，该方法一共有3行关键代码，总结如下：
initJobHandlerMethodRepository(xxx)：初始化jobHandlerMethod，实际上就是处理@XxlJob注解的方法 GlueFactory.refreshInstance(1)：初始化GlueFactory类，该类在glue模式下会用到，由于本系列重点分析bean模式，这块就不关注了 super.start()：调用的是XxlJobExecutor#start，这里是执行器真正启动的地方，后面我们会花大量篇幅来介绍该方法 1. initJobHandlerMethodRepository() XxlJobSpringExecutor#initJobHandlerMethodRepository主要用来处理被@XxlJob标记的方法，代码如下：
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) { if (applicationContext == null) { return; } // 1. 获取spring所有bean的名称 String[] beanDefinitionNames = applicationContext.getBeanNamesForType( Object.class, false, true); for (String beanDefinitionName : beanDefinitionNames) { Object bean = applicationContext.getBean(beanDefinitionName); // 2. 获取到所有标记了 @XxlJob 的方法 Map&amp;lt;Method, XxlJob&amp;gt; annotatedMethods = null; try { annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(), new MethodIntrospector.MetadataLookup&amp;lt;XxlJob&amp;gt;() { @Override public XxlJob inspect(Method method) { return AnnotatedElementUtils.findMergedAnnotation( method, XxlJob.class); } }); } catch (Throwable ex) { logger.error(&amp;#34;xxl-job method-jobhandler resolve error for bean[&amp;#34; &#43; beanDefinitionName &#43; &amp;#34;].&amp;#34;, ex); } if (annotatedMethods==null || annotatedMethods.isEmpty()) { continue; } // 3. 遍历得到的方法，注册 for (Map.Entry&amp;lt;Method, XxlJob&amp;gt; methodXxlJobEntry : annotatedMethods.entrySet()) { Method executeMethod = methodXxlJobEntry.getKey(); XxlJob xxlJob = methodXxlJobEntry.getValue(); if (xxlJob == null) { continue; } String name = xxlJob.value(); if (name.trim().length() == 0) { throw new RuntimeException(&amp;#34;xxl-job method-jobhandler name invalid, for[&amp;#34; &#43; bean.getClass() &#43; &amp;#34;#&amp;#34; &#43; executeMethod.getName() &#43; &amp;#34;] .&amp;#34;); } // 3.1 判断任务是否重复 if (loadJobHandler(name) != null) { throw new RuntimeException(&amp;#34;xxl-job jobhandler[&amp;#34; &#43; name &#43; &amp;#34;] naming conflicts.&amp;#34;); } executeMethod.setAccessible(true); // init and destory Method initMethod = null; Method destroyMethod = null; // 3.2 初始化方法 if (xxlJob.init().trim().length() &amp;gt; 0) { try { initMethod = bean.getClass().getDeclaredMethod(xxlJob.init()); initMethod.setAccessible(true); } catch (NoSuchMethodException e) { throw new RuntimeException(&amp;#34;xxl-job method-jobhandler initMethod &amp;#34; &#43; &amp;#34;invalid, for[&amp;#34; &#43; bean.getClass() &#43; &amp;#34;#&amp;#34; &#43; executeMethod.getName() &#43; &amp;#34;] .&amp;#34;); } } // 3.3 销毁方法 if (xxlJob.destroy().trim().length() &amp;gt; 0) { try { destroyMethod = bean.getClass().getDeclaredMethod(xxlJob.destroy()); destroyMethod.setAccessible(true); } catch (NoSuchMethodException e) { throw new RuntimeException(&amp;#34;xxl-job method-jobhandler destroyMethod &amp;#34; &#43; &amp;#34;invalid, for[&amp;#34; &#43; bean.getClass() &#43; &amp;#34;#&amp;#34; &#43; executeMethod.getName() &#43; &amp;#34;] .&amp;#34;); } } // 3.4 注册任务处理handler // registry jobhandler registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod)); } } } 该方法看着有点长，但逻辑还是非常清晰的，注释已经在代码中标明了，这里再来总结下该方法执行的流程：
获取spring所有bean的名称，使用的是spring提供的方法applicationContext.getBeanNamesForType，这表明被@XxlJob标记的方法一定要在spring bean中才会被识别到； 从第1步得到的bean中获取到所有标记了@XxlJob的方法，使用的是spring提供的方法MethodIntrospector.selectMethods 遍历得到的方法，注册 判断任务是否重复，即判断之前是否有注册过 解析初始化方法，@XxlJob注解可以指定初始化方法，这一步是把初始化方法名（字符串）转化为Method实例 解析初始化方法，@XxlJob注解可以指定销毁方法，这一步是把销毁方法名（字符串）转化为Method实例 注册任务处理handler，一个完整的handler包含bean、任务处理方法、初始化方法、销毁方法 任务注册方法是XxlJobExecutor#registJobHandler，相关代码如下：
// XxlJobExecutor.java /** 保存任务handler */ private static ConcurrentMap&amp;lt;String, IJobHandler&amp;gt; jobHandlerRepository = new ConcurrentHashMap&amp;lt;String, IJobHandler&amp;gt;(); /** 获取任务handler */ public static IJobHandler loadJobHandler(String name){ return jobHandlerRepository.get(name); } /** 注册任务handler */ public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){ logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job register jobhandler success, &amp;#34; &#43; &amp;#34;name:{}, jobHandler:{}&amp;#34;, name, jobHandler); return jobHandlerRepository.put(name, jobHandler); } XxlJobExecutor中有一个名为jobHandlerRepository的属性用来保存任务handler，该属性类型为ConcurrentMap，key是@XxlJob的value，value是MethodJobHandler，该类包含的属性如下：
Object target：实例，就是前面所说的spring bean Method method：任务处理方法 Method initMethod：任务的初始化方法 Method destroyMethod：任务的销毁方法 以示例项目中的SampleXxlJob#demoJobHandler2为例，该方法对应的各属性如下：
最终，任务handler会注册到XxlJobExecutor的jobHandlerRepository中了。从代码中可以看到，这块操作主要是使用spring提供的方法。
2. GlueFactory#refreshInstance GlueFactory.refreshInstance代码如下：
public static void refreshInstance(int type){ if (type == 0) { glueFactory = new GlueFactory(); } else if (type == 1) { glueFactory = new SpringGlueFactory(); } } 这段代码主要是创建glueFactory对象，由于代码中传入的参数是1，因此创建的是SpringGlueFactory的实例。
该类在glue模式下会用到，由于本系列重点分析bean模式，这块就不关注了。
3. XxlJobExecutor#start 继续，接着调用了一个非常重要的方法：XxlJobExecutor#start，这个方法就是xxl-job执行器的启动关键所在了，代码如下：
public void start() throws Exception { // init logpath // 1. 初始化日志路径，logBasePath 与 glueSrcPath XxlJobFileAppender.initLogPath(logPath); // init invoker, admin-client // 2. 初始化admin客户端，最后得到的是一个client list, 每个admin的 // 地址都会生成一个client initAdminBizList(adminAddresses, accessToken); // init JobLogFileCleanThread // 3. 清除job日志执行文件 JobLogFileCleanThread.getInstance().start(logRetentionDays); // init TriggerCallbackThread // 4. 处理回调admin的线程 TriggerCallbackThread.getInstance().start(); // init executor-server // 5. 初始化内部的http服务器，用于接收admin的请求 initEmbedServer(address, ip, port, appname, accessToken); } 该方法只有5行，不过每行都是关键，这5行代码分别的处理的功能如下：
初始化日志路径 初始化admin客户端 清除job日志执行文件 处理回调admin的线程 初始化内部的http服务器 关于这些功能，我们下一小节再详细展开。
启动方法：XxlJobExecutor#start 上一小节我们介绍了XxlJobExecutor#start方法，介绍了该方法所进行的功能如下：
初始化日志路径 初始化admin客户端 清除job日志执行文件 处理回调admin的线程 初始化内部的http服务器 本节我们将逐一分析这些功能。
1. 初始化日志路径 初始化日志路径的方法为XxlJobFileAppender#initLogPath，内容如下：
private static String logBasePath = &amp;#34;/data/applogs/xxl-job/jobhandler&amp;#34;; private static String glueSrcPath = logBasePath.concat(&amp;#34;/gluesource&amp;#34;); /** * 初始化日志路径 */ public static void initLogPath(String logPath){ // init if (logPath!=null &amp;amp;&amp;amp; logPath.trim().length()&amp;gt;0) { logBasePath = logPath; } // mk base dir // 如果路径不存在就创建 File logPathDir = new File(logBasePath); if (!logPathDir.exists()) { logPathDir.mkdirs(); } logBasePath = logPathDir.getPath(); // mk glue dir // 如果路径不存在就创建 File glueBaseDir = new File(logPathDir, &amp;#34;gluesource&amp;#34;); if (!glueBaseDir.exists()) { glueBaseDir.mkdirs(); } glueSrcPath = glueBaseDir.getPath(); } 这块内容比较简单，就是初始化了两个路径： logBasePath：任务执行日志路径，默认为/data/applogs/xxl-job/jobhandler，可以在application.properties中配置 glueSrcPath：glue脚本保存路径，默认为${logBasePath}/gluesource
关于任务的执行日志，效果如下：
122.log中的122就是xxl_job_log表的Id，在界面查看如下：
然后发现两者内容一致：
从而可知，执行日志就是从这里来的。对于任务来说，每执行1次任务，都会在xxl_job_log中生成一条记录，并且在相应的执行器上生成一个日志文件，日志文件的名称${jobLogId}.log（${jobLogId}为xxl_job_log表的Id）。
值得一提的是，执行日志保存在了执行器所在服务器，管理后台则是通过http请求到执行器获取执行日志的，关于admin与executor之间的通讯，我们后面再作讨论。
2. 初始化admin客户端 前面提到，在application.properties中配置admin的地址时，可以配置多个并且使用,分开，这里就是将一个个adminAddress变成一个AdminBizClient的操作了，处理方法为XxlJobExecutor#initAdminBizList，代码如下：
private static List&amp;lt;AdminBiz&amp;gt; adminBizList; private void initAdminBizList(String adminAddresses, String accessToken) throws Exception { if (adminAddresses!=null &amp;amp;&amp;amp; adminAddresses.trim().length()&amp;gt;0) { for (String address: adminAddresses.trim().split(&amp;#34;,&amp;#34;)) { if (address!=null &amp;amp;&amp;amp; address.trim().length()&amp;gt;0) { // 创建 AdminBizClient 对象 AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken); if (adminBizList == null) { adminBizList = new ArrayList&amp;lt;AdminBiz&amp;gt;(); } adminBizList.add(adminBiz); } } } } 这样之后，就可以得到一个AdminBizClient的List了，List中的每个AdminBizClient实例都对应着一个adminAddress。
AdminBizClient主要是用来处理中executor到admin的http请求，代码如下：
public class AdminBizClient implements AdminBiz { public AdminBizClient() { } /** * 构造方法 */ public AdminBizClient(String addressUrl, String accessToken) { this.addressUrl = addressUrl; this.accessToken = accessToken; // valid if (!this.addressUrl.endsWith(&amp;#34;/&amp;#34;)) { this.addressUrl = this.addressUrl &#43; &amp;#34;/&amp;#34;; } } /** admin项目的url地址 */ private String addressUrl ; /** 请求admin的token */ private String accessToken; /** 请求超时时间，默认3s */ private int timeout = 3; /** * */ @Override public ReturnT&amp;lt;String&amp;gt; callback(List&amp;lt;HandleCallbackParam&amp;gt; callbackParamList) { return XxlJobRemotingUtil.postBody(addressUrl&#43;&amp;#34;api/callback&amp;#34;, accessToken, timeout, callbackParamList, String.class); } /** * 执行器注册 * 在执行器启动的时候，需要把该执行器的地址信息注册到admin项目 */ @Override public ReturnT&amp;lt;String&amp;gt; registry(RegistryParam registryParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;api/registry&amp;#34;, accessToken, timeout, registryParam, String.class); } /** * 执行器移除 * 在执行器关闭前，需要把该执行器的地址信息从admin项目移除 */ @Override public ReturnT&amp;lt;String&amp;gt; registryRemove(RegistryParam registryParam) { return XxlJobRemotingUtil.postBody(addressUrl &#43; &amp;#34;api/registryRemove&amp;#34;, accessToken, timeout, registryParam, String.class); } } AdminBizClient主要包含了3个属性与3个方法，这3个方法就是用来处理admin交互的相关操作的。注意区别上一小节中日志文件内容的获取操作：
admin获取任务的执行日志，是由admin请求执行器获取的； 本小节说的AdminBizClient，是由执行器请求admin的 3. 清除任务执行日志 在前面提到过，每执行一次任务，都会在执行器上生成一个${jobLogId}.log的文件，久而久之，这些日志文件将会非常多，持续占用磁盘空间。为了解决这个问题，xxl-job提供了清理任务执行日志的线程，方法为JobLogFileCleanThread#start：
/** * 启动方法 */ public void start(final long logRetentionDays){ // limit min value // 日志至少要保存3天，如果小于3天，线程就不启动了，也就是不清理日志文件了 if (logRetentionDays &amp;lt; 3 ) { return; } localThread = new Thread(new Runnable() { // 省略run()方法，下面会介绍 ... }); localThread.setDaemon(true); localThread.setName(&amp;#34;xxl-job, executor JobLogFileCleanThread&amp;#34;); localThread.start(); } /** * 停止方法 */ public void toStop() { toStop = true; if (localThread == null) { return; } // interrupt and wait localThread.interrupt(); try { localThread.join(); } catch (InterruptedException e) { logger.error(e.getMessage(), e); } } JobLogFileCleanThread类的启动与停止方法，与前面介绍的admin模块的几个线程可谓是一模一样的套路啊，这些就不赘述了，直接来看localThread的run()方法：
@Override public void run() { while (!toStop) { try { // clean log dir, over logRetentionDays // 获取日志文件夹下的所有文件与文件夹 File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles(); if (childDirs!=null &amp;amp;&amp;amp; childDirs.length&amp;gt;0) { // today // 今天开始的时间 Calendar todayCal = Calendar.getInstance(); todayCal.set(Calendar.HOUR_OF_DAY,0); todayCal.set(Calendar.MINUTE,0); todayCal.set(Calendar.SECOND,0); todayCal.set(Calendar.MILLISECOND,0); Date todayDate = todayCal.getTime(); for (File childFile: childDirs) { // valid if (!childFile.isDirectory()) { continue; } if (childFile.getName().indexOf(&amp;#34;-&amp;#34;) == -1) { continue; } // file create date // 获取文件夹创建日期，文件夹以 yyyy-MM-dd 的形式命名 Date logFileCreateDate = null; try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&amp;#34;yyyy-MM-dd&amp;#34;); logFileCreateDate = simpleDateFormat.parse(childFile.getName()); } catch (ParseException e) { logger.error(e.getMessage(), e); } if (logFileCreateDate == null) { continue; } // 比较日期，超过最大保留时间，就删除该目录及其子目录中的文件 if ((todayDate.getTime()-logFileCreateDate.getTime()) &amp;gt;= logRetentionDays * (24 * 60 * 60 * 1000) ) { // 递归删除日志文件：删除目录及其子目录中的文件 FileUtil.deleteRecursively(childFile); } } } } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } // 休眠，时间为1天 try { TimeUnit.DAYS.sleep(1); } catch (InterruptedException e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor JobLogFileCleanThread&amp;#34; &#43;&amp;#34; thread destory.&amp;#34;); } 删除执行日志代码比较简单，相关逻辑注释中已经标明了，就细细分析了。总结下该线程的执行内容：从执行日志目录下找到日志文件夹，解析出任务执行日期，如果执行日期超过了最大保留天数，就删除该目录及其文件。
再来看一眼执行日志的目录结构：
4. 处理回调admin的线程 继续，看看 TriggerCallbackThread#start 方法：
/** * 启动方法 */ public void start() { // valid if (XxlJobExecutor.getAdminBizList() == null) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, executor callback config fail, &amp;#34; &#43; &amp;#34;adminAddresses is null.&amp;#34;); return; } // callback triggerCallbackThread = new Thread(new Runnable() { @Override public void run() { // 省略线程执行的内容 ... } }); triggerCallbackThread.setDaemon(true); triggerCallbackThread.setName(&amp;#34;xxl-job, executor TriggerCallbackThread&amp;#34;); triggerCallbackThread.start(); // retry triggerRetryCallbackThread = new Thread(new Runnable() { @Override public void run() { // 省略线程执行的内容 ... } }); triggerRetryCallbackThread.setDaemon(true); triggerRetryCallbackThread.start(); } /** * 停止方法 */ public void toStop(){ toStop = true; // stop callback, interrupt and wait if (triggerCallbackThread != null) { triggerCallbackThread.interrupt(); try { triggerCallbackThread.join(); } catch (InterruptedException e) { logger.error(e.getMessage(), e); } } // stop retry, interrupt and wait if (triggerRetryCallbackThread != null) { triggerRetryCallbackThread.interrupt(); try { triggerRetryCallbackThread.join(); } catch (InterruptedException e) { logger.error(e.getMessage(), e); } } } 这个方法创建了两个线程并启动：
triggerCallbackThread：处理回调的线程 triggerRetryCallbackThread：处理重试的线程 可以看到，xxl-job线程的启动与关闭套路是一模一样的，这套路前面已经分析过了，这里就不多说了。关于这两个线程的run()方法，后面分析任务的执行时再详细展开。
5. 初始化内部的http服务器 接下来就是重头戏：初始化内部的http服务器，方法是XxlJobExecutor#initEmbedServer，代码如下：
private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception { // 端口，如果未配置，就找一个可用的 port = port&amp;gt;0?port: NetUtil.findAvailablePort(9999); // ip，如果没指定就自动获取本机ip ip = (ip!=null&amp;amp;&amp;amp;ip.trim().length()&amp;gt;0)?ip: IpUtil.getIp(); // 如果未配置执行器的http访问地址，就生成，形式：http://{ip}:{端口} if (address==null || address.trim().length()==0) { // registry-address：default use address to registry , // otherwise use ip:port if address is null String ip_port_address = IpUtil.getIpPort(ip, port); address = &amp;#34;http://{ip_port}/&amp;#34;.replace(&amp;#34;{ip_port}&amp;#34;, ip_port_address); } // 执行器与admin的访问token if (accessToken==null || accessToken.trim().length()==0) { logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job accessToken is empty. To ensure system &amp;#34; &#43; &amp;#34;security, please set the accessToken.&amp;#34;); } // 启动http服务器 embedServer = new EmbedServer(); embedServer.start(address, port, appname, accessToken); } 实际上，该方法就做了两件事：
准备参数，如ip、port、address、accessToken等 启动http服务器，第1步准备的参数就是这里用到的 继续看http服务器的服务，进入EmbedServer#start方法：
public void start(final String address, final int port, final String appname, final String accessToken) { // 业务执行器 executorBiz = new ExecutorBizImpl(); thread = new Thread(new Runnable() { @Override public void run() { // 省略 run() 方法的内容 ... } }); thread.setDaemon(true); thread.start(); } 这个方法先是创建了一个executorBiz实例，类型是ExecutorBizImpl，这个处理的是执行器相关业务，后面再详细分析；接着就是新建了一个线程，在线程里进行一系列的操作，接下来我们重点关注这个run()方法：
@Override public void run() { // netty 的两个线程池 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); // 处理业务的线程池 ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor( 0, 200, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, &amp;#34;xxl-rpc, EmbedServer bizThreadPool-&amp;#34; &#43; r.hashCode()); } }, // 注意拒绝策略：直接抛出异常 new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { throw new RuntimeException(&amp;#34;xxl-job, EmbedServer bizThreadPool &amp;#34; &#43; &amp;#34; is EXHAUSTED!&amp;#34;); } }); try { // 组装服务器端 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) // 使用的是 nio .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() { @Override public void initChannel(SocketChannel channel) throws Exception { channel.pipeline() // 空闲监听，在 EmbedHttpServerHandler#userEventTriggered 处理空闲事件 .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // http 相关处理 .addLast(new HttpServerCodec()) .addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // 业务处理 .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool)); } }) .childOption(ChannelOption.SO_KEEPALIVE, true); // 绑定端口 ChannelFuture future = bootstrap.bind(port).sync(); logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job remoting server start success, nettype = {}, &amp;#34; &#43; &amp;#34;port = {}&amp;#34;, EmbedServer.class, port); // start registry // 注册执行器到 admin startRegistry(appname, address); // 在这里真正启动服务，并且会在这里阻塞 future.channel().closeFuture().sync(); } catch (InterruptedException e) { if (e instanceof InterruptedException) { logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job remoting server stop.&amp;#34;); } else { logger.error(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job remoting server error.&amp;#34;, e); } } finally { // 停止 try { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } catch (Exception e) { logger.error(e.getMessage(), e); } } } 在这个方法里终于看到了内置的http服务的神秘面纱：原来底层是由netty实现的！关于该方法，几点说明如下：
处理业务的线程池bizThreadPool：该线程池是用来处理业务，注意区别于netty的bossGroup与workerGroup（分别处理网络连接与读写事件），netty建议耗时久的操作使用专门的线程池来处理，避免阻塞网络事件的处理。
注意bizThreadPool的拒绝策略为直接抛出异常，不使用当前线程来处理也是在避免阻塞网络事件的处理。
netty的childHandler中，添加了空闲监听：IdleStateHandler，这是netty提供的空闲监测器，所谓的空闲，是指网络连接有一段时间没有发生读、写以及读写事件。
根据IdleStateHandler构造方法的参数来看，在90秒内连接上没有发生读写事件时，即表示出现了空闲。当监测到当前连接有空闲时，就发出IdleStateEvent事件，该事件会在EmbedHttpServerHandler#userEventTriggered中处理：
@Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // 处理空闲事件，即监听到当前channel空闲了 if (evt instanceof IdleStateEvent) { // 关闭当前连接 ctx.channel().close(); logger.debug(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job provider netty_http server&amp;#34; &#43; &amp;#34; close an idle channel.&amp;#34;); } else { super.userEventTriggered(ctx, evt); } } 这个内置的http服务器究竟是用来干嘛的呢？根据netty的代码套咱，答案就在EmbedHttpServerHandler#channelRead0方法中：
@Override protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { // 处理http的相关内容：请求体、uri、请求方法、请求token String requestData = msg.content().toString(CharsetUtil.UTF_8); String uri = msg.uri(); HttpMethod httpMethod = msg.method(); boolean keepAlive = HttpUtil.isKeepAlive(msg); String accessTokenReq = msg.headers().get( XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN); // 在业务线程池中处理 bizThreadPool.execute(new Runnable() { @Override public void run() { // 具体的执行操作在这里 Object responseObj = process(httpMethod, uri, requestData, accessTokenReq); // 转成json String responseJson = GsonTool.toJson(responseObj); // http的响应 writeResponse(ctx, keepAlive, responseJson); } }); } 这个方法比较简单，就是获取http的相关内容,如请求体、uri、请求方法、请求token等，然后在业务线程池中执行业务操作，执行业务操作的关键方法是EmbedHttpServerHandler#process，关于该方法的执行内容本文就不介绍了，后面介绍具体操作时再详细展开。
对于执行器提供的http接口，可以参考xxl-job官方文档中关于执行器 RESTful API的介绍。
处理执行器的注册，方法是EmbedServer#startRegistry，其实就是将执行器的地址告知admin，这样admin才会感知到执行器的存在，关于执行器的注册流程，后面会单独分析。
启动netty服务，代码为future.channel().closeFuture().sync()，到这里内置的http服务器就真正启动了，可以对外提供服务了，并且线程会在这里阻塞直到停止。
那么这个服务要如何停止呢？我们来看看EmbedServer#toStop方法：
public void stop() throws Exception { // destroy server thread if (thread!=null &amp;amp;&amp;amp; thread.isAlive()) { thread.interrupt(); } // stop registry stopRegistry(); logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job remoting server destroy success.&amp;#34;); } 停止方法比较粗暴，直接使用thread.interrupt()中断该线程的执行，接着就调用stopRegistry()方法来结束执行器的注册，其实就是从admin中删除当前执行器的信息。
总的来说，EmbedServer#start方法的逻辑还是很清晰明了的，不过该方法主要依赖了netty，对于netty不熟悉的小伙伴理解起来可能比较困难，这块可参考本人的《netty入门与实战》系列文章.
总结 本文先是介绍了xxl-job执行器集成流程，接着重点介绍了执行器启动流程，用一张图来总结下启动流程：
本文也挖了两个坑：
内置服务器的业务操作，也就是EmbedHttpServerHandler#process方法 执行器注册到admin，也就是EmbedServer#startRegistry方法 我们下一篇继续。
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob02：admin启动流程</title>
        <url>https://www.szlinkroutes.com/post/xxljob02admin%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html">  注：本系列源码分析基于XxlJob 2.3.0，gitee仓库链接：https://gitee.com/funcy/xxl-job.git.
本文我们将分析xxl-job-admin的启动流程。xxl-job-admin 是一个springboot项目，直接启动com.xxl.job.admin.XxlJobAdminApplication就可以了，但是在启动过程中，xxl-job相关功能是如何初始化的呢？
spring 配置类: XxlJobAdminConfig 经过本人的一番探索，发现xxl-job 相关组件的启动类为com.xxl.job.admin.core.conf.XxlJobAdminConfig：
@Component public class XxlJobAdminConfig implements InitializingBean, DisposableBean { private XxlJobScheduler xxlJobScheduler; @Override public void afterPropertiesSet() throws Exception { adminConfig = this; xxlJobScheduler = new XxlJobScheduler(); xxlJobScheduler.init(); } @Override public void destroy() throws Exception { xxlJobScheduler.destroy(); } ... // 省略了属性以及setter、getter方法 } 关于这个类，几点说明如下：
该类被@Component标记，表明这是个spring bean，享有spring bean的生命同期； 该类实现了InitializingBean与DisposableBean两个接口，提供了spring bean在初始化及销毁阶段的一些操作，对应的两个方法如下： afterPropertiesSet：来自InitializingBean接口，用来处理bean在初始化时的一些操作 destroy：来自DisposableBean接口，用来处理bean在销毁时的一些操作 而实际上，XxlJobAdminConfig的afterPropertiesSet与destroy两个方法就是xxl-job启动与关闭的关键所在，而这两个方法都是调用的xxlJobScheduler的方法，接下来我们就来分析XxlJobScheduler#init与XxlJobScheduler#destroy方法。
这两个方法代码如下：
两个方法对比下可以看到，init方法正序启动了一系列组件，而destroy方法逆序关闭了一系列组件，正所谓“先启动的后关闭”。他们启动或关闭的组件如下：
JobTriggerPool：任务的触发线程，用来把需要执行的任务提交到执行器 JobRegistry：任务执行器注册监听，用来监听执行器注册操作，及时移除无效的执行器 JobFailMonitor：失败任务监听，用来监听失败任务，发送告警，对于重试次数大于0的失败任务会发再次触发执行 JobComplete：任务完成监听，用来监听任务是否完成，把长时间处于运行中的任务标记为失败 JobLogReport：任务报表，用来汇总任务的整体执行情况，也是管理后台“运行报表”菜单的数据来源 JobSchedule：任务调度，用来获取接下来要执行的任务，将这些任务提交给触发线程 接下来我们就来看下这些组件的实现。
触发线程池：JobTriggerPoolHelper 触发线程池的处理类为JobTriggerPoolHelper，我们来看看它的启动方法JobTriggerPoolHelper.toStart()：
public class JobTriggerPoolHelper { // fast/slow thread pool private ThreadPoolExecutor fastTriggerPool = null; private ThreadPoolExecutor slowTriggerPool = null; // 单例对象 private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper(); // toStart() 方法 public static void toStart() { // 往下调用 helper.start(); } // toStop() 方法 public static void toStop() { // 往下调用 helper.stop(); } /** * 创建了两个线程池 */ public void start() { // 创建 fastTrigger 线程池 fastTriggerPool = new ThreadPoolExecutor( 10, // 在application.proerties中配置，默认 200 XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), 60L, TimeUnit.SECONDS, new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;(1000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, &amp;#34;xxl-job, admin &amp;#34; &#43; &amp;#34;JobTriggerPoolHelper-fastTriggerPool-&amp;#34; &#43; r.hashCode()); } }); // 创建 slowTrigger 线程池 slowTriggerPool = new ThreadPoolExecutor( 10, // 在application.proerties中配置，默认 100 XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(), 60L, TimeUnit.SECONDS, new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, &amp;#34;xxl-job, admin &amp;#34; &#43; &amp;#34;JobTriggerPoolHelper-slowTriggerPool-&amp;#34; &#43; r.hashCode()); } }); } public void stop() { //triggerPool.shutdown(); fastTriggerPool.shutdownNow(); slowTriggerPool.shutdownNow(); logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job trigger thread pool shutdown success.&amp;#34;); } // 省略其他代码 ... } 以上代码还是比较简单的，start()仅仅创建了两个线程池，从名称上来讲，fastTriggerPool用来处理耗时较短的任务的，slowTriggerPool用来处理耗时较长的任务的，这样分开是为了避免耗时长的任务挤满了线程池从而阻塞其他任务的执行。而stop()方法也简单，就是用来关闭这两个线程池。
这两个线程池启动成后，任务的触发就是由这两个线程池来处理的，不过这块内容本文就先不分析了，在后面分析任务的调度过程时再重点分析。
执行器注册监测：JobRegistryHelper 执行器的注册类为JobRegistryHelper，所谓的执行器，就是具体执行任务的服务，如xxl-job提供的示例执行器xxl-job-executor-sample-springboot，注意到xxl-admin与执行器处于不同进程，那么如何快速监测执行器的注册、下线呢？这就是JobRegistryHelper所做的工作，进入JobRegistryHelper#start方法：
public void start(){ // for registry or remove registryOrRemoveThreadPool = new ThreadPoolExecutor( 2, 10, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, &amp;#34;xxl-job, admin &amp;#34; &#43; &amp;#34;JobRegistryMonitorHelper-registryOrRemoveThreadPool-&amp;#34; &#43; r.hashCode()); } }, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 拒绝策略，如果线程池已满，则直接执行任务（谁添加谁执行） r.run(); logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, registry or remove too fast, &amp;#34; &#43; &amp;#34;match threadpool rejected handler(run now).&amp;#34;); } }); // for monitor registryMonitorThread = new Thread(new Runnable() { // 线程池内容 ... }); // 设置线程的一些属性，如：名称，设置为守护线程 registryMonitorThread.setDaemon(true); registryMonitorThread.setName(&amp;#34;xxl-job, admin &amp;#34; &#43; &amp;#34;JobRegistryMonitorHelper-registryMonitorThread&amp;#34;); // 真正地启动线程 registryMonitorThread.start(); } 以上方法比先是创建了一个线程池，然后创建了一个线程并启动，代码比较清晰，关键注释已经在代码中了，就不多说了。
注意到上面的代码其实省略了registryMonitorThread的run()方法，了解java多线程的小伙伴应该明白，Thread的run()方法就是线程运行的核心所在，下面我们就来看下registryMonitorThread的run()方法：
public void run() { while (!toStop) { try { // 1. 查找自动注册的执行器 List&amp;lt;XxlJobGroup&amp;gt; groupList = XxlJobAdminConfig.getAdminConfig() .getXxlJobGroupDao().findByAddressType(0); if (groupList!=null &amp;amp;&amp;amp; !groupList.isEmpty()) { // 2. 找到心跳超时的执行器 List&amp;lt;Integer&amp;gt; ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao() .findDead(RegistryConfig.DEAD_TIMEOUT, new Date()); if (ids!=null &amp;amp;&amp;amp; ids.size()&amp;gt;0) { // 移除操作，就是数据库的删除操作 XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids); } // 3. 处理在线的执行器，用HashMap&amp;lt;String,List&amp;lt;String&amp;gt;&amp;gt;保存，key为appName，value为ip HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; appAddressMap = new HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt;(); // 找到有效的执行器 List&amp;lt;XxlJobRegistry&amp;gt; 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&amp;lt;String&amp;gt; registryList = appAddressMap.get(appname); if (registryList == null) { registryList = new ArrayList&amp;lt;String&amp;gt;(); } if (!registryList.contains(item.getRegistryValue())) { registryList.add(item.getRegistryValue()); } // 将执行器的ip按appName分类，并保存到map中 appAddressMap.put(appname, registryList); } } } for (XxlJobGroup group: groupList) { List&amp;lt;String&amp;gt; registryList = appAddressMap.get(group.getAppname()); String addressListStr = null; if (registryList!=null &amp;amp;&amp;amp; !registryList.isEmpty()) { Collections.sort(registryList); StringBuilder addressListSB = new StringBuilder(); // 多个服务器ip使用,连接 for (String item:registryList) { addressListSB.append(item).append(&amp;#34;,&amp;#34;); } addressListStr = addressListSB.toString(); addressListStr = addressListStr.substring(0, addressListStr.length()-1); } group.setAddressList(addressListStr); group.setUpdateTime(new Date()); // 4. 更新组的执行器ip XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group); } } } catch (Exception e) { if (!toStop) { logger.error(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job registry monitor thread error:{}&amp;#34;, e); } } try { // 5. 休眠，时间为30s，等于心跳超时时间 TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); } catch (InterruptedException e) { if (!toStop) { logger.error(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job registry monitor thread error:{}&amp;#34;, e); } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job registry monitor thread stop&amp;#34;); } 对于该方法的关键之处，注释已经标明了，下面来一一看看这些操作。
1. 查找自动注册的执行器 代码如下：
List&amp;lt;XxlJobGroup&amp;gt; groupList = XxlJobAdminConfig.getAdminConfig() // addressType 指定为0，表示注册方式为自动注册 .getXxlJobGroupDao().findByAddressType(0); 在执行器注册时，可以选择注册方式（自动注册与手动录入）：
从上述代码的findByAddressType(0)可知，该线程只关注注册方式为自动注册的执行器。
跟进.findByAddressType(0)方法，最终进入mybatis的xml文件，执行的sql如下：
SELECT &amp;lt;include refid=&amp;#34;Base_Column_List&amp;#34; /&amp;gt; FROM xxl_job_group AS t WHERE t.address_type = #{addressType} ORDER BY t.app_name, t.title, t.id ASC 查询的是xxl_job_group表，语句比较简单，就不多分析了。
2. 查找心跳超时的执行器并移除 查到自动注册的执行器列表后，接下来就是找到各执行器下的有哪个服务挂了，并将其移除，代码如下：
// 找到心跳超时的执行器 List&amp;lt;Integer&amp;gt; ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao() .findDead(RegistryConfig.DEAD_TIMEOUT, new Date()); if (ids!=null &amp;amp;&amp;amp; ids.size()&amp;gt;0) { // 移除操作，就是数据库的删除操作 XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids); } 这段代码有两个关键的方法：
查找无效的执行器：.findDead(RegistryConfig.DEAD_TIMEOUT, new Date()) 移除无效的执行器：.removeDead(ids) 先来看.findDead(RegistryConfig.DEAD_TIMEOUT, new Date())方法，这里的RegistryConfig.DEAD_TIMEOUT为90，即执行器最近一次的注册时间与当时时间超时了90秒，就认为该执行器无效。进入该方法，最终执行的sql如下：
SELECT t.id FROM xxl_job_registry AS t WHERE t.update_time &amp;lt;![CDATA[ &amp;lt; ]]&amp;gt; DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND) 可以看到这次操作的表是xxl_job_registry，该表保存的是执行器的注册记录，sql语句用到了mysql的时间计算函数，这块就不多说了。
继续来看看.removeDead(ids)方法，参数中的id就是上面找到的无效的执行器id，进入该方法，最终执行的sql语句如下：
DELETE FROM xxl_job_registry WHERE id in &amp;lt;foreach collection=&amp;#34;ids&amp;#34; item=&amp;#34;item&amp;#34; open=&amp;#34;(&amp;#34; close=&amp;#34;)&amp;#34; separator=&amp;#34;,&amp;#34; &amp;gt; #{item} &amp;lt;/foreach&amp;gt; 所做的工作就是把无效的执行器从xxl_job_registry表中删除。
3. 查找有效的执行器 处理完无效的执行器后，觉得就是处理有效的执行器了，代码如下：
// 找到有效的执行器 List&amp;lt;XxlJobRegistry&amp;gt; list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao() .findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); // 将执行器的ip按appName分类，并保存到map中 HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; appAddressMap = new HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt;(); if (list != null) { for (XxlJobRegistry item: list) { if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { String appname = item.getRegistryKey(); List&amp;lt;String&amp;gt; registryList = appAddressMap.get(appname); if (registryList == null) { registryList = new ArrayList&amp;lt;String&amp;gt;(); } if (!registryList.contains(item.getRegistryValue())) { registryList.add(item.getRegistryValue()); } // 将执行器的ip按appName分类，并保存到map中 appAddressMap.put(appname, registryList); } } } 这块代码关键就两处：
查找有效的执行器： .findAll(RegistryConfig.DEAD_TIMEOUT, new Date()) 得到有效执行器列表：HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; appAddressMap，key是执行器的appName，value是一个列表，存放的是执行器列表，支持多个执行器 先看 .findAll(RegistryConfig.DEAD_TIMEOUT, new Date())方法，这里的RegistryConfig.DEAD_TIMEOUT同样为90，与无效的执行器相反，最近一次的注册时间与当时时间小于90秒，就认为该执行器有效，执行的sql如下：
SELECT &amp;lt;include refid=&amp;#34;Base_Column_List&amp;#34; /&amp;gt; FROM xxl_job_registry AS t WHERE t.update_time &amp;lt;![CDATA[ &amp;gt; ]]&amp;gt; DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND) 拿到有效的执行器之后该如何处理呢？关键就是HashMap&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; appAddressMap这个map了，上面的代码虽然看着好几行，其实所做的事就只有一点：将执行器的ip按appName分类，并保存到map中，map的key是执行器的appName，value是一个列表，存放的是执行器列表。
到这里，有效执行的查找流程就完成了。
4. 更新有效的执行器ip 继续，接着来看有效执行器的更新：
// 遍历第1步得到的 groupList for (XxlJobGroup group: groupList) { // 根据`group`的`appName`从`appAddressMap`中得到对应的执行器列表 List&amp;lt;String&amp;gt; registryList = appAddressMap.get(group.getAppname()); String addressListStr = null; if (registryList!=null &amp;amp;&amp;amp; !registryList.isEmpty()) { Collections.sort(registryList); StringBuilder addressListSB = new StringBuilder(); // 多个服务器ip使用,连接 for (String item:registryList) { addressListSB.append(item).append(&amp;#34;,&amp;#34;); } addressListStr = addressListSB.toString(); addressListStr = addressListStr.substring(0, addressListStr.length()-1); } group.setAddressList(addressListStr); group.setUpdateTime(new Date()); // 更新组的执行器ip XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group); } 这块代码所做的工作如下：
遍历第1步得到的 groupList，针对每个group进行操作 根据group的appName从appAddressMap中得到对应的执行器地址列表addressList 第2步得到的addressList是一个List，接下来会把这个List转换成String，地址之间使用&amp;quot;,&amp;ldquo;分隔 更新组的执行器ip 我们直接进入.update(group)方法，看看更新组的执行器ip是如何处理的：
UPDATE xxl_job_group SET `app_name` = #{appname}, `title` = #{title}, `address_type` = #{addressType}, `address_list` = #{addressList}, `update_time` = #{updateTime} WHERE id = #{id} 这次操作的是xxl_job_group表，主要是将addressList更新到表中，这块就不多说了。
为了看下多个执行器的数据，本人特地开了两个xxl-job-executor-sample-springboot实例，数据如下：
xxl_job_registry 表：
xxl_job_group表：
5. 休眠，等待下次唤醒 更新完有效的执行器后，本次监测任务就完成了，接下来就是休息了：
try { // 5. 休眠，时间为30s，等于心跳超时时间 TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); } catch (InterruptedException e) { if (!toStop) { logger.error(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job registry monitor thread error:{}&amp;#34;, e); } } 这块就是Thread.sleep操作，即线程休眠，没啥好说的。
分析到这里，该线程似乎有个缺陷：如果有两个xxl-job-admin实例，那么两个实例上都会运行该线程，这样就导致了两台机器上都会执行“查找无效的执行器&amp;ndash;&amp;gt;移除”、“查找有效的执行器&amp;ndash;&amp;gt;更新”两个操作。按道理讲，同一时间内这两个操作只需要在其中一台机器上执行就可以了，而代码中并无限制，频繁操作数据库无疑会造成资源浪费。
6. JobRegistryHelper#toStop 方法 分析完线程的启动后，接下来看看线程的停止操作，进入JobRegistryHelper#toStop方法：
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); } } 可以说，这个方法的每一步操作都是关键，我们一步步来看：
关闭下一次执行：toStop = ture 在registryMonitorThread的run方法中，有这样的操作：
public void run() { while (!toStop) { ... } } 将toStop设置为true之后，下次while循环开始时，就不会再执行while循环体的代码了，在这里就跳出while循环了。
关闭线程池：shutdownNow()，这个是立即关闭registryOrRemoveThreadPool，没啥好说的。
打断线程：interrupt() 前面已经将toStop设置为true了，还需求“打断”线程吗？注意到线程有休眠操作：
try { TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); } catch (InterruptedException e) { ... } 如果调用toStop()方法时，线程正好在休眠中，此时即时将toStop设置为true了，线程在漫长的休眠间唤醒后才会终结，因此interrupt()方法就是为了让线程立即结束休眠操作的。
等待线程终结：join() 这里就是一个小细节了，在一开始设置线程的属性时，会这样的设置：registryMonitorThread.setDaemon(true)，即把registryMonitorThread设置成了“守护线程”，区别于非守护线程，主线程在结束的时候不管守护线程的死活（主线程要等到非守护线程结束时才会结束），join()方法的注释如下： 注释的第一句就表明了，当前线程会在等待直到registryMonitorThread终结。
失败任务监测：JobFailMonitorHelper 接着来看看失败任务的监测，代码如下：
private Thread monitorThread; private volatile boolean toStop = false; /** * start 方法 */ public void start(){ monitorThread = new Thread(new Runnable() { // 先省略线程执行的内容 ... }); monitorThread.setDaemon(true); monitorThread.setName(&amp;#34;xxl-job, admin JobFailMonitorHelper&amp;#34;); monitorThread.start(); } /** * stop 方法 */ public void toStop(){ toStop = true; // interrupt and wait monitorThread.interrupt(); try { monitorThread.join(); } catch (InterruptedException e) { logger.error(e.getMessage(), e); } } 可以看到，JobFailMonitorHelper的start()/toStop()方法与JobRegistryHelper形式上几乎一致，而事实上，后面几个组件的代码形式上也基本一致，start()方法都是创建线程、设置属性、启动，toStop()方法也都是设置停止标识、打断线程、等待线程终结，考虑到篇幅原因，后面的分析中，我们将重点关注每个组件不同的部分，也就是各线程的run()方法。
monitorThread的run()方法如下：
@Override public void run() { // monitor while (!toStop) { try { // 1. 查找失败的任务，失败任务的定义：运行日志中，code非200 List&amp;lt;Long&amp;gt; failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .findFailJobLogIds(1000); if (failLogIds!=null &amp;amp;&amp;amp; !failLogIds.isEmpty()) { for (long failLogId: failLogIds) { // 2. lock log，简单的加锁操作，利用mysql行锁，更新成功表示加锁成功 int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .updateAlarmStatus(failLogId, 0, -1); if (lockRet &amp;lt; 1) { continue; } XxlJobLog log = XxlJobAdminConfig.getAdminConfig() .getXxlJobLogDao().load(failLogId); XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao() .loadById(log.getJobId()); // 3、fail retry monitor：失败重试次数大于0 if (log.getExecutorFailRetryCount() &amp;gt; 0) { // 触发执行 JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); String retryMsg = &amp;#34;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span style=\&amp;#34;color:#F39C12;\&amp;#34; &amp;gt; &amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;#34; &#43; I18nUtil.getString(&amp;#34;jobconf_trigger_type_retry&amp;#34;) &#43;&amp;#34;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; &amp;lt;/span&amp;gt;&amp;lt;br&amp;gt;&amp;#34;; log.setTriggerMsg(log.getTriggerMsg() &#43; retryMsg); XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .updateTriggerInfo(log); } // 4、fail alarm monitor 失败告警 // 告警状态：0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 int newAlarmStatus = 0; if (info!=null &amp;amp;&amp;amp; info.getAlarmEmail()!=null &amp;amp;&amp;amp; info.getAlarmEmail().trim().length()&amp;gt;0) { 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(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job fail monitor thread error:{}&amp;#34;, e); } } try { // 5. 休眠10s TimeUnit.SECONDS.sleep(10); } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job fail monitor thread stop&amp;#34;); } 这个方法所做的工作如下：
查找失败的任务 更新XxlJobLog的告警状态 失败重试 发送告警邮件 休眠操作 接着我们就逐一看下这几个操作。
1. 查找失败的任务 这块代码如下：
List&amp;lt;Long&amp;gt; failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .findFailJobLogIds(1000); 方法findFailJobLogIds(1000)中的1000，表示的是查询的最大记录数，这方法执行的sql语句如下：
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} sql语句的关键在于where后面的条件，trigger_code与handle_code含义如下： trigger_code：调度状态，初始状态为0，成功状态为200 handle_code：执行状态，初始状态为0，成功状态为200
由此，可以推断出两个条件含义：
trigger_code in (0, 200) and handle_code = 0：调度状态为初始状态或成功，且执行状态为初始状态 handle_code = 200：执行行状态为成功 trigger_code与handle_code的状态机流转如下（关于源码的实现细节在后面分析调度流程时再详细展开）：
由此，该sql得到的结果为除以上两种情况并且alarm_status为0的所有任务记录，即为失败任务。
2. 更新XxlJobLog的告警状态 得到失败的任务后，接下来就是对任务的进一步操作，代码如下：
// 更新任务运行日志的状态 int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .updateAlarmStatus(failLogId, 0, -1); if (lockRet &amp;lt; 1) { continue; } // 获取日志信息，以及任务信息 XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId); XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao() .loadById(log.getJobId()); 首先来看下.updateAlarmStatus(failLogId, 0, -1)方法，执行的sql如下：
UPDATE xxl_job_log SET `alarm_status` = #{newAlarmStatus} WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus} 结合sql语句与方法来看，该方法的功能是将指定任务日志的告警状态（alarm_status）由0更新到-1，这里的alarm_status含义如下：
0：默认 -1：锁定状态 1：无需告警 2：告警成功 3：告警失败 熟悉mysql的执行机制的小伙伴会知道，这条sql语句的执行会触发mysql的行锁，例如，当两个线程同时对logId的任务日志更新时，最终只会有一个线程执行成功，执行成功后.updateAlarmStatus(failLogId, 0, -1)方法的返回值会大于0。因此多个xxl-job-admin实例的情况下，该方法的执行结果可以当作分布式锁来使用，即只有.updateAlarmStatus(failLogId, 0, -1)方法的返回值大于0时，才能继续往下执行，这也对应了如下的判断：
if (lockRet &amp;lt; 1) { continue; } 在线程得到继续往下执行的资格后，接着就是加载详细的日志信息（XxlJobLog）与任务信息（XxlJobInfo）了，这块就是根据id查询记录的操作，就不多说了。
3. 失败重试 线程获得执行资格后，继续往下执行，就到了失败重试的处理，代码如下：
if (log.getExecutorFailRetryCount() &amp;gt; 0) { // 触发执行 JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); // 更新任务，重新设置了 retryMsg 的值 String retryMsg = &amp;#34;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;span style=\&amp;#34;color:#F39C12;\&amp;#34; &amp;gt; &amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;#34; &#43; I18nUtil.getString(&amp;#34;jobconf_trigger_type_retry&amp;#34;) &#43;&amp;#34;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; &amp;lt;/span&amp;gt;&amp;lt;br&amp;gt;&amp;#34;; log.setTriggerMsg(log.getTriggerMsg() &#43; retryMsg); XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log); } 对于重试次数大于0的失败，会调用JobTriggerPoolHelper.trigger(xxx)重新触发任务，然后再更新XxlJobLog的retryMsg字段，关于JobTriggerPoolHelper.trigger(xxx)的具体操作，我们在分析触发器时再详细展开，在这里只需要知道该方法是用来触发任务的就可以了。
对于以上代码，略一思考，发现有两个问题：
executorFailRetryCount的值最初从哪里来？ 执行完成后，executorFailRetryCount的值并没有减1，之后会不会重复执行？ 对于第一个问题，经过一番探索，发现来自于任务信息，可以管理后台界面配置，截图如下：
对于第二个问题，再往下看看的话，就会发现表中该记录的alarm_status的最终值并不是0，这就表示在下一次线程的执行中，该失败记录并不会查询出来（即上面第1步查询失败任务的操作），观察到JobTriggerPoolHelper.trigger(xxx)方法的参数(log.getExecutorFailRetryCount()-1)，即该任务再一次执行时，重试次数就会减1，这样如果后续的执行一直失败，后一次执行记录的重试次数就会比前一次的重试次数少1，直到最后一次执行记录的重试次数为0即止。
比如，现在有id为100的任务，重试次数指定为3，且一直执行失败，执行1次后，所产生的执行记录与重试记录如下：
日志id 任务id 重试次数 trigger_code handle_code alarm_status 1 100 3 200 500 1 2 100 2 200 500 1 3 100 1 200 500 1 4 100 0 200 500 1 4. 发送告警邮件 发送告警邮件的代码如下：
// 4、fail alarm monitor 失败告警 // 告警状态：0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 int newAlarmStatus = 0; if (info!=null &amp;amp;&amp;amp; info.getAlarmEmail()!=null &amp;amp;&amp;amp; info.getAlarmEmail().trim().length()&amp;gt;0) { boolean alarmResult = XxlJobAdminConfig.getAdminConfig() .getJobAlarmer().alarm(info, log); newAlarmStatus = alarmResult?2:3; } else { newAlarmStatus = 1; } 收件人的配置了是在管理后台界面配置:
5. 休眠操作 执行完一波操作之后，接下来线程要休息了，这个线程的休息时间是10s，就不多作介绍了。
任务完成监听：JobCompleteHelper 继续来看下任务完成监听的处理，进入JobCompleteHelper#start方法：
public void start(){ // 创建线程池 callbackThreadPool = new ThreadPoolExecutor( 2, 20, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;(3000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, &amp;#34;xxl-job, admin &amp;#34; &#43;&amp;#34;JobLosedMonitorHelper-callbackThreadPool-&amp;#34; &#43; r.hashCode()); } }, // 拒绝策略：直接使用当前线程处理 new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { r.run(); logger.warn(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, callback too fast, match threadpool &amp;#34; &#43; &amp;#34;rejected handler(run now).&amp;#34;); } }); // 创建监听线程 monitorThread = new Thread(new Runnable() { ... }); monitorThread.setDaemon(true); monitorThread.setName(&amp;#34;xxl-job, admin JobLosedMonitorHelper&amp;#34;); monitorThread.start(); } 这个方法就做了两件事：
创建了一个线程池 创建了一个线程，并启动 关于线程池的作用这里就先不分析了，这里我们重点来看monitorThread所做的工作，其run()方法如下：
@Override public void run() { // 1. 休眠，为了等待 JobTriggerPoolHelper 的初始化 try { TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { if (!toStop) { logger.error(e.getMessage(), e); } } // monitor while (!toStop) { try { // 2. 任务结果丢失处理：调度记录停留在 &amp;#34;运行中&amp;#34; 状态超过10min， // 且对应执行器无效，则将本地调度主动标记失败 Date losedTime = DateUtil.addMinutes(new Date(), -10); List&amp;lt;Long&amp;gt; losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .findLostJobIds(losedTime); if (losedJobIds!=null &amp;amp;&amp;amp; losedJobIds.size()&amp;gt;0) { // 3. 遍历更新，将任务状态更新为失败 for (Long logId: losedJobIds) { XxlJobLog jobLog = new XxlJobLog(); jobLog.setId(logId); jobLog.setHandleTime(new Date()); // FAIL_CODE 就是失败的状态 jobLog.setHandleCode(ReturnT.FAIL_CODE); jobLog.setHandleMsg( I18nUtil.getString(&amp;#34;joblog_lost_fail&amp;#34;) ); XxlJobCompleter.updateHandleInfoAndFinish(jobLog); } } } catch (Exception e) { if (!toStop) { logger.error(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, job fail monitor thread error:{}&amp;#34;, e); } } try { // 4. 休眠，每60s检测一次 TimeUnit.SECONDS.sleep(60); } catch (Exception e) { if (!toStop) { logger.error(e.getMessage(), e); } } } logger.info(&amp;#34;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; xxl-job, JobLosedMonitorHelper stop&amp;#34;); } 该方法所做工作如下：
休眠，为了等待 JobTriggerPoolHelper 的初始化 获取需要处理的任务列表 遍历列表，更新状态 休眠，这次的休眠时间为 60s 关于两个休眠操作没啥好讲的，这里我们重点关注第2与第3步的操作。
获取需要处理的任务列表 获取任务列表的代码如下：
Date losedTime = DateUtil.addMinutes(new Date(), -10); List&amp;lt;Long&amp;gt; losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao() .findLostJobIds(losedTime); 可以看到，代码中先是使用了一个Date实例，表示前10分钟的时候，然后调用 .findLostJobIds(losedTime)方法来查询任务列表，该方法执行的sql语句如下：
-- 查找执行时间超过10分钟，且执行器不存在的执行记录（执行器可能下线了） 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 &amp;lt;![CDATA[ &amp;lt;= ]]&amp;gt; #{losedTime} AND t2.id IS NULL; 从sql语句来看，xxl-job对失败任务的定义为执行时间超过10分钟，且执行器不存在的任务执行记录。
更新任务状态 获取到失败的任务记录后，接着就是更新任务状态了，代码如下：
for (Long logId: losedJobIds) { XxlJobLog jobLog = new XxlJobLog(); jobLog.setId(logId); jobLog.setHandleTime(new Date()); // FAIL_CODE 就是失败的状态 jobLog.setHandleCode(ReturnT.FAIL_CODE); jobLog.setHandleMsg( I18nUtil.getString(&amp;#34;joblog_lost_fail&amp;#34;) ); XxlJobCompleter.updateHandleInfoAndFinish(jobLog); } 上述代码比较简单，就是一个简单的按id更新的操作，最终会将jobLog的handleCode更新为ReturnT.FAIL_CODE，即失败（500）。
任务报表处理：JobLogReportHelper 所谓的报表处理，就是汇总任务的执行情况（总数、成功数、失败数等），然后在管理后台首页进行展示：
代码如下：
public void start(){ // 三天内的任务日志会以每分钟一次的频率异步同步至报表中 logrThread = new Thread(new Runnable() { @Override public void run() { ... } }); logrThread.setDaemon(true); logrThread.setName(&amp;#34;xxl-job, admin JobLogReportHelper&amp;#34;); logrThread.start(); } 关于报表处理这块，由于非任务调试的主流程，因此本文就不深入了，代码不复杂，想了解的小伙伴可自行进入JobLogReportHelper类分析。
调度线程：JobScheduleHelper 调度器是xxl-job的三大核心组件之一（另外两个核心组件分别是触发器、执行器），处理调度操作的类是JobScheduleHelper，它的start()方法如下：
public void start(){ // schedule thread scheduleThread = new Thread(new Runnable() { @Override public void run() { ... } }); scheduleThread.setDaemon(true); scheduleThread.setName(&amp;#34;xxl-job, admin JobScheduleHelper#scheduleThread&amp;#34;); scheduleThread.start(); // ring thread ringThread = new Thread(new Runnable() { @Override public void run() { ... } }); ringThread.setDaemon(true); ringThread.setName(&amp;#34;xxl-job, admin JobScheduleHelper#ringThread&amp;#34;); ringThread.start(); } 可以看到，在start()方法中启动了两个线程：
调度线程：scheduleThread 时间轮处理线程：ringThread 这两个线程至关重要，正是由于这两个线程的相互配合，才使得xxl-job能准时无误地执行任务。由于本文重点介绍admin的启动流程，对JobScheduleHelper#start方法就介绍这么多了，本文只需要知道JobScheduleHelper#start方法启动了两个线程就可以了，关于这两个线程的具体功能，后面分析任务的调度流程时再重点分析。
总结 本文介绍了xxl-job-admin的启动流程，xxl-job-admin是一个springboot项目，启动时初始化其spring配置类，XxlJobAdminConfig，这个类就是xxl-job-admin启动的关键，在这个类里启动了如下组件：
JobTriggerPool：任务的触发线程，用来把需要执行的任务提交到执行器 JobRegistry：任务执行器注册监听，用来监听执行器注册操作，及时移除无效的执行器 JobFailMonitor：失败任务监听，用来监听失败任务，发送告警，对于重试次数大于0的失败任务会发再次触发执行 JobComplete：任务完成监听，用来监听任务是否完成，把长时间处于运行中的任务标记为失败 JobLogReport：任务报表，用来汇总任务的整体执行情况，也是管理后台“运行报表”菜单的数据来源 JobSchedule：任务调度，用来获取接下来要执行的任务，将这些任务提交给触发线程 由于本文的重点在于分析xxl-job-admin的启动流程，因此对于部分组件未深入其中，组件与组件之间如果相互配合工作的也未深究，这些内容在分析具体功能在一一探索。
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
     <entry>
        <title>XxlJob01：环境准备</title>
        <url>https://www.szlinkroutes.com/post/xxljob01%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87/</url>
        <categories>
          
        </categories>
        <tags>
          <tag>xxljob</tag>
        </tags>
        <content type="html"> 关于定时任务，想必大家都不会陌生，简单的定时任务有jdk提供的定时任务、spring提供的定时任务，稍微复杂一些有quartz定时任务，分布式任务有xxl-job、elastic-job。由于项目中使用的定时任务是xxl-job，因此本系列将从源码的角度来分析xxl-job定时任务的设计与实现。
关于xxl-job的操作，xxl-job社区贴心地提供了操作文档：https://www.xuxueli.com/xxl-job/，文档不长，一遍看下来xxl-job的操作基本就会了，这块就不过多叙述了。
1. 获取xxl-job源码 既然是源码分析，当然要先获取源码了，xxl-job的github地址为https://github.com/xuxueli/xxl-job。一般来说，直接frok这个仓库就可以了，不过由于国内网络的原因，github 的访问并不稳定，比如checkout到一半网络断开、push超时等。为了规避这些问题，建议使用国内的git仓库——gitee，操作方式如下：
1.1 创建新的gitee仓库 登录个人的gitee，点击“&#43;”号，选择“从github/gitlab导入仓库”：
1.2 导入xxl-job仓库 复制xxl-job的github地址，点击导入：
等待一会，就得到了自己的xxl-job的gitee地址了，本人得到的地址为https://gitee.com/funcy/xxl-job，并且gitee上也提供一个按钮，可以很方便地同步github上的代码：
如果有一天，你发现作者在github提交了大把代码，想及时同步到自己的gitee仓库，只要点击这个按钮就可以了，不过这样可能会覆盖自己的代码，可以通过新建分支来规避。
1.3 创建新分支 接下来，就可以基于该仓库自由操作了，继续，选择一个自己准备放项目的目录，然后clone：
git clone https://gitee.com/funcy/xxl-job 接下来，需要基于 tag 创建分支（新分支命为2.3.0.LEARN）
git tag git checkout 2.3.0 git checkout -b 2.3.0.LEARN 2.3.0 git push -u origin 2.3.0.LEARN 后续我们所有的分析操作就都在2.3.0.LEARN上进行了。
关于创建分支一些小疑问：
为什么要创建新分支？
主要是防止点击同步时，代码被覆盖。如果直接在master分支，或其他在github中已有的分支上进行修改，点击同步后，所作的修改都会被github上的分支覆盖，而在gitee上新建的分支则不会被覆盖
为什么要基于tag创建新分支而不是master？
一般来说，master上的代码是最新稳定版本的代码，因此会不断变化，而tag上的代码，表示打tag那一刻的代码，之后不再变化，这样就保证了同一个tag上的代码，无论什么时候都是一样的。
事实上，了解了git的tag概念后，会发现tag上的代码checkout下来后，无法再怎么修改都不能提交，即不会被修改；相反地，master或其他分支，修改后可以再提交。由于tag上的代码是不变的，为了能提交代码，必须要基于tag上的代码创建新分支
为什么选择的tag版本是2.3.0而不是其他？ 因为当前最新版的xxl-job是2.3.0，当然要分析最新版的代码了。
2. xxl-job 项目结构 拿到代码后，我们来看下xxl-job的项目结构：
xxl-job-admin：管理后台，提供任务管理界面，同时也是任务的调度器、触发器，本系列文章中简称为admin。 xxl-job-core：xxl-job的核心组件，同时也是xxl-job对外提供的jar包，提供了任务执行相关逻辑。 xxl-job-executor-samples：执行器示例，在本系列文章中，执行器是指集成了xxl-job任务执行功能的项目，也称为executor xxl-job-executor-sample-frameless：不集成任何框架的示例 xxl-job-executor-sample-springboot：集成了 springboot 框架的示例 关于该项目的更多细节，可以参考官方文档https://www.xuxueli.com/xxl-job/，这里截取官方的架构图：
建议先了解上图中xxl-job的组件，后面的源码分析会一步步揭开这些组件工作的原理。
3. 启动xxl-job 大致了解xxl-job的结构后，接下来我们在idea中启动xxl-job，这样也是方便我们后面调试。
3.1 执行sql脚本 xxl-job的sql脚本位于xxl-job/doc/db目录下，文件名为tables_xxl_job.sql
准备一个mysql数据库，直接执行该脚本即可。需要注意的是，如果数据库中已存在名为xxl_job数据库，执行该脚本会覆盖原来的库，需要注意下。
执行成功后，数据库中会出现如下表：
各表的功能如下：
xxl_job_user: 用户信息，用来验证登录到管理后台的用户是否合法 xxl_job_info: 任务信息表，用来配置需要执行的任务 xxl_job_log: 日志表，用来记录任务的执行日志 xxl_job_log_report: 任务执行报表，用来汇总、统计任务的执行情况 xxl_job_logglue: gule模式任务表，主要记录glue模式任务的执行代码 xxl_job_registry: 执行器的注册记录表，每一个执行器都保存一条注册记录 xxl_job_group: 执行器的注册汇总表，多个执行器汇总成一条记录 xxl_job_lock: 锁表，xxl-job用来实现分布式锁的表 3.2 启动admin admin的启动类为com.xxl.job.admin.XxlJobAdminApplication：
不过在启动前，需要调整数据库的连接配置，配置文件为application.properties：
将数据库的连接信息调整正确后，运行XxlJobAdminApplication就可以启动了，控制台日志如下：
在浏览中访问http://localhost:8080/xxl-job-admin/，界面如下：
管理后台的菜单不多，操作比较简单，大家可根据官方文档可行摸索。
3.3 启动executor xxl-job提供了两个执行器示例，考虑到当前开发基本上基于springboot，本系列将重点分析集成了springboot的执行器示例，即xxl-job-executor-sample-springboot。
在启动xxl-job-executor-sample-springboot前，要先配置xxl-job-admin项目的地址，配置文件为application.properties:
配置key为xxl.job.admin.addresses，如果有多个xxl-job-admin地址，可以使用,分开。
配置好admin地址后，启动即可，启动类为com.xxl.job.executor.XxlJobExecutorApplication，控制台日志如下：
注意到这里有两个端口：
8081：这个是 项目的web 服务提供的对外访问端口，用来访问 springmvc 接口的 9999：这个是xxl-job用来与admin通讯的接口，用来处理xxl-job-admin发送过来的请求，这个后面我们再分析 3.4 第一个定时任务 xxl-job本身也提供了示例任务：
任务在任务管理菜单下，后续我们配置自己的任务时，也是在这里配置。
从界面上可以看到，目前已经有了一个测试任务，不过状态为stop，我们可以在“操作”栏启动该任务，也可以使用“执行一次”功能来手动执行该任务，这里我们使用“执行一次”来运行该任务：
直接点击“保存”即可，之后可以在“调度日志”菜单查看任务执行情况：
可以看到任务执行成功了。
该任务的代码位于xxl-job-executor-sample-springboot模块的com.xxl.job.executor.service.jobhandler.SampleXxlJob#demoJobHandler方法，代码如下：
关于其中的调度原理，我们将在后面的文章中再分析。
4. 总结 本文是xxl-job源码分析的开篇，介绍了源码分析前的准备工作，主要包含了三个方面：
源码的获取，介绍了如何从github上获取xxl-job源码，以及分支的创建流程 项目的启动，介绍了sql脚本的处理、admin注册地址的配置，以及项目的启动（admin与executor） 执行定时任务，介绍了如何执行xxl-job提供的任务示例，由于本系列文章以源码分析为主，因此任务的配置、cron表达式的意义等均无叙述，若要了解这些操作可以参考官方文档(https://www.xuxueli.com/xxl-job/)或者自行搜索资源。 作为源码分析的准备工作就到这里了，下-篇我们将正式进入xxl-job项目的源码分析中。
限于作者个人水平，文中难免有错误之处，欢迎指正！原创不易，商业转载请联系作者获得授权，非商业转载请注明出处。
</content>
    </entry>
    
</search>