Análisis del uso y principios de los puntos de extensión SPI en los negocios | Equipo Técnico de JD Logistics

1 ¿Qué es SPI?

El nombre completo de SPI es Interfaz de proveedor de servicios. En la programación orientada a interfaces, abstraeremos diferentes interfaces según diferentes negocios y luego estableceremos clases con diferentes reglas según diferentes implementaciones comerciales. Por lo tanto, una interfaz implementará múltiples clases de implementación. Durante el proceso de llamada específico, la clase de implementación correspondiente es especificado., cuando el negocio cambia, se agregará una nueva clase de implementación o una clase existente quedará obsoleta y será necesario cambiar el código de llamada, lo cual es intrusivo hasta cierto punto.
El diagrama general del mecanismo es el siguiente:

Java SPI es en realidad un mecanismo de carga dinámica implementado mediante una combinación de "programación basada en interfaz + modo de estrategia + archivo de configuración".

2 Uso de SPI en el negocio de Jingxi

2.1 Introducción

En la actualidad, la cooperación entre el centro de almacenamiento y Jingxi BP se realiza principalmente a través de los puntos de expansión de SPI. La ventaja es que está cerrado a modificaciones y abierto a expansión. La oficina central no necesita preocuparse por los detalles de implementación comercial de BP y puede lograr la personalización configurando las interfaces de los puntos de extensión para diferentes BP. En la actualidad, Jingxi BP proporciona principalmente dos métodos de implementación de interfaz, uno es el método del paquete jar y el otro es la interfaz jsf.
La definición e implementación de los dos métodos se presentan a continuación.

2.2 método de paquete jar

2.2.1 Descripción y ejemplos

La interfaz del punto de extensión hereda IDomainExtension. Esta interfaz es una interfaz de complemento en el paquete dddplus. La clase de implementación debe usar la anotación Extensión (io.github.dddplus.annotation) para marcar la parte comercial de BP y el nombre de identificación de la interfaz para una distinción personalizada. lograr.
Tomando como ejemplo el punto de extensión de inventario en la biblioteca, la interfaz se define en el jar proporcionado por la persona que llama y se define de la siguiente manera:

public interface IProfitLossEnrichExt extends IDomainExtension {
    @Valid
    @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail.putExtendAttr(key, value)"})
    List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> var1);
}

La clase de implementación se define en el jar del proveedor de servicios de la siguiente manera:

实现类:/**
 * ProfitLossEnrichExtImpl
 * 批量盘盈亏数据丰富扩展
 *
 * @author jiayongqiang6
 * @date 2021-10-15 11:30
 */
@Extension(code = IPartnerIdentity.JX_CODE, value = "jxProfitLossEnrichExt")
@Slf4j
public class ProfitLossEnrichExtImpl implements IProfitLossEnrichExt {
    private SkuInfoQueryService skuInfoQueryService;

    @Override
    public @Valid @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail" +
            ".putExtendAttr(key, value)"}) List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> list) {
        ...
        return list;
    }

    @Autowired
    public void setSkuInfoQueryService(SkuInfoQueryService skuInfoQueryService) {
        this.skuInfoQueryService = skuInfoQueryService;
    }
}

Esta clase de implementación dependerá del servicio jsf SkuQueryService de los datos principales, y SkuInfoQueryService realiza llamadas de encapsulación rpc a SkuQueryService. Inyectado a través de Autowired, el consumidor debe definirse en el archivo xml, que es el mismo que el consumidor jsf que presentamos habitualmente. Un ejemplo es el siguiente: jx/spring-jsf-consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jsf="http://jsf.jd.com/schema/jsf"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://jsf.jd.com/schema/jsf
       http://jsf.jd.com/schema/jsf/jsf.xsd"
       default-lazy-init="false" default-autowire="byName">
    <jsf:consumer id="skuQueryService" interface="com.jdwl.wms.masterdata.api.sku.SkuQueryService"
                  alias="${jsf.consumer.masterdata.alias}" protocol="jsf" check="false" timeout="10000"  retries="3"/>
</beans>

El usuario del paquete jar puede cargar directamente el archivo de recursos del consumidor o agregar directamente los servicios dependientes al directorio del proyecto manualmente. El primer método es más conveniente, pero puede causar conflictos fácilmente, mientras que el segundo método, aunque problemático, puede evitar conflictos.

2.2.2 Prueba de puntos de extensión

Debido a que el punto de extensión depende de la relación de Jeff, debe agregar la configuración del centro de registro y la configuración relacionada del servicio dependiente en el archivo de configuración. Un ejemplo es el siguiente: application-config.properties

jsf.consumer.masterdata.alias=wms6-test
jsf.registry.index=i.jsf.jd.com

Al cargar el archivo de recursos del consumidor y el archivo de configuración en la prueba unitaria y cargar todas las dependencias relevantes, se puede lograr la prueba directa de la interfaz. Como se muestra en el siguiente código:

package com.zhongyouex.wms.spi.inventory;

import com.alibaba.fastjson.JSON;
import com.jdwl.wms.inventory.spi.difference.entity.ProfitLossBatchDetailExt;
import com.zhongyouex.wms.spi.inventory.service.SkuInfoQueryService;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:jx/spring-jsf-consumer.xml"})
@PropertySource(value = {"classpath:application-config.properties"})
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = {"com.zhongyouex.wms"})
public class ProfitLossEnrichExtImplTest {
    @Resource
    SkuInfoQueryService skuInfoQueryService;

    ProfitLossEnrichExtImpl profitLossEnrichExtImpl = new ProfitLossEnrichExtImpl();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testEnrich() throws Exception {
        profitLossEnrichExtImpl.setSkuInfoQueryService(skuInfoQueryService);
        ProfitLossBatchDetailExt ext = new ProfitLossBatchDetailExt();
        ext.setSku("100008483105");
        ext.setWarehouseNo("6_6_618");
        ProfitLossBatchDetailExt ext1 = new ProfitLossBatchDetailExt();
        ext1.setSku("100009847591");
        ext1.setWarehouseNo("6_6_618");
        List<ProfitLossBatchDetailExt> list = new ArrayList<>();
        list.add(ext);
        list.add(ext1);
        profitLossEnrichExtImpl.enrich(list);
        System.out.write(JSON.toJSONBytes(list));
    }
}

//Generated with love by TestMe :) Please report issues and submit feature requests at: http://weirddev.com/forum#!/testme

2.3 método de interfaz jsf

La implementación del punto de extensión en el método jsf es la misma que la del método del paquete jar, la diferencia es que este método no necesita depender del jar implementado por el proveedor de servicios y no necesita cargar clases de implementación específicas. Identifique el punto de extensión y llame al punto de extensión configurando el alias de Jeff de la interfaz jsf.

3 análisis del principio SPI

3.1dddplus

ExtensionDef en el paquete dddplus-runtime se usa principalmente para cargar beans de punto de extensión en InternalIndexer:

public void prepare(@NotNull Object bean) {
    this.initialize(bean);
    InternalIndexer.prepare(this);
}

private void initialize(Object bean) {
    Extension extension = (Extension)InternalAopUtils.getAnnotation(bean, Extension.class);
    this.code = extension.code();
    this.name = extension.name();
    if (!(bean instanceof IDomainExtension)) {
        throw BootstrapException.ofMessage(new String[]{bean.getClass().getCanonicalName(), " MUST implement IDomainExtension"});
    } else {
        this.extensionBean = (IDomainExtension)bean;
        Class[] var3 = InternalAopUtils.getTarget(this.extensionBean).getClass().getInterfaces();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Class extensionBeanInterfaceClazz = var3[var5];
            if (extensionBeanInterfaceClazz.isInstance(this.extensionBean)) {
                this.extClazz = extensionBeanInterfaceClazz;
                log.debug("{} has ext instance:{}", this.extClazz.getCanonicalName(), this);
                break;
            }
        }

    }
}

3.2 java spi

A través de la simple demostración anterior, podemos ver que la implementación más crítica es la clase ServiceLoader. Puede echar un vistazo al código fuente de esta clase, de la siguiente manera:

public final class ServiceLoader<S> implements Iterable<S> {
 2 3 4     //扫描目录前缀 5     private static final String PREFIX = "META-INF/services/";
 6 7     // 被加载的类或接口 8     private final Class<S> service;
 910     // 用于定位、加载和实例化实现方实现的类的类加载器11     private final ClassLoader loader;
1213     // 上下文对象14     private final AccessControlContext acc;
1516     // 按照实例化的顺序缓存已经实例化的类17     private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
1819     // 懒查找迭代器20     private java.util.ServiceLoader.LazyIterator lookupIterator;
2122     // 私有内部类,提供对所有的service的类的加载与实例化23     private class LazyIterator implements Iterator<S> {
24         Class<S> service;
25         ClassLoader loader;
26         Enumeration<URL> configs = null;
27         String nextName = null;
2829         //...30         private boolean hasNextService() {
31             if (configs == null) {
32                 try {
33                     //获取目录下所有的类34                     String fullName = PREFIX + service.getName();
35                     if (loader == null)
36                         configs = ClassLoader.getSystemResources(fullName);
37                     else38                         configs = loader.getResources(fullName);
39                 } catch (IOException x) {
40                     //...41                 }
42                 //....43             }
44         }
4546         private S nextService() {
47             String cn = nextName;
48             nextName = null;
49             Class<?> c = null;
50             try {
51                 //反射加载类52                 c = Class.forName(cn, false, loader);
53             } catch (ClassNotFoundException x) {
54             }
55             try {
56                 //实例化57                 S p = service.cast(c.newInstance());
58                 //放进缓存59                 providers.put(cn, p);
60                 return p;
61             } catch (Throwable x) {
62                 //..63             }
64             //..65         }
66     }
67 }

El código anterior solo publica algunas implementaciones clave. Los lectores interesados ​​pueden estudiarlo por sí mismos. El proceso principal más intuitivo de carga de spi se publica a continuación como referencia:

4 Resumen

Los dos métodos para proporcionar SPI tienen sus propias ventajas y desventajas: el método del paquete jar tiene un bajo costo de implementación y muchas dependencias, lo que aumenta el costo de configuración de la persona que llama; el método de la interfaz jsf tiene un alto costo de implementación, pero la persona que llama tiene pocas dependencias y Solo necesita identificar diferentes BP a través de alias.

Resuma los beneficios que puede aportar spi:

  • La extensión y el desacoplamiento se pueden lograr sin cambiar el código fuente.
  • La implementación de extensiones apenas es intrusiva para el código original.
  • Solo necesita agregar configuraciones para lograr la expansión, lo cual es consistente con el principio de apertura y cierre.

Autor: JD Logística Jia Yongqiang

Fuente: Comunidad de desarrolladores de JD Cloud Ziyuanqishuo Tech Indique la fuente al reimprimir

Lanzamiento oficial de Spring Boot 3.2.0. La falla de servicio más grave en la historia de Didi. ¿El culpable es el software subyacente o "la reducción de costos y el aumento de las risas"? Los programadores manipularon los saldos de ETC y malversaron más de 2,6 millones de yuanes al año. Los empleados de Google criticaron al gran jefe después de dejar sus trabajos. Estuvieron profundamente involucrados en el proyecto Flutter y formularon estándares relacionados con HTML. Microsoft Copilot Web AI se lanzará oficialmente el 1 de diciembre, compatible con PHP 8.3 chino GA Firefox en 2023. El marco web Rust Rocket se ha vuelto más rápido y lanzó v0.5: admite asíncrono, SSE, WebSockets, etc. Se lanza oficialmente el procesador de escritorio Loongson 3A6000, ¡la luz de la producción nacional! Broadcom anuncia exitosa adquisición de VMware
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10313986
Recomendado
Clasificación