EventBus源码分析

news2024/10/7 13:20:59

差不多两年没写博客了,最近想着要找工作了,打算复习下一些常用的开源库,也是这篇博客的由来~

EventBus使用非常简单 参考:github
再贴一张官网的图
在这里插入图片描述

一、示例代码

示例代码是为了便于理解后面注解处理器生成代码的处理流程

public class TestRunnerActivity extends Activity {

    private EventBus eventBus;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_runtests);
        eventBus = new EventBus();
        eventBus.register(this);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(TestFinishedEvent event) {

    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onEventBackgroundThread(TestEvent event) {
    }

    public void onDestroy() {
        eventBus.unregister(this);
        super.onDestroy();
    }
}

二、EventBus#register流程

调用register进行订阅 ,参数this代表订阅者

public void register(Object subscriber) {       
        //获取订阅者的class类型
        Class<?> subscriberClass = subscriber.getClass();
        //获取subscriber所有的订阅方法
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                //对订阅类中的每个订阅方法进行订阅
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

首先调用了findSubscriberMethods获取了当前订阅者类里的所有订阅方法。

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        //如果缓存有,直接返回。 METHOD_CACHE类型是Map<Class<?>, List<SubscriberMethod>>
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        //ignoreGeneratedIndex默认是false
        if (ignoreGeneratedIndex) {
            //方式1:通过反射获取到所有的订阅方法
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            //方式2:通过注解处理器生成的代码来获取所有的订阅方法
            subscriberMethods = findUsingInfo(subscriberClass);
        }
        //如果订阅者类中没有任何的订阅方法,就抛出异常。该异常我们应该很熟悉
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass
                    + " and its super classes have no public methods with the @Subscribe annotation");
        } else {
            //将订阅类和该类中的所有订阅方法保存到缓存
            METHOD_CACHE.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

可以看到有两种方式获取订阅方法,一种是利用反射,另一种是利用注解处理器生成的代码。很多开源库都利用了注解生成器,通过它在编译时生成代码可以很好的帮助我们减少反射代码的调用。

先看看如何通过反射获取到所有的订阅方法

private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
        //从对象缓存池获取一个FindState,这只是一个工具对象
        FindState findState = prepareFindState();
   			//给findState成员变量subscriberClass 、class赋值为subscriberClass
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
          	//通过反射去找
            findUsingReflectionInSingleClass(findState);
            //继续找父类中的所有方法
            findState.moveToSuperclass();
        }
        //返回找到的List<SubscriberMethod>,回收FindState
        return getMethodsAndRelease(findState);
    }

反射的核心逻辑在findUsingReflectionInSingleClass方法,该方法通过反射查找class的所有方法,然后遍历这些方法找到带@Subscribe的方法,然后将查询的结果保存到findState.subscriberMethods中

private void findUsingReflectionInSingleClass(FindState findState) {
        Method[] methods;
        try {
            //getDeclaredMethod()获取的是类自身声明的所有方法,包含public、protected和private方法
            methods = findState.clazz.getDeclaredMethods();
        } catch (Throwable th) {
            ...
        }
        //遍历所有方法,找到带Subscribe注解的方法 比如:@Subscribe(threadMode = ThreadMode.MAIN)
        for (Method method : methods) {
            int modifiers = method.getModifiers();
            //方法是public的且不是 abstract | static 类的
            if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                //获取方法的参数类型,对于订阅方法来说 参数就是事件类型
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 1) {
                    //获取到方法上的Subscribe注解
                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                    if (subscribeAnnotation != null) {
                        //获取事件类型
                        Class<?> eventType = parameterTypes[0];
                        //检查method&eventType对于的订阅方法是否已经添加过了
                        if (findState.checkAdd(method, eventType)) {
                            //获取方法的线程模式
                            ThreadMode threadMode = subscribeAnnotation.threadMode();
                            //最终将找到的结果 封装到SubscriberMethod里面,然后保存到findState.subscriberMethods中
                            findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                    subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                        }
                    }
                } 
            } 
        }
    }

前面的findUsingReflection方法先通过反射找到订阅类里的所有订阅方法,然后保存到findState.subscriberMethods中,接着调用getMethodsAndRelease(findState),将
List< SubscriberMethod>返回,然后回收FindState到对象池

private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
        List<SubscriberMethod> subscriberMethods = new ArrayList<>(findState.subscriberMethods);
        findState.recycle();
        synchronized (FIND_STATE_POOL) {
            for (int i = 0; i < POOL_SIZE; i++) {
                if (FIND_STATE_POOL[i] == null) {
                    FIND_STATE_POOL[i] = findState;
                    break;
                }
            }
        }
        return subscriberMethods;
    }

Ok,现在我们已经通过反射获取到订阅类中的所有订阅方法了,并且将它们封装到List< SubscriberMethod>中了,我们先不管注解处理器获取List< SubscriberMethod>的逻辑,继续看register方法,获取到List< SubscriberMethod>后开始遍历List< SubscriberMethod>,对每个SubscriberMethod调用subscribe

subscribe主要干了两件事

1、将订阅方法和订阅者封装到Subscription中,然后为每个事件类型创建一个CopyOnWriteArrayList列表subscriptions,并将Subscription插入到subscriptions中。事件类型就是我们post的事件的class对象

2、对粘性事件进行处理

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        //获取事件类型
        Class<?> eventType = subscriberMethod.eventType;
        //将订阅者和订阅方法封装到Subscription中
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        //通过事件类型找到Subscription集合
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            //如果还没有订阅类 订阅了该事件的方法,就创建一个list,把当前的Subscription放进去
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            //如果订阅类中已经订阅了该事件
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            //根据优先级,将该订阅方法加入到指定位置
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        //获取订阅者的事件类型列表,如果没有就创建一个
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        //将当前事件类型加到subscribedEvents中
        subscribedEvents.add(eventType);

  			//下面这个逻辑可以先不看 后面可以结合postSticky流程看
        //如果订阅方法支持粘性事件
        if (subscriberMethod.sticky) {
            if (eventInheritance) {
                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
                //遍历stickyEvents map  这个map保存了粘性事件
                for (Map.Entry<Class<?>, Object> entry : entries) {
                    Class<?> candidateEventType = entry.getKey();
                    //如果当前订阅方法订阅的事件列席在stickyEvents中已经有了,就说明之前某个地方发送过该粘性事件
                    if (eventType.isAssignableFrom(candidateEventType)) {
                        Object stickyEvent = entry.getValue();
                        //检查发送粘性事件
                        checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                    }
                }
            } else {
                Object stickyEvent = stickyEvents.get(eventType);
                //检查发送粘性事件
                checkPostStickyEventToSubscription(newSubscription, stickyEvent);
            }
        }
    }

走到这里我们的注册逻辑基本就结束了,注册的结果就是得到了一个subscriptionsByEventType,subscriptionsByEventType是一个map key是事件类型,value是该事件的所有订阅(Subscription)

注册的流程走完了再来看看如何通过注解处理器生成的代码获取SubscriberMethod列表。
下面这个类MyEventBusIndex是注解处理器生成的

public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        key 订阅者类  value 订阅信息
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
     	//...
        //将订阅信息保存到SUBSCRIBER_INDEX
        putIndex(new SimpleSubscriberInfo(TestRunnerActivity.class, true, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onEventMainThread", TestFinishedEvent.class, ThreadMode.MAIN),
            new SubscriberMethodInfo("onEventBackgroundThread", TestEvent.class, ThreadMode.BACKGROUND),
        }));

    }

    private static void putIndex(SubscriberInfo info) {
      	//保存所有订阅类的订阅信息
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}

SimpleSubscriberInfo定义如下:

public class SimpleSubscriberInfo extends AbstractSubscriberInfo {

    private final SubscriberMethodInfo[] methodInfos;

    public SimpleSubscriberInfo(Class subscriberClass, boolean shouldCheckSuperclass, SubscriberMethodInfo[] methodInfos) {
        super(subscriberClass, null, shouldCheckSuperclass);
        this.methodInfos = methodInfos;
    }

    @Override
    public synchronized SubscriberMethod[] getSubscriberMethods() {
        int length = methodInfos.length;
        SubscriberMethod[] methods = new SubscriberMethod[length];
        for (int i = 0; i < length; i++) {
            SubscriberMethodInfo info = methodInfos[i];
            methods[i] = createSubscriberMethod(info.methodName, info.eventType, info.threadMode,
                    info.priority, info.sticky);
        }
        return methods;
    }
}

MyEventBusIndex类对象会被保存到EventBus的SubscriberMethodFinder中subscriberInfoIndexes成员列表中

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            //找到index类中的订阅信息 ,其实就是调用上面MyEventBusIndex的getSubscriberInfo获取订阅类的SubscriberInfo
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                //获取SimpleSubscriberInfo的getSubscriberMethods方法获取订阅信息
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    //如果没有添加过,就添加订阅方法
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {
                //如果index里没有,就通过反射来查找订阅类中的订阅方法
                findUsingReflectionInSingleClass(findState);
            }
            //继续找父类的
            findState.moveToSuperclass();
        }
        //返回订阅方法列表
        return getMethodsAndRelease(findState);
    }

ok到现在为止,注册的逻辑终于结束了。

三、EventBus#unregister流程

public synchronized void unregister(Object subscriber) {
        //获取订阅者订阅的事件列表
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            for (Class<?> eventType : subscribedTypes) {
                //通过事件类型解注册
                unsubscribeByEventType(subscriber, eventType);
            }
            //移除
            typesBySubscriber.remove(subscriber);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

最终是通过事件类型来解除该订阅者的所有注册

private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
        //获取所有eventType事件类型下的订阅对象
        List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions != null) {
            int size = subscriptions.size();
            for (int i = 0; i < size; i++) {
                Subscription subscription = subscriptions.get(i);
                //如果是当前订阅者
                if (subscription.subscriber == subscriber) {
                    //将active 状态改为 false
                    subscription.active = false;
                    //移除订阅关系
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

四、EventBus#post流程

public void post(Object event) {
    PostingThreadState postingState = currentPostingThreadState.get();
    List<Object> eventQueue = postingState.eventQueue;
    //将事件加入事件队列
    eventQueue.add(event);

    if (!postingState.isPosting) {
        //是否在主线程
        postingState.isMainThread = isMainThread();
        //设置为正在发送
        postingState.isPosting = true;
        try {
            while (!eventQueue.isEmpty()) {
                //遍历所有的事件,进行发送
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}

然后调用postSingleEvent进行单个event的发送

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        //是否考虑父类的事件类型
        if (eventInheritance) {
            //遍历获取当前的event类以及其父类的类型
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            //遍历所有类型
            for (int h = 0; h < countTypes; h++) {
                Class<?> clazz = eventTypes.get(h);
                //将事件根据事件类型发送出去
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            //将事件根据事件类型发送出去
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
        }
    }

然后走到postSingleEventForEventType,该方法根据事件类型,将事件发送给订阅了该类型事件类型的所有订阅方法

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            //获取所有该事件类型的订阅对象
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            for (Subscription subscription : subscriptions) {
                postingState.event = event;
                postingState.subscription = subscription;
                boolean aborted;
                try {
                    //遍历所有的Subscription 发送事件
                    postToSubscription(subscription, event, postingState.isMainThread);
                    aborted = postingState.canceled;
                } finally {
                    postingState.event = null;
                    postingState.subscription = null;
                    postingState.canceled = false;
                }
                if (aborted) {
                    break;
                }
            }
            return true;
        }
        return false;
    }

subscriptionsByEventType这个map 在分析register流程的时候分析过 它的key是事件类型,value是该事件的所有订阅(Subscription)。

接着遍历所有的Subscription调用postToSubscription方法

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
        switch (subscription.subscriberMethod.threadMode) {
            case POSTING://发送方在哪里线程,订阅方法就在哪个方法被调用
                //直接调用invokeSubscriber,关于invokeSubscriber后面会说
                invokeSubscriber(subscription, event);
                break;
            case MAIN://只在主线程调用订阅方法
                if (isMainThread) {
                    //如果当前是在主线程直接调用invokeSubscriber
                    invokeSubscriber(subscription, event);
                } else {
                    //如果不在主线程,利用handler机制切到主线程再调用invokeSubscriber
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;
            case MAIN_ORDERED:
                if (mainThreadPoster != null) {
                    //将事件加入队列,进行有序执行
                    mainThreadPoster.enqueue(subscription, event);
                } else {
                    // temporary: technically not correct as poster not decoupled from subscriber
                    invokeSubscriber(subscription, event);
                }
                break;
            case BACKGROUND://在子线程调用订阅方法
                if (isMainThread) {
                    //如果在主线程就切到子线程后调用invokeSubscriber
                    backgroundPoster.enqueue(subscription, event);
                } else {
                    //如果在子线程直接调用invokeSubscriber
                    invokeSubscriber(subscription, event);
                }
                break;
            case ASYNC:
                //利用线程池执行invokeSubscriber方法
                asyncPoster.enqueue(subscription, event);
                break;
            default:
                throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
        }
    }

不管是在子线程还是在主线程最终都是通过invokeSubscriber方法,利用反射调用订阅对象的订阅方法

void invokeSubscriber(Subscription subscription, Object event) {
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    }

五、EventBus#postSticky粘性事件发送流程

public void postSticky(Object event) {
    //粘性事件会被保存到stickyEvents中
    synchronized (stickyEvents) {
        stickyEvents.put(event.getClass(), event);
    }
    //正常发送事件
    post(event);
}

和普通事件不同之处在于粘性事件会被保存到stickyEvents中,这样即使在我们订阅之前已经发送了某个粘性事件,在我们订阅的时候也能收到,还记得订阅流程中有这样几行代码吗?

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        //...
        //如果订阅方法支持粘性事件
        if (subscriberMethod.sticky) {
            if (eventInheritance) {
                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
                //遍历stickyEvents map  这个map保存了粘性事件
                for (Map.Entry<Class<?>, Object> entry : entries) {
                    Class<?> candidateEventType = entry.getKey();
                    //如果当前订阅方法订阅的事件列席在stickyEvents中已经有了,就说明之前某个地方发送过该粘性事件
                    if (eventType.isAssignableFrom(candidateEventType)) {
                        Object stickyEvent = entry.getValue();
                        //检查发送粘性事件
                        checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                    }
                }
            } else {
                Object stickyEvent = stickyEvents.get(eventType);
                //检查发送粘性事件
                checkPostStickyEventToSubscription(newSubscription, stickyEvent);
            }
        }
    }

到这里EventBus的源码基本就分析完了,是不是很简单?

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

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

相关文章

1. MyBatis 整体架构

作为正式内容的第一篇&#xff0c;本次不会介绍具体的技术&#xff0c;而是先从全局视角上对 MyBatis 做一个俯瞰&#xff0c;了解 MyBatis 项目工程的组织结构&#xff0c;以及内部的核心功能模块。 工程结构 打开 MyBatis 的 Github 地址&#xff0c;就可以看到其代码工程结…

C语言:打印用 * 组成的带空格直角三角形图案

题目&#xff1a; 多组输入一个整数&#xff08;2~20&#xff09;&#xff0c;表示直角三角形直角边的长度&#xff0c;即 * 的数量&#xff0c;也表示输出行数。 思路&#xff1a; 总体思路&#xff1a; 找到规律&#xff1a; 行数 列数 < 三角形长度 - 1 打印 两个空格…

一步一步学OAK之十四: 获取OAK设备信息

这一节我们通过调用DepthAI API 来获取OAK设备信息 目录 DeviceBootloader简介获取OAK设备信息的方法Setup 1: 创建文件Setup 2: 安装依赖Setup 3: 导入需要的包Setup 4: 获取可用设备Setup 5: 判断infos的长度Setup 6: 遍历infosSetup 7: 打印提示消息Setup 8: 连接设备Setup…

html_4——知识总结

html_4——知识总结 一、计算机基础知识二、html4总结2.1 html基本结构2.2 全局属性-id,class,style,dir,title,lang2.3 格式排版标签-div,p,h1-h6,br,hr,pre2.4 文本标签-span,en,strong,del,ins,sub,sup2.5 图片标签-img:src,alt,width,height,boder2.6 超链接-a:herf,target…

内部函数和外部函数

文章目录 怎么来的&#xff1f;内部函数外部函数明确一下内外的概念&#xff1a;外部函数的实例fgets()函数 怎么来的&#xff1f; 函数本质上是全局的&#xff0c;因为定义一个函数的目的就是这个函数与其他函数之间相互调用&#xff0c;如果不声明的话&#xff0c;一个函数既…

YouTube正测试屏蔽“广告拦截器”,以确保其广告收入

YouTube目前正在进行一项全球范围内的小规模测试&#xff0c;警告用户关掉他们的广告屏蔽器&#xff0c;否则将被限制观看视频的次数。 周三&#xff08;6月28日&#xff09;&#xff0c;Reddit的一位用户发现&#xff0c;在使用YouTube时弹出了一个窗口&#xff0c;提示该用户…

Cali3F: Calibrated Fast Fair Federated Recommendation System

Decentralized Collaborative Learning Framework for Next POI Recommendation 标定的&#xff08;校准的&#xff09;快速公平联邦推荐系统 1. What does literature study? 提出一个经过校准的快速而公平的联邦推荐框架Cali3F&#xff0c;通过集群内参数共享解决了收敛问…

创新引领未来:RFID技术在汽车装配中的智能革命

射频识别&#xff08;RFID&#xff09;技术作为一种自动识别技术&#xff0c;已经在许多领域得到广泛应用。在汽车装配领域&#xff0c;RFID技术的应用可以提高装配过程的效率、降低人工错误率&#xff0c;并帮助实现自动化和智能化生产。本文将介绍RFID技术在汽车装配中的应用…

动态二维码生成器PHP Dynamic QRcode

什么是 PHP Dynamic QRcode &#xff1f; PHP Dynamic QRcode 是一个允许生成和保存动态和静态二维码&#xff08;QR码&#xff09;的应用。它具有简洁、响应灵敏且用户友好的设计。其中包含您网站中可能需要的一般功能&#xff0c;如&#xff1a;记录管理&#xff08;CRUD&…

【2023,学点儿新Java-27】是的——C语言中的const关键字 | 附:按照类型 快速了解与划分:C语言中的关键字 | goto关键字解释

前情回顾&#xff1a; 【2023&#xff0c;学点儿新Java-26】关键字介绍示例代码&#xff1a;assert 断言&#xff08;如何启用断言&#xff09;&#xff0c;以 验证一个数组的长度是否不为零 为例说明【2023&#xff0c;学点儿新Java-25】如何解决浮点计算存在误差&#xff1a…

Selenium Grid入门详解

目录 前言&#xff1a; 一、简介 二、使用场景 三、使用前提 四、使用方式 五、实现在另一台电脑运行脚本 前言&#xff1a; Selenium Grid是一个用于分布式测试的工具&#xff0c;它允许同时在多个机器上执行Selenium测试。通过使用Selenium Grid&#xff0c;你可以在不…

Linux系统Centos7 安装MySQL8.0详细步骤

MySql安装 1.下载wget命令 yum -y install wget 2. 在线下载mysql安装包 wget https://repo.mysql.com//mysql80-community-release-el7-3.noarch.rpm 3.MySQL的GPG升级了&#xff0c;需要更新&#xff0c;如果是新安装的MySQL&#xff0c;执行以下脚本即可&#xff1…

Vite + Vue3 + Electron实现进程通信

Vite Vue3 Electron实现进程通信 实现 渲染进程 / 主进程 通信&#xff08;IPC&#xff09; Electron 是一个基于 Chromium 和 Node.js 的桌面应用程序开发框架&#xff0c;而 Vue3 则是一种流行的前端框架。将两者结合使用可以快速地打造出跨平台的桌面应用程序。在这种组…

Sui x KuCoin Labs夏季黑客松第三批入围项目公布

自Sui x KuCoin Labs夏季黑客松开放注册以来&#xff0c;已收获了众多开发者的报名参与。赛程过半&#xff0c;截至目前为止&#xff0c;第一批和第二批入围项目已在前两周公布&#xff0c;第三批入围名单项目新鲜出炉&#xff0c;进入最终的Demo Day。 第三批入围名单 SuiVi…

在 Linux 中查找 IP 地址的 3 种简单方法

在 Linux 系统中&#xff0c;经常需要查找 IP 地址以进行网络配置、故障排除或安全管理。无论是查找本地主机的 IP 地址还是查找其他设备的 IP 地址&#xff0c;本文将介绍三种简单的方法&#xff0c;帮助你在 Linux 中轻松找到所需的 IP 地址。 总结 通过上述三种简单的方法&…

自动化测试进阶之路(WEB自动化二)

一、设计模式 Pom模块关键字驱动模式 pom模块&#xff1a;page object model 页面对象模式好处&#xff1a; 解决线性脚本的问题解决代码不能重复利用的问题后期的维护问题 分三层&#xff08;页面对象层调用基础层的方法&#xff0c;测试用例层调用页面对象层的方法&#xff0…

Spring Boot 有哪些方式可以实现热部署

一、什么是热部署&#xff1f; 释义 所谓热部署&#xff0c;就是在应用正在运行的时候升级软件&#xff0c;却不需要重新启动应用。 对于Java应用程序来说&#xff0c;热部署就是在运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中&#xff0c;类装入器扮演着…

EasyDSS视频直播点播平台视频回看列表显示为ID的排查与优化

视频直播点播EasyDSS平台具备灵活的视频能力&#xff0c;包括直播、点播、转码、管理、录像、检索、时移回看等&#xff0c;平台支持音视频采集、视频推拉流、播放H.265编码视频、存储、分发等能力服务&#xff0c;可应用在无人机推流、在线直播、虚拟直播、远程培训等场景中。…

String面试

以下讨论以jdk8为标准&#xff1a; String Pool&#xff1a;字符串常量池 存储字面量位于堆中&#xff0c;不在元空间intern()方法会去常量池找&#xff0c;没有的话就创建一个&#xff0c;返回常量池中的地址&#xff1b;有的话就直接返回对象地址 new String(“”)方法强制创…

3.Hive SQL数据定义语言(DDL)

1. 数据定义语言概述 1.1 常见的开发方式 &#xff08;1&#xff09; Hive CLI、Beeline CLI Hive自带的命令行客户端 优点&#xff1a;不需要额外安装 缺点&#xff1a;编写SQL环境恶劣&#xff0c;无有效提示&#xff0c;无语法高亮&#xff0c;误操作率高 &#xff08;2&…