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

18 篇文章 0 订阅
3 篇文章 0 订阅
解决方案直通车
注:如果不看解决思路,只看解决方案的,可以滚动到最后面查看解决方案!

最新在搭建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<? extends BeanNameGenerator> 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<Object> 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源码,对实际工作中有很大的帮助,不管是写项目,还是写框架,都会有很大的感悟!

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

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值