View Javadoc
1   /*
2    * Copyright © 2025 rosestack.github.io
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.springframework.cloud.openfeign;
17  
18  import io.github.rose.feign.FeignAutoConfiguration;
19  import org.slf4j.Logger;
20  import org.slf4j.LoggerFactory;
21  import org.springframework.beans.factory.BeanClassLoaderAware;
22  import org.springframework.beans.factory.config.BeanDefinitionHolder;
23  import org.springframework.beans.factory.support.AbstractBeanDefinition;
24  import org.springframework.beans.factory.support.BeanDefinitionBuilder;
25  import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
26  import org.springframework.beans.factory.support.BeanDefinitionRegistry;
27  import org.springframework.boot.context.annotation.ImportCandidates;
28  import org.springframework.context.EnvironmentAware;
29  import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
30  import org.springframework.core.annotation.AnnotatedElementUtils;
31  import org.springframework.core.annotation.AnnotationAttributes;
32  import org.springframework.core.env.Environment;
33  import org.springframework.core.io.support.SpringFactoriesLoader;
34  import org.springframework.core.type.AnnotationMetadata;
35  import org.springframework.lang.Nullable;
36  import org.springframework.util.StringUtils;
37  
38  import java.util.ArrayList;
39  import java.util.List;
40  import java.util.Map;
41  
42  /**
43   * <p>
44   * feign 自动配置功能 from mica
45   */
46  public class AutoFeignClientsRegistrar
47      implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, EnvironmentAware {
48      private static final Logger log = LoggerFactory.getLogger(AutoFeignClientsRegistrar.class);
49  
50      private static final String BASE_URL = "http://127.0.0.1:${server.port}${server.servlet.context-path}";
51  
52      private ClassLoader beanClassLoader;
53  
54      private Environment environment;
55  
56      public ClassLoader getBeanClassLoader() {
57          return beanClassLoader;
58      }
59  
60      @Override
61      public void setBeanClassLoader(ClassLoader classLoader) {
62          this.beanClassLoader = classLoader;
63      }
64  
65      public Environment getEnvironment() {
66          return environment;
67      }
68  
69      @Override
70      public void setEnvironment(Environment environment) {
71          this.environment = environment;
72      }
73  
74      @Override
75      public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
76          registerFeignClients(registry);
77      }
78  
79      private void registerFeignClients(BeanDefinitionRegistry registry) {
80  
81          List<String> feignClients = new ArrayList<>(
82              SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
83  
84          // 支持 springboot 2.7 + 最新版本的配置方式
85          ImportCandidates.load(FeignClient.class, getBeanClassLoader()).forEach(feignClients::add);
86          // 如果 spring.factories 里为空
87          if (feignClients.isEmpty()) {
88              return;
89          }
90          for (String className : feignClients) {
91              try {
92                  Class<?> clazz = beanClassLoader.loadClass(className);
93                  AnnotationAttributes attributes =
94                      AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, FeignClient.class);
95                  if (attributes == null) {
96                      continue;
97                  }
98  
99                  // 如果是单体项目自动注入 & url 为空
100                 Boolean isMicro = environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true);
101                 // 如果已经存在该 bean,支持原生的 Feign
102                 if (registry.containsBeanDefinition(className) && isMicro) {
103                     continue;
104                 }
105 
106                 registerClientConfiguration(registry, getClientName(attributes), attributes.get("configuration"));
107 
108                 validate(attributes);
109                 BeanDefinitionBuilder definition =
110                     BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
111                 definition.addPropertyValue("url", getUrl(registry, attributes));
112                 definition.addPropertyValue("path", getPath(attributes));
113                 String name = getName(attributes);
114                 definition.addPropertyValue("name", name);
115 
116                 // 兼容最新版本的 spring-cloud-openfeign,尚未发布
117                 StringBuilder aliasBuilder = new StringBuilder(18);
118                 if (attributes.containsKey("contextId")) {
119                     String contextId = getContextId(attributes);
120                     aliasBuilder.append(contextId);
121                     definition.addPropertyValue("contextId", contextId);
122                 } else {
123                     aliasBuilder.append(name);
124                 }
125 
126                 definition.addPropertyValue("type", className);
127                 definition.addPropertyValue("decode404", attributes.get("decode404"));
128                 definition.addPropertyValue("fallback", attributes.get("fallback"));
129                 definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
130                 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
131 
132                 AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
133 
134                 // alias
135                 String alias = aliasBuilder.append("FeignClient").toString();
136 
137                 // has a default, won't be null
138                 boolean primary = (Boolean) attributes.get("primary");
139 
140                 beanDefinition.setPrimary(primary);
141 
142                 String qualifier = getQualifier(attributes);
143                 if (StringUtils.hasText(qualifier)) {
144                     alias = qualifier;
145                 }
146 
147                 BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
148                 BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
149             } catch (ClassNotFoundException e) {
150                 log.error("ClassNotFound", e);
151             }
152         }
153     }
154 
155     /**
156      * Return the class used by {@link SpringFactoriesLoader} to load configuration
157      * candidates.
158      *
159      * @return the factory class
160      */
161     private Class<?> getSpringFactoriesLoaderFactoryClass() {
162         return FeignAutoConfiguration.class;
163     }
164 
165     private void validate(Map<String, Object> attributes) {
166         AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
167         // This blows up if an aliased property is overspecified
168         FeignClientsRegistrar.validateFallback(annotation.getClass("fallback"));
169         FeignClientsRegistrar.validateFallbackFactory(annotation.getClass("fallbackFactory"));
170     }
171 
172     private String getName(Map<String, Object> attributes) {
173         String name = (String) attributes.get("serviceId");
174         if (!StringUtils.hasText(name)) {
175             name = (String) attributes.get("name");
176         }
177         if (!StringUtils.hasText(name)) {
178             name = (String) attributes.get("value");
179         }
180         name = resolve(name);
181         return FeignClientsRegistrar.getName(name);
182     }
183 
184     private String getContextId(Map<String, Object> attributes) {
185         String contextId = (String) attributes.get("contextId");
186         if (!StringUtils.hasText(contextId)) {
187             return getName(attributes);
188         }
189 
190         contextId = resolve(contextId);
191         return FeignClientsRegistrar.getName(contextId);
192     }
193 
194     private String resolve(String value) {
195         if (StringUtils.hasText(value)) {
196             return this.environment.resolvePlaceholders(value);
197         }
198         return value;
199     }
200 
201     private String getUrl(BeanDefinitionRegistry registry, Map<String, Object> attributes) {
202 
203         // 如果是单体项目自动注入 & url 为空
204         Boolean isMicro = environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true);
205 
206         if (isMicro) {
207             return null;
208         }
209 
210         String url = resolve(BASE_URL);
211 
212         return FeignClientsRegistrar.getUrl(url);
213     }
214 
215     private String getPath(Map<String, Object> attributes) {
216         String path = resolve((String) attributes.get("path"));
217         return FeignClientsRegistrar.getPath(path);
218     }
219 
220     @Nullable
221     private String getQualifier(@Nullable Map<String, Object> client) {
222         if (client == null) {
223             return null;
224         }
225         String qualifier = (String) client.get("qualifier");
226         if (StringUtils.hasText(qualifier)) {
227             return qualifier;
228         }
229         return null;
230     }
231 
232     @Nullable
233     private String getClientName(@Nullable Map<String, Object> client) {
234         if (client == null) {
235             return null;
236         }
237         String value = (String) client.get("contextId");
238         if (!StringUtils.hasText(value)) {
239             value = (String) client.get("value");
240         }
241         if (!StringUtils.hasText(value)) {
242             value = (String) client.get("name");
243         }
244         if (!StringUtils.hasText(value)) {
245             value = (String) client.get("serviceId");
246         }
247         if (StringUtils.hasText(value)) {
248             return value;
249         }
250 
251         throw new IllegalStateException(
252             "Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName());
253     }
254 
255     private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
256         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
257         builder.addConstructorArgValue(name);
258         builder.addConstructorArgValue(configuration);
259         registry.registerBeanDefinition(
260             name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
261     }
262 }