JDK/Dubbo/Spring 三种 SPI 机制,谁更好?

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

本文主要是特性 & 用法介绍,不涉及源码解析(源码都很简单,相信你一定一看就懂)


# SPI 有什么用?


举个栗子,现在我们设计了一款全新的日志框架:super-logger。默认以XML文件作为我们这款日志的配置文件,并设计了一个配置文件解析的接口:

package com.github.kongwu.spisamples;
public interface SuperLoggerConfiguration { void configure(String configFile);}

然后来一个默认的XML实现:

package com.github.kongwu.spisamples;
public class XMLConfiguration implements SuperLoggerConfiguration{ public void configure(String configFile){ ...... }}

那么我们在初始化,解析配置时,只需要调用这个XMLConfiguration来解析XML配置文件即可。

package com.github.kongwu.spisamples;
public class LoggerFactory { static { SuperLoggerConfiguration configuration = new XMLConfiguration(); configuration.configure(configFile); }
public static getLogger(Class clazz){ ...... }}

这样就完成了一个基础的模型,看起来也没什么问题。不过扩展性不太好,因为如果想定制/扩展/重写解析功能的话,我还得重新定义入口的代码,LoggerFactory 也得重写,不够灵活,侵入性太强了。


比如现在用户/使用方想增加一个 yml 文件的方式,作为日志配置文件,那么只需要新建一个YAMLConfiguration,实现 SuperLoggerConfiguration 就可以。但是……怎么注入呢,怎么让 LoggerFactory中使用新建的这个 YAMLConfiguration ?难不成连 LoggerFactory 也重写了?


如果借助SPI机制的话,这个事情就很简单了,可以很方便的完成这个入口的扩展功能。


下面就先来看看,利用JDK 的 SPI 机制怎么解决上面的扩展性问题。


JDK SPI


JDK 中 提供了一个 SPI 的功能,核心类是 java.util.ServiceLoader。其作用就是,可以通过类名获取在"META-INF/services/"下的多个配置实现文件。


为了解决上面的扩展问题,现在我们在META-INF/services/下创建一个com.github.kongwu.spisamples.SuperLoggerConfiguration文件(没有后缀)。


文件中只有一行代码,那就是我们默认的com.github.kongwu.spisamples.XMLConfiguration(注意,一个文件里也可以写多个实现,回车分隔)

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:
com.github.kongwu.spisamples.XMLConfiguration

然后通过 ServiceLoader 获取我们的 SPI 机制配置的实现类:

ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();SuperLoggerConfiguration configuration;
while(iterator.hasNext()) { //加载并初始化实现类 configuration = iterator.next();}
//对最后一个configuration类调用configure方法configuration.configure(configFile);

最后在调整LoggerFactory中初始化配置的方式为现在的SPI方式:

package com.github.kongwu.spisamples;
public class LoggerFactory { static { ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class); Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator(); SuperLoggerConfiguration configuration;
while(iterator.hasNext()) { configuration = iterator.next();//加载并初始化实现类 } configuration.configure(configFile); }
public static getLogger(Class clazz){ ...... }}

等等,这里为什么是用 iterator ? 而不是get之类的只获取一个实例的方法?


试想一下,如果是一个固定的get方法,那么get到的是一个固定的实例,SPI 还有什么意义呢?


SPI 的目的,就是增强扩展性。将固定的配置提取出来,通过 SPI 机制来配置。那既然如此,一般都会有一个默认的配置,然后通过 SPI 的文件配置不同的实现,这样就会存在一个接口多个实现的问题。要是找到多个实现的话,用哪个实现作为最后的实例呢?


所以这里使用iterator来获取所有的实现类配置。刚才已经在我们这个 super-logger 包里增加了默认的SuperLoggerConfiguration 实现。


为了支持 YAML 配置,现在在使用方/用户的代码里,增加一个YAMLConfiguration的 SPI 配置:

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:
com.github.kongwu.spisamples.ext.YAMLConfiguration

此时通过iterator方法,就会获取到默认的XMLConfiguration和我们扩展的这个YAMLConfiguration两个配置实现类了。


在上面那段加载的代码里,我们遍历iterator,遍历到最后,我们**使用最后一个实现配置作为最终的实例。


再等等?最后一个?怎么算最后一个?


使用方/用户自定义的的这个 YAMLConfiguration 一定是最后一个吗?


这个真的不一定,取决于我们运行时的 ClassPath 配置,在前面加载的jar自然在前,最后的jar里的自然当然也在后面。所以如果用户的包在ClassPath中的顺序比super-logger的包更靠后,才会处于最后一个位置;如果用户的包位置在前,那么所谓的最后一个仍然是默认的XMLConfiguration。


举个栗子,如果我们程序的启动脚本为:

java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main

默认的XMLConfiguration SPI配置在super-logger.jar,扩展的YAMLConfiguration SPI配置文件在main.jar,那么iterator获取的最后一个元素一定为YAMLConfiguration。


但这个classpath顺序如果反了呢?main.jar 在前,super-logger.jar 在后

java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main

这样一来,iterator 获取的最后一个元素又变成了默认的XMLConfiguration,我们使用 JDK SPI 没啥意义了,获取的又是第一个,还是默认的XMLConfiguration。


由于这个加载顺序(classpath)是由用户指定的,所以无论我们加载第一个还是最后一个,都有可能会导致加载不到用户自定义的那个配置。


所以这也是JDK SPI机制的一个劣势,无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠ClassPath的顺序是一个非常不严谨的方式。


# Dubbo SPI

Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。


Dubbo 中实现了一套新的 SPI 机制,功能更强大,也更复杂一些。相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下(以下demo来自dubbo官方文档)。

optimusPrime = org.apache.spi.OptimusPrimebumblebee = org.apache.spi.Bumblebee

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI 注解。下面来演示 Dubbo SPI 的用法:

@SPIpublic interface Robot {    void sayHello();}
public class OptimusPrime implements Robot {
@Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); }}
public class Bumblebee implements Robot {
@Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); }}

public class DubboSPITest {
@Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); }}

Dubbo SPI 和 JDK SPI 最大的区别就在于支持“别名”,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取 Robot 多个 SPI 实现中别名为“optimusPrime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!


通过 @SPI 注解的 value 属性,还可以默认一个“别名”的实现。比如在Dubbo 中,默认的是Dubbo 私有协议:dubbo protocol - dubbo://** 来看看Dubbo中协议的接口:

@SPI("dubbo")public interface Protocol { ......}

在 Protocol 接口上,增加了一个 @SPI 注解,而注解的 value 值为 Dubbo ,通过 SPI 获取实现时就会获取      Protocol SPI 配置中别名为dubbo的那个实现,com.alibaba.dubbo.rpc.Protocol文件如下:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapperlistener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrappermock=com.alibaba.dubbo.rpc.support.MockProtocol

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocolrmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocolhessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocolcom.alibaba.dubbo.rpc.protocol.http.HttpProtocolcom.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocolthrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocolmemcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocolredis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocolrest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocolregistry=com.alibaba.dubbo.registry.integration.RegistryProtocolqos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper

然后只需要通过getDefaultExtension,就可以获取到 @SPI 注解上value对应的那个扩展实现了

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();//protocol: DubboProtocol

还有一个 Adaptive 的机制,虽然非常灵活,但……用法并不是很“优雅”,这里就不介绍了


Dubbo 的 SPI 中还有一个“加载优先级”,优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载,如果遇到重复就跳过不会加载了


所以如果想靠classpath加载顺序去覆盖内置的扩展,也是个不太理智的做法,原因同上 - 加载顺序不严谨。


# Spring SPI


Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:

//获取所有factories文件中配置的LoggingSystemFactoryList<LoggingSystemFactory>> factories =     SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

下面是一段 Spring Boot 中 spring.factories 的配置

# Logging Systemsorg.springframework.boot.logging.LoggingSystemFactory=\org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\org.springframework.boot.logging.java.JavaLoggingSystem.Factory
# PropertySource Loadersorg.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader
# ConfigData Location Resolversorg.springframework.boot.context.config.ConfigDataLocationResolver=\org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\org.springframework.boot.context.config.StandardConfigDataLocationResolver
......

Spring SPI 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。至于多个接口的扩展配置,是用一个文件好,还是每个单独一个文件好这个,这个问题就见仁见智了(个人喜欢 Spring 这种,干净利落)。


Spring的SPI 虽然属于spring-framework(core),但是目前主要用在spring boot中……


和前面两种 SPI 机制一样,Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少。


但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的Factories中,项目中spring.factories里配置的那个实现类也会排在第一个。


如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个META-INF/spring.factories文件,只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的  spring.factories 文件然后修改** 比如我只想添加一个新的 LoggingSystemFactory 实现,那么我只需要新建一个META-INF/spring.factories文件,而不是完整的复制+修改:

org.springframework.boot.logging.LoggingSystemFactory=\com.example.log4j2demo.Log4J2LoggingSystem.Factory

# 对比



三种 SPI 机制对比之下,JDK 内置的机制是最弱鸡的,但是由于是 JDK 内置,所以还是有一定应用场景,毕竟不用额外的依赖;Dubbo 的功能最丰富,但机制有点复杂了,而且只能配合 Dubbo 使用,不能完全算是一个独立的模块;Spring 的功能和JDK的相差无几,最大的区别是所有扩展点写在一个 spring.factories 文件中,也算是一个改进,并且 IDEA 完美支持语法提示。


各位看官们大佬们,你们觉得 JDK/Dubbo/Spring 三种 SPI 的机制,哪个更好呢?欢迎评论区留言!


参考

  • Introduction to the Service Provider Interfaces - Oracle

  • Dubbo SPI - Apache Dubbo

  • Creating Your Own Auto-configuration - Spring


版权申明:内容来源网络,版权归原创者所有。除非无法确认,都会标明作者及出处,如有侵权烦请告知,我们会立即删除并致歉。谢谢!


               
       
    <section style="margin-bottom: 15px;">
             
     <span style="font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;"><strong><span style="font-size: 15px;color: rgb(255, 76, 65);">往期热门文章:</span></strong><br></span>
            
    </section>
           
   </section></pre></h4><h4 style="letter-spacing: 0.544px;white-space: normal;font-family: -apple-system-font, system-ui, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">
          
  <section style="margin-top: 15px;margin-bottom: 15px;font-size: inherit;font-variant-numeric: normal;font-variant-east-asian: normal;letter-spacing: 0.544px;color: rgb(62, 62, 62);line-height: 1.75em;text-indent: 0em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">1、</span>
           
   <a href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247489898&amp;idx=1&amp;sn=5c5e41a0f695649046de3db1b1810df5&amp;chksm=e9c5e0dbdeb269cd52f798c675f58295ad0f632283c0fb07db4140949c72796deed7dfe7d434&amp;scene=21#wechat_redirect" target="_blank" data-itemshowtype="0" data-linktype="2" hasload="1" style="color: var(--weui-LINK);-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">《</a>
           
   <a href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247489402&amp;idx=2&amp;sn=af5c3bb38717e828d92ed48874f03fe8&amp;chksm=e9c5eecbdeb267dd3d05c159bdb9c611f24c4ca7fb7dafa12daf459becb0ef4fab7bcc2d1a67&amp;scene=21#wechat_redirect" target="_blank" data-itemshowtype="0" data-linktype="2" hasload="1" style="color: var(--weui-LINK);-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">历史文章分类导读列表!精选优秀博文都在这里了!》</a>
          
  </section></h4><h4 style="letter-spacing: 0.544px;font-family: -apple-system-font, system-ui, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;white-space: pre-wrap;line-height: 1.2;">
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="letter-spacing: 0.544px;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">2、<a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496637&amp;idx=1&amp;sn=fff1a9a55b39f1617934cbdb40b2e62b&amp;chksm=e9c60a0cdeb1831a8539d951d9cf98a295470b7924475efbb0eb9f084b60c43538cec8f25206&amp;scene=21#wechat_redirect" data-itemshowtype="0" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;">Java必会的工具库,让你的代码量减少90%</a></span>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="letter-spacing: 0.544px;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">3、<a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496460&amp;idx=1&amp;sn=db2f3f3d23fd65f683cfa7321c2ee4e1&amp;chksm=e9c60abddeb183ab65f4c8d1f9668e3d1618dd09cd098f093e6e663222fd4d0754deabfcca44&amp;scene=21#wechat_redirect" data-itemshowtype="0" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;">腾讯最大股东收购了 Stack Overflow,以后“抄代码”都要付费了么?</a></span>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">4、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496422&amp;idx=1&amp;sn=3a11bda6ecbb9c09f2ee389926bfd695&amp;chksm=e9c60b57deb182413b6f617e3d3a7ff320bc37876afe2d4b2bbaa7920cfa8c1b23ae9a3dda89&amp;scene=21#wechat_redirect" data-itemshowtype="0" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">灵隐寺招聘:没有KPI,佛系上班……</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">5、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496418&amp;idx=1&amp;sn=2095e5e5e9e8b5d322a2329a003edddb&amp;chksm=e9c60b53deb18245cc39ab84520082ea34f08dd5147a331f529405491c80a1206acfea6fc4ca&amp;scene=21#wechat_redirect" data-itemshowtype="0" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">如何优雅处理重复请求/并发请求?</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">6、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496413&amp;idx=1&amp;sn=2a15b2734dac08d0e18a33f9987dced7&amp;chksm=e9c60b6cdeb1827a51b714c1e5b55c041db651a43e573e0acd19fdbcef765ee3f75719a59abe&amp;scene=21#wechat_redirect" data-itemshowtype="11" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">不用到2038年,MySQL的TIMESTAMP就能把我们系统搞崩!</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">7、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496364&amp;idx=1&amp;sn=7e74a865d142e2c13f2ee92b22cef396&amp;chksm=e9c60b1ddeb1820bc587e93a854f8946b73dea4c4ec6627d7f7a227e1e01ab1235cdbb2b90a8&amp;scene=21#wechat_redirect" data-itemshowtype="0" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">翻车!在项目中用了Arrays.asList、ArrayList的subList,被公开批评</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">8、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496360&amp;idx=1&amp;sn=b0287346c03bc901acf539d3053c3025&amp;chksm=e9c60b19deb1820fad8011037ca12a4248b9c5c8ae37de8a37ee41b5127fbd0a135a210a7be8&amp;scene=21#wechat_redirect" data-itemshowtype="0" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">想接私活时薪再翻一倍,建议根据这几个开源的Spring Boot项目改改~</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">9、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496350&amp;idx=1&amp;sn=b3486a14bbb315df1039b8c29c70c419&amp;chksm=e9c60b2fdeb1823960d7812831d24d5c47a947f44a102b00ca8e1509917d84dde189f7da7451&amp;scene=21#wechat_redirect" data-itemshowtype="11" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">细数ThreadLocal三大坑,内存泄露仅是小儿科</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <span style="font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">10、</span>
           
   <a target="_blank" href="http://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&amp;mid=2247496335&amp;idx=1&amp;sn=40246299850b84d5c4bb677ddd377cfc&amp;chksm=e9c60b3edeb18228de1588c663846c709233467d03423e0390f798faac7fb4a6ccc9e76d58f5&amp;scene=21#wechat_redirect" data-itemshowtype="11" tab="innerlink" data-linktype="2" hasload="1" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);cursor: pointer;font-size: 14px;font-family: &quot;Helvetica Neue&quot;, Helvetica, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;">Redis与MySQL双写一致性如何保证?</a>
          
  </section>
          
  <section style="margin-top: 15px;margin-bottom: 15px;letter-spacing: 0.544px;color: rgb(62, 62, 62);caret-color: rgb(51, 51, 51);line-height: 1.5em;">
           
   <img data-backh="287" data-backw="574" data-before-oversubscription-url="https://mmbiz.qpic.cn/mmbiz_png/UtWdDgynLda8TWB0rCPDRObrOHuCRKNtndsCNwbsMejpEvEwWTibrcy9xhLbFwbOCarFP5NrZTOZtzGFjeLh6yg/640?" data-copyright="0" data-ratio="0.5" data-s="300,640" src="/upload/e46fc654dfcccf189c5d81430fa2708f.jpg" data-type="jpeg" data-w="800" style="letter-spacing: 0.544px;font-size: inherit;font-weight: 700;text-indent: 0em;font-family: -apple-system, system-ui, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif;white-space: normal;display: inline;box-sizing: border-box !important;visibility: visible !important;width: auto !important;" width="auto">
          
  </section></h4></pre></pre></pre></h4></pre>

已有 0 条评论

    欢迎您,新朋友,感谢参与互动!
    // 友盟