Java的SPI实现解耦

概述

SPI的全称是服务提供接口,可以用其来启动框架的扩展和替换组件。

其本质是利用 接口实现+策略模式+配置文件来实现对实现类的动态加载。

在具体的使用中,存在一些约定:

(1)规定在 classPath 的 META-INF/services/ 下,创建该接口的全名称文件

(2)在该文件中,写入该接口实现类全称(路径+文件名),多个实现类的话,分行写。

(3)用的2时候,使用 java.util.ServiceLoader 的 load(Interface.class),获取到实现类,就可以使用了。

值得注意的是,接口实现类必须有一个不带参数的构造方法。

实现案例

在本应用中,存在两个模块,分别为A模块和B模块,这两个模块中,A模块是主模块,B是从模块,B模块是依赖A模块的。但是在目前有一个类,该类中实现在B模块中,A模块需要调用这个类的函数,而模块不能再依赖B模块,此时需要进行解耦。在本实现中,利用SPI的方式进行解耦实现。具体实现方案为:

(1)在A模块新建一个接口:MyLogAppender,具体实现为:


/**
 * @author Huang gen(kenfeng)
 * @description 自定义的appender接口
 * @Since 2021/02/21
 **/

public interface MyLogAppender {

    /**
     * 获取实现的appender
     * @return  返回新建的appender对象
     * */
    Appender getAppender();
}
复制代码

这个接口很简单,只是返回一个appender的对象。对于对象的实际操作,在接口的实现中进行操作。

(2)在B模块添加对这个接口的实现,具体的操作为:


/**
 * @author Huang gen(kenfeng)
 * @description 自定义的appender
 * @Since 2021/02/21
 **/
@Component
public class MeshLogAppender extends UnsynchronizedAppenderBase<ILoggingEvent> implements MyLogAppender,ApplicationContextAware {

    private ApplicationContext applicationContext;

    public MeshLogAppender(){ }

    @Override
    public Appender getAppender() {
        MeshLogAppender meshLogAppender = new MeshLogAppender();
        return meshLogAppender;
    }

    @Override
    protected void append(ILoggingEvent iLoggingEvent) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String std = simpleDateFormat.format(new Date(Long.parseLong(String.valueOf(iLoggingEvent.getTimeStamp()))));
        String log = std + "\t" + iLoggingEvent.getLevel() +"\t"+"--- ["+ iLoggingEvent.getThreadName()+"]\t"+iLoggingEvent.getCallerData()[0]+":\t "+iLoggingEvent.getMessage();
        FlowMessage input = new FlowMessage();
        MeshFlowService meshFlowService = SandboxSystemServiceFactory.getService(MeshFlowService.class);
        Map<String, Object> body = new HashMap<>(2);
        body.put("log",log);
        input.setTenantCode(DefaultTenant.get());
        input.setAppCode("epoch");
        input.setFlowCode("log_broadcast");
        input.setBody(body);
        FlowMessage output = meshFlowService.process(input);
        if(!StringUtils.isEmpty(output.getErrorMessage())){
            throw new RuntimeException("发布日志时,广播失败");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
复制代码

在该接口的申明和接口的实现中,存在一些小的技巧的实现。在接口中,只声明一个类的获取,并没有实现具体的方法。在实现类中,对这个类进行实例化,new一个新的类并返回,此时用户根据这个get方法就可以拿到这个实现类,然后进行实现类的一些操作。这样写可以带来两个好处: i. 代码更简洁,接口的代码简单易懂 ii. 可以在实现类的构造方法中注入一些参数,当用户使用时,直接在get方法里面注入即可。

(3) 在实现类所在文件夹下,也就是sandbox-app-epoch-starter中添加一个配置文件,其配置文件的路径默认为: resources/META-INF/services/,在这个文件夹下新建一个问题,文件名为接口的路径,内容是实现类的路径。由此可以实现接口-->实现类的映射。

如上图中,文件名为:com.alibaba.halo.sandbox.app.util.MyLogAppender

其文件中的内容为:com.alibaba.lattice2.epoch.util.MeshLogAppender

其原理是,当用户使用接口时,会扫描项目下的所有文件,查找文件名为com.alibaba.halo.sandbox.app.util.MyLogAppender,然后根据其内容来查找到相关的实现类

(4)在A,可以直接使用接口来进行调用,具体实现如下:

				ServiceLoader<MyLogAppender> myLoaderInterfaceServiceLoader = ServiceLoader.load(MyLogAppender.class);
        Iterator<MyLogAppender> myLoaderInterfaceIterator = myLoaderInterfaceServiceLoader.iterator();
        while (myLoaderInterfaceIterator.hasNext()){
            MyLogAppender myLoaderInterface = myLoaderInterfaceIterator.next();

            Appender newAppender = myLoaderInterface.getAppender();
            newAppender.setName("application");
            newAppender.setContext(loggerContext);
            newAppender.start();
            rootLogger.addAppender(newAppender);
        }
复制代码

从上面可以看到,其可以直接调用MyLogAppender接口,利用这个接口获取的Appender,之后直接赋值即可。

优势和不足

优点:可以实现代码的解耦

缺点:存在多个实现类的话,无法根据某个参数或者标志位获取实例,只能通过遍历获取,没有实现所谓的懒加载

猜你喜欢

转载自juejin.im/post/7218866717739974711