首页 文章 精选 留言 我的

精选列表

搜索[深度集成],共10000篇文章
优秀的个人博客,低调大师

多模态测试技术深度解析

2024年,GPT-4V、Qwen-VL、Gemini 1.5等大模型已具备跨文本、图像、音频、视频甚至传感器信号的联合理解与生成能力。这意味着软件系统正从单模态交互(如纯Web表单)快速演进为多模态智能体——用户可拍照提问、语音指令叠加AR标注、车载系统融合摄像头+雷达+语音实时决策。而传统基于API断言或UI元素定位的测试方法,在这类系统面前频频失效:一张模糊截图可能被模型正确理解,却因OCR识别失败被自动化脚本判定为‘缺陷’;一段带口音的语音指令被人类接受,却被语音转文本模块错误切分,继而触发错误业务逻辑。这标志着——多模态测试已不是‘可选项’,而是保障AI原生系统质量的生命线。

优秀的个人博客,低调大师

源码级深度理解 Java SPI

作者:vivo 互联网服务器团队- Zhang Peng SPI 是一种用于动态加载服务的机制。它的核心思想就是解耦,属于典型的微内核架构模式。SPI 在 Java 世界应用非常广泛,如:Dubbo、Spring Boot 等框架。本文从源码入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的应用。 一、SPI 简介 SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。 Java SPI 有四个要素: SPI 接口:为服务提供者实现类约定的的接口或抽象类。 SPI 实现类:实际提供服务的实现类。 SPI 配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。 ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。 二、SPI 示例 正所谓,实践出真知,我们不妨通过一个具体的示例来看一下,如何使用 Java SPI。 2.1 SPI 接口 首先,需要定义一个 SPI 接口,和普通接口并没有什么差别。 package io.github.dunwu.javacore.spi;public interface DataStorage { String search(String key);} 2.2 SPI 实现类 假设,我们需要在程序中使用两种不同的数据存储——MySQL 和 Redis。因此,我们需要两个不同的实现类去分别完成相应工作。 MySQL查询 MOCK 类 package io.github.dunwu.javacore.spi;public class MysqlStorage implements DataStorage { @Override public String search(String key) { return "【Mysql】搜索" + key + ",结果:No"; }} Redis 查询 MOCK 类 package io.github.dunwu.javacore.spi;public class RedisStorage implements DataStorage { @Override public String search(String key) { return "【Redis】搜索" + key + ",结果:Yes"; }} service 传入的是期望加载的 SPI 接口类型 到目前为止,定义接口,并实现接口和普通的 Java 接口实现没有任何不同。 2.3 SPI 配置 如果想通过 Java SPI 机制来发现服务,就需要在 SPI 配置中约定好发现服务的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。以本示例代码为例,其文件名应该为 io.github.dunwu.javacore.spi.DataStorage, 文件中的内容如下: io.github.dunwu.javacore.spi.MysqlStorageio.github.dunwu.javacore.spi.RedisStorage 2.4 ServiceLoader 完成了上面的步骤,就可以通过 ServiceLoader 来加载服务。示例如下: import java.util.ServiceLoader;public class SpiDemo { public static void main(String[] args) { ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class); System.out.println("============ Java SPI 测试============"); serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No"))); }} 输出: ============ Java SPI 测试============【Mysql】搜索Yes Or No,结果:No【Redis】搜索Yes Or No,结果:Yes 三、SPI 原理 上文中,我们已经了解 Java SPI 的要素以及使用 Java SPI 的方法。你有没有想过,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代码本身很精练,接下来,让我们通过走读源码的方式,逐一理解 ServiceLoader 的工作流程。 3.1 ServiceLoader 的成员变量 先看一下 ServiceLoader 类的成员变量,大致有个印象,后面的源码中都会使用到。 public final class ServiceLoader<S> implements Iterable<S> { // SPI 配置文件目录 private static final String PREFIX = "META-INF/services/"; // 将要被加载的 SPI 服务 private final Class<S> service; // 用于加载 SPI 服务的类加载器 private final ClassLoader loader; // ServiceLoader 创建时的访问控制上下文 private final AccessControlContext acc; // SPI 服务缓存,按实例化的顺序排列 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 懒查询迭代器 private LazyIterator lookupIterator; // ...} 3.2 ServiceLoader 的工作流程 (1)ServiceLoader.load静态方法 应用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态方法。 ServiceLoader.load 静态方法的作用是: ① 指定类加载 ClassLoader 和访问控制上下文; ② 然后,重新加载 SPI 服务 清空缓存中所有已实例化的 SPI 服务 根据ClassLoader和 SPI 类型,创建懒加载迭代器 这里,摘录 ServiceLoader.load 相关源码,如下: // service 传入的是期望加载的 SPI 接口类型// loader 是用于加载 SPI 服务的类加载器public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader);}public void reload() { // 清空缓存中所有已实例化的 SPI 服务 providers.clear(); // 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器 lookupIterator = new LazyIterator(service, loader);}// 私有构造方法// 重新加载 SPI 服务private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); // 指定类加载 ClassLoader 和访问控制上下文 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; // 然后,重新加载 SPI 服务 reload();} (2)应用程序通过ServiceLoader的iterator方法遍历 SPI 实例 ServiceLoader 的类定义,明确了 ServiceLoader 类实现了 Iterable<T>接口,所以,它是可以迭代遍历的。实际上,ServiceLoader 类维护了一个缓存 providers( LinkedHashMap 对象),缓存 providers 中保存了已经被成功加载的 SPI 实例,这个 Map 的 key 是 SPI 接口实现类的全限定名,value 是该实现类的一个实例对象。 当应用程序调用 ServiceLoader 的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据:如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒加载迭代器的迭代器。 public Iterator<S> iterator() { return new Iterator<S>() { // 缓存 SPI providers Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); // lookupIterator 是 LazyIterator 实例,用于懒加载 SPI 实例 public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } };} (3)懒加载迭代器的工作流程 上面的源码中提到了,lookupIterator 是 LazyIterator 实例,而 LazyIterator 用于懒加载 SPI 实例。那么, LazyIterator 是如何工作的呢? 这里,摘取LazyIterator关键代码 hasNextService 方法: 拼接META-INF/services/+ SPI 接口全限定名 通过类加载器,尝试加载资源文件 解析资源文件中的内容,获取 SPI 接口的实现类的全限定名nextName nextService 方法: hasNextService()方法解析出了 SPI 实现类的的全限定名 nextName,通过反射,获取 SPI 实现类的类定义 Class。 然后,尝试通过 Class 的 newInstance 方法实例化一个 SPI 服务对象。如果成功,则将这个对象加入到缓存 providers 中并返回该对象。 private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 1.拼接 META-INF/services/ + SPI 接口全限定名 // 2.通过类加载器,尝试加载资源文件 // 3.解析资源文件中的内容 String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true;}private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a s"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen} 3.3 SPI 和类加载器 通过上面两个章节中,走读 ServiceLoader 代码,我们已经大致了解 Java SPI 的工作原理,即通过 ClassLoader 加载 SPI 配置文件,解析 SPI 服务,然后通过反射,实例化 SPI 服务实例。我们不妨思考一下,为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢? 学习过 JVM 的读者,想必都了解过类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的 BootstrapClassLoader 外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。 双亲委派机制约定了:一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。 双亲委派的好处:使得 Java 类伴随着它的类加载器,天然具备一种带有优先级的层次关系,从而使得类加载得到统一,不会出现重复加载的问题: 系统类防止内存中出现多份同样的字节码 保证 Java 程序安全稳定运行 例如:java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 classpath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 classpath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。 双亲委派的限制:子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。——这就导致了双亲委派模型并不能解决所有的类加载器问题。Java SPI 就面临着这样的问题: SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的; 而 SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。这也解释了本节开始的问题——为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢?因为如果不指定 ClassLoader,则无法获取 SPI 服务。 如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。 通常可以通过 Thread.currentThread().getClassLoader() 和 Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。 3.4 Java SPI 的不足 Java SPI 存在一些不足: 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。 多个并发多线程使用 ServiceLoader 类的实例是不安全的。 四、SPI 应用场景 SPI 在 Java 开发中应用十分广泛。首先,在 Java 的 java.util.spi package 中就约定了很多 SPI 接口。下面,列举一些 SPI 接口: TimeZoneNameProvider:为 TimeZone 类提供本地化的时区名称。 DateFormatProvider:为指定的语言环境提供日期和时间格式。 NumberFormatProvider:为 NumberFormat 类提供货币、整数和百分比值。 Driver:从 4.0 版开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName() 方法加载驱动程序。 PersistenceProvider:提供 JPA API 的实现。 等等 除此以外,SPI 还有很多应用,下面列举几个经典案例。 4.1 SPI 应用案例之 JDBC DriverManager 作为 Java 工程师,尤其是 CRUD 工程师,相必都非常熟悉 JDBC。众所周知,关系型数据库有很多种,如:MySQL、Oracle、PostgreSQL 等等。JDBC 如何识别各种数据库的驱动呢? 4.1.1创建数据库连接 我们先回顾一下,JDBC 如何创建数据库连接的呢? 在JDBC4.0 之前,连接数据库的时候,通常会用 Class.forName(XXX)方法来加载数据库相应的驱动,然后再获取数据库连接,继而进行 CRUD 等操作。 Class.forName("com.mysql.jdbc.Driver") 而 JDBC4.0 之后,不再需要用 Class.forName(XXX)方法来加载数据库驱动,直接获取连接就可以了。显然,这种方式很方便,但是如何做到的呢? (1)JDBC 接口:首先,Java 中内置了接口 java.sql.Driver。 (2)JDBC 接口实现:各个数据库的驱动自行实现 java.sql.Driver 接口,用于管理数据库连接。 ① MySQL:在 MySQL的 Java 驱动包 mysql-connector-java-XXX.jar 中,可以找到 META-INF/services 目录,该目录下会有一个名字为java.sql.Driver 的文件,文件内容是com.mysql.cj.jdbc.Driver。 com.mysql.cj.jdbc.Driver 正是 MySQL 版的 java.sql.Driver 实现。如下图所示: ②PostgreSQL 实现:在 PostgreSQL 的 Java 驱动包 postgresql-42.0.0.jar 中,也可以找到同样的配置文件,文件内容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 实现。 (3)创建数据库连接 以 MySQL 为例,创建数据库连接代码如下: final String DB_URL = String.format("jdbc:mysql://%s:%s/%s", DB_HOST, DB_PORT, DB_SCHEMA);connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); 4.1.2 DriverManager 从前文,我们已经知道 DriverManager 是创建数据库连接的关键。它究竟是如何工作的呢? 可以看到是加载实例化驱动的,接着看 loadInitialDrivers 方法: private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 通过 classloader 获取所有实现 java.sql.Driver 的驱动类 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 利用 SPI,记载所有 Driver 服务 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 获取迭代器 Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ // 遍历迭代器 while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); // 打印数据库驱动信息 println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); // 尝试实例化驱动 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } }} 上面的代码主要步骤是: 从系统变量中获取驱动的实现类。 利用 SPI 来获取所有驱动的实现类。 遍历所有驱动,尝试实例化各个实现类。 根据第 1 步获取到的驱动列表来实例化具体的实现类。 需要关注的是下面这行代码: ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 这里实际获取的是 java.util.ServiceLoader.LazyIterator 迭代器。调用其 hasNext 方法时,会搜索 classpath 下以及 jar 包中的 META-INF/services 目录,查找 java.sql.Driver 文件,并找到文件中的驱动实现类的全限定名。调用其 next 方法时,会根据驱动类的全限定名去尝试实例化一个驱动类的对象。 4.2SPI 应用案例之 Common-Loggin common-logging(也称 Jakarta Commons Logging,缩写 JCL)是常用的日志门面工具包。 common-logging 的核心类是入口是 LogFactory,LogFatory 是一个抽象类,它负责加载具体的日志实现。 其入口方法是 LogFactory.getLog 方法,源码如下: public static Log getLog(Class clazz) throws LogConfigurationException { return getFactory().getInstance(clazz);}public static Log getLog(String name) throws LogConfigurationException { return getFactory().getInstance(name);} 从以上源码可知,getLog 采用了工厂设计模式,是先调用 getFactory 方法获取具体日志库的工厂类,然后根据类名称或类型创建日志实例。 LogFatory.getFactory 方法负责选出匹配的日志工厂,其源码如下: public static LogFactory getFactory() throws LogConfigurationException { // 省略... // 加载 commons-logging.properties 配置文件 Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES); // 省略... // 决定创建哪个 LogFactory 实例 // (1)尝试读取全局属性 org.apache.commons.logging.LogFactory if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY + "] to define the LogFactory subclass to use..."); } try { // 如果指定了 org.apache.commons.logging.LogFactory 属性,尝试实例化具体实现类 String factoryClass = getSystemProperty(FACTORY_PROPERTY, null); if (factoryClass != null) { if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass + "' as specified by system property " + FACTORY_PROPERTY); } factory = newFactory(factoryClass, baseClassLoader, contextClassLoader); } else { if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined."); } } } catch (SecurityException e) { // 异常处理 } catch (RuntimeException e) { // 异常处理 } // (2)利用 Java SPI 机制,尝试在 classpatch 的 META-INF/services 目录下寻找 org.apache.commons.logging.LogFactory 实现类 if (factory == null) { if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID + "] to define the LogFactory subclass to use..."); } try { final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID); if( is != null ) { // This code is needed by EBCDIC and other strange systems. // It's a fix for bugs reported in xerces BufferedReader rd; try { rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); } catch (java.io.UnsupportedEncodingException e) { rd = new BufferedReader(new InputStreamReader(is)); } String factoryClassName = rd.readLine(); rd.close(); if (factoryClassName != null && ! "".equals(factoryClassName)) { if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] Creating an instance of LogFactory class " + factoryClassName + " as specified by file '" + SERVICE_ID + "' which was present in the path of the context classloader."); } factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader ); } } else { // is == null if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found."); } } } catch (Exception ex) { // note: if the specified LogFactory class wasn't compatible with LogFactory // for some reason, a ClassCastException will be caught here, and attempts will // continue to find a compatible class. if (isDiagnosticsEnabled()) { logDiagnostic( "[LOOKUP] A security exception occurred while trying to create an" + " instance of the custom factory class" + ": [" + trim(ex.getMessage()) + "]. Trying alternative implementations..."); } // ignore } } // (3)尝试从 classpath 目录下的 commons-logging.properties 文件中查找 org.apache.commons.logging.LogFactory 属性 if (factory == null) { if (props != null) { if (isDiagnosticsEnabled()) { logDiagnostic( "[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY + "' to define the LogFactory subclass to use..."); } String factoryClass = props.getProperty(FACTORY_PROPERTY); if (factoryClass != null) { if (isDiagnosticsEnabled()) { logDiagnostic( "[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'"); } factory = newFactory(factoryClass, baseClassLoader, contextClassLoader); // TODO: think about whether we need to handle exceptions from newFactory } else { if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass."); } } } else { if (isDiagnosticsEnabled()) { logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from.."); } } } // (4)以上情况都不满足,实例化默认实现类 org.apache.commons.logging.impl.LogFactoryImpl if (factory == null) { if (isDiagnosticsEnabled()) { logDiagnostic( "[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT + "' via the same classloader that loaded this LogFactory" + " class (ie not looking in the context classloader)."); } factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader); } if (factory != null) { /** * Always cache using context class loader. */ cacheFactory(contextClassLoader, factory); if (props != null) { Enumeration names = props.propertyNames(); while (names.hasMoreElements()) { String name = (String) names.nextElement(); String value = props.getProperty(name); factory.setAttribute(name, value); } } } return factory;} 从 getFactory 方法的源码可以看出,其核心逻辑分为 4 步: 首先,尝试查找全局属性org.apache.commons.logging.LogFactory,如果指定了具体类,尝试创建实例。 利用 Java SPI 机制,尝试在 classpatch 的 META-INF/services 目录下寻找org.apache.commons.logging.LogFactory 的实现类。 尝试从 classpath 目录下的 commons-logging.properties 文件中查找org.apache.commons.logging.LogFactory 属性,如果指定了具体类,尝试创建实例。 以上情况如果都不满足,则实例化默认实现类,即org.apache.commons.logging.impl.LogFactoryImpl。 4.3 SPI 应用案例之 Spring Boot Spring Boot 是基于 Spring 构建的框架,其设计目的在于简化 Spring 应用的配置、运行。在 Spring Boot 中,大量运用了自动装配来尽可能减少配置。 下面是一个 Spring Boot 入口示例,可以看到,代码非常简洁。 import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@SpringBootApplication@RestControllerpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @GetMapping("/hello") public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { return String.format("Hello %s!", name); }} 那么,Spring Boot 是如何做到寥寥几行代码,就可以运行一个 Spring Boot 应用的呢。我们不妨带着疑问,从源码入手,一步步探究其原理。 4.3.1 @SpringBootApplication 注解 首先,Spring Boot 应用的启动类上都会标记一个 @SpringBootApplication 注解。 @SpringBootApplication 注解定义如下: @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})})public @interface SpringBootApplication { // 略} 除了@Target、@Retention、@Documented、@Inherited 这几个元注解, @SpringBootApplication 注解的定义中还标记了@SpringBootConfiguration、 @EnableAutoConfiguration、@ComponentScan 三个注解。 4.3.2 @SpringBootConfiguration 注解 从@SpringBootConfiguration 注解的定义来看,@SpringBootConfiguration 注解本质上就是一个@Configuration 注解,这意味着被@SpringBootConfiguration 注解修饰的类会被 Spring Boot 识别为一个配置类。 @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods() default true;} 4.3.3 @EnableAutoConfiguration 注解 @EnableAutoConfiguration 注解定义如下: @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {};} @EnableAutoConfiguration 注解包含了@AutoConfigurationPackage 与@Import({AutoConfigurationImportSelector.class})两个注解。 4.3.4 @AutoConfigurationPackage 注解 @AutoConfigurationPackage 会将被修饰的类作为主配置类,该类所在的 package 会被视为根路径,Spring Boot 默认会自动扫描根路径下的所有 Spring Bean(被@Component 以及继承@Component 的各个注解所修饰的类)。——这就是为什么 Spring Boot 的启动类一般要置于根路径的原因。这个功能等同于在 Spring xml 配置中通过 context:component-scan 来指定扫描路径。@Import 注解的作用是向 Spring 容器中直接注入指定组件。@AutoConfigurationPackage 注解中注明了@Import({Registrar.class})。Registrar 类用于保存 Spring Boot 的入口类、根路径等信息。 4.3.5 SpringFactoriesLoader.loadFactoryNames 方法 @Import(AutoConfigurationImportSelector.class)表示直接注入 AutoConfigurationImportSelector。 AutoConfigurationImportSelector 有一个核心方法 getCandidateConfigurations 用于获取候选配置。该方法调用了 SpringFactoriesLoader.loadFactoryNames 方法,这个方法即为 Spring Boot SPI 的关键,它负责加载所有 META-INF/spring.factories 文件,加载的过程由 SpringFactoriesLoader 负责。 Spring Boot 的 META-INF/spring.factories 文件本质上就是一个 properties 文件,数据内容就是一个个键值对。 SpringFactoriesLoader.loadFactoryNames 方法的关键源码: // spring.factories 文件的格式为:key=value1,value2,value3// 遍历所有 META-INF/spring.factories 文件// 解析文件,获得 key=factoryClass 的类名称public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());}private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { // 尝试获取缓存,如果缓存中有数据,直接返回 MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { // 获取资源文件路径 Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); // 遍历所有路径 while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); // 解析文件,得到对应的一组 Properties Properties properties = PropertiesLoaderUtils.loadProperties(resource); // 遍历解析出的 properties,组装数据 for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }} 归纳上面的方法,主要作了这些事: 加载所有 META-INF/spring.factories 文件,加载过程有 SpringFactoriesLoader 负责。 在 CLASSPATH 中搜寻所有 META-INF/spring.factories 配置文件。 然后,解析 spring.factories 文件,获取指定自动装配类的全限定名。 4.3.6 Spring Boot 的 AutoConfiguration 类 Spring Boot 有各种 starter 包,可以根据实际项目需要,按需取材。在项目开发中,只要将 starter 包引入,我们就可以用很少的配置,甚至什么都不配置,即可获取相关的能力。通过前面的 Spring Boot SPI 流程,只完成了自动装配工作的一半,剩下的工作如何处理呢 ? 以 spring-boot-starter-web 的 jar 包为例,查看其 maven pom,可以看到,它依赖于 spring-boot-starter,所有 Spring Boot 官方 starter 包都会依赖于这个 jar 包。而 spring-boot-starter 又依赖于 spring-boot-autoconfigure,Spring Boot 的自动装配秘密,就在于这个 jar 包。 从 spring-boot-autoconfigure 包的结构来看,它有一个 META-INF/spring.factories ,显然利用了 Spring Boot SPI,来自动装配其中的配置类。 下图是 spring-boot-autoconfigure 的 META-INF/spring.factories 文件的部分内容,可以看到其中注册了一长串会被自动加载的 AutoConfiguration 类。 以 RedisAutoConfiguration 为例,这个配置类中,会根据@ConditionalXXX 中的条件去决定是否实例化对应的 Bean,实例化 Bean 所依赖的重要参数则通过 RedisProperties 传入。 RedisProperties 中维护了 Redis 连接所需要的关键属性,只要在 yml 或 properties 配置文件中,指定 spring.redis 开头的属性,都会被自动装载到 RedisProperties 实例中。 通过以上分析,已经一步步解读出 Spring Boot 自动装载的原理。 五、SPI 应用案例之 Dubbo Dubbo 并未使用 Java SPI,而是自己封装了一套新的 SPI 机制。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容形式如下: optimusPrime = org.apache.spi.OptimusPrimebumblebee = org.apache.spi.Bumblebee 与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样可以按需加载指定的实现类。Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性。 5.1 ExtensionLoader 入口 Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,可以加载指定的实现类。 ExtensionLoader 的 getExtension 方法是其入口方法,其源码如下: public T getExtension(String name) { if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null"); if ("true".equals(name)) { // 获取默认的拓展实现类 return getDefaultExtension(); } // Holder,顾名思义,用于持有目标对象 Holder<Object> holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } Object instance = holder.get(); // 双重检查 if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 创建拓展实例 instance = createExtension(name); // 设置实例到 holder 中 holder.set(instance); } } } return (T) instance;} 可以看出,这个方法的作用就是:首先检查缓存,缓存未命中则调用 createExtension 方法创建拓展对象。那么,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 (wrapperClasses != null && !wrapperClasses.isEmpty()) { // 循环创建 Wrapper 实例 for (Class<?> wrapperClass : wrapperClasses) { // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。 // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量 instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("..."); }} createExtension 方法的的工作步骤可以归纳为: 通过getExtensionClasses获取所有的拓展类 通过反射创建拓展对象 向拓展对象中注入依赖 将拓展对象包裹在相应的Wrapper对象中 以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。 5.2获取所有的拓展类 Dubbo 在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码分析如下: 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 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。 private Map<String, Class<?>> loadExtensionClasses() { // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的 final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { // 对 SPI 注解内容进行切分 String[] names = NAME_SEPARATOR.split(value); // 检测 SPI 注解内容是否合法,不合法则抛出异常 if (names.length > 1) { throw new IllegalStateException("more than 1 default extension name on extension..."); } // 设置默认名称,参考 getDefaultExtension 方法 if (names.length == 1) { cachedDefaultName = names[0]; } } } Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); // 加载指定文件夹下的配置文件 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadDirectory(extensionClasses, DUBBO_DIRECTORY); loadDirectory(extensionClasses, SERVICES_DIRECTORY); return extensionClasses;} loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) { // fileName = 文件夹路径 + type 全限定名 String fileName = dir + type.getName(); try { Enumeration<java.net.URL> urls; ClassLoader classLoader = findClassLoader(); // 根据文件名加载所有的同名文件 if (classLoader != null) { urls = classLoader.getResources(fileName); } else { urls = ClassLoader.getSystemResources(fileName); } if (urls != null) { while (urls.hasMoreElements()) { java.net.URL resourceURL = urls.nextElement(); // 加载资源 loadResource(extensionClasses, classLoader, resourceURL); } } } catch (Throwable t) { logger.error("..."); }} loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。 private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { try { BufferedReader reader = new BufferedReader( new InputStreamReader(resourceURL.openStream(), "utf-8")); try { String line; // 按行读取配置内容 while ((line = reader.readLine()) != null) { // 定位 # 字符 final int ci = line.indexOf('#'); if (ci >= 0) { // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略 line = line.substring(0, ci); } line = line.trim(); if (line.length() > 0) { try { String name = null; int i = line.indexOf('='); if (i > 0) { // 以等于号 = 为界,截取键与值 name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0) { // 加载类,并通过 loadClass 方法对类进行缓存 loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { IllegalStateException e = new IllegalStateException("Failed to load extension class..."); } } } } finally { reader.close(); } } catch (Throwable t) { logger.error("Exception when load extension class..."); }} loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下: private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException("..."); } // 检测目标类上是否有 Adaptive 注解 if (clazz.isAnnotationPresent(Adaptive.class)) { if (cachedAdaptiveClass == null) { // 设置 cachedAdaptiveClass缓存 cachedAdaptiveClass = clazz; } else if (!cachedAdaptiveClass.equals(clazz)) { throw new IllegalStateException("..."); } // 检测 clazz 是否是 Wrapper 类型 } else if (isWrapperClass(clazz)) { Set<Class<?>> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); wrappers = cachedWrapperClasses; } // 存储 clazz 到 cachedWrapperClasses 缓存中 wrappers.add(clazz); // 程序进入此分支,表明 clazz 是一个普通的拓展类 } else { // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常 clazz.getConstructor(); if (name == null || name.length() == 0) { // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException("..."); } } // 切分 name String[] names = NAME_SEPARATOR.split(name); if (names != null && names.length > 0) { Activate activate = clazz.getAnnotation(Activate.class); if (activate != null) { // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键, // 存储 name 到 Activate 注解对象的映射关系 cachedActivates.put(names[0], activate); } for (String n : names) { if (!cachedNames.containsKey(clazz)) { // 存储 Class 到名称的映射关系 cachedNames.put(clazz, n); } Class<?> c = extensionClasses.get(n); if (c == null) { // 存储名称到 Class 的映射关系 extensionClasses.put(n, clazz); } else if (c != clazz) { throw new IllegalStateException("..."); } } } }} 如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、 cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了。 参考资料 Java SPI 思想梳理 Dubbo SPI springboot 中 SPI 机制 SpringBoot 的自动装配原理、自定义 starter 与 spi 机制,一网打尽 END 猜你喜欢 vivo平台化实践探索之旅-平台产品系列01 探究Presto SQL引擎(4)-统计计数 本文分享自微信公众号 - vivo互联网技术(vivoVMIC)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Inside Java Newscast #1 深度解读

本文是 Inside Java Newscast #1 的个人体验与解读。视频地址:点击这里 ⎯⎯⎯⎯⎯⎯ Chapters ⎯⎯⎯⎯⎯⎯ 0:00 - Intro 0:57 - Java 16 – Intro 1:16 - Java 16 – Records 1:43 - Java 16 – Type Pattern Matching 1:58 - Java 16 – Sealed Classes - Preview 2:25 - Java 16 – Stream API 2:51 - Java 16 – HTTP/2 API 3:14 - Java 16 – Unix Domain Sockets 3:32 - Java 16 – Project Panama (Incubating) 4:07 - Java 16 – JDK Flight Recorder 4:39 - Java 16 – jpackage 5:02 - Java 16 – Performance 5:23 - Java 16 – Security 5:48 - Java 16 – Ports 5:55 - Java 16 – Deprecations/Limitations 6:49 - Java 16 – Outro 7:08 - Java 17 7:22 - Java 17 – Another Port 7:34 - Java 17 – Applet for Removal 7:55 - Java 17 – Sealed Classes 8:12 - Outro Java 16 – Records 相关 JEP 地址: JEP 359: Records(Preview) JEP 384: Records (Second Preview) JEP 395: Records Records 这个特性我仔细研究过实现:参考我写的另一篇文章Java Record 的一些思考 - 默认方法使用以及基于预编译生成相关字节码的底层实 简单说来其实就是(编译后查看下字节码就能看出来),在编译后,根据 Record 源码插入相关域与方法的字节码,包括: 自动生成的 private final field 自动生成的全属性构造器 自动生成的 public getter 方法 自动生成的 hashCode(),equals(),toString() 方法: 从字节码可以看出,这三个方法的底层实现是 invokeDynamic 另一个方法 调用的是 ObjectMethods.java 这个类中的 bootstrap 方法 这个还让我闹了个笑话,我以为这个是 Project Valhala 的 Inline Object 已经实现了(参考我的这个系列: JEP 尝鲜系列),还去 StackOverflow 问,这个 Record 为啥能有 wait() 方法,并且可以进行 synchronized 同步(因为如果是 Project Valhala 的 Inline Object 的话是没有普通类的对象头的,没法用普通类对象的方法实现同步),结果。。。。。最后还是 Goetz 大佬一眼就看出我是误会了: Record 这个特性当初是为了适应什么场景设计的,以及某些设计为何被舍弃,可以参考 Gotez 大佬的这篇文章 java-14-feature-spotlight. 其重中主要的看点总结如下: 1.Java Records 最常用于的地方就是方法多个返回结果,原来我们可能需要用 Apache-commons 里面的 Pair 或者 Tuple 这样的对象封装,或者自己新建一个类型。现在可以使用 Record。 2.第二个常见应用即在 Stream 中传递的过程中保持原有对象,并且减少运算,例如 Stream 排序: List<Player> topN = players.stream() .sorted(Comparator.comparingInt(p -> getScore(p))) .limit(N) .collect(toList()); 这么写的话,每次作比较都会调用一次 getScore(p),这个调用次数是 O(n^2)。利用 Record 可以用比较少的代码和改动实现减少运算: record PlayerScore(Player player, Score score) { // convenience constructor for use by Stream::map PlayerScore(Player player) { this(player, getScore(player)); } } List<Player> topN = players.stream() .map(PlayerScore::new) .sorted(Comparator.comparingInt(PlayerScore::score)) .limit(N) .map(PlayerScore::player) .collect(toList()); 最后再推荐下我写的这篇关于 Record 的序列化的一些解析和思考:Java Record 的一些思考 - 序列化相关 Java 16 – Type Pattern Matching 相关 JEP 地址: JEP 305: Pattern Matching for instanceof (Preview) JEP 375: Pattern Matching for instanceof (Second Preview) JEP 394: Pattern Matching for instanceof 类型模式匹配一直是一个呼声很高的特性,如果和下一小节的 Sealed Class 特性 以及 Patterns in switch 结合起来使用会有更好的效果,这个我们在下一节会更详细的说明. Nicolai 对 Type Pattern Matching 的说明 Nicolai 的这篇文章 对 Type Pattern Matching 的说明非常详细,总结如下: 原来需要这么写的代码: void feed(Animal animal) { if (animal instanceof Elephant) { ((Elephant) animal).eatPlants(); } else if (animal instanceof Tiger) { ((Tiger) animal).eatMeat(); } } 现在可以直接这么写: void feed(Animal animal) { if (animal instanceof Elephant elephant) elephant.eatPlants(); else if (animal instanceof Tiger tiger) tiger.eatMeat(); } 不需要空指针判断,因为 instanceof 已经自带 null 判断了,符合条件的 Type Pattern Matching 变量不会为 null。并且, Type Pattern Matching 不支持向上匹配,因为这个没有意义,即下面的代码会编译报错: public void upcast(String string) { // compile error if (string instanceof CharSequence sequence) System.out.println("Duh"); } 还有一个常用的地方即实现 equals: // old @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Equals)) return false; Type other = (Type) o; return someField.equals(other.someField) && anotherField.equals(other.anotherField); } // new @Override public final boolean equals(Object o) { return o instanceof Type other && someField.equals(other.someField) && anotherField.equals(other.anotherField); } 其实 Type Pattern Matching 是个语法糖 其实这个特性是一个语法糖,我们可以简单测试下: public class TypePatternMatching { public static void main(String[] args) { Object object = new Object(); if (object instanceof String s) { System.out.println("a"); } } } 查看编译后的字节码: public class test.TypePatternMatching { public test.TypePatternMatching(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 8: aload_1 9: instanceof #7 // class java/lang/String 12: ifeq 28 15: aload_1 16: checkcast #7 // class java/lang/String 19: astore_2 20: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 23: ldc #15 // String a 25: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return } 可以看出,字节码其实和下面的写法是一样的: public static void main(String[] args) { Object object = new Object(); if (object instanceof String) { String s = (String)object; System.out.println("a"); } } 大家可以反编译下这个 class,就能看出来。 Java 16 – Sealed Classes - Preview Sealed Class 在 Java 17 已经发布了,相关的 JEP 如下: JEP 360: Sealed Classes (Preview) JEP 397: Sealed Classes (Second Preview) JEP 409: Sealed Classes 在某些情况下,我们可能想枚举一个接口的所有实现类,例如: interface Shape { } record Circle(double radius) implements Shape { } record Rectangle(double width, double height) implements Shape { } double area(Shape shape) { if (shape instanceof Circle circle) return circle.radius() * circle.radius() * Math.PI; if (shape instanceof Rectangle rect) return rect.width() * rect.height(); throw new IllegalArgumentException("Unknown shape"); } 我们如何能确定我们枚举完了所有的 Shape 呢? Sealed Class 这个特性为我们解决这个问题,Sealed Class 可以在声明的时候就决定这个类可以被哪些类继承: sealed interface Shape permits Rectangle, Circle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} double area(Shape shape) { if (shape instanceof Circle circle) return circle.radius() * circle.radius() * Math.PI; if (shape instanceof Rectangle rect) return rect.width() * rect.height(); throw new IllegalArgumentException("Unknown shape"); } Sealed Class (可以是 abstract class 或者 interface )在声明时需要指定所有的实现类的名称。针对继承类,有如下限制: Sealed Class 的继承类必须和 Sealed Class 在同一个模块下,如果没有指定模块,就必须在同一个包下 每个继承类必须直接继承 Sealed Class,不能间接继承 每个继承类必须是下面三种之一: final 的 class,Java Record 本身就是 final 的 sealed 的 class,可以进一步指定会被哪些子类实现 non-sealed 的 class,也是一种扩展,但是打破 Sealed Class 的限制,Sealed Class 不知道也不关心这种的继承类还会有哪些子类。 举个例子即: sealed interface Shape permits Rectangle, Circle, Triangle, WeirdShape {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} sealed interface Triangle extends Shape permits RightTriangle, NormalTriangle {} record RightTriangle(double width, double height) implements Triangle {} record NormalTriangle(double width, double height) implements Triangle {} static non-sealed class WeirdShape implements Shape {} class Star extends WeirdShape {} double area(Shape shape) { if (shape instanceof Circle circle) return circle.radius() * circle.radius() * Math.PI; if (shape instanceof Rectangle rect) return rect.width() * rect.height(); if (shape instanceof RightTriangle rt) return rt.width() * rt.height() / 2; if (shape instanceof NormalTriangle nt) return nt.width() * nt.height() / 2; throw new IllegalArgumentException("Unknown shape"); } 如果结合 Pattern Matching for switch 这个特性,就能实现更加方便的写法,但是目前 Java 17 中,Pattern Matching for switch 还处于 Preview:JEP 406: Pattern Matching for switch (Preview)。我们需要在编译参数和启动参数中加上 --enable-preview,这样就能像下面这样写代码: double area(Shape shape) { return switch (shape) { case Circle circle -> circle.radius() * circle.radius() * Math.PI; case Rectangle rect -> rect.width() * rect.height(); case RightTriangle rt -> rt.width() * rt.height() / 2; case NormalTriangle nt -> nt.width( ) * nt.height() / 2; default -> throw new IllegalArgumentException("Unknown shape"); }; } Java 16 – Stream API 更新 Java 16 中针对 Stream API 有两个更新,这里先提一个题外话,如果想看 JDK 不同版本之间有何差异,增加或者删除了哪些 API,可以通过下面这个链接查看: https://javaalmanac.io/jdk/17/apidiff/11/ 路径中的两个版本就是要对比的两个版本,其界面如下: 同时,我们也可以通过 JDK 内置 jdeps 工具查找过期以及废弃API以及对应的替换 jdeps --jdk-internals -R --class-path 'libs/*' $project libs是你的所有依赖的目录,$project是你的项目jar包,示例输出: ... JDK Internal API Suggested Replacement ---------------- --------------------- sun.misc.BASE64Encoder Use java.util.Base64 @since 1.8 sun.reflect.Reflection Use java.lang.StackWalker @since 9 关于这个更新,我写了一篇文章进行解析:Java 16 中新增的 Stream 接口的一些思考,核心内容总结如下: 假设有邮件这个 Record 类,包含 id,以及发送到的邮箱和抄送到的邮箱: record Mail(int id, Set<String> sendTo, Set<String> cc) {} 我们想找到一批邮件的所有不同的联系人,最后放到一个 List 中,可能会这么写: Set<String> collect = mails.stream().flatMap(mail -> { Set<String> result = new HashSet<>(); result.addAll(mail.sendTo()); result.addAll(mail.cc()); return result.stream(); }).collect(Collectors.toSet()); 但是,这样写显然很不优雅,首先是对于每一个 Mail 都创建了额外的 Set 和对应的 Stream,并且,对于每个 mail 的 sendTo 还有 cc 都遍历了两遍(addAll 一遍,后续 Stream 又一遍)。其实我们的目前只是将 mail 中的 cc 以及 sendTo 取出来,用于参与后续的 Stream。在这种场景下,就非常适合用 mapMulti: Set<String> collect = mails.stream().<String>mapMulti((mail, consumer) -> { mail.cc().forEach(consumer::accept); mail.sendTo().forEach(consumer::accept); }).collect(Collectors.toSet()); 可以看出: mapMulti 的入参是一个 BiConsumer,其实就是使用其参数中的 consumer 接收参与 Stream 后续的对象 mapMulti 的思路就是将参数中的需要参与后续 Stream 的对象传入 consumer 来继续 Stream consumer 没有限制对象类型,想要限制必须加上形参 <String> 否则最后返回的是 Set<Object> 而不是 Set<String> 对于 Stream 增加了 toList 直接转换成 List,由于不涉及 collect 里面的截断操作,所以比 collect 占用的内存更小,需要的操作更少并且更快。之前转换成 List,需要 collect(Collectors.toList()),生成的 List 是 ArrayList,是可变的。但是这次新加的 Api,toList 生成的是 UnmodifiableList,是不可变的。所以这两个 API 不能直接互相替换,需要做一些检查确认没有更改才能替换。 Java 16 – HTTP/2 API Java 16 中还引入了两个关于 HTTP/2 API 的 JDK 补充,参考: JDK-8252304: Seed an HttpRequest.Builder from an existing HttpRequest JDK-8252382: Add a new factory method to concatenate a sequence of BodyPublisher instances into a single publisher Java 16 – Unix Domain Sockets 相关 JEP: JEP 380: Unix-Domain Socket Channels Unix domain sockets 以本地文件的形式命名,让我们可以像访问本地文件一样访问本地网络连接。这个用于在同一个机器部署的不同进程之间的通信,下面是一个简单的 BIO 的例子: //创建 UnixDomainSocketAddress Path socketFile = Path.of("/home/zhanghaxi/process1"); UnixDomainSocketAddress address = UnixDomainSocketAddress.of(socketFile); //服务端监听 ServerSocketChannel serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); serverChannel.bind(address); SocketChannel channel = serverChannel.accept(); //客户端连接 SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX); channel.connect(address); 关于 NIO 的例子,请参考:https://docs.oracle.com/en/java/javase/16/core/internet-protocol-and-unix-domain-sockets-nio-example.html 相比于 TCP/IP 本地回环连接访问,由于 Unix Domain Sockets 知道他访问的是本地进程,所以减少了很多检查与校验(例如寻址与路由),同时由于不用做这些检查,包的大小也要小一些。支持 Unix Domain Sockets 的操作系统有 Linux, MacOS 和 Windows 10 以上的版本以及 Windows Server 2019 以上的版本. Java 16 – Project Panama (Incubating) Project Panama 是一个让 Java 变得更全面的项目,目前还处于孵化中的状态。他目前主要包括以下三个 API: Vector API:让 Java 也能使用新的 CPU 指令例如 SIMD(Single Instruction Multiple Data)相关指令来优化计算速度 Foreign Linker API:让 Java 可以直接调用系统库,不用通过 JNI 再封装一层。 Foreign-Memory Access API:让 Java 可以直接操作外部内存,突破现有对外内存 API 的限制,同时也是可以整合统一现有堆外内存操作的 API。 Vector API 相关 JEP: JEP 338: Vector API (Incubator) JEP 414: Vector API (Second Incubator):Java 17 中的 JEP 417: Vector API (Third Incubator):Java 18 中的 其中最主要的应用就是使用了 CPU 的 SIMD(单指令多数据)处理,它提供了通过程序的多通道数据流,可能有 4 条通道或 8 条通道或任意数量的单个数据元素流经的通道。并且 CPU 一次在所有通道上并行组织操作,这可以极大增加 CPU 吞吐量。通过 Vector API,Java 团队正在努力让 Java 程序员使用 Java 代码直接访问它;过去,他们必须在汇编代码级别对向量数学进行编程,或者使用 C/C++ 与 Intrinsic 一起使用,然后通过 JNI 提供给 Java。 一个主要的优化点就是循环,过去的循环(标量循环),一次在一个元素上执行,那很慢。现在,您可以使用 Vector API 将标量算法转换为速度更快的数据并行算法。一个使用 Vector 的例子: //测试指标为吞吐量 @BenchmarkMode(Mode.Throughput) //需要预热,排除 jit 即时编译以及 JVM 采集各种指标带来的影响,由于我们单次循环很多次,所以预热一次就行 @Warmup(iterations = 1) //单线程即可 @Fork(1) //测试次数,我们测试10次 @Measurement(iterations = 10) //定义了一个类实例的生命周期,所有测试线程共享一个实例 @State(value = Scope.Benchmark) public class VectorTest { private static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256; final int size = 1000; final float[] a = new float[size]; final float[] b = new float[size]; final float[] c = new float[size]; public VectorTest() { for (int i = 0; i < size; i++) { a[i] = ThreadLocalRandom.current().nextFloat(0.0001f, 100.0f); b[i] = ThreadLocalRandom.current().nextFloat(0.0001f, 100.0f); } } @Benchmark public void testScalar(Blackhole blackhole) throws Exception { for (int i = 0; i < a.length; i++) { c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; } } @Benchmark public void testVector(Blackhole blackhole) { int i = 0; //高于数组长度的 SPECIES 一次处理数据长度的倍数 int upperBound = SPECIES.loopBound(a.length); //每次循环处理 SPECIES.length() 这么多的数据 for (; i < upperBound; i += SPECIES.length()) { // FloatVector va, vb, vc; var va = FloatVector.fromArray(SPECIES, a, i); var vb = FloatVector.fromArray(SPECIES, b, i); var vc = va.mul(va) .add(vb.mul(vb)) .neg(); vc.intoArray(c, i); } for (; i < a.length; i++) { c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; } } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(VectorTest.class.getSimpleName()).build(); new Runner(opt).run(); } } 注意使用处于孵化的 Java 特性需要加上额外的启动参数将模块暴露,这里是--add-modules jdk.incubator.vector,需要在 javac 编译和 java 运行都加上这些参数,使用 IDEA 即: 测试结果: Benchmark Mode Cnt Score Error Units VectorTest.testScalar thrpt 10 7380697.998 ± 1018277.914 ops/s VectorTest.testVector thrpt 10 37151609.182 ± 1011336.900 ops/s 其他使用,请参考:fizzbuzz-simd-style,这是一篇比较有意思的文章(虽然这个性能优化感觉不只由于 SIMD,还有算法优化的功劳,哈哈) Foreign Linker API 相关 JEP: JEP 389: Foreign Linker API (Incubator) JEP 412: Foreign Function & Memory API (Incubator):在 Java 17 中,和 Foreign Linker API 整合到了一起 JEP 419: Foreign Function & Memory API (Second Incubator):位于 Java 18 中 通过这个 API,我们可以使用纯 Java 代码来调用系统的库,例如使用 Java 代码弹出一个 Windows 提示框: 以上例子来自于 https://headcrashing.wordpress.com/2021/02/06/spare-keystrokes-with-the-record-keyword-modern-java-jdk-16-head-crashing-informatics-26-2/ ,感兴趣的可以查看下 Foreign-Memory Access API JEP 370: Foreign-Memory Access API (Incubator) JEP 383: Foreign-Memory Access API (Second Incubator) JEP 393: Foreign-Memory Access API (Third Incubator) JEP 412: Foreign Function & Memory API (Incubator):在 Java 17 中,和 Foreign Linker API 整合到了一起 JEP 419: Foreign Function & Memory API (Second Incubator):位于 Java 18 中 很多流行的高性能 Java 框架和中间件使用了堆外内存,但是目前 Java 中操作堆外内存的 API 不够完善: ByteBuffer API 可以提供直接内存的访问 (DirectBuffer 还有 MMAP Buffer),但是大小受限(2 GB,原因因为 Buffer classes limited by 32-bit addressing),并且有很多问题遗留了很多年未能解决,例如:MappedByteBuffer.release()/close() to release system resources,Please make DirectByteBuffer performance enhancements, Add absolute bulk put and get methods Unsafe API:性能很高,可以被 JIT 优化,但是没有限制内存访问,哪一块内存都能访问,如果访问到一块已经释放的内存,就会导致 JVM 崩溃。 JNI 调用:性能较差,因为无法被 JIT 优化(例如方法内联) 如果这些 API 开发完成,使用 Java 操作内存将更加容易理解和高效 Java 16 – JDK Flight Recorder JFR 是我最喜欢的 Java 特性功能,我针对 JFR 写了很多篇文章,使用 JFR 定位过很多性能瓶颈以及线上问题,请参考以下系列或者文章: JFR 全解系列 JFR导致的雪崩问题定位与解决 JFR 定位因为 SSL 导致 CPU Load 飚高的问题 一次鞭辟入里的 Log4j2 日志输出阻塞问题的定位 spring-data-redis 上百万的 QPS 压力太大连接失败,我 TM 人傻了 Java 16 中,针对 JFR, 在 Java 14 引入的 JFR Stream 的基础上,增加了通过 JMX 暴露的 JFR Stream。原来我们只能内部消费处理 JFR Event,现在可以通过 JMX 远程消费 JFR Event:JDK-8253898: JFR: Remote Recording Stream Java 16 – jpackage 相关 JEP: 已废弃 JEP 311: Java Packager API & CLI JEP 343: Packaging Tool (Incubator) JEP 392: Packaging Tool 这个是将 Java 程序打包成可安装包的工具,目前支持的操作系统以及格式包括: Linux: deb and rpm macOS: pkg and dmg Windows: msi and exe 可以参考这个文章试用下:Building Self-Contained, Installable Java Applications with JEP 343: Packaging Tool Java 16 - Performance 性能相关的更新有很多 Hotspot 实现 Elastic Metaspace 相关 JEP: Elastic Metaspace 原来的元空间实现中,每个类加载器占用一个单独的元空间抽象,当类加载器被回收后,这块内存被释放但是不会退还给系统而是继续给其他类加载器复用,元空间的系统内存占用只会一直增大不会缩小,也就是不会将内存退还给系统。现在优化了这一点,可以动态伸缩元空间。这块的详细源码分析,我会在之后出一期类似于 全网最硬核 TLAB 解析 的文章解析这块。 G1 和 Parallel GC 优化 如果想详细了解这块的优化,可以参考这篇文章:JDK 16 G1/Parallel GC changes ZGC 优化 相关 JEP: JEP 376: ZGC: Concurrent Thread-Stack Processing ZGC 本来已经基本将 GC 每个阶段都做成并发的了,GC 根扫描还是需要 STW。这个 JEP 优化了 GC 根扫描中的线程栈扫描,让这个扫描也可以 “半并行化” 了。这块我也会在日后进行详细的分析。 Shenandoah GC 优化 Shenandoah: should not block pacing reporters Shenandoah: reconsider free budget slice for marking Shenandoah: pacer should wait on lock instead of exponential backoff Shenandoah: support manageable SoftMaxHeapSize option Shenandoah: Concurrent weak reference processing Shenandoah: "adaptive" heuristic is prone to missing load spikes Java 16 – Security 关于安全性相关的优化,请参考:JDK 16 Security Enhancements Java 16 – Deprecations/Limitations Primitive Wrapper Warnings 相关 JEP: JEP 390: Warnings for Value-Based Classes 这是一个令人激动的更新,是为了我期待已久的 Project Valhala 做铺垫的(对,就是我之前把 Record 误会了的那个)。 目前,原始类型的封装类型类(例如 Integer )的构造器标记为了过期,并且会在将来的版本被移除,用他们里面的静态方法 valueOf() 代替。 我单独写了一篇文章来分析这个,参考:JEP解读与尝鲜系列4 - Java 16 中对于 Project Valhalla 的铺垫 Strong Encapsulation By Default 相关 JEP: JEP 396: Strongly Encapsulate JDK Internals by Default JEP 403: Strongly Encapsulate JDK Internals:Java 17 发布的,已经完全去掉了 --illegal-access,如果使用,会有 OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=warn; support was removed in 17.0 的提示。 为了推进 Java 模块化,针对 --illegal-access 的特性进行了修改。Java 16 之前默认是 permit,遇到访问没有开放的包会在第一次有提示,但是还是可以正常运行: WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by j9ms.internal.Nimbus (file:...) to constructor NimbusLookAndFeel() WARNING: Please consider reporting this to the maintainers of j9ms.internal.Nimbus WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release Java 16 则是 deny。即默认禁止非法包访问,用户可以通过启动参数 --illegal-access=permit 修改。Java 17 则是移除了这个参数,加上这个启动参数也无效了,会有提示并且反射访问内部未暴露的包会报错,例如: var dc = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); dc.setAccessible(true); 使用启动参数--illegal-access=warn 运行: OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=warn; support was removed in 17.0 Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @378bf509 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) 但是,通过启动参数 --add-opens java.base/java.lang=ALL-UNNAMED 还是可以打破封包控制.

资源下载

更多资源
Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册