将提交、保存、校验复杂逻辑解耦的插件框架

前言

在平时的业务开发过程中,有时候需要在保存完主表信息后,需要同步更新其他表,或者调用外组件更新等,并且还有时候需要回调操作。

正常开发的时候,就是简单快速的在操作后面, 引入其他service,然后编写逻辑即可。但是这样会导致代码耦合度高。如果有需求变更,就比较难以应对与扩展。

但是也有人说了,我就一种情况写一个方法就好了。确实是可以,那就不用往下看了。

思路

因为在项目中,我们是迭代式的开发,需求是一点一点补充上去的。所以代码就会一直堆啊堆的,最后就会非常臃肿。单单一个保存方法,可能经过四五次迭代与需求扩展。其逻辑就会非常难以理解。

这时候,就可以思考,如何因对之后的需求迭代,并且尽量减少对已有逻辑的影响。尽量符合开闭原则(能力优先,不能达到完全开闭的境界)。

因为需求扩展是对一个点进行扩展的,所以就可以理解为,我有一个扩展点(组)的确定位置。扩展肯定是不止一个内容,所以就是对这个点的实现有多个。那就很容易转换成代码,就是要一个Map<String, List<?>这样的变量来存放所有可扩展的点(组)与对应的实现了。

设计

将项目命名为plugs,就是可以插拔的用途

1 插件注解 PlugsSlot

插件注解有 @Component 可以直接当做spring的bean使用。

order是为了如果有需要依赖执行顺序的话,就添加order。为什么是float类型而不是int类型呢,是因为就怕中中途穿插了一个逻辑,不需要去修改整体的执行顺序(只需要查找到两个的顺序,然后添加小数点位数就好了,应该够用吧)

可以没有,但是需要使用 @Component 添加到spring容器中。

package com.cah.project.plugs.annotation;

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 功能描述: 插件注释,用于定位是插件类 <br/>
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
@Component
public @interface PlugsSlot {

    /** 插件名称 */
    @AliasFor(annotation = Component.class)
    String value() default "";

    /** 插件组,如果为空,则默认使用包名 */
    String group() default "";

    /** 插件执行顺序 */
    float order() default 0F;

}

2.插件接口 IPlugsSlot

isExec: 用于判断是否需要执行该插件。

入参为Object数组,方便行事,需要用的时候,再强转就好了。

package com.cah.project.plugs;

/**
 * 功能描述: 插件接口 <br/>
 */
public interface IPlugsSlot {

    /** 是否执行插件 */
    default boolean isExec(Object... objects) {
        return true;
    }

    /** 业务处理逻辑 */
    void handler(Object... objects);

    /** 执行 */
    default void exec(Object... objects) {
        if(isExec(objects)) {
            handler(objects);
        }
    }

}

3.插件存储 PlugsModel

用于扫描插件是,存储插件对象使用。模仿spring扫描的做法。

package com.cah.project.plugs;

import lombok.Data;

/**
 * 功能描述: 插件扫描存储对象 <br/>
 */
@Data
public class PlugsModel {

    /** 插件类 */
    private IPlugsSlot plugsSlot;

    /** 插件所属组 */
    private String group;

    /** 插件执行顺序 */
    private float order;

}

4.插件扫描注册 PlusAware

用于在spring启动的时候,将插件分类存放,便于拿取使用。

package com.cah.project.plugs.aware;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.cah.project.plugs.IPlugsSlot;
import com.cah.project.plugs.PlugsModel;
import com.cah.project.plugs.annotation.PlugsSlot;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 功能描述: 插件扫描注册类 <br/>
 */
@Component
public class PlusAware implements ApplicationContextAware {

    private final Map<String, List<PlugsModel>> plugsMap = new HashMap<>();

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        PlusAware.applicationContext = applicationContext;
        Map<String, IPlugsSlot> beanMap = applicationContext.getBeansOfType(IPlugsSlot.class);
        Set<Map.Entry<String, IPlugsSlot>> entries = beanMap.entrySet();
        if(CollUtil.isNotEmpty(entries)) {
            List<PlugsModel> list = new ArrayList<>(beanMap.size());
            for (Map.Entry<String, IPlugsSlot> entry : entries) {
                // 获取注解,如果没有插件注解,则使用约定和默认值
                PlugsSlot annotation = entry.getValue().getClass().getAnnotation(PlugsSlot.class);
                PlugsModel pm = new PlugsModel();
                pm.setPlugsSlot(entry.getValue());
                pm.setGroup(entry.getValue().getClass().getPackage().getName());
                if(annotation == null) {
                    pm.setOrder(0F);
                } else {
                    String group = annotation.group();
                    if(StrUtil.isNotBlank(group)) {
                        // 如果组别为空,则默认为包名。使代码分包规范,不会乱跑。使用时,随便指定一个类获取包名即可
                        pm.setGroup(group);
                    }
                    pm.setOrder(annotation.order());
                }
                list.add(pm);
            }
            // 最终排序后,添加到map中
            plugsMap.putAll(list.stream()
                    .sorted(Comparator.comparing(PlugsModel::getOrder))
                    .collect(Collectors.groupingBy(PlugsModel::getGroup)));
        }
    }

    public static PlusAware getPlusAware() {
        return applicationContext.getBean(PlusAware.class);
    }

    public List<PlugsModel> getPlugsModel(String group) {
        return plugsMap.getOrDefault(group, new ArrayList<>());
    }

}

5.插件上下文

对外开放的使用类

package com.cah.project;

import cn.hutool.core.collection.CollUtil;
import com.cah.project.plugs.PlugsModel;
import com.cah.project.plugs.aware.PlusAware;

import java.util.List;

/**
 * 功能描述: 插件执行上下文 <br/>
 */
public class PlugsContext {

    /** 执行插件 */
    public static void exec(String group, Object... objects) {
        List<PlugsModel> list = PlusAware.getPlusAware().getPlugsModel(group);
        if(CollUtil.isNotEmpty(list)) {
            list.forEach(p -> p.getPlugsSlot().exec(objects));
        }
    }

}

使用

再使用插件的过程中,我们通过包名,将插件进行归档。方便定位和排查问题。

1.插件分组常量类

将分组常量放在一个类里面,方便使用。

package com.cah.project.module.meta.service.plugs;

/**
 * 功能描述: 元数据插件常量管理 <br/>
 */
public class MetaPlugsGroup {

    public static final String DATA_SOURCE = "dataSource";

}

2.数据源插件 dataSource

都放在 plugs.dataSource 这个包下面。

这里使用了@PlugsSlot注解,并且group通过常量来指定,对于后续使用比较方便。

而且重写了isExec执行判断条件,再满足条件的时候才执行。

package com.cah.project.module.meta.service.plugs.dataSource;

import com.cah.project.module.meta.service.plugs.MetaPlugsGroup;
import com.cah.project.plugs.IPlugsSlot;
import com.cah.project.plugs.annotation.PlugsSlot;

/**
 * 功能描述: 数据源插件1 <br/>
 */
@PlugsSlot(group = MetaPlugsGroup.DATA_SOURCE, order = 1f)
public class DataSourcePlug1 implements IPlugsSlot {

    @Override
    public boolean isExec(Object... objects) {
        // 重写判断,是否需要执行handler
        return "123".equals(objects[0]);
    }

    @Override
    public void handler(Object... objects) {
        System.out.println("执行处理逻辑1--->" + objects[0]);
    }

}
package com.cah.project.module.meta.service.plugs.dataSource;

import com.cah.project.module.meta.service.plugs.MetaPlugsGroup;
import com.cah.project.plugs.IPlugsSlot;
import com.cah.project.plugs.annotation.PlugsSlot;

/**
 * 功能描述: 数据源插件2 <br/>
 */
@PlugsSlot(group = MetaPlugsGroup.DATA_SOURCE, order = 0.9f)
public class DataSourcePlug2 implements IPlugsSlot {

    @Override
    public void handler(Object... objects) {
        System.out.println("执行处理逻辑2--->" + objects[0]);
    }

}

3.表管理插件 tableManage

都放在 plugs.tableManage 这个包下面。

这里就没有定义 group,测试默认使用包名分组的情况。

package com.cah.project.module.meta.service.plugs.tableManage;

import com.cah.project.plugs.IPlugsSlot;
import com.cah.project.plugs.annotation.PlugsSlot;

/**
 * 功能描述: 数据源插件1 <br/>
 */
@PlugsSlot(order = 1f)
public class TableManageDataSourcePlug implements IPlugsSlot {

    @Override
    public void handler(Object... objects) {
        System.out.println(this.getClass());
    }

}
package com.cah.project.module.meta.service.plugs.tableManage;

import com.cah.project.plugs.IPlugsSlot;
import org.springframework.stereotype.Component;

/**
 * 功能描述: 数据源插件1 <br/>
 */
@Component
public class TableManageDataSourcePlug2 implements IPlugsSlot {

    @Override
    public void handler(Object... objects) {
        System.out.println(this.getClass());
    }

}

由于没有加入到spring容器中,所以不会被扫描到。

package com.cah.project.module.meta.service.plugs.tableManage;

import com.cah.project.plugs.IPlugsSlot;

/**
 * 功能描述: 数据源插件1 <br/>
 */
public class TableManageDataSourcePlug3 implements IPlugsSlot {

    @Override
    public void handler(Object... objects) {
        System.out.println(this.getClass());
    }

}

4.执行测试

对于以上几种场景的测试代码如下,简单易懂。

package com.cah.project.module;

import com.cah.project.BaseTest;
import com.cah.project.PlugsContext;
import com.cah.project.module.meta.service.plugs.MetaPlugsGroup;
import com.cah.project.module.meta.service.plugs.tableManage.TableManageDataSourcePlug;
import org.junit.Test;

public class PlugsTest extends BaseTest {

    @Test
    public void test() {
        PlugsContext.exec("dataSource", "123");
        System.out.println("------------------------------------\n");
        PlugsContext.exec(MetaPlugsGroup.DATA_SOURCE, 456);
        System.out.println("------------------------------------\n");
        PlugsContext.exec(TableManageDataSourcePlug.class.getPackage().getName(), 123);
    }

}

5.执行结果

完全符合预期结果。

执行处理逻辑2--->123
执行处理逻辑1--->123
------------------------------------

执行处理逻辑2--->456
------------------------------------

class com.cah.project.module.meta.service.plugs.tableManage.TableManageDataSourcePlug2
class com.cah.project.module.meta.service.plugs.tableManage.TableManageDataSourcePlug

代码

测试插件

测试类

插件上下文

插件

总结

一个简单的框架,就能够实现部分功能的解耦。并且可以类推到其他场景中。

感谢关注(微信小程序)

柒宅mini二维码.jpg

猜你喜欢

转载自juejin.im/post/7127468498842255374
今日推荐