解决Spring Cloud Ribbon之RibbonClientConfiguration父子上下文重复扫描的问题

371 阅读2分钟

Spring Cloud还允许您通过使用@RibbonClient声明其他配置(在RibbonClientConfiguration之上)来完全控制客户端,如以下示例所示:

@Configuration
@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
public class TestConfiguration {
}

在这种情况下,客户端由RibbonClientConfiguration中已有的组件以及CustomConfiguration中的任何组件组成(其中后者通常会覆盖前者)。

CustomConfiguration类必须是@Configuration类,但请注意,对于主应用程序上下文,它不在@ComponentScan中。否则,它由所有@RibbonClients共享。如果您使用@ComponentScan(或@SpringBootApplication),则需要采取措施避免将其包括在内(例如,可以将其放在单独的,不重叠的程序包中,或指定要在@ComponentScan)。

以上官方的解释说,这样说可能有点绕,说白了,如果我们自定义一个CustomConfiguration来指定对应RibbonClient的负载均衡规则的时候,CustomConfiguration不能被SpringBootComponentScan扫描到,如果被扫描扫,这样会造成全局配置,也是就所有的client可能会使用同一个IRule规则。

Spring Cloud官方说明

image.png

简单来说:SpringBootApplication默认扫描的component是位于当前启动类所在的包以及所在包以下的所有component,包括:@Service@Controller``@Bean等等,@SpringBootApplication扫描的叫主上下文,而Ribbon也有一个上下文,可以称为子上下文,如果父子上下文扫描重叠,可能会带来预想不到的问题,类似如果开发过Spring、SpringMVC项目的知道,通过都会定义Spring配置文件applicationContext.xml以及SpringMVC配置文件spring-mvc.xml,比如:

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:cache="http://www.springframework.org/schema/cache" 
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
          http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
          http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
          http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd 
          http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
          
    <!-- 在applicationContext.xml中只加载除表现层之外的所有bean,因此下面一行中不需要加载@Controller -->                       
    <!-- 扫描注解Bean 不包括@Controller(表现层) 保证@Service @Repository的属性被注入-->
    <context:component-scan base-package="com.zml.oa">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
    <!-- 引入配置文件 --> 
	<context:property-placeholder ignore-unresolvable="true" local-override="true" location="classpath:application.properties"/>
    
    <util:properties id="APP_PROPERTIES" location="classpath:application.properties" local-override="true"/>
    
	<!-- 数据源 -->
	<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource" >
		<!-- 数据源别名 -->
		<property name="alias" value="proxoolDataSource"/>
		<property name="driver" value="${connection.driverClassName}" />
		<property name="driverUrl" value="${connection.url}" />
		<property name="user" value="${connection.username}" />
		<property name="password" value="${connection.password}" />
		<!--最大连接数(默认5个),超过了这个连接数,再有请求时,就排在队列中等候,最大的等待请求数由maximum-new-connections决定 -->
		<property name="maximumConnectionCount" value="${proxool.maximum.connection.count}"/>
		<!--最小连接数(默认2个)-->  
		<property name="minimumConnectionCount" value="${proxool.minimum.connection.count}" />
		<property name="statistics" value="${proxool.statistics}" />
		<property name="simultaneousBuildThrottle" value="${proxool.simultaneous.build.throttle}"/>
		
		
	</bean>  
	
    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> 
        <property name="dataSource" ref="dataSource" /> 
		<property name="packagesToScan" value="com.zml.oa.entity"/>
        <property name="hibernateProperties"> 
         	<props>
		         <prop key="hibernate.show_sql">true</prop>
		         <prop key="hibernate.format_sql">true</prop>
		         <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
		 	 <prop key="hibernate.hbm2ddl.auto">update</prop> 
  			</props>
        </property> 
    </bean>   
	 
	<!-- jdbc模板 -->
	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<constructor-arg ref="dataSource" />
	</bean>
  	
	<!-- jdbc命名参数模板 -->
	<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
		<constructor-arg ref="dataSource" />
	</bean>
	
	<bean id="applicationContextHelper" class="com.zml.oa.util.ApplicationContextHelper"/>
  	
   	<!-- 开启AOP监听 只对当前配置文件有效 -->
	<aop:aspectj-autoproxy expose-proxy="true"/>
  	
  	<!-- 开启注解事务 只对当前配置文件有效 -->
  	<tx:annotation-driven transaction-manager="transactionManager"/>
  
    <!-- 事务管理器 -->
	<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory"></property>
	</bean>
	
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="doSave*" propagation="REQUIRED" />  
           	<tx:method name="doAdd*" propagation="REQUIRED" />  
           	<tx:method name="add*" propagation="REQUIRED" />  
           	<tx:method name="set*" propagation="REQUIRED" />  
           	<tx:method name="doCreate*" propagation="REQUIRED" />  
          	<tx:method name="doInsert*" propagation="REQUIRED" />  
           	<tx:method name="doUpdate*" propagation="REQUIRED" />  
           	<tx:method name="doMerge*" propagation="REQUIRED" />  
           	<tx:method name="doDelete*" propagation="REQUIRED" />  
           	<tx:method name="doRemove*" propagation="REQUIRED" />  
           	<tx:method name="delete" propagation="REQUIRED" />  
           	<tx:method name="doPut*" propagation="REQUIRED" />
           	<!-- workflowService.bankTransfer 需要回滚,但是传播行为设置为REQUIRES_NEW 相应的 act_ru_identitylink表中又不能保存task_id_   -->
			<!-- 先记下来,待解决 -->
			<tx:method name="bankTransfer*" propagation="REQUIRES_NEW" />
            <!--hibernate4必须配置为开启事务 否则 getCurrentSession()获取不到-->
            <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="count*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="find*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="list*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="toList*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <aop:config expose-proxy="true" proxy-target-class="true">
        <!-- 只对业务逻辑层实施事务 -->
        <aop:pointcut id="txPointcut" expression="execution(* com.zml.oa..service..*.*(..))"/>
        <aop:advisor id="txAdvisor" advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>
    
    
    <!-- Activiti的bean -->
	<!-- 流程引擎的配置bean -->
	<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
		<property name="dataSource" ref="dataSource" />
		<property name="databaseSchemaUpdate" value="true"/>
		<property name="transactionManager" ref="transactionManager" />
        <!-- 生成流程图的字体 -->
        <property name="activityFontName" value="${diagram.activityFontName}"/>
        <property name="labelFontName" value="${diagram.labelFontName}"/>
		<!-- <property name="deploymentResources" value="classpath*:/deploy/*" /> -->
	</bean>
	<!-- 流程引擎的bean -->
	<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
		<property name="processEngineConfiguration" ref="processEngineConfiguration" />
	</bean>
	<!-- 服务组件的bean -->
	<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService" />
	<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService" />
	<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService" />
	<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService" />
	<bean id="managementService" factory-bean="processEngine" factory-method="getManagementService" />
	<bean id="identityService" factory-bean="processEngine" factory-method="getIdentityService" />
	
    <!-- 启用缓存注解功能(请将其配置在Spring主配置文件中) -->
	<cache:annotation-driven cache-manager="springCacheManager" mode="proxy" />
	
    <import resource="classpath:spring-config-cache.xml"/>
    <import resource="classpath:spring-config-shiro.xml"/>
	
</beans>

在Spring配置文件applicationContext.xml会看到:

  <!-- 扫描注解Bean 不包括@Controller(表现层) 保证@Service @Repository的属性被注入-->
    <context:component-scan base-package="com.zml.oa">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

目的是为了不让Spring排除扫描到com.zml.oa下的controller,防止与springMvc重复扫描。

spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
    
    <description>Spring MVC Configuration</description>
     <!-- 加载配置属性文件 -->
	<context:property-placeholder ignore-unresolvable="true" location="classpath:config.properties" />
	<!-- 使用Annotation自动注册Bean,只扫描@Controller -->
	<context:component-scan base-package="com.itstyle" use-default-filters="false"><!-- base-package 如果多个,用“,”分隔 -->
		<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	</context:component-scan>
	<!-- 默认的注解映射的支持,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping -->
	<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">
		<mvc:message-converters register-defaults="true">
			<!-- 将StringHttpMessageConverter的默认编码设为UTF-8 -->
			<bean class="org.springframework.http.converter.StringHttpMessageConverter">
		    	<constructor-arg value="UTF-8" />
			</bean>
			<!-- 将Jackson2HttpMessageConverter的默认格式化输出为false -->
			<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="supportedMediaTypes">
                	<list><value>application/json;charset=UTF-8</value></list>
                </property>
                <property name="prettyPrint" value="false"/>
            </bean>
		</mvc:message-converters>
	</mvc:annotation-driven>
	 <!--启动Spring MVC的注解功能,设置编码方式,防止乱码--> 
	<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">  
      <property name="messageConverters">     
         <list>     
             <bean class = "org.springframework.http.converter.StringHttpMessageConverter">     
                <property name = "supportedMediaTypes">  
                      <list>  
                          <value>text/html;charset=UTF-8</value>
                     </list>     
                </property>     
             </bean>     
         </list>     
      </property>   
    </bean>
    <!-- REST中根据URL后缀自动判定Content-Type及相应的View -->
	<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
	    <property name="mediaTypes" >
	        <map> 
                <entry key="xml" value="application/xml"/> 
                <entry key="json" value="application/json"/> 
            </map>
	    </property>
        <property name="ignoreAcceptHeader" value="true"/>
        <property name="favorPathExtension" value="true"/>
	</bean>
	<!-- Thymeleaf模版 -->
	<bean id="templateResolver" class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
        <property name="prefix" value="/WEB-INF/modules/" />
        <property name="suffix" value=".html" />
        <property name="templateMode" value="HTML" />
    </bean>
    <bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
        <property name="templateResolvers">
            <set>
                <ref bean="templateResolver" />
            </set>
        </property>
    </bean>
    <bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">  
       <property name="templateEngine" ref="templateEngine" />  
        <property name="characterEncoding" value="UTF-8"/> 
    </bean>  
	<!-- 对静态资源文件的访问, 将无法mapping到Controller的path交给default servlet handler处理 -->
	<mvc:default-servlet-handler />
	<!-- 静态资源映射  SpringMVC会自动给静态资源Response添加缓存头Cache-Control和Expires值 cache-period="31536000"-->
    <mvc:resources mapping="/static/**" location="/static/" />
    
    <!-- 定义无Controller的path<->view直接映射 -->
	<mvc:view-controller path="/" view-name="redirect:${web.view.index}"/>
	
	<!-- 自定义拦截器之类的 -->
	
</beans>

spring-mvc.xml配置文件会看到:

<!-- 使用Annotation自动注册Bean,只扫描@Controller -->
<context:component-scan base-package="com.itstyle" use-default-filters="false">
     <!-- base-package 如果多个,用“,”分隔 -->
     <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

Spring、SpringMVC这样的配置其实就是为了防止父子上下文重叠的问题,这样解释应该就明了了~

解决办法:

将Ribbon配置定义到启动类所在包的同级文件夹中,防止被SpringBoot重复扫描,类似:

image.png

这样就可以防止父子上下文扫描重叠问题了~