Spring Cloud microservice declarative REST client -- Spring Cloud Feign

Spring Cloud microservice series articles

Spring Cloud microservice (1) registry and configuration center -- Nacos
Spring Cloud microservice (2) microservice Gateway -- Spring Cloud Gateway
Spring Cloud microservice (3) declarative REST client -- Spring Cloud Feign

preface

This paper mainly introduces one of the communication methods between micro services, which realizes the mutual call between services based on Feign.

1, Introduction

1. Official description

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 annotations used by default in httpmessageconverters spring Web. When using feign, Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client.

Official website documents: https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html

2. Bottom implementation scheme

  • Implementation based on HTTP protocol

  • OpenFeign is a declarative Web service client, which makes it easier to write Web service clients. You only need to create an interface and add annotations. At the same time, it integrates Ribbon, which can easily achieve the effect of load balancing.

  • At the bottom of Feign, the implementation class is generated based on the interface oriented dynamic agent, and the request call is delegated to the dynamic agent implementation class

  • Trigger Spring application to scan @ FeignClient modifier class in classpath through @ enablefeigncleins

  • After resolving to the @ FeignClient modifier class, the Feign framework finally registers a feignclientfactorybean into the Spring container by extending the registration logic of Spring bean definition

  • When initializing other classes that use the @ FeignClient interface, the Spring container obtains a Proxy object Proxy generated by feignclientfactorybean

  • Based on java's native dynamic Proxy mechanism, all calls to Proxy will be uniformly forwarded to an InvocationHandler defined by Feign framework, which will complete the subsequent HTTP conversion, sending, receiving and translation of HTTP responses

2, Using Feign for microservices

1. Create project

Create two Spring Boot projects, producer server and consumer server, one as a service provider and the other as a service consumer (there is no clear concept of service provider and consumer in actual production, which is defined here for the convenience of example illustration)

2. Service provider producer server

  • Introduce dependencies (only key configurations are listed here)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
  • Add service profile application YML (this example only lists the basic configuration information)

    server:
      port: 2121
    spring:
      application:
        name: producer-server
    
      cloud:
        nacos: # Registry Nacos
          discovery:
            enabled: true
            server-addr: localhost:8848
            watch:
              enabled: true
    
  • Write the project startup class, add the @ EnableFeignClients annotation, and enable the Feign function

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class ProducerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ProducerApplication.class, args);
        }
    }
    
    
  • Write service interface class

    @Slf4j
    @RestController
    @RequestMapping("/producer")
    public class ProducerController {
    
        @Value("${server.port}")
        private int serverPort;
    
        @RequestMapping(value = "/getById", method = RequestMethod.GET)
        public String getById(@RequestParam("id") Long id) {
            log.info("producer Service called ...");
            return serverPort + ": id=" + id;
        }
        
    	@RequestMapping(value = "/getUser", method = RequestMethod.GET)
    	public User getUser(User user) {
        	return user;
    	}
    
        @RequestMapping(value = "/postUser", method = RequestMethod.POST)
        public User postUser(@RequestBody User user) {
            return user;
        }
    }
    

3. Consumer server for service consumers

  • Introduce dependencies (only key configurations are listed here)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- Feign Remote call -->
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  • Add service profile application YML (this example only lists the basic configuration information)

    server:
      port: 2131
    
    spring:
      application:
        name: consumer-server
    
      cloud:
        nacos: # Registry Nacos
          discovery:
            enabled: true
            server-addr: localhost:8848
            watch:
              enabled: true
    
  • Write the project startup class, add the @ EnableFeignClients annotation, and enable the Feign function

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class ConsumerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class, args);
        }
    }
    
  • Write Feign call interface producer remote

    /**
     * @ FeignClient("same service name") on multiple interfaces will report an error
     * Use the contextId parameter to distinguish
     */
    @FeignClient(contextId = "mvcContract", name = "producer-server", fallback = ProducerBack.class)
    public interface ProducerRemote {
    
        @RequestMapping(value = "/producer/getById", method = RequestMethod.GET)
        String getById(@RequestParam("id") Long id);
        
        /**
         * Feign When the remote call parameter is a complex parameter or object, the request method is GET, and the request will be automatically converted to POST to request the service
         * Use @ SpringQueryMap annotation or POST request directly
         *
         * @param user
         * @return
         */
        @RequestMapping(value = "/producer/getUser", method = RequestMethod.GET)
        User getUser(@SpringQueryMap User user);
    
        /**
         * Post Request to pass complex parameters
         *
         * @param user
         * @return
         */
        @RequestMapping(value = "/producer/postUser", method = RequestMethod.POST)
        User postUser(@RequestBody User user);
    }
    
  • Write an exception handling class ProducerBack to implement ProducerRemote. Feign returns the specified data or exception prompt when calling the exception. This is usually used together with Hystrix. The startup class needs to add the @ enablercircuitbreaker annotation to open the circuit breaker (described in detail below)

    @Component // Here, you need to instantiate the @ Component annotation into the Spring container
    public class ProducerBack implements ProducerRemote {
    
        @Override
        public String getById(Long id) {
            return "Hystrix.enable true!";
        }
    
        @Override
        public User getUser(User user) {
            return null;
        }
    
        @Override
        public User postUser(User user) {
            return null;
        }
    }
    
  • Write a service interface class and test Feign interface call

    @Slf4j
    @RestController
    @RequestMapping("/consumer/feign")
    public class ConsumerFeignController {
    
        @Autowired
        private ProducerRemote producerRemote;
    
        @RequestMapping(value = "/getById/{id}", method = RequestMethod.GET)
        public Object getById(@PathVariable("id") Long id) {
            log.info("call producer service ...");
            return producerRemote.getById(id);
        }
        
        @RequestMapping(value = "/getUser", method = RequestMethod.GET)
        public Object getUser(User user) {
            return producerRemote.getUser(user);
        }
    
        @RequestMapping(value = "/postUser", method = RequestMethod.GET)
        public Object postUser(User user) {
            return producerRemote.postUser(user);
        }
    }
    

4. Test

  • Launch the Nacos registry
  • Start consumer server and producer server services
  • PostMan test interface

3, Integrated with Hystrix for service degradation

  • Introduce dependency

    <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    
  • Modify the service configuration file application YML add Hystrix configuration

    # Hystrix configuration
    feign:
      hystrix:
        enabled: true # Enable Feign Hystrix support
    
    # There are two ways to handle the Hystrix timeout
    hystrix:
      command:
        default:
          execution:
          # timeout:
    	  # enabled: false # Turn off the Hystrix timeout configuration
            isolation:
              thread:
                timeoutInMilliseconds: 60000 # Set timeout
    
  • Write the exception handling class ProducerBack to implement ProducerRemote (see the previous part of the code)

  • Add @ enablercircuitbreaker to startup class to open circuit breaker

4, Feign realizes file upload and download

1. Service provider: producer server

  • Add upload and download interface

    package com.zz.controller;
    
    import com.zz.model.User;
    import com.zz.remote.ConsumerRemote;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.*;
    import java.util.*;
    
    @Slf4j
    @RestController
    @RequestMapping("/producer")
    public class ProducerController {
    
        /**
         * File upload
         * @param file file
         * @return
         * @throws Exception
         */
        @RequestMapping(value = "/uploadFile", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
        public String uploadFile(@RequestPart(value = "file") MultipartFile file) throws Exception {
            return file.getOriginalFilename();
        }
    
        /**
         * Multiple file uploads
         * @param files File list
         * @return
         */
        @RequestMapping(value = "/uploadFiles", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
        public String uploadFiles(@RequestPart(value = "files") MultipartFile[] files) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < files.length; i++) {
                sb.append(files[i].getOriginalFilename() + "====");
            }
            return sb.toString();
        }
    
        /**
         * File download
         * @param filepath File path
         * @param fileName file name
         * @param response
         */
        @RequestMapping(value = "/downloadFile", method = RequestMethod.GET)
        public void downloadFile(@RequestParam("filepath") String filepath, @RequestParam("fileName") String fileName, HttpServletResponse response) {
            File file = new File(filepath);
            response.reset();
            InputStream in = null;
            OutputStream out = null;
            try {
                if (StringUtils.isEmpty(fileName)) {
                    fileName = file.getPath().substring(file.getPath().lastIndexOf("/") + 1);
                }
                response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes("utf-8"), "iso-8859-1"));
                // Download files as a stream
                in = new BufferedInputStream(new FileInputStream(file));
                byte[] buffer = new byte[in.available()];
                in.read(buffer);
                out = new BufferedOutputStream(response.getOutputStream());
                out.write(buffer);
                out.flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

2. Consumer server for service consumers

  • Add a custom configuration class to support multi file upload

    SpringMultipartEncoder.java

    import feign.RequestTemplate;
    import feign.codec.EncodeException;
    import feign.codec.Encoder;
    import feign.form.ContentType;
    import feign.form.FormEncoder;
    import feign.form.MultipartFormContentProcessor;
    import feign.form.multipart.PojoWriter;
    import feign.form.multipart.Writer;
    import feign.form.spring.SpringManyMultipartFilesWriter;
    import feign.form.spring.SpringSingleMultipartFileWriter;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.lang.reflect.Type;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Iterator;
    import java.util.Map;
    
    /**
     * User defined Encoder configuration supports multi file upload
     */
    public class SpringMultipartEncoder extends FormEncoder {
    
        public SpringMultipartEncoder() {
            this(new Encoder.Default());
        }
    
        public SpringMultipartEncoder(Encoder delegate) {
            super(delegate);
    
            MultipartFormContentProcessor processor = (MultipartFormContentProcessor) getContentProcessor(ContentType.MULTIPART);
            // Note that the adding order will cause problems in judging the file type
            processor.addFirstWriter(new SpringSingleMultipartFileWriter());
            processor.addFirstWriter(new SpringManyMultipartFilesWriter());
        }
    
        @Override
        public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
            if (bodyType.equals(MultipartFile[].class)) {
                MultipartFile[] files = (MultipartFile[]) object;
                Map data = Collections.singletonMap(files.length == 0 ? "" : files[0].getName(), object);
                super.encode(data, MAP_STRING_WILDCARD, template);
            } else if (bodyType.equals(MultipartFile.class)) {
                MultipartFile file = (MultipartFile) object;
                Map data = Collections.singletonMap(file.getName(), object);
                super.encode(data, MAP_STRING_WILDCARD, template);
            } else if (isMultipartFileCollection(object)) {
                Iterable iterable = (Iterable<?>) object;
                String fileName = "";
                for (Object item : iterable) {
                    MultipartFile file = (MultipartFile) item;
                    fileName = file.getName();
                    break;
                }
                Map data = Collections.singletonMap(fileName, object);
                super.encode(data, MAP_STRING_WILDCARD, template);
            } else {
                super.encode(object, bodyType, template);
            }
        }
    
        private boolean isMultipartFileCollection(Object object) {
            if (!(object instanceof Iterable)) {
                return false;
            }
            Iterable iterable = (Iterable<?>) object;
            Iterator iterator = iterable.iterator();
            return iterator.hasNext() && iterator.next() instanceof MultipartFile;
        }
    }
    

    FeignEncoderConfig.java

    import feign.codec.Encoder;
    import org.springframework.beans.factory.ObjectFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
    import org.springframework.cloud.openfeign.support.SpringEncoder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
      * Custom Feign policies can override the default configuration in FeignClientsConfiguration
      * It cannot be placed in the same level package or sub package of the @ SpringBootApplication annotation startup class
      * Cannot be placed under the package scanned by @ ComponentScan
     * Avoid ApplicationContext conflicts
     * Otherwise, all connections will use this configuration
     * Custom feign The encoder configuration supports multiple file uploads
     */
    @Configuration
    public class FeignEncoderConfig {
    
        @Autowired
        private ObjectFactory<HttpMessageConverters> messageConverters;
    
        @Bean
        public Encoder feignEncoder() {
            return new SpringMultipartEncoder(new SpringEncoder(messageConverters));
        }
    }
    
    
  • Modify the ProducerRemote class and add upload and download methods

    /**
     * @ FeignClient("same service name") on multiple interfaces will report an error
     * Use the contextId parameter to distinguish
     */
    @FeignClient(contextId = "mvcContract", name = "producer-server", fallback = ProducerBack.class, configuration = FeignEncoderConfig.class)
    public interface ProducerRemote {
    
        /**
         * Single file upload
         *
         * @param file
         * @return
         */
        @RequestMapping(value = "/producer/uploadFile", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
        String uploadFile(@RequestPart(value = "file") MultipartFile file);
    
        /**
         * Multi file upload
         *
         * @param files
         * @return
         */
        @RequestMapping(value = "/producer/uploadFiles", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
        String uploadFiles(@RequestPart(value = "files") MultipartFile[] files);
    
        /**
         * File download
         *
         * @param filepath
         * @param fileName
         * @return
         */
        @RequestMapping(value = "/producer/downloadFile", method = RequestMethod.GET)
        Response downloadFile(@RequestParam("filepath") String filepath, @RequestParam("fileName") String fileName);
    }
    
  • Modify the service interface class to test whether Feign file upload and download are available

    @Slf4j
    @RestController
    @RequestMapping("/consumer/feign")
    public class ConsumerFeignController {
    
        @Autowired
        private ProducerRemote producerRemote;
    
        @RequestMapping(value = "/uploadFile", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        public String uploadFile(@RequestPart(value = "file") MultipartFile file) {
            return producerRemote.uploadFile(file);
        }
    
        @RequestMapping(value = "/uploadFiles", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        public String uploadFiles(@RequestPart(value = "files") MultipartFile[] files) {
            return producerRemote.uploadFiles(files);
        }
    
        @RequestMapping(value = "/downloadFile", method = RequestMethod.GET)
        public Object downloadFile() {
            String path = "/Users/yehao/work/img/111.jpg";
            String fileName = "fileName.jpg";
            ResponseEntity<byte[]> result = null;
            InputStream inputStream = null;
            try {
                Response response = producerRemote.downloadFile(path, fileName);
                Response.Body body = response.body();
                inputStream = body.asInputStream();
                byte[] b = new byte[inputStream.available()];
                inputStream.read(b);
                HttpHeaders heads = new HttpHeaders();
                heads.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + new String(fileName.getBytes("utf-8"), "iso-8859-1"));
                heads.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
                result = new ResponseEntity<byte[]>(b, heads, HttpStatus.OK);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return result;
        }
    }
    

5, Use Feign style to call interface

  • Custom configuration class

    import feign.Contract;
    import org.springframework.beans.factory.ObjectFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FeignContractConfig {
    
        /**
    	  * Custom Feign policies can override the default configuration in FeignClientsConfiguration
     	  * It cannot be placed in the same level package or sub package of the @ SpringBootApplication annotation startup class
     	  * Cannot be placed under the package scanned by @ ComponentScan
         * Avoid ApplicationContext conflicts
         * Otherwise, all connections will use this configuration
    	 */
        @Bean
        public Contract feignContract() {
            return new feign.Contract.Default();
        }
    }
    
  • The Feign interface uses this configuration to producerfeignremote java

    import com.zz.config.FeignContractConfig;
    import com.zz.config.FeignLogConfig;
    import com.zz.model.User;
    import com.zz.remote.back.ProducerBackFactory;
    import feign.Param;
    import feign.QueryMap;
    import feign.RequestLine;
    import org.springframework.cloud.openfeign.FeignClient;
    
    import java.util.Map;
    
    @FeignClient(contextId = "feignContract", name = "producer-server", fallback = ProducerBack.class, configuration = FeignContractConfig.class)
    public interface ProducerFeignRemote {
    
        @RequestLine("GET /producer/getById/{id}")
        String getByIdPath(@Param("id") Long id);
    
        /**
         * Pass multiple parameters and use @ QueryMap annotation. The parameters can be Map or POJO object
         * Otherwise, the application/json format will be used by default to automatically convert POST request calls
         *
         * @param map
         * @return
         * @QueryMap Note: if a value is null, the value will be ignored by the reference
         */
        @RequestLine("GET /producer/getById")
        String getById(@QueryMap Map<String, Object> map);
    
        @RequestLine("POST /producer/postUser")
        User postUser(User user);
    }
    
    

6, Use FallbackFactory to get call exception information

  • Modify ProducerBack to add exception properties

    @Slf4j
    @Component // Here, you need to instantiate the @ Component annotation into the Spring container
    public class ProducerBack implements ProducerRemote {
    
        @Setter
    	private Throwable cause;
    	
        @Override
        public String getById(Long id) {
            log.error("Feign Call exception:{}", sysLog, cause);
        	return null;
        }
    
        @Override
        public User getUser(User user) {
            return null;
        }
    
        @Override
        public User postUser(User user) {
            return null;
        }
    }
    
  • Add producer fallbackfactory to get exception information

    @Component
    public class ProducerFallbackFactory implements FallbackFactory<ProducerRemote> {
    
        @Override
        public ProducerRemote create(Throwable cause) {
            ProducerBack producerBack = new ProducerBack ();
            producerBack.setCause(cause);
            return producerBack;
        }
    }
    
  • Modify the @ FeignClient annotation configuration of ProducerRemote method and add fallbackFactory

    	@FeignClient(contextId = "mvcContract", name = "producer-server", fallbackFactory = ProducerFallbackFactory .class)
    

Keywords: Java Spring Boot Spring Cloud rpc

Added by wizzkid on Fri, 11 Feb 2022 18:18:43 +0200