需求:對當前請求的 url 重新構建
debug feign 的執行可知,重寫 LoadBalancerFeignClient 類中的 execute 方法即可控制當前請求的url
代碼分析
當引入 spring-cloud-sleuth-stream 時, seluth也重寫了feign 的 feign.Client 查看 TraceFeignClientAutoConfiguration 類,
@Configuration @ConditionalOnProperty(name = "spring.sleuth.feign.processor.enabled", matchIfMissing = true) protected static class FeignBeanPostProcessorConfiguration { @Bean FeignContextBeanPostProcessor feignContextBeanPostProcessor(BeanFactory beanFactory) { return new FeignContextBeanPostProcessor(beanFactory);// 對 FeignContext 進行了重新包裝 } }
@Bean // 對 LoadBalancerFeignClient bean 的包裝類
TraceFeignObjectWrapper traceFeignObjectWrapper(BeanFactory beanFactory) {
return new TraceFeignObjectWrapper(beanFactory);
}
@Bean // 對 feign.Client 的所有方法進行攔截,如果 執行的 bean 不是 TraceFeignClient ,則返 TraceFeignClient bean,執行該類中的方法,該類也實現了 feign的 Client 接口
TraceFeignAspect traceFeignAspect(BeanFactory beanFactory) {
return new TraceFeignAspect(beanFactory);
}
final class FeignContextBeanPostProcessor implements BeanPostProcessor { private final BeanFactory beanFactory; private TraceFeignObjectWrapper traceFeignObjectWrapper; FeignContextBeanPostProcessor(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) { //如果bean 是 FeignContext,則返回 TraceFeignContext 的bean return new TraceFeignContext(getTraceFeignObjectWrapper(), (FeignContext) bean); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } private TraceFeignObjectWrapper getTraceFeignObjectWrapper() { if (this.traceFeignObjectWrapper == null) {
// 查看 TraceFeignObjectWrapper 類 this.traceFeignObjectWrapper = this.beanFactory.getBean(TraceFeignObjectWrapper.class); } return this.traceFeignObjectWrapper; } }
查看 TraceFeignObjectWrapper 類,代碼如下:
final class TraceFeignObjectWrapper { private final BeanFactory beanFactory; private CachingSpringLoadBalancerFactory cachingSpringLoadBalancerFactory; private SpringClientFactory springClientFactory; TraceFeignObjectWrapper(BeanFactory beanFactory) { this.beanFactory = beanFactory; } Object wrap(Object bean) { if (bean instanceof Client && !(bean instanceof TraceFeignClient)) { if (bean instanceof LoadBalancerFeignClient) { //如果 bean 是 LoadBalancerFeignClient 的 bean,則返回 TraceLoadBalancerFeignClient 的 bean LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean); return new TraceLoadBalancerFeignClient( client.getDelegate(), factory(), clientFactory(), this.beanFactory); } return new TraceFeignClient(this.beanFactory, (Client) bean); } return bean; } CachingSpringLoadBalancerFactory factory() { if (this.cachingSpringLoadBalancerFactory == null) { this.cachingSpringLoadBalancerFactory = this.beanFactory .getBean(CachingSpringLoadBalancerFactory.class); } return this.cachingSpringLoadBalancerFactory; } SpringClientFactory clientFactory() { if (this.springClientFactory == null) { this.springClientFactory = this.beanFactory .getBean(SpringClientFactory.class); } return this.springClientFactory; } }
當請求執行時,查看 LoadBalancerFeignClient類可知如圖所示, delegate 顯示為TraceFeignClient 的bean ,說明feign 的 負載均衡客戶端現在用的是 TraceLoadBalancerFeignClient
查看 AbstractLoadBalancerAwareClient 類,獲取當前server 的ip,端口,請求url,如圖所示
feign 的默認 Client 為 Default 類,根據請求服務的ip,端口和url ,發起請求,所以最終請求的執行,如下面所示
由上面的流程可知,引入sleuth 時,sleuth 中的feign相關類重寫了 feign 的負載均衡類,所以要關閉 這個功能,當 配置文件中有 spring.sleuth.feign.enabled=false 時,則
sleuth 重寫 feign 負載均衡器 ,則不生效。
當配置文件中有 ribbon.eureka.enabled=false 屬性時,才會使用 ribbon.listOfServers 中配置的信息,所有要 實現 EnvironmentPostProcessor 類,將屬性放入
defaultProperties 配置文件級別中,方便當項目中有相同屬性時,覆蓋已添加的屬性信息
@Slf4j public class MockEnvironmentPostProcessor implements EnvironmentPostProcessor { private static final String PROPERTY_SOURCE_NAME = "defaultProperties"; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { //獲取mock屬性配置,並綁定到MockProperties 對象中 MockProperties target = new MockProperties(); RelaxedDataBinder binder = new RelaxedDataBinder(target, MockProperties.MOCK_PREFIX); binder.bind(new PropertySourcesPropertyValues(environment.getPropertySources())); boolean enabled = target.isEnabled(); if (true == enabled) { System.out.println("mock server 構建環境屬性"); Map<String, Object> map = new HashMap<>(); //對單個服務mock的處理 if (StringUtils.isNotBlank(target.getServices())) { String[] services = target.getServices().split(","); for (String service : services) { map.put(service.toUpperCase() + ".ribbon.listOfServers", target.getIpAddress()); System.out.println(String.format("對[%s]服務配置 mock地址[%s]", service, target.getIpAddress())); } } // 自定義 每個服務的ip地址 服務直連情況 if (!target.getServicesMap().isEmpty()) { Map<String, String> servicesMap = target.getServicesMap(); for (String key : servicesMap.keySet()) { String ip = servicesMap.get(key); map.put(key.toUpperCase() + ".ribbon.listOfServers", ip); System.out.println(String.format("對[%s]服務配置直連地址[%s]", key, ip)); } } // 服務重試切換次數 map.put("ribbon.MaxAutoRetriesNextServer", 0); // 服務重試次數 map.put("ribbon.MaxAutoRetries", 0); // ribbon 連接時間 map.put("ribbon.connectTimeoutMillis", 10000); // ribbon 讀取時間 map.put("ribbon.readTimeoutMillis", 60000); // 對所有操作請求都不重試 map.put("ribbon.OkToRetryOnAllOperations", false); //ribbon不使用eureka上的服務信息 map.put("ribbon.eureka.enabled", false); // 關閉 sleuth 對 feign client 的包裝 map.put("spring.sleuth.feign.enabled", false); // 設置全局的超時時間 hystrix 熔斷超時時間 map.put("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds", 60000); addOrReplace(environment.getPropertySources(), map); } } private void addOrReplace(MutablePropertySources propertySources, Map<String, Object> map) { MapPropertySource target = null; if (propertySources.contains(PROPERTY_SOURCE_NAME)) { PropertySource<?> source = propertySources.get(PROPERTY_SOURCE_NAME); if (source instanceof MapPropertySource) { target = (MapPropertySource) source; for (String key : map.keySet()) { if (!target.containsProperty(key)) { target.getSource().put(key, map.get(key)); } } } } if (target == null) { target = new MapPropertySource(PROPERTY_SOURCE_NAME, map); } if (!propertySources.contains(PROPERTY_SOURCE_NAME)) { propertySources.addLast(target); } } }
在 spring.factories 文件中配置
# Environment Post Processor org.springframework.boot.env.EnvironmentPostProcessor=\ MockEnvironmentPostProcessor
繼承 LoadBalancerFeignClient 類,重寫 execute 方法
@Slf4j public class MockLoadBalancerFeignClient extends LoadBalancerFeignClient { private MockProperties mockProperties; private DiscoveryClient discoveryClient; private Client delegate; private CachingSpringLoadBalancerFactory lbClientFactory; private SpringClientFactory clientFactory; public MockLoadBalancerFeignClient(Client delegate, CachingSpringLoadBalancerFactory lbClientFactory, SpringClientFactory clientFactory, MockProperties mockProperties, DiscoveryClient discoveryClient) { super(delegate, lbClientFactory, clientFactory); this.delegate = delegate; this.lbClientFactory = lbClientFactory; this.clientFactory = clientFactory; this.mockProperties = mockProperties; this.discoveryClient = discoveryClient; log.info("mock feign 負載均衡器初始化"); } /** * 1. 如果配置 mock全局屬性(默認false),則請求的所有服務都走 mock 服務器 * 2. 請求的服務在mock服務列表中,則請求走mock服務器 * 3. 請求的服務不在 mock 服務列表中,則先從直連配置獲取服務信息,沒有則從注冊心上獲取服務信息,請求轉發到注冊中心上的服務 * 4. 請求的服務不在 mock 服務列表中,也沒有開啟全局 mock 功能,則請求服務走 直連配置和注冊中心相結合 * * @param request * @param options * @return * @throws IOException */ @Override public Response execute(Request request, Request.Options options) throws IOException { String url = request.url(); URI uri = URI.create(url); String clientName = uri.getHost(); String[] mockServer = mockProperties.getIpAddress().split(":"); //請求的客戶名稱轉為小寫 clientName = clientName.toUpperCase(); String newUrl = null; // 從全局中獲取該服務名稱的配置信息 IClientConfig clientConfig = this.clientFactory.getClientConfig(clientName); if (null != clientConfig && mockProperties.getGlobal()) { // 配置當前服務的ip地址信息 // clientConfig.set(CommonClientConfigKey.ListOfServers, mockProperties.getIpAddress()); // 獲取當前服務的負載均衡器,對當前服務的負載均衡器添加服務ip地址信息 if (this.clientFactory.getLoadBalancer(clientName).getAllServers().isEmpty()) { this.clientFactory.getLoadBalancer(clientName). addServers(Arrays.asList(new Server(mockServer[0], Integer.parseInt(mockServer[1])))); } // 重新構建請求 URL newUrl = this.getNewRequestUrl(url, clientName); log.info("請求的 {} 服務已開啟全局 mock 功能,服務地址:{}", clientName, this.clientFactory.getLoadBalancer(clientName).getAllServers()); } else { //獲取 配置 mock 服務的列表 String services = this.mockProperties.getServices(); if (StringUtils.isNotBlank(services)) { // 配置 mock 服務的列表轉為小寫 services = services.toUpperCase(); // mock 服務列表 List<String> service = Arrays.asList(services.split(",")); // 當前服務是否在 mock 服務列表中 if (service.contains(clientName)) { log.info("請求的 {} 服務在 mock 服務列表中,服務地址:{}", clientName, clientConfig.get(CommonClientConfigKey.ListOfServers)); newUrl = this.getNewRequestUrl(url, clientName); } else { // 服務直連情況 加 注冊中心 處理 newUrl = getServiceInfoFromDiscoveryClient(url, clientName, clientConfig); } } else { if (mockProperties.getServicesMap().isEmpty()) { String msg = "沒有配置 mock 服務列表,也沒有開啟全局mock功能,也沒有配置服務直連,請檢查配置或關閉mock功能"; throw new HystrixRuntimeException(HystrixRuntimeException.FailureType.BAD_REQUEST_EXCEPTION, null, msg, null, null); } // 服務直連情況 加 注冊中心 處理 newUrl = getServiceInfoFromDiscoveryClient(url, clientName, clientConfig); } } return this.getResponse(request, options, newUrl); } /** * 1.如果有服務直接配置,則直接返回url * 2.如果不是直連,則從注冊中上獲取服務信息,並返回url * * @param url * @param clientName * @param clientConfig * @return */ private String getServiceInfoFromDiscoveryClient(String url, String clientName, IClientConfig clientConfig) { String newUrl; //服務直連處理 if (mockProperties.getServicesMap().size() > 0) { // 處理一些自定義的服務和ip地址,服務的直連情況 Set<String> customServiceInfo = mockProperties.getServicesMap().keySet(); if (customServiceInfo.contains(clientName.toUpperCase())) { log.info("請求的 {} 服務在直連列表中,服務地址:{}", clientName, mockProperties.getServicesMap().get(clientName)); newUrl = url; return newUrl; } } if (null == this.discoveryClient) { String format = String.format("%s 服務沒有配置在mock列表中,並且也沒有開啟注冊中心功能,請檢查配置", clientName); throw new HystrixRuntimeException(HystrixRuntimeException.FailureType.COMMAND_EXCEPTION, null, format, null, null); } // 獲取 服務名的 服務信息 List<ServiceInstance> instances = this.discoveryClient.getInstances(clientName); String host; if (null == instances || instances.isEmpty()) { host = String.format("%s 服務沒有配置在mock列表中,也沒有注冊在住冊中心上,請檢查配置", clientName); throw new HystrixRuntimeException(HystrixRuntimeException.FailureType.COMMAND_EXCEPTION, null, host, null, null); } // 獲取 服務在 注冊中心的地址信息 host = instances.get(0).getHost() + ":" + instances.get(0).getPort(); log.info("請求的 {} 服務在注則中心上,服務地址:{}", clientName, host); newUrl = url; if (null != clientConfig) { // clientConfig.set(CommonClientConfigKey.ListOfServers, host); // 獲取當前服務的負載均衡器,對當前服務的負載均衡器添加服務ip地址信息 if (this.clientFactory.getLoadBalancer(clientName).getAllServers().isEmpty()) { this.clientFactory.getLoadBalancer(clientName). addServers(Arrays.asList(new Server(instances.get(0).getHost(), instances.get(0).getPort()))); } } return newUrl; } /** * 請求響應 * * @param request * @param options * @param newUrl * @return * @throws IOException */ private Response getResponse(Request request, Request.Options options, String newUrl) throws IOException { //重新構建 request 對象 Request newRequest = Request.create(request.method(), newUrl, request.headers(), request.body(), request.charset()); return super.execute(newRequest, options); } /** * 修改請求 url * * @param url * @param clientName * @return */ private String getNewRequestUrl(String url, String clientName) { StringBuilder sb = new StringBuilder(); sb.append(clientName); String mockServerUrl = mockProperties.getMockServerUrl(); if (mockServerUrl.endsWith("/")) { sb.append(mockServerUrl); } else { sb.append(mockServerUrl).append("/"); } sb.append(clientName.toLowerCase()); String newUrl = url.replaceFirst(clientName, sb.toString()); log.info("mock 服務重新構建請求 URL 地址:{}", newUrl); return newUrl; } }
配置自化配置類
@Slf4j @ConditionalOnProperty(prefix = MockProperties.MOCK_PREFIX, name = "enabled", havingValue = "true") @EnableConfigurationProperties(value = {MockProperties.class}) @Configuration public class MockAutoConfiguration { @Bean public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory, @Autowired(required = false) DiscoveryClient discoveryClient, MockProperties mockProperties) { return new MockLoadBalancerFeignClient(new Client.Default(null, null), cachingFactory, clientFactory, mockProperties, discoveryClient); } @Bean public MockFeignInterceptor mockFeignInterceptor() { return new MockFeignInterceptor(); } }
在 spring.factories 中配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ MockAutoConfiguration
自定義相關屬性配置和提示,關於mock的屬性配置
@ConfigurationProperties(prefix = MockProperties.MOCK_PREFIX, ignoreUnknownFields = true) public class MockProperties { public static final String MOCK_PREFIX = "mock.server"; /** * 開啟mock ,true:開啟 false:關閉 */ @Getter @Setter private boolean enabled = false; /** * mock 服務的ip地址或域名 例:10.12.141.146:18081 */ @Getter @Setter private String ipAddress; /** * 如果每個服務的 mock server 的地址都一樣的使用該配置,多個服務以 ,號 隔開 例:order-service,user-service */ @Getter @Setter private String services; /** * 如果每個服務的 mock server 地址不一樣,使用該配置,key:服務名 value: ip地址 ,例 */ @Getter @Setter private Map<String, String> servicesMap = new ConcurrentHashMap<>(); /** * mock server 服務url */ @Getter @Setter private String mockServerUrl; /** * 是否需要所有服務都用 mock */ private boolean global = false; public boolean getGlobal() { return global; } public void setGlobal(boolean global) { this.global = global; } }
Mock server 相關屬性
當開啟mock 功能時,所有的 feign 請求都會帶上請求頭: X-Mock-Application: true
# 開啟 mock 功能
mock.server.enabled=true
# mock 服務器ip地址和端口
mock.server.ip-address=123.90.8.1:18820
# mock 服務器的 url
mock.server.mock-server-url=/admin/mock
# mock 服務的列表,這里填寫 feign 服務的服務名,多個以 ,號隔開,並大寫
mock.server.services=PLATFORM-SERVICE,HELLO-SERVICE-1
mock.server.enabled=true
# mock 服務器ip地址和端口
mock.server.ip-address=123.90.8.1:18820
# mock 服務器的 url
mock.server.mock-server-url=/admin/mock
# mock 服務的列表,這里填寫 feign 服務的服務名,多個以 ,號隔開,並大寫
mock.server.services=PLATFORM-SERVICE,HELLO-SERVICE-1
如果項目中需要所有服務都要使用 mock 功能,則添加下面的屬性
# 開啟全局 mock 功能,也就是項目中所有的 feign服務都需要 mock
mock.server.global=true
如果項目中需要服務直連,則添加下面的屬性
# user-service 服務的直連ip地址和端口
mock.server.services-map.user-service=192.168.111.10:9010
# cache-service 服務的直連ip地址和端口
mock.server.services-map.cache-service=10.10.90.23:9090