SpringBoot-插件化以及springboot扩展接口

news2024/11/16 23:36:28

插件化常用的实现思路

  • spi机制,Service Provider Interface ,是JDK内置的一种服务发现机制,SPI是一种动态替换扩展机制
  • 约定配置和目录,利用反射配合实现
  • springboot中的Factories机制
  • Java agent(探针)技术
  • Spring内置扩展点
  • 第三方插件包,例如:spring-plugin-core
  • spring aop技术

SPI实现插件化案例

一、目录结构如下
在这里插入图片描述

二、自定义接口及其实现类

/**
	接口
*/
public interface MessagePlugin{
	
	public String sendMsg(Map msgMap);
}
/**
	AliyunMsg实现类
*/
public class AliyunMsg implements MessagePlugin{
	
	@Override
	public String sendMsg(Map msgMap){
		System.out.println("aliyun sendMsg");
		return "aliyun sendMsg";
	}
}
/**
	TencentMsg实现类
*/
public class TencentMsg implements MessagePlugin {
 
    @Override
    public String sendMsg(Map msgMap) {
        System.out.println("tencent sendMsg");
        return "tencent sendMsg";
    }
}

三、在resources目录下按照规范要求创建文件目录,并填写实现类的全类名

文件目录规范为接口所在的包全类名。
在这里插入图片描述

四、自定义服务加载类

使用ServiceLoader的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。

public static void main(String[] args){
	
	ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
	
	Interator<MessagePlugin> iterator = serviceLoader.iterator();
	Map map = new HashMap();
	while(iterator.hasNext()){
		MessagePlugin messagePlugin = iterator.next();
		messagePlugin.sendMsg(map);
	}
}

注意:serviceloader有缺陷,使用中必须在META-INF里定义接口名称的文件,在文件中才能写上实现类的类名。 如果一个项目里插件化的东西太多,可能会出现越来越多配置文件。

所以对上面的缺陷调整

1、配置文件中,将具体的实现类配置进去

server:
	port: 8081
	
impl:
	name: com.michael.plugins.spi.MessagePlugin
	clazz: 
		- com.michael.plugins.impl.TencentMsg
		- com.michale.plugins.impl.AliyunMsg
	

2、自定义配置文件加载类

通过该类,将上述配置文件中的实现类封装到类对象中,方便后续使用

@ConfigurationProperties("impl")
@ToString
public class ClassImpl{
	
	@Getter
    @Setter
    String name;
 
    @Getter
    @Setter
    String[] clazz;
}

3、自定义测试接口

@RestController
public class SendMsgController{
	
	@Autowired
	ClassImpl classImpl;

	@GetMapping("/sendMsg")
	public String sendMsg() throws Exception{
		
		for(int i =0;i < classImpl.getClazz().length; i++){
			
			Class pluginClass = Class.forName(classImpl.getClazz()[i]);
			MessagePlugin messagePlugin = (MessagePlugin) pluginClass.newInstance();
			messagePlugin.sendMsg(new HashMap());
		}
		return "success";
	}
}

4、启动类

@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public class PluginApp{
	
	public static void main(String[] args) {
        SpringApplication.run(PluginApp.class,args);
    }
}

启动工程代码后,调用接口:localhost:8081/sendMsg,在控制台中可以看到下面的输出信息,即通过这种方式也可以实现类似serviceloader的方式,不过在实际使用时,可以结合配置参数进行灵活的控制;
在这里插入图片描述

在很多场景下,可能我们并不想直接在工程中引入接口实现的依赖包,这时候可以考虑通过读取指定目录下的依赖jar的方式,利用反射的方式进行动态加载,这也是生产中一种比较常用的实践经验

继续调整

1、创建约定目录

在当前工程下创建一个lib目录,并将依赖jar放进去
在这里插入图片描述

2、新增读取jar的工具类

添加一个工具类,用于读取指定目录下的jar,通过反射的方式,结合配置文件中的约定配置进行反射方法的执行

@Component
public class ServiceLoaderUtils{
	
	@Autowired
	ClassImpl classImpl;

	public static void loadJarFromAppFolder() throws Exception{
		
		String path = "E:\\code-self\\bitzpp\\lib";
		File f = new File(path);

		if(f.isDirectory()){
			for(File subf:f.listFiles()){
				loadJarFile(subf);
			}
		}else{
			loadJarFile(f);
		}
	}

	public static void loadJarFile(File path) throws Exception{
		
		URL url = path.toURI().toURL();

		URLClassLoader classLoader = (URLClassLoader).ClassLoader.getSystemClassLoader();
		//加载
		//Method method = URLClassLoader.class.getDeclareMethod("sendMsg",Map.class);
		Method method = URLClassLoader.class.getMethod("sendMsg",Map.class);
	
		method.setAccessible(true);
		method.invoke(classLoader,url);
	}

	 public  void main(String[] args) throws Exception{
        System.out.println(invokeMethod("hello"));;
    }
 
    public String doExecuteMethod() throws Exception{
        String path = "E:\\code-self\\bitzpp\\lib";
        File f1 = new File(path);
        Object result = null;
        if (f1.isDirectory()) {
            for (File subf : f1.listFiles()) {
                //获取文件名称
                String name = subf.getName();
                String fullPath = path + "\\" + name;
                //执行反射相关的方法
                //ServiceLoaderUtils serviceLoaderUtils = new ServiceLoaderUtils();
                //result = serviceLoaderUtils.loadMethod(fullPath);
                File f = new File(fullPath);
                URL urlB = f.toURI().toURL();
                URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
                        .getContextClassLoader());
                String[] clazz = classImpl.getClazz();
                for(String claName : clazz){
                    if(name.equals("biz-pt-1.0-SNAPSHOT.jar")){
                        if(!claName.equals("com.congge.spi.BitptImpl")){
                            continue;
                        }
                        Class<?> loadClass = classLoaderA.loadClass(claName);
                        if(Objects.isNull(loadClass)){
                            continue;
                        }
                        //获取实例
                        Object obj = loadClass.newInstance();
                        Map map = new HashMap();
                        //获取方法
                        Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
                        result = method.invoke(obj,map);
                        if(Objects.nonNull(result)){
                            break;
                        }
                    }else if(name.equals("miz-pt-1.0-SNAPSHOT.jar")){
                        if(!claName.equals("com.congge.spi.MizptImpl")){
                            continue;
                        }
                        Class<?> loadClass = classLoaderA.loadClass(claName);
                        if(Objects.isNull(loadClass)){
                            continue;
                        }
                        //获取实例
                        Object obj = loadClass.newInstance();
                        Map map = new HashMap();
                        //获取方法
                        Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
                        result = method.invoke(obj,map);
                        if(Objects.nonNull(result)){
                            break;
                        }
                    }
                }
                if(Objects.nonNull(result)){
                    break;
                }
            }
        }
        return result.toString();
    }
 
    public Object loadMethod(String fullPath) throws Exception{
        File f = new File(fullPath);
        URL urlB = f.toURI().toURL();
        URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
                .getContextClassLoader());
        Object result = null;
        String[] clazz = classImpl.getClazz();
        for(String claName : clazz){
            Class<?> loadClass = classLoaderA.loadClass(claName);
            if(Objects.isNull(loadClass)){
                continue;
            }
            //获取实例
            Object obj = loadClass.newInstance();
            Map map = new HashMap();
            //获取方法
            Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
            result = method.invoke(obj,map);
            if(Objects.nonNull(result)){
                break;
            }
        }
        return result;
    }

	 public static String invokeMethod(String text) throws Exception{
        String path = "E:\\code-self\\bitzpp\\lib\\miz-pt-1.0-SNAPSHOT.jar";
        File f = new File(path);
        URL urlB = f.toURI().toURL();
        URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
                .getContextClassLoader());
        Class<?> product = classLoaderA.loadClass("com.congge.spi.MizptImpl");
        //获取实例
        Object obj = product.newInstance();
        Map map = new HashMap();
        //获取方法
        Method method=product.getDeclaredMethod("sendMsg",Map.class);
        //执行方法
        Object result1 = method.invoke(obj,map);
        // TODO According to the requirements , write the implementation code.
        return result1.toString();
    }
 
    public static String getApplicationFolder() {
        String path = ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        return new File(path).getParent();
    }
 
}

3、添加测试接口

@GetMapping("/sendMsgV2")
public String index() throws Exception{

	String result = serviceLoaderUtils.doExecuteMethod();
	return result;
}

以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;
在这里插入图片描述

SpringBoot插件化实现

其实框架自身提供了非常多的扩展点,其中最适合做插件扩展的莫过于spring.factories的实现;

在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化,这种自定义的SPI机制是Spring Boot Starter实现的基础。

spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:

  • loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表;
  • loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表;

上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    try {
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String factoryClassNames = properties.getProperty(factoryClassName);
            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
        }
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件,就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:

com.xxx.interface=com.xxx.classname

如果一个接口希望配置多个实现类,可以使用’,’进行分割

一、定义一个服务接口以及两个服务实现类

public interface SmsPlugin{
	
	public void sendMessage(String message);
}
public class BizSmsImpl implements SmsPlugin {
 
    @Override
    public void sendMessage(String message) {
        System.out.println("this is BizSmsImpl sendMessage..." + message);
    }
}
public class SystemSmsImpl implements SmsPlugin {
 
    @Override
    public void sendMessage(String message) {
        System.out.println("this is SystemSmsImpl sendMessage..." + message);
    }
}

二、添加spring.factories文件

在resources目录下,创建一个名叫:META-INF的目录,然后在该目录下定义一个spring.factories的配置文件,内容如下,其实就是配置了服务接口,以及两个实现类的全类名的路径;

com.congge.plugin.spi.SmsPlugin=\
com.congge.plugin.impl.SystemSmsImpl,\
com.congge.plugin.impl.BizSmsImpl

三、添加自定义接口

这里和java 的spi有点类似,只不过是这里换成了SpringFactoriesLoader去加载服务;

@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception{
	
	List<SmsPlugin> smsServices = SpringFactoriesLoader.loadFactories();
	for(SmsPlugin smsService : smsServices){
        smsService.sendMessage(msg);
    }
    return "success";
}

启动工程之后,调用一下该接口进行测试,localhost:8087/sendMsgV3?msg=hello,通过控制台,可以看到,这种方式能够正确获取到系统中可用的服务实现;
在这里插入图片描述
利用spring的这种机制,可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现;

SpringBoot扩展接口

可扩展接口的启动调用顺序
在这里插入图片描述

一、ApplicationContextInitializer

org.springframework.context.ApplicationContextInitializer
这是整个spring容器在刷新之前初始化ConfigurableApplicationContext的回调接口

容器刷新之前调用此类的initialize方法。这个点允许被用户自己扩展。用户可以在整个spring容器还没被初始化之前做一些事情。可以想到的场景可能为,在最开始激活一些配置,或者利用这时候class还没被类加载器加载的时机,进行动态字节码注入等操作。

扩展方式:此时spring容器还没被初始化,所以想要自己扩展有三种方式

  • 在启动类中用springApplication.addInitializers(new TestApplicationContextInitializer())加入
  • 配置文件中context.initializer.classes=com.example.demo.TestApplicationContextInitializer
  • Spring SPI扩展,在spring.factories中加入org.springframework.context.ApplicationContextInitializer=com.example.demo.TestApplicationContextInitializer
public class TestApplicationContextInitializer implements ApplicationContextInitializer{
	
	@Override
	public void initialize(ConfigurableApplicationContext applicationContext){
		System.out.println("[ApplicationContextInitializer]");
	}
}

二、BeanDefinitionRegistryPostProcessor

org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
这个接口在读取项目中的beanDefinition之后执行,提供一个补充的扩展点

使用场景:你可以在这里动态注册自己的beanDefinition,可以加载classpath之外的bean

public class TestBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {      
    @Override      
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {      
        System.out.println("[BeanDefinitionRegistryPostProcessor] postProcessBeanDefinitionRegistry");      
    }      
      
    @Override      
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {      
        System.out.println("[BeanDefinitionRegistryPostProcessor] postProcessBeanFactory");      
    }      
}      

三、BeanFactoryPostProcessor

org.springframework.beans.factory.config.BeanFactoryPostProcessor

这个接口是beanFactory的扩展接口,调用时机在spring在读取beanDefinition信息之后,实例化bean之前
用户可以通过实现这个扩展接口来自行处理一些东西,比如修改已经注册的beanDefinition的元信息。

public class TestBeanFactoryPostProcessor implements BeanFactoryPostProcessor {      
    @Override      
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {      
        System.out.println("[BeanFactoryPostProcessor]");      
    }      
}      

四、InstantiationAwareBeanPostProcessor

org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor

该接口继承了BeanPostProcess接口,区别如下:
BeanPostProcess接口只在bean的初始化阶段进行扩展(注入spring上下文前后)
InstantiationAwareBeanPostProcessor接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段。

  • postProcessBeforeInstantiation:实例化bean之前,相当于new这个bean之前
  • postProcessAfterInstantiation:实例化bean之后,相当于new这个bean之后
  • postProcessPropertyValues:bean已经实例化完成,在属性注入时阶段触发,@Autowired,@Resource等注解原理基于此方法实现
  • postProcessBeforeInitialization:初始化bean之前,相当于把bean注入spring上下文之前
  • postProcessAfterInitialization:初始化bean之后,相当于把bean注入spring上下文之后

使用场景:这个扩展点非常有用 ,无论是写中间件和业务中,都能利用这个特性。比如对实现了某一类接口的bean在各个生命期间进行收集,或者对某个类型的bean进行统一的设值等等。

public class TestInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {      
      
    @Override      
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {      
        System.out.println("[TestInstantiationAwareBeanPostProcessor] before initialization " + beanName);      
        return bean;      
    }      
      
    @Override      
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {      
        System.out.println("[TestInstantiationAwareBeanPostProcessor] after initialization " + beanName);      
        return bean;      
    }      
      
    @Override      
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {      
        System.out.println("[TestInstantiationAwareBeanPostProcessor] before instantiation " + beanName);      
        return null;      
    }      
      
    @Override      
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {      
        System.out.println("[TestInstantiationAwareBeanPostProcessor] after instantiation " + beanName);      
        return true;      
    }      
      
    @Override      
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {      
        System.out.println("[TestInstantiationAwareBeanPostProcessor] postProcessPropertyValues " + beanName);      
        return pvs;      
    }      

五、SmartInstantiationAwareBeanPostProcessor

org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor

该扩展接口有3个触发点方法:
在这里插入图片描述

public class TestSmartInstantiationAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {      
      
    @Override      
    public Class<?> predictBeanType(Class<?> beanClass, String beanName) throws BeansException {      
        System.out.println("[TestSmartInstantiationAwareBeanPostProcessor] predictBeanType " + beanName);      
        return beanClass;      
    }      
      
    @Override      
    public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException {      
        System.out.println("[TestSmartInstantiationAwareBeanPostProcessor] determineCandidateConstructors " + beanName);      
        return null;      
    }      
      
    @Override      
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {      
        System.out.println("[TestSmartInstantiationAwareBeanPostProcessor] getEarlyBeanReference " + beanName);      
        return bean;      
    }      
}      

六、BeanFactoryAware

org.springframework.beans.factory.BeanFactoryAware

这个类只有一个触发点,发生在bean的实例化之后,注入属性之前,也就是Setter之前。这个类的扩展点方法为setBeanFactory,可以拿到BeanFactory这个属性。

使用场景为,你可以在bean实例化之后,但还未初始化之前,拿到 BeanFactory,在这个时候,可以对每个bean作特殊化的定制。也或者可以把BeanFactory拿到进行缓存,日后使用。

public class TestBeanFactoryAware implements BeanFactoryAware {      
    @Override      
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {      
        System.out.println("[TestBeanFactoryAware] " + beanFactory.getBean(TestBeanFactoryAware.class).getClass().getSimpleName());      
    }      
}      

七、ApplicationContextAwareProcessor

org.springframework.context.support.ApplicationContextAwareProcessor
该类本身并没有扩展点,但是该类内部却有6个扩展点可供实现 ,这些类触发的时机在bean实例化之后,初始化之前
在这里插入图片描述
可以看到,该类用于执行各种驱动接口,在bean实例化之后,属性填充之后,通过执行以上红框标出的扩展接口,来获取对应容器的变量。所以这里应该来说是有6个扩展点,这里就放一起来说了
在这里插入图片描述
在这里插入图片描述

八、BeanNameAware

org.springframework.beans.factory.BeanNameAware

可以看到,这个类也是Aware扩展的一种,触发点在bean的初始化之前,也就是postProcessBeforeInitialization之前,这个类的触发点方法只有一个:setBeanName

使用场景为:用户可以扩展这个点,在初始化bean之前拿到spring容器中注册的的beanName,来自行修改这个beanName的值。

public class NormalBeanA implements BeanNameAware{      
    public NormalBeanA() {      
        System.out.println("NormalBean constructor");      
    }      
      
    @Override      
    public void setBeanName(String name) {      
        System.out.println("[BeanNameAware] " + name);      
    }      
}      

九、@PostConstruct

javax.annotation.PostConstruct
这个并不算一个扩展点,其实就是一个标注。其作用是在bean的初始化阶段,如果对一个方法标注了@PostConstruct,会先调用这个方法。这里重点是要关注下这个标准的触发点,这个触发点是在postProcessBeforeInitialization之后,InitializingBean.afterPropertiesSet之前。

使用场景:用户可以对某一方法进行标注,来进行初始化某一个属性

public class NormalBeanA {      
    public NormalBeanA() {      
        System.out.println("NormalBean constructor");      
    }      
      
    @PostConstruct      
    public void init(){      
        System.out.println("[PostConstruct] NormalBeanA");      
    }      
}      

十、InitializingBean

org.springframework.beans.factory.InitializingBean

这个类,顾名思义,也是用来初始化bean的。InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。这个扩展点的触发时机在postProcessAfterInitialization之前。

使用场景:用户实现此接口,来进行系统启动的时候一些业务指标的初始化工作。

public class NormalBeanA implements InitializingBean{      
    @Override      
    public void afterPropertiesSet() throws Exception {      
        System.out.println("[InitializingBean] NormalBeanA");      
    }      
}      

十一、FactoryBean

org.springframework.beans.factory.FactoryBean

一般情况下,Spring通过反射机制利用bean的class属性指定支线类去实例化bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在bean中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。

FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean的形式

使用场景:用户可以扩展这个类,来为要实例化的bean作一个代理,比如为该对象的所有的方法作一个拦截,在调用前后输出一行log,模仿ProxyFactoryBean的功能。

public class TestFactoryBean implements FactoryBean<TestFactoryBean.TestFactoryInnerBean> {      
      
    @Override      
    public TestFactoryBean.TestFactoryInnerBean getObject() throws Exception {      
        System.out.println("[FactoryBean] getObject");      
        return new TestFactoryBean.TestFactoryInnerBean();      
    }      
      
    @Override      
    public Class<?> getObjectType() {      
        return TestFactoryBean.TestFactoryInnerBean.class;      
    }      
      
    @Override      
    public boolean isSingleton() {      
        return true;      
    }      
      
    public static class TestFactoryInnerBean{      
      
    }      
}      

十二、SmartInitializingSingleton

org.springframework.beans.factory.SmartInitializingSingleton

这个接口中只有一个方法afterSingletonsInstantiated,其作用是是 在spring容器管理的所有单例对象(非懒加载对象)初始化完成之后调用的回调接口。其触发时机为postProcessAfterInitialization之后。

使用场景:用户可以扩展此接口在对所有单例对象初始化完毕后,做一些后置的业务处理。

public class TestSmartInitializingSingleton implements SmartInitializingSingleton {      
    @Override      
    public void afterSingletonsInstantiated() {      
        System.out.println("[TestSmartInitializingSingleton]");      
    }      
}      

十三、CommandLineRunner

org.springframework.boot.CommandLineRunner

这个接口也只有一个方法:run(String… args),触发时机为整个项目启动完毕后,自动执行。如果有多个CommandLineRunner,可以利用@Order来进行排序。

使用场景:用户扩展此接口,进行启动项目之后一些业务的预处理。

public class TestCommandLineRunner implements CommandLineRunner {      
      
    @Override      
    public void run(String... args) throws Exception {      
        System.out.println("[TestCommandLineRunner]");      
    }      
}      

十四、DisposableBean

org.springframework.beans.factory.DisposableBean

这个扩展点也只有一个方法:destroy(),其触发时机为当此对象销毁时,会自动执行这个方法。比如说运行applicationContext.registerShutdownHook时,就会触发这个方法。

public class NormalBeanA implements DisposableBean {      
    @Override      
    public void destroy() throws Exception {      
        System.out.println("[DisposableBean] NormalBeanA");      
    }      
}      

十五、ApplicationListener

org.springframework.context.ApplicationListener

准确的说,这个应该不算spring&springboot当中的一个扩展点,ApplicationListener可以监听某个事件的event,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。

但是spring内部也有一些内置事件,这种事件,可以穿插在启动调用中。我们也可以利用这个特性,来自己做一些内置事件的监听器来达到和前面一些触发点大致相同的事情。

接下来罗列下spring主要的内置事件:
在这里插入图片描述
在这里插入图片描述

插件化机制案例实战

案例背景

  • 3个微服务模块,在A模块中有个插件化的接口;
  • 在A模块中的某个接口,需要调用插件化的服务实现进行短信发送;
  • 可以通过配置文件配置参数指定具体的哪一种方式发送短信;
  • 如果没有加载到任何插件,将走A模块在默认的发短信实现;

模块结构

  • biz-pp,插件化接口工程;定义服务接口,并提供出去jar被其他实现工程依赖
  • bitpt,aliyun短信发送实现;依赖biz-pp的jar并实现SPI中的方法,按照API规范实现完成后,打成jar包,或者安装到仓库中
  • miz-pt,tencent短信发送实现;依赖biz-pp的jar并实现SPI中的方法,按照API规范实现完成后,打成jar包,或者安装到仓库中

biz-pp在pom中依赖bitpt与miz-pt的jar,或者通过启动加载的方式即可得到具体某个实现;

一、biz-app添加服务接口,并打成jar安装到仓库

public interface MessagePlugin{
	
	public String sendMsg(Map msgMap);
}

自定义服务加载工具类

/**
	这个类,可以理解为在真实的业务编码中,可以根据业务定义的规则,具体加载哪个插件的实现类进行发送短信的操作;
*/
public class PluginFactory {
 
    public void installPlugin(){
        Map context = new LinkedHashMap();
        context.put("_userId","");
        context.put("_version","1.0");
        context.put("_type","sms");
        ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
        Iterator<MessagePlugin> iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
            MessagePlugin messagePlugin = iterator.next();
            messagePlugin.sendMsg(context);
        }
    }
 
    public static MessagePlugin getTargetPlugin(String type){
        ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
        Iterator<MessagePlugin> iterator = serviceLoader.iterator();
        List<MessagePlugin> messagePlugins = new ArrayList<>();
        while (iterator.hasNext()){
            MessagePlugin messagePlugin = iterator.next();
            messagePlugins.add(messagePlugin);
        }
        MessagePlugin targetPlugin = null;
        for (MessagePlugin messagePlugin : messagePlugins) {
            boolean findTarget = false;
            switch (type) {
                case "aliyun":
                    if (messagePlugin instanceof BitptImpl){
                        targetPlugin = messagePlugin;
                        findTarget = true;
                        break;
                    }
                case "tencent":
                    if (messagePlugin instanceof MizptImpl){
                        targetPlugin = messagePlugin;
                        findTarget = true;
                        break;
                    }
            }
            if(findTarget) break;
        }
        return targetPlugin;
    }
 
    public static void main(String[] args) {
        new PluginFactory().installPlugin();
    }
  
}

自定义Controller接口、Service

@RestController
public class SmsController {
 
    @Autowired
    private SmsService smsService;
 
    @Autowired
    private ServiceLoaderUtils serviceLoaderUtils;
 
    //localhost:8087/sendMsg?msg=sendMsg
    @GetMapping("/sendMsg")
    public String sendMessage(String msg){
        return smsService.sendMsg(msg);
    }
 
}
@Service
public class SmsService {
 
    @Value("${msg.type}")
    private String msgType;
 
    @Autowired
    private DefaultSmsService defaultSmsService;
 
    public String sendMsg(String msg) {
        MessagePlugin messagePlugin = PluginFactory.getTargetPlugin(msgType);
        Map paramMap = new HashMap();
        if(Objects.nonNull(messagePlugin)){
            return messagePlugin.sendMsg(paramMap);
        }
        return defaultSmsService.sendMsg(paramMap);
    }
}

在该模块中,需要引入对具体实现的两个工程的jar依赖(也可以通过启动加载的命令方式)

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--依赖具体的实现-->
    <dependency>
        <groupId>com.congge</groupId>
        <artifactId>biz-pt</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>com.congge</groupId>
        <artifactId>miz-pt</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

</dependencies>

二、插件化具体实现
在这里插入图片描述

<dependencies>
    <dependency>
        <groupId>com.congge</groupId>
        <artifactId>biz-app</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

添加MessagePlugin接口的实现

public class BitptImpl implements MessagePlugin {
 
    @Override
    public String sendMsg(Map msgMap) {
        Object userId = msgMap.get("userId");
        Object type = msgMap.get("_type");
        //TODO 参数校验
        System.out.println(" ==== userId :" + userId + ",type :" + type);
        System.out.println("aliyun send message success");
        return "aliyun send message success";
    }
}

按照前文的方式,在resources目录下创建一个文件,注意文件名称为SPI中的接口全名,文件内容为实现类的全类名

com.congge.spi.BitptImpl

完成实现类的编码后,通过maven命令将jar安装到仓库中,然后再在上一步的biz-app中引入即可;

启动biz-app服务,调用接口:localhost:8087/sendMsg?msg=sendMsg
在这里插入图片描述

为什么会出现这个效果呢?因为我们在实现类配置了具体使用哪一种方式进行短信的发送,而加载插件的时候正好能够找到对应的服务实现,这样的话就给当前的业务提供了一个较好的扩展点。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1012305.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度对话|Sui在商业技术堆栈中的地位

近日&#xff0c;我们采访了Mysten Labs的商业产品总监Lola Oyelayo-Pearson&#xff0c;共同探讨了区块链技术如何为企业提供商业服务&#xff0c;以及为什么Sui特别适合这些用例。 1.请您简要介绍一下自己、您的角色以及您是如何开始涉足Web3领域的&#xff1f; 目前&#…

进程属性/进程状态

task_struct-PCB的一种 在Linux中描述进程的结构体叫做task_struct。进程也叫任务 task_struct是Linux内核的一种数据结构&#xff0c;它会被装载到RAM(内存)里并且包含着进程的信息。 task_ struct内容分类 标示符: 描述本进程的唯一标示符&#xff0c;用来区别其他进程。 …

通过篡改cred结构体实现提权利用

前言 在之前的HeapOverflow文章中&#xff0c;作者还构造了任意地址读写的操作&#xff0c;使用了任意地址读写去进行提权&#xff0c;还挺有意思的&#xff0c;记录一下如何利用任意地址读写进行提权。 作者利用任意地址读写分别改写modprobe_path以及cred结构体去实现提权的…

JavaEE初阶(5)多线程案例(定时器、标准库中的定时器、实现定时器、线程池、标准库中的线程池、实现线程池)

接上次博客&#xff1a;JavaEE初阶&#xff08;4&#xff09;&#xff08;线程的状态、线程安全、synchronized、volatile、wait 和 notify、多线程的代码案例&#xff1a;单例模式——饿汉懒汉、阻塞队列&#xff09;_di-Dora的博客-CSDN博客 目录 多线程案例 定时器 标准…

Go语言在人工智能时代的崭露头角:为何越来越多公司选择使用Go语言?

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to Golang Language.✨✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1…

ABB机器人常用指令功能说明

ABB机器人常用指令功能说明 一、 程序控制 程序的调用 例行程序内的逻辑控制 停止程序执行 二、 变量指令 变量指令主要用于以下方面:  对数据进行赋值  等待指令  注释指令  程序模块控制指令 赋值指令 等待指令

STViT-R 代码阅读记录

目录 一、SwinTransformer 1、原理 2、代码 二、STViT-R 1、中心思想 2、代码与原文 本次不做具体的训练。只是看代码。所以只需搭建它的网络&#xff0c;执行一次前向传播即可。 一、SwinTransformer 1、原理 主要思想&#xff0c;将token按区域划分成窗口&#xff0c…

“批量剪辑,统一视频封面,让你的创作更高效!“

作为一个创作者&#xff0c;你是否经常为每个视频封面而烦恼&#xff1f;使用我们的批量剪辑功能&#xff0c;轻松统一视频封面&#xff0c;让你的创作更高效&#xff01; 首先&#xff0c;我们要进入媒体梦工厂主页面&#xff0c;并在主页面的板块栏里选择“视频封面”板块 第…

运维面试宝典

【Linux基础篇】 1.描述Linux运行级别0-6的各自含义 0 &#xff1a;关机模式 1 &#xff1a;单用户模式 < 破解 root 密码 2 &#xff1a;无网络支持的多用户模式 3 &#xff1a;有网络支持的多用户模式&#xff08;文本模式&#xff0c;工作中最常用的模式&#xff09;…

大数据时代元数据的重要性

元数据&#xff0c;是描述了数据本身&#xff08;如数据库、数据元素、数据模型&#xff09;&#xff0c;数据表示的概念&#xff08;如业务流程、应用系统、软件代码、技术基础设施&#xff0c;数据与概念之间的联系。元数据可以帮助组织理解其自身的数据、系统和流程&#xf…

【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表)

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 【数据结构初阶】二、 线性表里的顺序表_高高的胖子的博客-CSDN博客 引言 通过上期对顺序表的介绍和使用 我们可以知道顺序表有以下优点和缺点&#xff1a; 顺序表优点 尾插 和 尾…

SpringBoot接受请求参数

1.简单参数 1.1原始方法 说明&#xff1a;获取请求传来的name参数&#xff0c;age参数的值。 //简单方式 RestController public class RequestController {GetMapping("/books")public String simpleParam(HttpServletRequest request) {//获取请求参数 name和ag…

推荐国产低功耗20位分辨率模数转换器

RAMSUN提供的类比精密、低功耗、20位分辨率、兼容SPI的模数转换器(ADC)。采用QFN-10和MSOP-10两种封装形式&#xff0c;集成了低漂移电压基准&#xff0c;振荡器&#xff0c;可编程增益放大器(PGA)&#xff0c;抗工频干扰滤波器和数字比较器等功能模块&#xff0c;以简化系统设…

微信公众号怎么添加抢福袋抽奖活动

在微信公众号中添加抢福袋抽奖活动&#xff0c;可以增加用户互动和粘性&#xff0c;同时也能为公众号带来更多的流量和曝光度。下面将从以下几个方面详细阐述在微信公众号中如何添加抢福袋抽奖活动。 一、活动策划 在策划活动之前&#xff0c;需要明确活动的目的和目标用户&am…

接口测试入门

1. 什么是接口测试 顾名思义&#xff0c;接口测试是对系统或组件之间的接口进行测试&#xff0c;主要是校验数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及相互逻辑依赖关系。其中接口协议分为HTTP,WebService,Dubbo,Thrift,Socket等类型&#xff0c;测试类型又主要…

ITR服务体系的常见问题和华为构建ITR的经验分享

大家好&#xff01; 前两天有一个企业负责客户服务、售后部门的朋友和华研荟探讨&#xff0c;企业的服务体系如何搭建&#xff0c;以及如何像华为一样构建ITR流程 他的苦恼是&#xff0c;自己所带领的部门叫做客户服务中心&#xff0c;但是在公司内部不受重视&#xff0c;公司…

如何将文件或者图片压缩成zip文件压缩包

代码&#xff1a; RestController RequestMapping("/download") public class DownloadController {GetMapping("/studentWork")public ResponseEntity<StreamingResponseBody> downloadStudentWork() {HttpHeaders headers new HttpHeaders();hea…

仔仔细细的给您讲,如何建立数据仓库

数据仓库的定位 在整个数据价值生产链路中&#xff0c;数据仓库的主要作用就是中心化分发&#xff0c;将原始数据与数据价值挖掘活动隔离。所有的原始数据都会进入数据仓库&#xff0c;再由数据仓库统一分发给下游的数据使用者。这样的结构实现了原始数据与数据分析工作的解耦…

Linux 企业级夜莺监控分析工具远程访问

目录 前言 1. Linux 部署Nightingale 2. 本地访问测试 3. Linux 安装cpolar 4. 配置Nightingale公网访问地址 5. 公网远程访问Nightingale管理界面 6. 固定Nightingale公网地址 前言 夜莺监控是一款开源云原生观测分析工具&#xff0c;采用 All-in-One 的设计理念&…

xml配置文件密码特殊字符处理

错误姿势&#xff1a; 正确姿势&#xff1a;采取转义符的方式 常用转义符&#xff1a;