Apache 神禹(shenyu)源码阅读(三)——被网关路由的后端服务 Client 向 Admin 注册的数据传输(Client端)

前言

在真正测试 Divide 插件时,想要知道后端服务(以下称为 Client)是如何将自己的信息注册到管理台(以下称为 Client)。这里后端服务用的是 shenyu 自带的 http 的例子,项目名字为 shenyu-examples-http。

下图描述了本文研究的内容——服务注册时 Client端向 Admin 注册的数据同步——在 shenyu 架构中处于什么位置。红色部分都是我自己加的,在官网的图中没有。
在这里插入图片描述

阅读准备

Disruptor入门及应用

正文

Client事件监听器监听本地的 Context 的刷新事件

当 Client (Spring 应用)依赖注入后,Spring 框架会刷新上下文 Context,这时,shenyu 自定义的一个监听 ContextRefreshedEvent 的监听器 SpringMvcClientEventListener (AbstractContextRefreshedEventListener 的子类)会触发 onApplicationEvent 方法。

  • AbstractContextRefreshedEventListener.onApplicationEvent()
public abstract class AbstractContextRefreshedEventListener<T, A extends Annotation> implements ApplicationListener<ContextRefreshedEvent> {// ...@Overridepublic void onApplicationEvent(@NonNull final ContextRefreshedEvent event) {context = event.getApplicationContext();// 1. 拿到 beansMap<String, T> beans = getBeans(context);if (MapUtils.isEmpty(beans)) {return;}// 2. 原子地设置 registered 为 trueif (!registered.compareAndSet(false, true)) {return;}if (isDiscoveryLocalMode) {// 3. 如果是“本地发现”模式,发布用于注册 URI 的 DTOpublisher.publishEvent(buildURIRegisterDTO(context, beans));}// 4. 处理每个 bean,具体是发布 bean 的注册信息给 Disruptor 的 QueueConsumerbeans.forEach(this::handle);// 5. apiModules 的 key 是 beanName,value 是 bean 的成员变量Map<String, Object> apiModules = context.getBeansWithAnnotation(ApiModule.class);// 6. 处理每个 apiModules,具体是发布 apiModules 的注册信息给 Disruptor 的 QueueConsumerapiModules.forEach((k, v) -> handleApiDoc(v, beans));}protected void handle(final String beanName, final T bean) {// ...}private void handleApiDoc(final Object bean, final Map<String, T> beans) {// ...}
}

从 SpringMvcClientEventListener.getBeans() 拿到 Beans

  • SpringMvcClientEventListener.java
public class SpringMvcClientEventListener extends AbstractContextRefreshedEventListener<Object, ShenyuSpringMvcClient> {// ...private final ShenyuClientRegisterEventPublisher publisher = ShenyuClientRegisterEventPublisher.getInstance();@Overrideprotected Map<String, Object> getBeans(final ApplicationContext context) {// Filter out// isFull 这个 Boolean 值代表的是:是否代理整个服务,目前适用于 SpringMvc/SpringCouldif (Boolean.TRUE.equals(isFull)) {// 在全代理模式下,发布一个事件,这个事件包含了服务的元数据,用于注册服务getPublisher().publishEvent(MetaDataRegisterDTO.builder().contextPath(getContextPath()) // 设置服务的上下文路径.addPrefixed(addPrefixed) // 设置是否添加前缀.appName(getAppName()) // 设置应用名称.path(UriComponentsBuilder.fromUriString(PathUtils.decoratorPathWithSlash(getContextPath()) + EVERY_PATH).build().encode().toUriString())// 设置服务的路径,这里使用了 UriComponentsBuilder 来构建URI,将上下文路径装饰后加上一个通配符,代表匹配所有路径.rpcType(RpcTypeEnum.HTTP.getName()) // 设置远程调用类型为 HTTP.enabled(true) // 设置服务为启用状态.ruleName(getContextPath()) // 使用上下文路径作为规则名称.build());LOG.info("init spring mvc client success with isFull mode");// 发布一个 URI 注册的事件,传入空的映射作为参数publisher.publishEvent(buildURIRegisterDTO(context, Collections.emptyMap()));return Collections.emptyMap();}// shenyu-examples-http 用的不是全代理模式,因为 isFull 为 false,此时直接返回带 Controller 注解的 beanreturn context.getBeansWithAnnotation(Controller.class);}
}

publisher.publishEvent(buildURIRegisterDTO(context, Collections.emptyMap())); 发布一个 URI 注册的事件,传入空的映射作为参数。

ShenyuClientRegisterEventPublisher 给 Client 端的 Disruptor 的 QueueConsumer 发布要向 Admin 注册的数据(是的,此时还没传给 Admin,还停留在 Client 端)
  • ShenyuClientRegisterEventPublisher.publishEvent() 调用 DisruptorProvider.onData() 传递数据
public class ShenyuClientRegisterEventPublisher {// ...private DisruptorProviderManage<DataTypeParent> providerManage;public void publishEvent(final DataTypeParent data) {DisruptorProvider<DataTypeParent> provider = providerManage.getProvider();// data 传给 Disruptor provider provider.onData(data);}
}
  • DisruptorProvider 传递给 RingBuffer.publishEvent(),最终将注册的信息发布给 Diruptor 的 QueueConsumer。

    ps: Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,能够在无锁的情况下实现网络的Queue并发操作,基于Disruptor开发的系统单线程能支撑每秒600万订单。

public class DisruptorProvider<T> {// ...private final RingBuffer<DataEvent<T>> ringBuffer;private final boolean isOrderly;private final EventTranslatorOneArg<DataEvent<T>, T> translatorOneArg = (event, sequence, t) -> event.setData(t);// ...public void onData(final T data) {if (isOrderly) {throw new IllegalArgumentException("The current provider is  of orderly type. Please use onOrderlyData() method.");}try {// 由  ringBuffer 发布事件ringBuffer.publishEvent(translatorOneArg, data);} catch (Exception ex) {logger.error("ex", ex);}}
}
由 QueueConsumer.onEvent() 接收 RingBuffer.publishEvent() 发布的事件,并进行处理
  • 从 DisruptorProviderManage.startup 的源码中可以看到,在创建 Disruptor 时,线程池 OrderlyExecutor 被传进了 QueueConsumer,
public class DisruptorProviderManage<T> {// ...private final Integer consumerSize;private final QueueConsumerFactory<T> consumerFactory;// ...public void startup(final boolean isOrderly) {// 创建一个定制的线程池,用于消费者OrderlyExecutor executor = new OrderlyExecutor(isOrderly, consumerSize, consumerSize, 0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(),DisruptorThreadFactory.create("shenyu_disruptor_consumer_", false), new ThreadPoolExecutor.AbortPolicy());int newConsumerSize = this.consumerSize;EventFactory<DataEvent<T>> eventFactory;// 根据是否有序来调整消费者数量和选择事件工厂if (isOrderly) {// 有序模式下,消费者数量设为1,使用有序的事件工厂newConsumerSize = 1;eventFactory = new OrderlyDisruptorEventFactory<>();} else {// 无序模式下,使用默认的事件工厂eventFactory = new DisruptorEventFactory<>();}// 创建Disruptor实例,配置其基本参数Disruptor<DataEvent<T>> disruptor = new Disruptor<>(eventFactory,size,DisruptorThreadFactory.create("shenyu_disruptor_provider_" + consumerFactory.fixName(), false),ProducerType.MULTI,new BlockingWaitStrategy());// 创建消费者数组,根据newConsumerSize指定的大小@SuppressWarnings("all")QueueConsumer<T>[] consumers = new QueueConsumer[newConsumerSize];for (int i = 0; i < newConsumerSize; i++) {consumers[i] = new QueueConsumer<>(executor, consumerFactory);}// 将消费者注册到Disruptor,使用工作池模式disruptor.handleEventsWithWorkerPool(consumers);// 设置默认的异常处理器,这里选择忽略异常disruptor.setDefaultExceptionHandler(new IgnoreExceptionHandler());// 启动Disruptordisruptor.start();// 获取Disruptor的环形缓冲区,用于发布事件RingBuffer<DataEvent<T>> ringBuffer = disruptor.getRingBuffer();// 创建并存储DisruptorProvider实例,用于向Disruptor发布事件provider = new DisruptorProvider<>(ringBuffer, disruptor, isOrderly);}
}
  • 当接收到一个事件时,QueueConsumer 将任务交给线程池去处理事件,处理事件的 Runnable 接口由工厂 factory 产生。
public class QueueConsumer<T> implements WorkHandler<DataEvent<T>> {// ...private final QueueConsumerFactory<T> factory;// ...@Overridepublic void onEvent(final DataEvent<T> t) {if (Objects.nonNull(t)) {ThreadPoolExecutor executor = orderly(t);QueueConsumerExecutor<T> queueConsumerExecutor = factory.create();queueConsumerExecutor.setData(t.getData());// help gct.setData(null);executor.execute(queueConsumerExecutor);}}
}
  • QueueConsumerExecutor 在 Client 端的消费者执行器 RegisterClientConsumerExecutor
/*** The type Consumer executor.*/
public final class RegisterClientConsumerExecutor<T extends DataTypeParent> extends QueueConsumerExecutor<T> {private final Map<DataType, ExecutorTypeSubscriber<T>> subscribers;private RegisterClientConsumerExecutor(final Map<DataType, ExecutorTypeSubscriber<T>> executorSubscriberMap) {this.subscribers = new EnumMap<>(executorSubscriberMap);}@Override// run 接口继承自 QueueConsumerExecutor,而 QueueConsumerExecutor 继承自 Runnablepublic void run() {final T data = getData();// subscribers 拿到 ExecutorTypeSubscriber 去处理数据 datasubscribers.get(data.getType()).executor(Lists.newArrayList(data));}/*** The type Register client executor factory.*/public static class RegisterClientExecutorFactory<T extends DataTypeParent> extends AbstractQueueConsumerFactory<T> {@Overridepublic RegisterClientConsumerExecutor<T> create() {Map<DataType, ExecutorTypeSubscriber<T>> map = getSubscribers().stream()// 将 AbstractQueueConsumerFactory.getSubscribers()// 接口返回的 ExecutorSubscriber<T> 转为 ExecutorTypeSubscriber<T>,// 其带有 getType 接口.map(e -> (ExecutorTypeSubscriber<T>) e).collect(Collectors.toMap(ExecutorTypeSubscriber::getType, e -> e));return new RegisterClientConsumerExecutor<>(map);}@Overridepublic String fixName() {return "shenyu_register_client";}}
}

ExecutorTypeSubscriber 继承自 ExecutorSubscriber :

public interface ExecutorTypeSubscriber<T extends DataTypeParent> extends ExecutorSubscriber<T> {`

从下图的 ExecutorTypeSubscriber 接口的实现类可以看到,在 Client 端有 3 个 Subscriber
在这里插入图片描述

我们这个例子看的是URI,所以就以 ShenyuClientURIExecutorSubscriber 举例。

数据交由 ShenyuClientURIExecutorSubscriber 执行处理

  • ShenyuClientURIExecutorSubscriber.execute()
public class ShenyuClientURIExecutorSubscriber implements ExecutorTypeSubscriber<URIRegisterDTO> {// ...private final ShenyuClientRegisterRepository shenyuClientRegisterRepository;@Overridepublic void executor(final Collection<URIRegisterDTO> dataList) {for (URIRegisterDTO uriRegisterDTO : dataList) {Stopwatch stopwatch = Stopwatch.createStarted();while (true) {// 连得上就跳出死循环try (Socket ignored = new Socket(uriRegisterDTO.getHost(), uriRegisterDTO.getPort())) {break;} catch (IOException e) {long sleepTime = 1000;// maybe the port is delay exposedif (stopwatch.elapsed(TimeUnit.SECONDS) > 5) {LOG.error("host:{}, port:{} connection failed, will retry",uriRegisterDTO.getHost(), uriRegisterDTO.getPort());// If the connection fails for a long time, Increase sleep timeif (stopwatch.elapsed(TimeUnit.SECONDS) > 180) {sleepTime = 10000;}}try {TimeUnit.MILLISECONDS.sleep(sleepTime);} catch (InterruptedException ex) {LOG.error("interrupted when sleep", ex);}}}// 1. 延迟应用关闭时的其他钩子ShenyuClientShutdownHook.delayOtherHooks();// 2. 给 Admin 端发送 DTO 注册信息shenyuClientRegisterRepository.persistURI(uriRegisterDTO);// 3. 向应用添加一个钩子,使得在应用关闭时,应用自动开启一个新线程去注销注册信息ShutdownHookManager.get().addShutdownHook(new Thread(() -> {final URIRegisterDTO offlineDTO = new URIRegisterDTO();BeanUtils.copyProperties(uriRegisterDTO, offlineDTO);offlineDTO.setEventType(EventType.OFFLINE);// 给 Admin 端发送下线 DTOshenyuClientRegisterRepository.offline(offlineDTO);}), 2);}}
}

有三个方法需要说明:

  1. ShenyuClientShutdownHook.delayOtherHooks() 延迟应用关闭时的其他钩子
  2. ShenyuClientRegisterRepository.persistURI() 给 Admin 端发送 DTO 注册信息
  3. ShutdownHookManager.get().addShutdownHook() 向应用添加一个钩子,使得在应用关闭时,应用自动开启一个新线程去注销注册信息
延迟应用关闭时的其他钩子
  • ShenyuClientShutdownHook.delayOtherHooks()

    1. 利用 CAS 不加锁地确保并发时 TakeoverOtherHooksThread 线程只被运行一次
    2. 一个接管其他钩子的线程
public class ShenyuClientShutdownHook {// ...private static final AtomicBoolean DELAY = new AtomicBoolean(false);private static String hookNamePrefix = "ShenyuClientShutdownHook";private static AtomicInteger hookId = new AtomicInteger(0);private static Properties props;private static IdentityHashMap<Thread, Thread> delayHooks = new IdentityHashMap<>();private static IdentityHashMap<Thread, Thread> delayedHooks = new IdentityHashMap<>();// ..../*** Delay other shutdown hooks.*/public static void delayOtherHooks() {// 1. 利用 CAS 不加锁地确保并发时 TakeoverOtherHooksThread 线程只被运行一次if (!DELAY.compareAndSet(false, true)) {return;}// 2. 一个接管其他钩子的线程TakeoverOtherHooksThread thread = new TakeoverOtherHooksThread();thread.start();}/*** Delay other shutdown hooks thread.*/private static class TakeoverOtherHooksThread extends Thread {@Override// 1. 该线程用于生成钩子,这些钩子用来延迟执行已经添加的钩子,为的是处理一些资源的关闭,和注册信息的注销public void run() {int shutdownWaitTime = Integer.parseInt(props.getProperty("shutdownWaitTime", "3000"));int delayOtherHooksExecTime = Integer.parseInt(props.getProperty("delayOtherHooksExecTime", "2000"));IdentityHashMap<Thread, Thread> hooks = null;try {// 2. 通过反射拿到应用关闭时的所有钩子Class<?> clazz = Class.forName(props.getProperty("applicationShutdownHooksClassName", "java.lang.ApplicationShutdownHooks"));Field field = clazz.getDeclaredField(props.getProperty("applicationShutdownHooksFieldName", "hooks"));field.setAccessible(true);hooks = (IdentityHashMap<Thread, Thread>) field.get(clazz);} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException ex) {LOG.error(ex.getMessage(), ex);}long s = System.currentTimeMillis();// 3. 限制处理钩子的时间在 delayOtherHooksExecTime 之内,为什么要控制时间,难道不会遗漏一些钩子无法延迟吗?// GPT:// 答:1. 避免死锁或长时间阻塞//    2. 可以确保这个延迟逻辑不会过度拖延应用的关闭过程//    3. 实用性考虑: 在大多数情况下,如果在给定的时间内无法连接到或修改某些钩子,可能是因为存在一些异常或特殊情况。//       在这种情况下,继续等待可能不会带来太多好处,而是增加了关闭过程的复杂性和不确定性。//    确实,这种方法可能会遗漏一些在延迟期间新注册的钩子,但这通常是一个权衡的结果,设计者可能认为这种情况很少发生,或者遗漏的风险相对较小。while (System.currentTimeMillis() - s < delayOtherHooksExecTime) {for (Iterator<Thread> iterator = Objects.requireNonNull(hooks).keySet().iterator(); iterator.hasNext();) {Thread hook = iterator.next();// 4. 用于延迟执行原本钩子的钩子不必再延迟,所以跳过if (hook.getName().startsWith(hookNamePrefix)) {continue;}// 5. 正在处理的延迟的钩子和处理过的延迟的钩子不必再延迟,所以跳过if (delayHooks.containsKey(hook) || delayedHooks.containsKey(hook)) {continue;}Thread delayHook = new Thread(() -> {LOG.info("sleep {}ms", shutdownWaitTime);try {// 6. 先睡眠 shutdownWaitTime,然后再执行原本的在应用关闭时的钩子TimeUnit.MILLISECONDS.sleep(shutdownWaitTime);} catch (InterruptedException ex) {LOG.error(ex.getMessage(), ex);}hook.run();}, hook.getName());delayHooks.put(delayHook, delayHook);// 7. 从原本的钩子 map 中移除这个原本要执行的钩子,即 delayHookiterator.remove();}for (Iterator<Thread> iterator = delayHooks.keySet().iterator(); iterator.hasNext();) {Thread delayHook = iterator.next();// 8. 向运行时加入用来延迟执行原本钩子的钩子,即 delayedHooksRuntime.getRuntime().addShutdownHook(delayHook);// 9. 加入已处理过的钩子 map,delayedHooks.put(delayHook, delayHook);iterator.remove();LOG.info("hook {} will sleep {}ms when it start", delayHook.getName(), shutdownWaitTime);}try {// 10. 睡眠 100ms,目的是?// GPT:// 答:1. 减少CPU使用率//    2. 给其他操作留出处理时间,通过在每次循环后短暂休眠,可以给其他线程运行的机会TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException ex) {LOG.error(ex.getMessage(), ex);}}// 帮助 GChookNamePrefix = null;hookId = new AtomicInteger(0);props = null;delayHooks = null;delayedHooks = null;}}	
}
  • TakeoverOtherHooksThread.run()

    代码如上面给出的:

    1. 该线程用于生成钩子,这些钩子用来延迟执行已经添加的钩子,为的是处理一些资源的关闭,和注册信息的注销
    2. 通过反射拿到应用关闭时的所有钩子
    3. 限制处理钩子的时间在 delayOtherHooksExecTime 之内,为什么要控制时间,难道不会遗漏一些钩子无法延迟吗?
      GPT:
      答:
      1. 避免死锁或长时间阻塞
      2. 可以确保这个延迟逻辑不会过度拖延应用的关闭过程
      3. 实用性考虑: 在大多数情况下,如果在给定的时间内无法连接到或修改某些钩子,可能是因为存在一些异常或特殊情况。 在这种情况下,继续等待可能不会带来太多好处,而是增加了关闭过程的复杂性和不确定性。确实,这种方法可能会遗漏一些在延迟期间新注册的钩子,但这通常是一个权衡的结果,设计者可能认为这种情况很少发生,或者遗漏的风险相对较小。
    4. 用于延迟执行原本钩子的钩子不必再延迟,所以跳过
    5. 正在处理的延迟的钩子和处理过的延迟的钩子不必再延迟,所以跳过
    6. 先睡眠 shutdownWaitTime,然后再执行原本的在应用关闭时的钩子
    7. 从原本的钩子 map 中移除这个原本要执行的钩子,即 delayHook
    8. 向运行时加入用来延迟执行原本钩子的钩子,即 delayedHooks
    9. 加入已处理过的钩子 map
    10. 睡眠 100ms,目的是?
      GPT:
      答:
      1. 减少CPU使用率
      2. 给其他操作留出处理时间,通过在每次循环后短暂休眠,可以给其他线程运行的机会
给 Admin 端发送 DTO 注册信息
  • ShenyuClientRegisterRepository.persistURI()

    ShenyuClientRegisterRepositoryFailbackRegistryRepositoryHttpClientRegisterRepository继承关系如下图 在这里插入图片描述

  • ShenyuClientRegisterRepository.persistURI()

/*** Shenyu client register repository.*/
@SPI
public interface ShenyuClientRegisterRepository {/*** Init.** @param config the config*/default void init(ShenyuRegisterCenterConfig config) {}/*** Persist metadata.** @param metadata metadata*/void persistInterface(MetaDataRegisterDTO metadata);/*** Persist uri.** @param registerDTO the register dto*/default void persistURI(URIRegisterDTO registerDTO) {}/*** Node active offline when shutdown.** @param offlineDTO the offline dto*/default void offline(URIRegisterDTO offlineDTO) {}/*** persistApiDoc.* @param apiDocRegisterDTO apiDocRegisterDTO*/default void persistApiDoc(ApiDocRegisterDTO apiDocRegisterDTO) {}/*** closeRepository.* If the close method is used, Spring will call it by default when the bean is destroyed,* So its method name is closeRepository to avoid being called by default when the bean is destroyed.*/default void closeRepository() {}
}
  • FailbackRegistryRepository.persistURI()

    这里同样用到了模板方法,doPersistURI 交由子类 HttpClientRegisterRepository 实现

public abstract class FailbackRegistryRepository implements ShenyuClientRegisterRepository {// ... @Overridepublic void persistURI(final URIRegisterDTO registerDTO) {try {// 1. 同样是模板方法,交由子类 HttpClientRegisterRepository 实现this.doPersistURI(registerDTO);} catch (Exception ex) {//If a failure occurs, it needs to be added to the retry list.logger.warn("Failed to persistURI {}, cause:{}", registerDTO, ex.getMessage());this.addFailureUriDataRegister(registerDTO);}}
}
  • HttpClientRegisterRepository.doPersistURI()

    1. 如果端口已被其他进程监听,则直接返回,不需要再注册
    2. 否则注册
public class HttpClientRegisterRepository extends FailbackRegistryRepository {// ...private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientRegisterRepository.class);private static URIRegisterDTO uriRegisterDTO;private static ApiDocRegisterDTO apiDocRegisterDTO;private String username;private String password;private List<String> serverList;/*** server -> accessToken.*/private LoadingCache<String, String> accessToken;// ...@Overridepublic void doPersistURI(final URIRegisterDTO registerDTO) {if (RuntimeUtils.listenByOther(registerDTO.getPort())) {// 1. 如果端口已被其他进程监听,则直接返回,不需要再注册return;}// 2. 否则注册doRegister(registerDTO, Constants.URI_PATH, Constants.URI);uriRegisterDTO = registerDTO;}private <T> void doRegister(final T t, final String path, final String type) {int i = 0;for (String server : serverList) {i++;String concat = server.concat(path);try {String accessToken = this.accessToken.get(server);if (StringUtils.isBlank(accessToken)) {throw new NullPointerException("accessToken is null");}// 1. 调用注册工具类进行注册RegisterUtils.doRegister(GsonUtils.getInstance().toJson(t), concat, type, accessToken);// considering the situation of multiple clusters, we should continue to execute here} catch (Exception e) {LOGGER.error("Register admin url :{} is fail, will retry. cause:{}", server, e.getMessage());if (i == serverList.size()) {throw new RuntimeException(e);}}}}
}
  • HttpClientRegisterRepository.doRegister()

    1. 调用注册工具类进行注册(代码如上)
  • RegisterUtils.doRegister()

    1. 构建 http 的 heade
    2. 在此通过 http 调用 Admin 的服务进行注册,
      url 为 Admin 端的注册用的接口,有 localhost:9095/shenyu-client/register-metadata 等url;
      json 为要传输的注册信息
    3. OkHttpTools 是 shenyu 对 okhttp 外部组件的封装
public final class RegisterUtils {// ...public static void doRegister(final String json, final String url, final String type, final String accessToken) throws IOException {if (StringUtils.isBlank(accessToken)) {LOGGER.error("{} client register error accessToken is null, please check the config : {} ", type, json);return;}// 1. 构建 http 的 headerHeaders headers = new Headers.Builder().add(Constants.X_ACCESS_TOKEN, accessToken).build();// 2. 在此通过 http 调用 Admin 的服务进行注册,//    url 为 Admin 端的注册用的接口,有 localhost:9095/shenyu-client/register-metadata 等url;//    json 为要传输的注册信息// 3. OkHttpTools 是 shenyu 对 okhttp 外部组件的封装String result = OkHttpTools.getInstance().post(url, json, headers);if (Objects.equals(SUCCESS, result)) {LOGGER.info("{} client register success: {} ", type, json);} else {LOGGER.error("{} client register error: {} ", type, json);}}
}
向应用添加一个钩子,使得在应用关闭时,应用自动开启一个新线程去注销注册信息
  • ShutdownHookManager.addShutdownHook()

    1. 向运行时添加一个关机钩子,这个钩子是一个新线程,新线程去执行 ShutdownHookManager 管理的要在关机时执行的钩子
    2. 添加关闭应用时要执行的注销注册的钩子
public final class ShutdownHookManager {// ...private static final ShutdownHookManager MGR = new ShutdownHookManager();private final Set<HookEntry> hooks =Collections.synchronizedSet(new HashSet<HookEntry>());	static {// 1. 向运行时添加一个关机钩子,这个钩子是一个新线程,// 新线程去执行 ShutdownHookManager  管理的要在关机的钩子Runtime.getRuntime().addShutdownHook(new Thread(() -> {MGR.shutdownInProgress.set(true);for (Runnable hook : MGR.getShutdownHooksInOrder()) {try {hook.run();} catch (Throwable ex) {LOG.error(ex.getMessage(), ex);}}}));}// ...public void addShutdownHook(final Runnable shutdownHook, final int priority) {if (shutdownHook == null) {throw new IllegalArgumentException("shutdownHook cannot be NULL");}if (shutdownInProgress.get()) {throw new IllegalStateException("Shutdown in progress, cannot add a shutdownHook");}// 2. 添加关闭应用时要执行的注销注册的钩子hooks.add(new HookEntry(shutdownHook, priority));}
}

一张图总结

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/684152.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 13.0 SystemUI下拉状态栏定制二 锁屏页面横竖屏解锁图标置顶显示功能实现

1.前言 在13.0的系统rom定制化开发中,在关于systemui的锁屏页面功能定制中,由于在平板横屏锁屏功能中,时钟显示的很大,并且是在左旁边居中显示的, 由于需要和竖屏显示一样,所以就需要用到小时钟显示,然后同样需要居中,所以就来分析下相关的源码,来实现具体的功能 如图…

Imgui(3) | 基于 imgui-SFML 的 mnist 数据集查看器

Imgui(3) | 基于 imgui-SFML 的 mnist 数据集查看器 文章目录 Imgui(3) | 基于 imgui-SFML 的 mnist 数据集查看器0. 介绍1. 处理 mnist 数据集2. 显示单张图像和label2.1 显示单张图像2.2 点选列表后更新显示的图像2.3 显示 label2.4 使用完整的列表 总结 0. 介绍 把mnist数据…

每日一练:LeeCode-98、 验证二叉搜索树【二叉搜索树+DFS】

本文是力扣LeeCode-98、 验证二叉搜索树【二叉搜索树DFS】】 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐LeeCode。 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&am…

C#,巴都万数列(Padonve Number)的算法与源代码

1 巴都万数列&#xff08;Padovan Sequence&#xff09; 巴都万数列&#xff08;Padovan Sequence&#xff09;是一个整数数列。 首数个值为1, 1, 1, 2, 2, 3, 4, 5, 7, 9, 12, 16, 21, 28, 37 ... 此数列以建筑师理察巴都万命名&#xff0c;他的论文Dom&#xff08;1994年&a…

嵌入式CAN通信协议原理(下)

本篇文章结合实际CAN控制器继续介绍协议相关的内容&#xff0c;还有示例讲解。 好了&#xff0c;继续吧&#xff01; 二. STM32 CAN 控制器介绍 STM32 的芯片中具有 bxCAN 控制器 (Basic Extended CAN)&#xff0c;它支持 CAN 协议 2.0A 和 2.0B 标准。 该 CAN 控制器支持最…

Vi 和 Vim 编辑器

Vi 和 Vim 编辑器 vi 和 vim 的基本介绍 Linux 系统会内置 vi 文本编辑器 Vim 具有程序编辑的能力&#xff0c;可以看做是 Vi 的增强版本&#xff0c;可以主动的以字体颜色辨别语法的正确性&#xff0c;方便程序设计。 代码补完、编译及错误跳转等方便编程的功能特别丰富&…

读十堂极简人工智能课笔记03_遗传算法与进化

1. 寻找正确答案 1.1. 卡尔西姆斯 1.1.1. 计算机图形艺术家和研究者 1.1.2. 演示过数字进化之创造性和新颖性的先驱 1.1.3. 1994年 1.1.3.1. 创造一批能游泳、走路、跳跃&#xff0c;甚至互相竞争的虚拟动物震惊了整个科学界 1.1.3.2. 它们的人工大脑却是个极其复杂的网络…

Json-序列化字符串时间格式问题

序列化字符串时间格式问题 一、项目场景二、问题描述三、解决方案 一、项目场景 最近C#中需要将实体进行json序列化&#xff0c;使用了Newtonsoft.Json public static void TestJson(){DataTable dt new DataTable();dt.Columns.Add("Age", Type.GetType("Sys…

java8使用流

这种处理数据的方式很有用&#xff0c;因为你让Stream API管理如何处理数据。这样StreamAPI就可以在背后进行多种优化。此外&#xff0c;使用内部迭代的话&#xff0c;SteamAPI可以决定并行运行你的代码。这要是用外部迭代的话就办不到了&#xff0c;因为你只能用单一线程挨个迭…

VMwareWorkstation17.0虚拟机安装Windows2.03

VMwareWorkstation17.0虚拟机安装Windows2.03 第一篇 下载Windows2.03第二篇 配置Windows2.03虚拟机机器环境第三篇 启动Windows2.03系统 第一篇 下载Windows2.03 1.Windows2.0原版软盘下载地址是 暂不提供&#xff0c;后续更新 2.Windows2.03虚拟机镜像下载地址是 Windows2…

STM32物联网(ESP-01S模块及STM32和ESP-01S通信方式介绍)

文章目录 前言一、ESP-01S模块介绍二、STM32和ESP-01S通信方式介绍三、什么是AT指令四、创建基础工程总结 前言 本篇文章我们开始正式进入STM32物联网的专栏&#xff0c;在这个专栏中将会带大家学习使用STM32进行联网&#xff0c;联网模块的话主要就是使用到了ESP-01S WIFI模块…

『运维备忘录』之 Zip 命令详解

运维人员不仅要熟悉操作系统、服务器、网络等只是&#xff0c;甚至对于开发相关的也要有所了解。很多运维工作者可能一时半会记不住那么多命令、代码、方法、原理或者用法等等。这里我将结合自身工作&#xff0c;持续给大家更新运维工作所需要接触到的知识点&#xff0c;希望大…

WebStorm | 如何修改webstorm中新建html文件默认生成模板中title的初始值

在近期的JS的学习中&#xff0c;使用webstorm&#xff0c;总是要先新建一个html文件&#xff0c;然后再到里面书写<script>标签&#xff0c;真是麻烦&#xff0c;而且标题也是默认的title&#xff0c;想改成文件名还总是需要手动去改 经过小小的研究&#xff0c;找到了修…

单片机学习笔记---LCD1602

LCD1602介绍 LCD1602&#xff08;Liquid Crystal Display&#xff09;液晶显示屏是一种字符型液晶显示模块&#xff0c;可以显示ASCII码的标准字符和其它的一些内置特殊字符&#xff08;比如日文的片假名&#xff09;&#xff0c;还可以有8个自定义字符 显示容量&#xff1a;…

AlmaLinux更换鼠标样式为Windows样式

文章目录 前言先看看条件与依赖第一步&#xff1a;测试最终效果第二步&#xff1a;使用CursorXP修改鼠标样式CurosrXP安装CursorXP使用 第三步&#xff1a;Linux端环境搭建与命令执行UbuntuFedora其他系统均失败 第四步&#xff1a;应用主题 前言 只不过是突发奇想&#xff0c…

jacob使用教程---环境搭建及基本组成元素

参考资料: jacob的GitHub地址 jacob官网(个人感觉不重要) microsoft官方VBA文档(很重要,jacob所有的参数都来自于这里) jacob找COM组件 jacob环境配置教程 jacob将word转为各种格式 提取word中审阅内容 本人参考例子及相关资料 具有参考价值的博客 jacob操作e…

【Python】通过conda安装Python的IDE

背景 系统&#xff1a;win11 软件&#xff1a;anaconda Navigator 问题现象&#xff1a;①使用Navigator安装jupyter notebook以及Spyder IDE 一直转圈。②然后进入anaconda prompt执行conda install jupyter notebook一直卡在Solving environment/-\。 类似问题&#xff1a; …

计算机网络——12DNS

DNS DNS的必要性 IP地址标识主机、路由器但IP地址不好记忆&#xff0c;不便于人类用使用&#xff08;没有意义&#xff09;人类一般倾向于使用一些有意义的字符串来标识Internet上的设备存在着“字符串”——IP地址的转换的必要性人类用户提供要访问机器的“字符串”名称由DN…

FreeRTOS知识点

1>任务调度算法 1.抢占式调度&#xff1a;高优先级的任务优先执行&#xff0c;并且可以打断低优先级的任务执行。 在FreeRTOSConfig.h中开启configUSE_PREEMPTION宏&#xff0c;将宏设置为1&#xff0c;关闭&#xff08;将宏设置为0&#xff09; 2.时间片轮转&#xff1a…

【动态规划】【C++算法】1563 石子游戏 V

作者推荐 【数位dp】【动态规划】【状态压缩】【推荐】1012. 至少有 1 位重复的数字 本文涉及知识点 动态规划汇总 LeetCoce:1563 石子游戏 V 几块石子 排成一行 &#xff0c;每块石子都有一个关联值&#xff0c;关联值为整数&#xff0c;由数组 stoneValue 给出。 游戏中…