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();
}

再跑一次单元测试可完美实现预期效果(温馨提示:因时间关系伪代码写的很糙,此处有极大的设计和发挥空间,后续系列中逐步展开探讨)。

结语

我的公众号《有刻》,尽量会每天更新一篇,邀请关注一波~,我们共同成长!