栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

使用springboot启动监听器ApplicationContextInitializer实现外部jar包设置BeanNameGenerator,解决控制层beanName冲突问题

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

使用springboot启动监听器ApplicationContextInitializer实现外部jar包设置BeanNameGenerator,解决控制层beanName冲突问题

解决方案直通车
注:如果不看解决思路,只看解决方案的,可以滚动到最后面查看解决方案!

最新在搭建springcldoualibaba微服务架构,由于习惯了之前的写法(控制层将后台,APP端,小程序端,公众号端分包管理,所以同一个业务的控制器,又懒得每个控制器指定一个名字,对于spring来说,类名一样,那么名字就一样,则会启动报错),则自定义了BeanNameGenerator,之前对于单体springboot项目来说,指定这个很简单,如下所示

@SpringBootApplication(nameGenerator = ControllerAnnotationBeanNameGenerator.class)
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class);
    }
}

只需要在**@SpringbootApplication注解上指定nameGenerator**为自定义的即可。

但是现在是微服务架构,本人又是那种很不愿意干重复事情的人,所以想着将这一串配置放在公共jar包中,然后来实现这个功能。

既然要放在公共jar包中,那么就不能再继续操作SpringBootApplication注解了,只需要找到springboot启动的时候,想办法将我们自定义的nameGenerator设置进去即可。

首先打开SpringbootApplication注解,查看此注解源码的nameGenerator:

@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class nameGenerator() default BeanNameGenerator.class;

可以看到,这个注解的属性是等同于:ComponentScan注解的nameGenerator

第一个解决方案来了:

@ComponentScan(nameGenerator = ControllerAnnotationBeanNameGenerator.class)
public class ControllerAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
    @Override
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        String defaultName = super.generateBeanName(definition, registry);
        String beanClassName = definition.getBeanClassName();
        if (beanClassName.contains("com.xxx.wechat")) {
            defaultName += "WECHAT";
        }
        return defaultName;
    }
}

将自定义的BeanNameGenerator类写在springboot可以扫码到的地方,然后指定@ComponentScan的nameGenerator,让spring扫描到并且注入进去。

按道理这个方法是可行的,不过最后启动还是报错,原因是:springboot扫描的顺序问题,因为springboot启动的时候,由于springboot注解自带scanbasePackages属性,会先默认扫描一遍启动类下面的所有包,然后在扫码jar包,所以通过spring扫描再加入进去不妥。

那么就想到在springboot启动流程上入手,再最开始的是就将其设置进去:
问题1:nameGenerator是设置在哪个对象的?
问题2:什么地方可以拿到这个对象?
问题3:springboot是否支持得有这个对象的回调?

解决问题1:思路,按照原来的方式,在SpringbootApplication注解上指定nameGenerator,然后增加一个无参构造方法(既然要使用这个,那么就会实例化,实例化的时候,我们就设置一个无参构造方法,那么spring就会调用无参构造方法来进行实例化)

@SpringBootApplication(nameGenerator = ControllerAnnotationBeanNameGenerator.class)
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class);
    }
}
public class ControllerAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {

    public ControllerAnnotationBeanNameGenerator(){
        System.out.println("ddddddd");
    }
    
    @Override
    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        String defaultName = super.generateBeanName(definition, registry);
        String beanClassName = definition.getBeanClassName();
        if (beanClassName.contains("com.xxx.wechat")) {
            defaultName += "WECHAT";
        }
        return defaultName;
    }
}

打断点可以看到

spring当前的上下文是:
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
可以看出,我们只需要设置此值即可,那么问题1得到解决:
nameGenerator是设置在哪个对象的?答案:AnnotationConfigServletWebServerApplicationContext
那么我们如何拿到这个对象呢,看springboot是否有回调:

根据调用方法栈可以看到这个地方:refreshContext():刷新上下文,前一个方法是:prepareContext(),准备刷新上下文,猜想这里面肯定是有回调的,并且会将上下文对象传输过去。
如下代码:关键代码为:this.applyInitializers(context); 容器初始化回调

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
        context.setEnvironment(environment);
        this.postProcessApplicationContext(context);
        this.applyInitializers(context); // 关键代码:这里会进行一个回调
        listeners.contextPrepared(context);
        if (this.logStartupInfo) {
            this.logStartupInfo(context.getParent() == null);
            this.logStartupProfileInfo(context);
        }

        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
        if (printedBanner != null) {
            beanFactory.registerSingleton("springBootBanner", printedBanner);
        }

        if (beanFactory instanceof DefaultListableBeanFactory) {
            ((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }

        if (this.lazyInitialization) {
            context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
        }

        Set sources = this.getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        this.load(context, sources.toArray(new Object[0]));
        listeners.contextLoaded(context);
    }
 
protected void applyInitializers(ConfigurableApplicationContext context) {
		for (ApplicationContextInitializer initializer : getInitializers()) {
			Class requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
					ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			initializer.initialize(context);
		}
	}

由此可以看到我们写一个扩展来实现ApplicationContextInitializer即可,springboot在准备刷新上下文之前就会回调。

至此,方案已出:

第一步:自定义容器初始化器

import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;


public class MyApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
    	// 这里要进行判断,选择自己项目中的上下文来判断即可,因为ConfigurableApplicationContext接口中没有setBeanNameGenerator方法。
        if(applicationContext instanceof AnnotationConfigServletWebServerApplicationContext){
        // 类型相同,则强转
            AnnotationConfigServletWebServerApplicationContext annotationConfigServletWebServerApplicationContext = (AnnotationConfigServletWebServerApplicationContext)applicationContext;
            annotationConfigServletWebServerApplicationContext.setBeanNameGenerator(new ControllerAnnotationBeanNameGenerator());
        }
    }
}

第二步:
在classpath下面meta-INF/spring.factories文件(如果不懂的,可以了解一下springboot的扩展)

并在文件中写入:(自定义全限定类名换成自己项目的即可)

org.springframework.context.ApplicationContextInitializer=com.xxx.extension.MyApplicationContextInitializer

再次启动之后,代码就不会报错了,springboot在准备刷新上下文的时候,springboot有比较多的扩展点提供给我们开发者,熟练的掌握spring的这一套思想以及熟读spring源码,对实际工作中有很大的帮助,不管是写项目,还是写框架,都会有很大的感悟!

如有不正确的地方,欢迎评论指正,一同讨论交流学习!

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号