Spring cloud learning -- Spring Cloud OpenFeign service call

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

  1. JDK 1.8.0_201
  2. Spring Boot 2.2.0.RELEASE,Spring Cloud Hoxton.RELEASE
  3. Build tool (apache maven 3.6.3)
  4. 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

6. Reference

  1. Official document - Spring Cloud OpenFeign

Tags: Java Spring Spring Cloud

Posted by Someone789 on Fri, 20 May 2022 21:15:35 +0300