1. Introduction
1.1 general
Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
Feign is a declarative Web service client. It makes it easier to write Web service clients. To use feign, create an interface and annotate it. It has pluggable annotation support, including feign annotation and JAX-RS annotation. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and supports the use of the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon, Eureka and Spring Cloud LoadBalancer to provide a load balanced http client when using feign.
1.2 features
In a general scenario, if you want to send an http request, you need to call it according to the ip, port and url of the service provider. openfeign provides an interface based call method.
- Original calling method: client request(“ http://ip:port/service ”);
- openfeign calls the method: service request(args); openfeign calls according to the service name, and the caller configures the service name of the service provider spring application. name ;
Using openfeign to call remote services is like calling methods on interfaces in java code, and there is no need to write complex http request logic; If you integrate registration centers such as eureka, you don't even need to configure the url of the service provider, just configure eureka.
2. Demonstration environment
- JDK 1.8.0_201
- Spring Boot 2.2.0.RELEASE,Spring Cloud Hoxton.RELEASE
- Build tool (apache maven 3.6.3)
- Development tool (IntelliJ IDEA)
3. Demo code
Mixed application of Feign+Eureka+Hystrix
General structure description:
- OFC feign eureka server: eureka server, which provides service registration function;
- OFC feign user api: public api, defining model and interface, fallback;
- OFC feign user client: the service caller registers with eureka server using feign calling interface;
- OFC feign user server: a service provider that implements the interface defined in the api and registers with eureka server.
3.1 ofc-feign-eureka-server
3.1.1 code description
eureka server provides service registration function.
3.1.2 maven dependency
pom.xml
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
3.1.3 configuration file
application.properties
spring.application.name=ofc-feign-eureka-server # Application service web access port server.port=11040 # Service registry host name eureka.instance.hostname=localhost # Register yourself eureka.client.register-with-eureka=false # Retrieve service eureka.client.fetch-registry=false # eureka server address eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
3.1.4 java code
OfcFeignEurekaApplication.java
// Start eureka server @EnableEurekaServer @SpringBootApplication public class OfcFeignEurekaApplication { public static void main(String[] args) { SpringApplication.run(OfcFeignEurekaApplication.class, args); } }
3.2 ofc-feign-user-api
3.2.1 code description
Public api, which defines the entity model, public interface and fallback class.
3.2.2 maven dependency
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
3.2.3 configuration file
application.properties
spring.application.name=ofc-feign-user-api # Application service web access port server.port=8080
3.2.4 java code
UserModel.java
public class UserModel { private Long id; private String name; private Integer age; private String birthday; private String address; private String phone; public UserModel() {} public UserModel(Long id, String name, Integer age, String birthday, String address, String phone) { this.id = id; this.name = name; this.age = age; this.birthday = birthday; this.address = address; this.phone = phone; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getBirthday() { return birthday; } public void setBirthday(String birthday) { this.birthday = birthday; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String toString() { return "UserModel{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", birthday='" + birthday + '\'' + ", address='" + address + '\'' + ", phone='" + phone + '\'' + '}'; } }
UserService.java
// The service provider name is defined in value, and fallback is degraded @FeignClient(value = "ofc-feign-user-server", fallback = UserServiceFallback.class) public interface UserService { @GetMapping(value = "/user/list") List<UserModel> list(); @PostMapping(value = "/user/save") UserModel save(@RequestBody UserModel userModel); }
UserServiceFallback.java
public class UserServiceFallback implements UserService { @Override public List<UserModel> list() { return Collections.emptyList(); } @Override public UserModel save(UserModel userModel) { return new UserModel(); } }
OfcUserApiApplication.java
@SpringBootApplication public class OfcUserApiApplication { public static void main(String[] args) { SpringApplication.run(OfcUserApiApplication.class, args); } }
3.3 ofc-feign-user-server
3.3.1 code description
Implement the interface defined in api, provide external services and register with eureka server.
Integrate hystrix to realize service degradation.
3.3.2 maven dependency
pom.xml
<dependencies> <dependency> <groupId>com.soulballad.usage</groupId> <artifactId>ofc-feign-user-api</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies>
3.3.3 configuration file
application.properties
spring.application.name=ofc-feign-user-server # Application service web access port server.port=11041 eureka.server.host=localhost eureka.server.port=11040 eureka.client.service-url.defaultZone=http://${eureka.server.host}:${eureka.server.port}/eureka/
3.3.4 java code
UserServerController.java
@RestController public class UserServerController { private static final Map<Long, UserModel> USER_MAP = new HashMap<>(); private static final AtomicLong ID_GENERATOR = new AtomicLong(2); private final Random random = new Random(); private static final Logger LOGGER = LoggerFactory.getLogger(UserServerController.class); @GetMapping(value = "/user/list") @HystrixCommand(fallbackMethod = "fallBackList", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100") }) public List<UserModel> list() throws InterruptedException { int seconds = random.nextInt(200); LOGGER.info("user server controller list sleep for {} seconds!", seconds); Thread.sleep(seconds); return new ArrayList<>(USER_MAP.values()); } @PostMapping(value = "/user/save") public UserModel save(@RequestBody UserModel userModel) { long id = ID_GENERATOR.incrementAndGet(); userModel.setId(id); USER_MAP.put(id, userModel); return userModel; } public List<UserModel> fallBackList() { LOGGER.warn("user server controller list fallback!"); return Collections.emptyList(); } // Initialize 2 pieces of data @PostConstruct public void init() { UserModel user1 = new UserModel(1L, "zhangsan", 20, "2000-01-01", "shenzhen", "13888888888"); UserModel user2 = new UserModel(2L, "lisi", 21, "1999-01-01", "shanghai", "13777777777"); USER_MAP.put(user1.getId(), user1); USER_MAP.put(user2.getId(), user2); } }
OfcFeignUserServerApplication.java
@EnableHystrix @EnableEurekaClient @SpringBootApplication public class OfcFeignUserServerApplication { public static void main(String[] args) { SpringApplication.run(OfcFeignUserServerApplication.class, args); } }
3.4 ofc-feign-user-client
3.4.1 code description
Call the service according to the UserService interface in the api, and also register with eureka server.
3.4.2 maven dependency
pom.xml
<dependencies> <dependency> <groupId>com.soulballad.usage</groupId> <artifactId>ofc-feign-user-api</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies>
3.2.3 configuration file
application.properties
spring.application.name=ofc-feign-user-client # Application service web access port server.port=11042 eureka.server.host=localhost eureka.server.port=11040 eureka.client.service-url.defaultZone=http://${eureka.server.host}:${eureka.server.port}/eureka/ # Set timeout and log level feign.client.config.default.connect-timeout=5000 feign.client.config.default.read-timeout=5000 feign.client.config.default.logger-level=full logging.level.com.soulballad.usage.springcloud=debug
3.2.4 java code
UserClientController.java
@RestController public class UserClientController implements UserService { private final UserService userService; @Autowired public UserClientController(UserService userService) { this.userService = userService; } @Override public List<UserModel> list() { return userService.list(); } @Override public UserModel save(@RequestBody UserModel userModel) { return userService.save(userModel); } }
UserService.java
// Enable feign, and the interface is UserService; If it is not configured, all classes modified by @ FeignClient annotation will be scanned by default @EnableEurekaClient @SpringBootApplication @EnableFeignClients(clients = UserService.class) public class OfcFeignUserClientApplication { public static void main(String[] args) { SpringApplication.run(OfcFeignUserClientApplication.class, args); } }
3.5 git address
spring-cloud-ofc-04-feign : distributed service invocation scheme officially provided by Spring Cloud
4. Effect display
Start springboot03webapplication Main method, in spring-boot-03-webmvc HTTP visit the following addresses and observe whether the output information meets the expectations.
Start OFC feign Eureka server, OFC feign user server and OFC feign user client services successively;
They are monitored at ports 11040, 11041 and 11042 respectively. After startup, they can be seen on the eureka management console:
### GET eureka GET http://localhost:11040/
In spring cloud OFC feign Visit the following address in HTTP to check whether the request result meets the expectation
4.1 ofc-feign-user-server
Query user list
### GET /user/list GET http://localhost:11041/user/list
New user
### POST /user/save POST http://localhost:11041/user/save Accept: application/json Content-Type: application/json { "name": "wangwu", "age": 30, "birthday": "1980-03-01", "address": "guangzhou", "phone": "13666666666" }
4.2 ofc-feign-user-client
Query user list
### GET /user/list GET http://localhost:11042/user/list
New user
### POST /user/save POST http://localhost:11042/user/save Accept: application/json Content-Type: application/json { "name": "zhaoliu", "age": 40, "birthday": "1970-04-02", "address": "wuhan", "phone": "13555555555" }
5. Source code analysis
5.1 how does feign invoke services?
When calling / user/list, you can see that UserService is a proxy object, which is dynamically proxy by jdk.
If you continue, you will call reflectivefeign Feigninvocationhandler #invoke method, the calling path is as follows
5.2 how are proxy objects created?
The @ EnableFeignClients annotation is added to the offcfeignuserclientapplication to enable the feign function. In @ EnableFeignClients, @ feignclientsregister is introduced through @ Import
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { String[] value() default {}; String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] defaultConfiguration() default {}; Class<?>[] clients() default {}; }
@Feignclientsregister implements the importbeandefinitionregister interface, which can dynamically load beans
Definition of
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // Register default configuration registerDefaultConfiguration(metadata, registry); // Register FeignClient registerFeignClients(metadata, registry); }
registerFeignClients scans all classes added with @ FeignClient, and then calls the registerFeignClient method to register
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // Get a scanner scan object ClassPathScanningCandidateComponentProvider scanner = getScanner(); // Resource loader, current application context scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; // Get the properties of @ EnableFeignClients Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName()); AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( FeignClient.class); // Determine whether the clients property is configured final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients"); if (clients == null || clients.length == 0) { // If not configured, get basePackage scanner.addIncludeFilter(annotationTypeFilter); basePackages = getBasePackages(metadata); } else { // Otherwise, only the path of the class in clients is scanned final Set<String> clientClasses = new HashSet<>(); basePackages = new HashSet<>(); for (Class<?> clazz : clients) { basePackages.add(ClassUtils.getPackageName(clazz)); clientClasses.add(clazz.getCanonicalName()); } AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { @Override protected boolean match(ClassMetadata metadata) { String cleaned = metadata.getClassName().replaceAll("\\$", "."); return clientClasses.contains(cleaned); } }; scanner.addIncludeFilter( new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); } // Traverse basePackages for (String basePackage : basePackages) { Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage); for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); String name = getClientName(attributes); // Register the configuration configuration on FeignClient registerClientConfiguration(registry, name, attributes.get("configuration")); // Register FeignClient registerFeignClient(registry, annotationMetadata, attributes); } } } }
registerFeignClient() builds a BeanDefinitionBuilder object through genericBeanDefinition, which passes in a FeignClientFactoryBean parameter
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); // Generate a BeanDefinitionBuilder object through FeignClientFactoryBean BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); validate(attributes); // Set some properties definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); String contextId = getContextId(attributes); definition.addPropertyValue("contextId", contextId); definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = contextId + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be // null beanDefinition.setPrimary(primary); // Determine whether there is an alias String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } // Packaging with BeanDefinitionHolder BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); // Register in registry BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
FeignClientFactoryBean is a factory bean, which implements the FactoryBean interface, rewrites the getObject method, and customizes the initialization of the bean
@Override public Object getObject() throws Exception { return getTarget(); }
getTarget determines whether there is a specified url. If a url is specified, it is called directly; Otherwise, select a service provider using load balancing. It is not specified here, so loadBalance is called
<T> T getTarget() { // Get FeignContext from application context FeignContext context = this.applicationContext.getBean(FeignContext.class); // Initialize feign Builder Feign.Builder builder = feign(context); if (!StringUtils.hasText(this.url)) { // If no url is specified, select a service provider using load balancing if (!this.name.startsWith("http")) { this.url = "http://" + this.name; } else { this.url = this.name; } this.url += cleanPath(); // Load balancing call return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } // If the url is specified, call the corresponding service directly if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { // not load balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient) client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); }
loadBalance through target Proxy target
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { // Get an instance of Client Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = get(context, Targeter.class); // agent return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); }
In the target method, create a ReflectiveFeign object through build(), and then call its newInstance method to generate a proxy object
@Override public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { return feign.target(target); }
public <T> T target(Target<T> target) { // Call newInstance to generate proxy object return this.build().newInstance(target); } public Feign build() { // Create factory class Factory synchronousMethodHandlerFactory = new Factory(this.client, this.retryer, this.requestInterceptors, this.logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy); ParseHandlersByName handlersByName = new ParseHandlersByName(this.contract, this.options, this.encoder, this.decoder, this.queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory); // Create a ReflectiveFeign object return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder); }
Call ReflectiveFeign#newInstance to generate jdk proxy object
public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList(); Method[] var5 = target.type().getMethods(); int var6 = var5.length; for(int var7 = 0; var7 < var6; ++var7) { Method method = var5[var7]; if (method.getDeclaringClass() != Object.class) { if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } } // Create InvocationHandler InvocationHandler handler = this.factory.create(target, methodToHandler); // Generate proxy object T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); Iterator var12 = defaultMethodHandlers.iterator(); while(var12.hasNext()) { DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next(); defaultMethodHandler.bindTo(proxy); } return proxy; }
The factory here is InvocationHandlerFactory, and its create method is called
public InvocationHandler create(Target target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) { return new FeignInvocationHandler(target, dispatch); }
So the final generated proxy object is FeignInvocationHandler