Dubbo – Dubbo的SPI机制

Dubbo – Dubbo的SPI机制SPI 是什么 SPI 全称 serviceprovi 比如你有一个接口 现在这个接口有三个实现类 那么在系统运行的时候对这个接口到底选择哪个实现类呢 这就需要 SPI 了 需要根据指定的配置或者默认的配置 去找到对应的实现类加载进来 然后用这个实现类的实例对象 举个例子 你有一个接口 A A1 A2 A3 分别是接口 A 的不同实现 你通过配置接口 A 实现类 A2 那么系统在

国庆期间闲来无事,写了一个简单的小程序,小程序名称叫做 IT藏经楼。目的是分享这些年自己积累的一些学习材料,方面大家查找使用,包括电子书、案例项目、学习视频、面试题和一些PPT模板。里面所有材料都免费分享。目前小程序中只发布了非常小的一部分,后续会陆续上传分享。当前版本的小程序页面也比较简单,还在逐渐的优化中。

在这里插入图片描述
在Dubbo中,SPI贯穿整个Dubbo的核心,所以理解Dubbo中的SPI对于理解Dubbo的原理有着至关重要的作用。在Spring中,我们知道SpringFactoriesLoader这个类,它也是一种SPI机制。

关于Java SPI

在了解Dubbo的SPI机制之前,我们先了解下Java提供的SPI (service provider interface) 机制,SPI是JDK内置的一种服务提供发现机制。目前市面上很多框架都用它来做服务的扩展发现。简单的说,它是一种动态替换发现的机制。

举个简单的例子,我们想在运行时动态给它添加实现,你只需要添加一个实现,然后把新的实现描述给JDK知道就行了。大家耳熟能详的如JDBC,日志框架都有用到。

实现 SPI 需要遵循的标准

我们如何去实现一个标准的 SPI 发现机制呢?其实很简单,只需要满足以下提交就行了 :

  1. 需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service
  2. 在该目录下创建一个 properties 文件,该文件需要满足以下几个条件 :
    2.1 文件名必须是扩展的接口的全路径名称
    2.2 文件内部描述的是该扩展接口的所有实现类
    2.3 文件的编码格式是 UTF-8






  3. 通过 java.util.ServiceLoader 的加载机制来发现

在这里插入图片描述

SPI 的实际应用

SPI 在很多地方有应用,可能大家都没有关注,最常用的就是 JDBC 驱动,我们来看看是怎么应用的。

SPI 的缺点

  1. JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,什么意思呢?就是如果你在 META-INF/service 下的文件里面加了 N 个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到, 那么会很浪费资源
  2. 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因

Dubbo中的SPI机制

Dubbo也用了SPI思想,不过没有用JDK的SPI机制,是自己实现的一套SPI机制。在Dubbo的源码中,很多地方会存在下面这样的三种代码,分别是自适应扩展点、指定名称的扩展点、激活扩展点。

ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension(); ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name); ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key); 

比如:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 

Protocol接口,在运行的时候dubbo会判断一下应该选用这个Protocol接口的哪个实现类来实例化对象。

它会去找你配置的Protocol,将你配置的Protocol实现类加载到JVM中来,然后实例化对象,就用你配置的那个Protocol实现类就可以了。

上面那行代码就是dubbo里面大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态的根据配置去找到对应的实现类。如果你没有配置,那就走默认的实现类。

 @SPI("dubbo") public interface Protocol { 
    int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); } 

在dubbo自己的jar中,在META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol文件中:

filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper mock=org.apache.dubbo.rpc.support.MockProtocol dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol http=org.apache.dubbo.rpc.protocol.http.HttpProtocol org.apache.dubbo.rpc.protocol.webservice.WebServiceProtocol thrift=org.apache.dubbo.rpc.protocol.thrift.ThriftProtocol native-thrift=org.apache.dubbo.rpc.protocol.nativethrift.ThriftProtocol memcached=org.apache.dubbo.rpc.protocol.memcached.MemcachedProtocol redis=org.apache.dubbo.rpc.protocol.redis.RedisProtocol rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol xmlrpc=org.apache.dubbo.xml.rpc.protocol.xmlrpc.XmlRpcProtocol registry=org.apache.dubbo.registry.integration.RegistryProtocol qos=org.apache.dubbo.qos.protocol.QosProtocolWrapper 

所以这就看到了dubbo的SPI机制默认是怎么玩的了,其实就是Protocol接口,@SPI(“dubbo”) 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认的实现类就是 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

如果想要动态替换掉默认的实现类,需要使用 @Adaptive 接口,Protocol 接口中,有两个方法加了 @Adaptive 注解,就是说那俩接口会被代理实现。

比如这个 Protocol 接口搞了俩 @Adaptive 注解标注了方法,在运行的时候会针对 Protocol 生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据 url 中的 protocol 来获取那个 key,默认是 dubbo,你也可以自己指定,你如果指定了别的 key,那么就会获取别的实现类的实例了。

如何自己扩展 dubbo 中的组件

在调用处执行如下代码 :

public class App { 
    public static void main( String[] args ) { 
    // Main.main(args); Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol"); System.out.println(protocol.getDefaultPort()); } } 

我们的猜想是,一定有一个地方通过读取指定路径下的所有文件进行 load。然后讲对应的结果保存到一个 map 中,key 对应为 名称,value 对应为实现类。那么这个实现,一定就在 ExtensionLoader 中了。接下来我们就可以基于这个猜想去看看代码的实 现。

Dubbo 的扩展点原理实现

在看Dubbo SPI的实现代码之前,我们先思考一个问题,所谓的扩展点,就是通过指定目录下配置一个对应接口的实现类,然后程序会进行查找和解析,找到对应的扩展点,那么这里就涉及到两个问题:

  1. 怎么解析
  2. 被加载的类如何存储和使用

ExtensionLoader.getExtensionLoader.getExtension
我们通过上面的例子可以知道,我们是通过下面这个代码去加载扩展点的:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol"); 

我们从这段代码着手,去看看到底做了什么事情,能够通过这样一段代码实现扩展协议的查找和加载。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { 
    if (type == null) { 
    throw new IllegalArgumentException("Extension type == null"); } if (!type.isInterface()) { 
    throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!"); } if (!withExtensionAnnotation(type)) { 
    throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!"); } ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); if (loader == null) { 
    EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); } return loader; } 

从上面代码可以看出, 先会去检查我们想要的扩展点是否已经存在于EXTENSION_LOADERS这个缓存中,如果存在则直接返回,否则新创建一个ExtensionLoader。

private ExtensionLoader(Class<?> type) { 
    this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); } 

如果当前的 type=ExtensionFactory,那么 objectFactory=null, 否则会创建一个自适应扩展点给到 objectFacotry,目前来说具 体做什么咱们先不关心,现在只要知道objectFactory 在这里赋值了,并且是返回一个 AdaptiveExtension(). 这个暂时不展开,后面再分析

getExtension

public T getExtension(String name) { 
    if (StringUtils.isEmpty(name)) { 
    throw new IllegalArgumentException("Extension name == null"); } if ("true".equals(name)) { 
    return getDefaultExtension(); } Holder<Object> holder = getOrCreateHolder(name); Object instance = holder.get(); if (instance == null) { 
    synchronized (holder) { 
    instance = holder.get(); if (instance == null) { 
    instance = createExtension(name); holder.set(instance); } } } return (T) instance; } 

这个方法就是根据一个名字来获得一个对应类的实例,所以我们来猜想一下,回想一下前面咱们配置的自定义协议,name 实际上就是 myProtocol,而返回的实现类应该就是 MyProtocol。

同样的,先去看缓存中是否已经存在我们想要的扩展点,如果没有则新建一个

createExtension

private T createExtension(String name) { 
    Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { 
    throw findException(name); } try { 
    T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { 
    EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (CollectionUtils.isNotEmpty(wrapperClasses)) { 
    for (Class<?> wrapperClass : wrapperClasses) { 
    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { 
    throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t); } } 

这里会先调用getExtensionClasses()加载指定目录下的所有文件:

private Map<String, Class<?>> getExtensionClasses() { 
    Map<String, Class<?>> classes = cachedClasses.get(); if (classes == null) { 
    synchronized (cachedClasses) { 
    classes = cachedClasses.get(); if (classes == null) { 
    classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; } // synchronized in getExtensionClasses private Map<String, Class<?>> loadExtensionClasses() { 
    cacheDefaultExtensionName(); Map<String, Class<?>> extensionClasses = new HashMap<>(); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); return extensionClasses; } 

injectExtension

private T injectExtension(T instance) { 
    try { 
    if (objectFactory != null) { 
    //objectFactory在这里用到了  for (Method method : instance.getClass().getMethods()) { 
    if (isSetter(method)) { 
    / * Check {@link DisableInject} to see if we need auto injection for this property */ if (method.getAnnotation(DisableInject.class) != null) { 
    continue; } Class<?> pt = method.getParameterTypes()[0]; if (ReflectUtils.isPrimitives(pt)) { 
    continue; } try { 
    String property = getSetterProperty(method); Object object = objectFactory.getExtension(pt, property); if (object != null) { 
    method.invoke(instance, object); } } catch (Exception e) { 
    logger.error("Failed to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e); } } } } } catch (Exception e) { 
    logger.error(e.getMessage(), e); } return instance; } 

这个方法是用来实现依赖注入的,如果被加载的实例中,有成员属性本身也是一个扩展点,则会通过 set 方法进行注入。

分析到这里我们发现,所谓的扩展点,套路都一样,不管是 springfactorieyLoader,还是 Dubbo 的 spi。实际上,Dubbo 的功能 会更加强大,比如自适应扩展点,比如依赖注入

Adaptive 自适应扩展点
什么叫自适应扩展点呢?我们先演示一个例子,在下面这个例子中,我们传入一个 Compiler 接口,它会返回一个 AdaptiveCompiler。这个就叫自适应。

public class App { 
    public static void main( String[] args ) { 
    Compiler compiler=ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension(); System.out.println(compiler.getClass()); } } 

比如拿 Protocol 这个接口来说,它里面定义了 export 和 refer 两个抽象方法,这两个方法分别带有@Adaptive 的标识,标识是 一个自适应方法。 我们知道 Protocol 是一个通信协议的接口,具体有多种实现,那么这个时候选择哪一种呢? 取决于我们在使用 dubbo 的时候所 配置的协议名称。而这里的方法层面的 Adaptive 就决定了当前这个方法会采用何种协议来发布服务。

@SPI("dubbo") public interface Protocol { 
    // 省略部分代码 @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; // 省略部分代码 } 

getAdaptiveExtension
这个方法主要就是要根据传入的接口返回一个自适应的实现类 :

public T getAdaptiveExtension() { 
    Object instance = cachedAdaptiveInstance.get(); if (instance == null) { 
    if (createAdaptiveInstanceError == null) { 
    synchronized (cachedAdaptiveInstance) { 
    instance = cachedAdaptiveInstance.get(); if (instance == null) { 
    try { 
    instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } catch (Throwable t) { 
    createAdaptiveInstanceError = t; throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t); } } } } else { 
    throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); } } return (T) instance; } 

cachedAdaptiveInstance是一个缓存,如果缓存中没有,则通过createAdaptiveExtension创建一个。

private T createAdaptiveExtension() { 
    try { 
    return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } catch (Exception e) { 
    throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e); } } private Class<?> getAdaptiveExtensionClass() { 
    getExtensionClasses(); if (cachedAdaptiveClass != null) { 
    return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass(); } 

createAdaptiveExtension这个方法主要做了以下两件事:

  1. 获得一个自适应扩展点的实例
  2. 实现依赖注入

getAdaptiveExtensionClass方法中先调用getExtensionClasses()方法,这个方法我们前面已经提到,会加载当前传入的类型的所有扩展点,保存在一个 hashmap 中 这里有一个判断逻辑,如果 cachedApdaptiveClas!=null ,直接返回这个 cachedAdaptiveClass,这里大家可以猜一下,这个 cachedAdaptiveClass 是一个什么?

 private Class<?> createAdaptiveExtensionClass() { 
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate(); ClassLoader classLoader = findClassLoader(); org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader); } 

dubbo 会动态生成一个代理类 Protocol$Adaptive. 前面的名字 protocol 是根据当前 ExtensionLoader 所加载的扩展点来定义的。

Protocol$Adaptive
动态生成的代理类,以下是我通过 debug 拿到的代理类
前面传入进来的 cachedDefaultName,在这个动态生成的类中,会体现在下面标红的部分,也就是它的默认实现是 DubboProtocol




import org.apache.dubbo.common.extension.ExtensionLoader; public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { 
    public void destroy() { 
    throw new UnsupportedOperationException( "The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } public int getDefaultPort() { 
    throw new UnsupportedOperationException( "The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { 
    if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null"); if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); org.apache.dubbo.common.URL url = arg0.getUrl(); String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.export(arg0); } public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException { 
    if (arg1 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg1; String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.refer(arg0, arg1); } } 

我们可以看到在export方法中,会先通过String extName = (url.getProtocol() == null ? “dubbo” : url.getProtocol());去获取extName, 在我们之前的例子中,url.getProtocol()方法返回的应该是”myProtocol”,所以extName是myProtocol,然后通过下面代码去获取Protocol的实例:

org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); 

图形理解
简单来说,上面的基于方法层面的@Adaptive,基本实现原理的图形大概是这样
在这里插入图片描述




injectExtension
对于扩展点进行依赖注入,简单来说就是如果当前加载的扩展点中存在一个成员属性(对象),并且提供了 set 方法,那么这个 方法就会执行依赖注入.

private T injectExtension(T instance) { 
    try { 
    if (objectFactory != null) { 
    for (Method method : instance.getClass().getMethods()) { 
    if (isSetter(method)) { 
    / * Check {@link DisableInject} to see if we need auto injection for this property */ if (method.getAnnotation(DisableInject.class) != null) { 
    continue; } Class<?> pt = method.getParameterTypes()[0]; if (ReflectUtils.isPrimitives(pt)) { 
    continue; } try { 
    String property = getSetterProperty(method); Object object = objectFactory.getExtension(pt, property); if (object != null) { 
    method.invoke(instance, object); } } catch (Exception e) { 
    logger.error("Failed to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e); } } } } } catch (Exception e) { 
    logger.error(e.getMessage(), e); } return instance; } 

在 injectExtension 这个方法中,我们发现入口出的代码首先判断了 objectFactory 这个对象是否为空。这个是在哪里初始化的呢? 实际上我们在获得 ExtensionLoader 的时候,就对 objectFactory 进行了初始化。

private ExtensionLoader(Class<?> type) { 
    this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); } 

Activate 自动激活扩展点
自动激活扩展点,有点类似我们讲 springboot 的时候用到的 conditional,根据条件进行自动激活。但是这里设计的初衷是,对 于一个类会加载多个扩展点的实现,这个时候可以通过自动激活扩展点进行动态加载, 从而简化配置我们的配置工作。

@Activate 提供了一些配置来允许我们配置加载条件,比如 group 过滤,比如 key 过滤。

public class App { 
    public static void main( String[] args ) { 
    ExtensionLoader<Filter> loader=ExtensionLoader.getExtensionLoader(Filter.class); URL url= new URL("","",0); // url=url.addParameter("cache","cache");  List<Filter> filters=loader.getActivateExtension(url,"cache"); System.out.println(filters.size()); filters.forEach(filter -> { 
    System.out.println(filter); }); } } 

在这里插入图片描述

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/224801.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 上午11:06
下一篇 2026年3月17日 上午11:07


相关推荐

  • preload linux 多个,Linux下LD_PRELOAD的简单用法

    preload linux 多个,Linux下LD_PRELOAD的简单用法LD PRELOAD 是个环境变量 用于动态库的加载 动态库加载的优先级最高 一般情况下 其加载顺序为 LD PRELOAD gt LD LIBRARY PATH gt etc ld so cache gt lib gt usr lib 程序中我们经常要调用一些外部库的函数 以 rand 为例 如果我们有个自定义的 rand 函数 把它编译成动态库后 通过 LD PRELOAD 加载 当程序中

    2025年12月1日
    6
  • 实对称矩阵一定可以对角化

    实对称矩阵一定可以对角化UTF8gbsn 实对称矩阵一定可以对角化 最近看共轭梯度下降的时候看到有人的推导里面用到了这个命题 虽然以前学过 但是学得很渣 所以没有自己想过这个命题怎么样成立的 现在将这些证明过程梳理一下 实对称矩阵含有 n 个实根首先我们来证明一个命题 实对称矩阵含有 n 个实根 注意 n 个实根并不一定都是不同的 可能含有重根 比如 r 1 2 0 r 1 2 0 r 1 2 0 就含有两个重根 r 1r 1r 1 在计算根数目的时候这个方程的解算两个 首先 任意的矩阵 A mathbf A A

    2026年3月18日
    2
  • CSS 图片去色处理

    CSS 图片去色处理说到对图片进行处理,我们经常会想到PhotoShop这类的图像处理工具。作为前端开发者,我们经常会需要处理一些特效,例如根据不同的状态,让图标显示不同的颜色。或者是hover的时候,对图片的对比度,阴影进行处理。//黑白色img{transition:all.3sease;filter:grayscale(100%);opacity:.6;}//正常颜色img:hover{filter:none;opacity:1;

    2022年10月6日
    5
  • Python读取CSV文件(附CSV模块及方法详情地址)

    Python读取CSV文件(附CSV模块及方法详情地址)Python 读取 CSV 文件 附 CSV 模块及方法详情地址 1 需要用到 csv 模块 CSV 模块及方法使用详情地址 https docs python org zh cn 3 library csv htmlCSV 模块读取文件的具体方法 2 实例演示首先 创建一个 csv 文件 CSV 文件内容如下 然后在 pycharm 里导入 CSV 模块 并按照官方提供的模块操作即可 具体代码 importcsvwit r C Users wangfei2 Desktop demo csv

    2026年3月16日
    1
  • mysql截取数字_mysql 截取字符串中的数字

    mysql截取数字_mysql 截取字符串中的数字展开全部selectREVERSE(right(REVERSE(filename),length(filename)-LEAST(if(Locate(‘0’,REVERSE(filename))>0,Locate(‘0’,REVERSE(filename)),999),if(Locate(‘1’,REVERSE(filename))>0,Locate(‘1’,REVERSE(f…

    2022年6月3日
    97
  • xshell5连接不上虚拟机_虚拟机的网络连接设置

    xshell5连接不上虚拟机_虚拟机的网络连接设置一:首先解决的关于ping的问题1.在虚拟机中ping百度看能不能先ping通,如果虚拟机连接不上网络的话Xshell肯定是连接不上的,如果有上述情况的请点击二:检查你虚拟机中防火墙是否关闭CentOs6中查看防火墙状态:serviceiptablesstatus关闭防火墙:serviceiptablesstop禁用防火墙:chkconfigiptablesoffCentOs7中查看防火墙状态:systemctlstatusfirewalld.service关闭防火墙:

    2026年2月15日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号