1. 程式人生 > >DSL 系列(1) - 擴充套件點的論述與實現

DSL 系列(1) - 擴充套件點的論述與實現

前言

DSL 全稱為 domain-specific language(領域特定語言),本系列應當會很長,其中包含些許不成熟的想法,歡迎私信指正。

1. DSL 簡述

我理解的 DSL 的主要職能是對領域的描述,他存在於領域服務之上,如下圖所示:

其實,我們也可以認為 DomainService 是 AggregateRoot 的 DSL,區別是 DomainService 表達的是更原子化的描述,下圖是我理解的更通俗的層次關係:

一句話總結:DSL 應當如同程式碼的組裝說明書,他描述了各個子域的關係及其表達流程。

2. 擴充套件點論述

擴充套件點,顧名思義其核心在於擴充套件二字,如果你的領域只表達一種形態,那沒必要關注他。但假設你的領域存在不同維度或者多種形式的表達,那擴充套件點極具價值,如下圖所示:

此時程式碼中的各個子域都成為了各種型別的標準件,而擴充套件點可以看做領域的骨架,由他限定整個域的職責(比如規定這個工廠只能生產汽車),然後由 DSL 去描述該職責有哪些表達(比如生產哪種型號的車)。

3. 擴充套件點的實現方案

3.1 效果預期

在實現功能之前,我簡單寫了以下虛擬碼:
介面:

public interface Engine {
    void launch();
}

例項 A:

@Service
public class AEngine implements Engine {
    @Override
    public void launch() {
        System.out.println("aengine launched");
    }
}

例項 B:

@Service
public class BEngine_1 implements Engine {
    @Override
    public void launch() {
        System.out.print("union 1 + ");
    }
}

@Service
public class BEngine_2 implements Engine {
    @Override
    public void launch() {
        System.out.print("union 2 +");
    }
}

@Service
public class BEngine_3 implements Engine {
    @Override
    public void launch() {
        System.out.print("union 3");
        System.out.println("bengine launched");
    }
}

測試:

public class DefaultTest {
    @Autowired
    private Engine engine;

    @Test
    public void testA() {
        // set dsl a
        engine.launch();
    }

    @Test
    public void testB() {
        // set dsl b
        engine.launch();
    }

}

我期待的結果是當 testA 執行時輸出:aengine launched,當 testB 執行時輸出:union 1 + union 2 + union 3 bengine launched

3.2 實現介面到例項的一對多路由

一對一的路由就是依賴注入,Spring 已經幫我們實現了,那怎樣實現一對多?我的想法是仿照 @Autowired ,匹配例項的那部分程式碼使用 jdk 代理進行重寫, 示例如下:
註解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExtensionNode {
}

Processor:

@Configuration
public class ETPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
        implements MergedBeanDefinitionPostProcessor, BeanFactoryAware {

    private final Log logger = LogFactory.getLog(getClass());

    private final Map<Class<?>, Constructor<?>[]> candidateConstructorsCache = new ConcurrentHashMap<>(256);

    private final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(256);

    private NodeProxy nodeProxy;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
            throw new IllegalArgumentException(
                    "ETPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
        }
        this.nodeProxy = new NodeProxy((ConfigurableListableBeanFactory) beanFactory);
    }


    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
        metadata.checkConfigMembers(beanDefinition);
    }

    @Override
    public void resetBeanDefinition(String beanName) {
        this.injectionMetadataCache.remove(beanName);
    }

    @Override
    @Nullable
    public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
            throws BeanCreationException {
        // Quick check on the concurrent map first, with minimal locking.
        Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
        if (candidateConstructors == null) {
            // Fully synchronized resolution now...
            synchronized (this.candidateConstructorsCache) {
                candidateConstructors = this.candidateConstructorsCache.get(beanClass);
                if (candidateConstructors == null) {
                    Constructor<?>[] rawCandidates;
                    try {
                        rawCandidates = beanClass.getDeclaredConstructors();
                    } catch (Throwable ex) {
                        throw new BeanCreationException(beanName,
                                "Resolution of declared constructors on bean Class [" + beanClass.getName() +
                                        "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
                    }
                    List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                    Constructor<?> requiredConstructor = null;
                    Constructor<?> defaultConstructor = null;
                    Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);
                    int nonSyntheticConstructors = 0;
                    for (Constructor<?> candidate : rawCandidates) {
                        if (!candidate.isSynthetic()) {
                            nonSyntheticConstructors++;
                        } else if (primaryConstructor != null) {
                            continue;
                        }
                        AnnotationAttributes ann = findETAnnotation(candidate);
                        if (ann == null) {
                            Class<?> userClass = ClassUtils.getUserClass(beanClass);
                            if (userClass != beanClass) {
                                try {
                                    Constructor<?> superCtor =
                                            userClass.getDeclaredConstructor(candidate.getParameterTypes());
                                    ann = findETAnnotation(superCtor);
                                } catch (NoSuchMethodException ignore) {
                                }
                            }
                        }
                        if (ann != null) {
                            if (requiredConstructor != null) {
                                throw new BeanCreationException(beanName,
                                        "Invalid autowire-marked constructor: " + candidate +
                                                ". Found constructor with 'required' ET annotation already: " +
                                                requiredConstructor);
                            }

                            requiredConstructor = candidate;

                            candidates.add(candidate);
                        } else if (candidate.getParameterCount() == 0) {
                            defaultConstructor = candidate;
                        }
                    }
                    if (!candidates.isEmpty()) {
                        // Add default constructor to list of optional constructors, as fallback.
                        candidateConstructors = candidates.toArray(new Constructor<?>[0]);
                    } else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
                        candidateConstructors = new Constructor<?>[]{rawCandidates[0]};
                    } else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&
                            defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {
                        candidateConstructors = new Constructor<?>[]{primaryConstructor, defaultConstructor};
                    } else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {
                        candidateConstructors = new Constructor<?>[]{primaryConstructor};
                    } else {
                        candidateConstructors = new Constructor<?>[0];
                    }
                    this.candidateConstructorsCache.put(beanClass, candidateConstructors);
                }
            }
        }
        return (candidateConstructors.length > 0 ? candidateConstructors : null);
    }

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        } catch (BeanCreationException ex) {
            throw ex;
        } catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of ET dependencies failed", ex);
        }
        return pvs;
    }

    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
        // Fall back to class name as cache key, for backwards compatibility with custom callers.
        String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
        // Quick check on the concurrent map first, with minimal locking.
        InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    metadata = buildAutowiringMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }
        return metadata;
    }

    private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
        List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
        Class<?> targetClass = clazz;

        do {
            final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

            ReflectionUtils.doWithLocalFields(targetClass, field -> {
                AnnotationAttributes ann = findETAnnotation(field);
                if (ann != null) {
                    if (Modifier.isStatic(field.getModifiers())) {
                        if (logger.isInfoEnabled()) {
                            logger.info("ET annotation is not supported on static fields: " + field);
                        }
                        return;
                    }
                    currElements.add(new ETPostProcessor.ETFieldElement(field));
                }
            });

            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);

        return new InjectionMetadata(clazz, elements);
    }

    @Nullable
    private AnnotationAttributes findETAnnotation(AccessibleObject ao) {
        if (ao.getAnnotations().length > 0) {
            AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ao, ExtensionNode.class);
            if (attributes != null) {
                return attributes;
            }
        }
        return null;
    }

    private class ETFieldElement extends InjectionMetadata.InjectedElement {

        ETFieldElement(Field field) {
            super(field, null);
        }

        @Override
        protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
            Field field = (Field) this.member;
            Object value = nodeProxy.getProxy(field.getType());
            if (value != null) {
                ReflectionUtils.makeAccessible(field);
                field.set(bean, value);
            }
        }
    }
}

代理:

@Configuration
public class NodeProxy implements InvocationHandler {

    private final ConfigurableListableBeanFactory beanFactory;

    public NodeProxy(ConfigurableListableBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }


    public Object getProxy(Class<?> clazz) {
        ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
        return Proxy.newProxyInstance(classLoader, new Class[]{clazz}, this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        List<Object> targetObjects = new ArrayList<>(beanFactory.getBeansOfType(method.getDeclaringClass()).values());
        Object result = null;
        for (Object object : targetObjects) {
            result = method.invoke(object, args);
        }
        return result;
    }
}

此時我們跑一下單元測試,得到:

一對多例項路由完美實現。

3.3 新增 DSL 描述

零件有了,骨架有了,最後就是怎樣給他加一張圖紙,讓擴充套件點按需表達,虛擬碼如下:

public class DslUtils {

    private static final ThreadLocal<Map<String, Class<?>>> LOCAL = new ThreadLocal<>();

    public static void setDslA() {
        Map<String, Class<?>> map = new HashMap<>();
        map.put(AEngine.class.getName(), AEngine.class);
        LOCAL.set(map);
    }

    public static void setDslB() {
        Map<String, Class<?>> map = new HashMap<>();
        map.put(BEngine_1.class.getName(), BEngine_1.class);
        map.put(BEngine_2.class.getName(), BEngine_2.class);
        map.put(BEngine_3.class.getName(), BEngine_3.class);
        LOCAL.set(map);
    }

    public static Class<?> get(String name) {
        Map<String, Class<?>> map = LOCAL.get();
        return map.get(name);
    }
}

修改代理:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    List<Object> targetObjects = new ArrayList<>(beanFactory.getBeansOfType(method.getDeclaringClass()).values());
    Object result = null;
    for (Object object : targetObjects) {
        if (DslUtils.get(getRealName(object)) != null) {
            result = method.invoke(object, args);
        }
    }
    return result;
}

private String getRealName(Object o) {
    String instanceName = o.getClass().getName();
    int index = instanceName.indexOf("$");
    if (index > 0) {
        instanceName = instanceName.substring(0, index);
    }
    return instanceName;
}

修改測試:

@ExtensionNode
private Engine engine;

@Test
public void testA() {
    DslUtils.setDslA();
    engine.launch();
}

@Test
public void testB() {
    DslUtils.setDslB();
    engine.launch();
}

再跑一次單元測試可完美實現預期效果(溫馨提示:因時間關係虛擬碼寫的很糙,此處有極大的設計和發揮空間,後續系列中逐步展開探討)。

結語

我的公眾號《有刻》,儘量會每天更新一篇,邀請關注一波~,我們共同成長!