使用spring的注意事项

持续更新,本文来自spring官方文档

BeanPostProcessor

BeanPostProcessor的实现类通过@bean注解在配置类中配置时,方法的返回类型必须是实现类本身或者是BeanPostProcessor接口,这样能够清晰的指出其是BeanPostProcessor的实现类。否则ApplicationContext在完整的创建它之前,无法自动通过类型探测到它属于BeanPostProcessor接口。BeanPostProcessor接口实现类需要优先被实例化,然后处理后续其他bean,所以早期的类型检测至关重要。
以上内容翻译至spring的官方文档,简单点讲就是通过@bean配置BeanPostProcessor的实现类时,方法的返回值必须是BeanPostProcessor实现类或者BeanPostProcessor接口本身。这样通过返回值的类型可以知道它是一个BeanPostProcessor。

说明

ApplicationContext会通过refresh方法进行初始化,以下是该方法的部分内容

			try {
				postProcessBeanFactory(beanFactory);
				invokeBeanFactoryPostProcessors(beanFactory);
				# 注册BeanPostProcessor
				registerBeanPostProcessors(beanFactory);
				initMessageSource();
				initApplicationEventMulticaster();
				onRefresh();
				registerListeners();
				finishBeanFactoryInitialization(beanFactory);
				finishRefresh();
			}

追踪registerBeanPostProcessors方法可以到PostProcessorRegistrationDelegate#registerBeanPostProcessors方法
可以看到该方法通过类型获取所有的BeanPostProcessor定义,
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
所以通过@bean配置BeanPostProcessor时,返回值不是BeanPostProcessor接口或者其实现类,在该阶段是无法注册到ApplicationContext,以至于后面在普通bean的实例化过程中,无法对bean进行处理。

Spring AOP

Spring AOP 默认使用标准 JDK 动态代理。这使得任何接口(或一组接口)都可以被代理。

Spring AOP 也可以使用 CGLIB 代理,这样被代理的类不需要实现接口。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是类进行编程是一种很好的做法,因此业务类通常实现一个或多个业务接口。在那些(希望很少见)需要代理未在接口上声明的方法或需要将被代理对象作为具体类型传递给方法的情况下,可以强制使用 CGLIB。
重点在最后一句话
在那些(希望很少见)需要代理未在接口上声明的方法或需要将被代理对象作为具体类型传递给方法的情况下,可以强制使用 CGLIB。

解释

如上所述,当一个类有实现接口时,spring aop默认通过jdk实现代理,如果自己写过jdk的代理就知道,jdk生成的只是接口的实现类,所以spring aop生成的代理类丢掉了非接口中的方法,自然这个方法不能被代理。
jdk生成的实例只是接口的实现类,与被代理对象本身的类不相同,因此如果字段声明的类型是实现类,会因为类型不匹配而无法注入。

条件bean闭坑

背景

系统将redis由哨兵模式改造成集群模式,key过期事件监听需要适配。集群模式下key过期时间不会广播,所以需要监听每一个redis实例。为此基于RedisMessageListenerContainer写了兼容集群模式的RedisClusterMessageListenerContainer。
为了保证上线出问题后能够回退,通过spring的"条件bean"及配置来确认到底是启动集群模式,还是哨兵模式。
同时为了表达同一个业务,因此使用函数重载(函数名相同,参数不同),来创建不同模式下的MessageListenerContainer。代码如下

    @Bean
    @ConditionalOnMissingRedisClusterConfig
    public RedisMessageListenerContainer messageListener(xx) {
        ... ...
    }

    @ConditionalOnRedisClusterConfig
    @Bean
    public RedisClusterMessageListenerContainer messageListener(xx) {
        ... ...
    }

测试的时候发现,启动集群配置后,RedisClusterMessageListenerContainer messageListener(xx)虽然@ConditionalOnRedisClusterConfig生效了,但bean并没有被创建出来。通过调试可以跟踪到问题代码如下:
org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod

	private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
		ConfigurationClass configClass = beanMethod.getConfigurationClass();
		MethodMetadata metadata = beanMethod.getMetadata();
		String methodName = metadata.getMethodName();

		// Do we need to mark the bean as skipped by its condition?
		if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
			configClass.skippedBeanMethods.add(methodName);
			return;
		}
		# 统一的过滤逻辑
		if (configClass.skippedBeanMethods.contains(methodName)) {
			return;
		}
		... ...
	}

该类是用于解析@Configuration注解类中的bean定义,loadBeanDefinitionsForBeanMethod用于将@bean注解的函数生成bean定义。
从上述源码中可以看到,如果bean的条件表达式校验不通过,spring会将这个方法名缓存到configClass.skippedBeanMethods中,用于统一的过滤逻辑。
因此前面同名的重载函数因为条件表达式计算为false,函数名被添加到configClass.skippedBeanMethods;后面的同名重载函数及时条件表达式计算为true,也会被统一的过滤逻辑拦截,无法生成bean定义,以致不会生成bean实例。

BeanPostProcessor不生效

背景

我们网关使用的zuul,并在上面做了定制;配置类继承了ZuulProxyAutoConfiguration,并在里面增加了定制配置。如下图所示

public class DynamicZuulRoutesConfiguration extends ZuulProxyAutoConfiguration {
    @Autowired
    ApplicationContext context;

    @Autowired
    ZuulRouteStore zuulRouteStore;

    @Autowired
    List<ExtendPropertyRefresher> refreshers;

    @Autowired
    ApiGatewayProperties apiGatewayProperties;
    
    @Bean
    @Override
    public SimpleRouteLocator simpleRouteLocator() {
        return new RefreshableSimpleRoutesLocator(this.server.getServlet().getContextPath(), this.zuulProperties);
    }
    ... ...
}

后面因为公司规则需要升级到高版本的spring cloud,升级后发现网关接受任何请求都报错,经定位发现是因为高版本spring注入的errorController以及没有getErrorPath这个方法了。
org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping

	@Override
	protected Object lookupHandler(String urlPath, HttpServletRequest request)
			throws Exception {
		if (this.errorController != null
		                                # 新版本的spring注入的errorController,已经没有getErrorPath方法。
				&& urlPath.equals(this.errorController.getErrorPath())) {
			return null;
		}
        ... ...
		return super.lookupHandler(urlPath, request);
	}

组内同学百度后决定通过BeanPostProcessor拦截lookupHandler的调用,绕过“this.errorController.getErrorPath()”这段逻辑。估计看的是这个文章https://www.cnblogs.com/kire-cat/p/16361577.html。
因为该配置根zuul相关,因此将该BeanPostProcessor配置在了前面提到的DynamicZuulRoutesConfiguration,配置后代码如下

public class DynamicZuulRoutesConfiguration extends ZuulProxyAutoConfiguration {
  # 其他部分与之前保持一致,增加了BeanPostProcessor的配置,就是将https://www.cnblogs.com/kire-cat/p/16361577.html代码放到了本类中
  @Bean
  public ZuulPostProcessor zuulPostProcessor(@Autowired RouteLocator routeLocator,
                                             @Autowired ZuulController zuulController,
                                             @Autowired(required = false) ErrorController errorController) {
    return new ZuulPostProcessor(routeLocator, zuulController, errorController);
  }

  private enum LookupHandlerCallbackFilter implements CallbackFilter {
    INSTANCE;

    @Override
    public int accept(Method method) {
      if (METHOD.equals(method.getName())) {
        return 0;
      }
      return 1;
    }
  }
  ... ...
}

调试发现新增的ZuulPostProcessor 并没有对ZuulHandlerMapping进行处理。

分析

因为只是无脑调试spring源码,因此这里不对定位逻辑进行详述,进展示分析结果。

结论

如果BeanPostProcessor是在@Configuration的配置类中配置的,且配置类也有自己需要注入的字段。如下所示
a,b两个字段均不会经过xxBeanPostProcessor的处理。

@Configuration
public class ApiGatewayAutoConfiguration {
    @Autowired
    private A a;
    @Autowired
    private B b;

    @Bean
    BeanPostProcessor xxBeanPostProcessor() {
        return new xxBeanPostProcessor()
    }
}
原因

分析org.springframework.context.support.AbstractApplicationContext#refresh的源码可以看到,spring容器初始化时会按顺序执行一下步骤

  1. postProcessBeanFactory
  2. invokeBeanFactoryPostProcessors
  3. registerBeanPostProcessors-实例化所有的BeanPostProcessor
  4. … …
  5. finishBeanFactoryInitialization-实例化所有的单例bean
  6. finishRefresh

从流程上可以看到在实例化单例bean之前需要实例化所有的BeanPostProcessor。

BeanPostProcessor的实例化与普通的bean实例化并无区别,代码中的“xxBeanPostProcessor”的实例化最终可以追踪到org.springframework.beans.factory.support.ConstructorResolver#instantiateUsingFactoryMethod

	public BeanWrapper instantiateUsingFactoryMethod(
			String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {

		BeanWrapperImpl bw = new BeanWrapperImpl();
		this.beanFactory.initBeanWrapper(bw);

		Object factoryBean;
		Class<?> factoryClass;
		boolean isStatic;

		String factoryBeanName = mbd.getFactoryBeanName();
		# 实例化bean之前得实例化factoryBean,factoryBean即为宿主bean或者说@Configuration注解bean
		if (factoryBeanName != null) {
			if (factoryBeanName.equals(beanName)) {
				throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
						"factory-bean reference points back to the same bean definition");
			}
			factoryBean = this.beanFactory.getBean(factoryBeanName);
			if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
				throw new ImplicitlyAppearedSingletonException();
			}
			this.beanFactory.registerDependentBean(factoryBeanName, beanName);
			factoryClass = factoryBean.getClass();
			isStatic = false;
		}
		... ...
	}

从上述可以看到在实例化BeanPostProcessor之前,得先实例化宿主bean(“xxBeanPostProcessor”),实例化宿主bean的时候又会去注入其依赖。创建宿主bean的依赖时,BeanPostProcessor并未创建出来,因此不会被其处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值