“This is the 15th day of my participation in the More Text Challenge. For more details, see more Text Challenge.”
Introduction to Spring Cloud OpenFeign Basics
OpenFeign of actual combat
Replace the default Client
By default, Feign uses the JDK’s native URLConnection to send HTTP requests. There is no connection pool, but it maintains one persistent connection per address, using HTTP’s persistence connection. This can be replaced with a good Client. This allows you to set connection pools, timeouts, and so on to tune calls between services. Here’s how to replace Feign’s default Client with Http Client and Okhttp. The steps are simple.
Use Http Client to replace the default Client
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<! -- Dependencies on Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<! -- Use Apache HttpClient instead of Feign native HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
</dependencies>
Copy the code
application.yml
server:
port: 8010
spring:
application:
name: openfeign-httpclient
feign:
httpclient:
enabled: true
Copy the code
Some of the Http Client configuration can also be configured in the configuration file
In the org. Springframework. Cloud. Openfeign. Clientconfig. Is about HttpClient HttpClientFeignConfiguration configuration:
@Configuration( proxyBeanMethods = false )
@ConditionalOnMissingBean({CloseableHttpClient.class})
public class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer".true);
private CloseableHttpClient httpClient;
@Autowired( required = false )
private RegistryBuilder registryBuilder;
public HttpClientFeignConfiguration(a) {}@Bean
@ConditionalOnMissingBean({HttpClientConnectionManager.class})
public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
public void run(a) { connectionManager.closeExpiredConnections(); }},30000L, (long)httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
@Bean
@ConditionalOnProperty( value = {"feign.compression.response.enabled"}, havingValue = "true" )
public CloseableHttpClient customHttpClient(HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement().useSystemProperties();
this.httpClient = this.createClient(builder, httpClientConnectionManager, httpClientProperties);
return this.httpClient;
}
@Bean
@ConditionalOnProperty( value = {"feign.compression.response.enabled"}, havingValue = "false", matchIfMissing = true )
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
this.httpClient = this.createClient(httpClientFactory.createBuilder(), httpClientConnectionManager, httpClientProperties);
return this.httpClient;
}
private CloseableHttpClient createClient(HttpClientBuilder builder, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) { RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProp erties.isFollowRedirects()).build(); CloseableHttpClient httpClient = builder.setDefaultRequestConfig(defaultRequestConfig).setConnectionManager(httpClientConnectionManager).build();return httpClient;
}
@PreDestroy
public void destroy(a) throws Exception {
this.connectionManagerTimer.cancel();
if (this.httpClient ! =null) {
this.httpClient.close(); }}}Copy the code
Obviously this class will generate the default configuration for the HttpClient when there is no CloseableHttpClient bean. So custom configuration for HttpClient can be done by injecting CloseableHttpClient yourself. And the bean HttpClientConnectionManager connection management. OpenFeign actually supports HttpClient well because some of its properties can be configured in a configuration file.
Replace the default Client with Okhttp
The same configuration as the Http Client is enabled in the configuration file
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<! -- Dependencies on Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
</dependencies>
Copy the code
application.yml
server:
port: 8011
spring:
application:
name: openfeign-okhttp
feign:
okhttp:
enabled: true
# log
logging:
level:
com.msr.better.feign.service.HelloFeignService: debug
Copy the code
When this is enabled, the Client is replaced. Similarly in the org. Springframework. Cloud. Openfeign. Clientconfig package, also have a about Okhttp configuration class.
@Configuration( proxyBeanMethods = false )
@ConditionalOnMissingBean({OkHttpClient.class})
public class OkHttpFeignConfiguration {
private OkHttpClient okHttpClient;
public OkHttpFeignConfiguration(a) {}@Bean
@ConditionalOnMissingBean({ConnectionPool.class})
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}
@Bean
public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()).connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects).connectionPool(connectionPool).build();
return this.okHttpClient;
}
@PreDestroy
public void destroy(a) {
if (this.okHttpClient ! =null) {
this.okHttpClient.dispatcher().executorService().shutdown();
this.okHttpClient.connectionPool().evictAll(); }}}Copy the code
Obviously OkHttpClient is the class that performs the core functionality. Because there is a class of OpenFeign FeignHttpClientProperties, with this kind of HttpClient properties can be set up in the configuration file. Okhttp does not have a similar class, so you can generally inject your own OkHttpClient to set these properties
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class OkHttpConfig {
@Bean
public okhttp3.OkHttpClient okHttpClient(a) {
return new okhttp3.OkHttpClient.Builder()
// Set connection timeout
.connectTimeout(60, TimeUnit.SECONDS)
// Set the read timeout
.readTimeout(60, TimeUnit.SECONDS)
// Set write timeout
.writeTimeout(60, TimeUnit.SECONDS)
// Whether to automatically reconnect
.retryOnConnectionFailure(true)
.connectionPool(new ConnectionPool())
// Build the OkHttpClient object.build(); }}Copy the code
For a custom OkHttpClient configuration, see OpenFeign’s OkHttpFeignConfiguration, such as the ConnectionPool bean.
Multi-parameter passing for Post and Get
When using OpenFeign to implement calls between services, many times multiple parameters are passed.
Create the cloud-OpenFeign-Eureka-server module
Eureka Server registry
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<! -- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<! Use undertow instead of Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
</dependency>
</dependencies>
Copy the code
Configuration file application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
server :
enable-self-preservation: false
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Copy the code
Start the class
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); }}Copy the code
Create cloud-OpenFeign-Provider module
Service provider
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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>
Copy the code
Configuration file application.yml
server:
port: 8012
spring:
application:
name: openfeign-provider
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
#eureka.instance.prefer-ip-address indicates that the IP address is registered with the Eureka Server.
If not configured, the host name of the current service provider will be registered with the Eureka Server.
instance:
prefer-ip-address: true
Copy the code
Entity classes and controllers
public class Order {
private Long id;
private String name;
private int age;
public Long getId(a) {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge(a) {
return age;
}
public void setAge(int age) {
this.age = age; }}Copy the code
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping(value = "/add")
public String addUser(Order order, HttpServletRequest request) {
String token = request.getHeader("oauthToken");
return "hello," + order.getName();
}
@PostMapping(value = "/update")
public String updateUser(@RequestBody Order order) {
return "hello,"+ order.getName(); }}Copy the code
Start the class
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {
public static void main(String[] args) { SpringApplication.run(ProviderApplication.class, args); }}Copy the code
Create the cloud-OpenFeign-Consumer module
Consumer services
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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>
<! -- Use Apache HttpClient instead of Feign native HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
</dependencies>
Copy the code
Configuration file application.yml
server:
port: 8011
spring:
application:
name: openfeign-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
feign:
httpclient:
enabled: true
Copy the code
Entity class
package com.msr.better.feign.model;
public class Order {
private Long id;
private String name;
private int nums;
// The getter and setter are omitted
}
Copy the code
FeignClient interface
@FeignClient("openfeign-provider")
public interface OrderApiService {
@GetMapping(value = "/order/add")
String addUser(@SpringQueryMap Order order);
@PostMapping(value = "/order/update")
String updateUser(@RequestBody Order order);
}
Copy the code
The Client interface here uses the annotation @SpringQueryMap for the GET request passing entity class. The OpenFeign@QueryMap annotation supports poJOs as GET parameter mappings. But the default OpenFeign QueryMap annotation is incompatible with Spring because it lacks the value attribute.
Spring Cloud OpenFeign provides the equivalent @SpringQueryMap annotation for annotating POJO or Map parameters as query parameter mappings.
In some sources, it is said that OpenFeign’s GET can’t pass POJOs. I wrote an interceptor to convert entity classes. I think OpenFeign has a lower version, and the new OpenFeign has support for QueryMap.
The configuration class
@Configuration
public class CoreAutoConfiguration {
@Autowired
private HttpClient httpClient;
@Bean
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory(a) {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
factory.setReadTimeout(3000);
factory.setConnectTimeout(3000);
factory.setConnectionRequestTimeout(3000);
return factory;
}
/ * * * {@linkThe setRequestFactory method of RestTemplate} supports HttpClient and Okhttp clients.@linkHttp request is to use native URLConnection SimpleClientHttpRequestFactory} * *@returnRestTemplate bean * /
@LoadBalanced
@Bean
public RestTemplate restTemplate(a) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(httpComponentsClientHttpRequestFactory());
returnrestTemplate; }}Copy the code
Above is the Client that replaced the RestTemplate. Because RestTemplate uses URLConnection by default. HttpClient is used instead.
The controller
@RestController
@RequestMapping("api")
public class OrderController {
@Autowired
private OrderApiService orderApiService;
/ * * *@param order
* @return* /
@PostMapping("/get/pojo")
public String getPojo(@RequestBody Order order) {
return orderApiService.addUser(order);
}
@PostMapping("/post/pojo")
String postPojo(@RequestBody Order order){
returnorderApiService.updateUser(order); }}Copy the code
Finally can test the http://localhost:8011/get/pojo and http://localhost:8011/post/pojo.
File upload
Continue using the Eureka Server created in the previous section. Then create the following two modules for file upload.
To achieve file upload function, you need to write Encoder to achieve file upload. Now OpenFeign offers a subproject called Feign – Form (github.com/OpenFeign/f…)
Create a cloud – openfeign – fileupload – server
Provider of the file upload interface
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
Copy the code
Configuration file application.yml
server:
port: 8012
spring:
application:
name: openfeign-file-server
eureka:
server:
enableSelfPreservation: false
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
Copy the code
Start the class
@SpringBootApplication
@EnableDiscoveryClient
public class SCFeignFileServerApplication {
public static void main(String[] args) { SpringApplication.run(SCFeignFileServerApplication.class, args); }}Copy the code
Upload interface
@RestController
public class FileController {
@PostMapping(value = "/uploadFile/server", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String fileUploadServer(MultipartFile file ) throws Exception{
returnfile.getOriginalFilename(); }}Copy the code
Create a cloud – openfeign – fileupload – client
The caller of the file upload interface
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<! -- Dependencies on Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<! Feign file upload dependencies -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
</dependency>
</dependencies>
Copy the code
Configuration file application.yml
server:
port: 8011
spring:
application:
name: openfeign-upload-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
Copy the code
The configuration class
@Configuration
public class FeignMultipartSupportConfig {
@Bean
@Primary
@Scope("prototype")
public Encoder multipartFormEncoder(a) {
return newSpringFormEncoder(); }}Copy the code
The controller
@RestController
@RequestMapping("file")
public class FeignUploadController {
@Autowired
private FileUploadApiService fileUploadApiService;
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String imageUpload(MultipartFile file) throws Exception {
returnfileUploadApiService.fileUpload(file); }}Copy the code
FeignClient
@FeignClient(value = "openfeign-file-server", configuration = FeignMultipartSupportConfig.class)
public interface FileUploadApiService {
/*** * 1. Produces, Consumes specifies * 2. Pay attention to distinguish between@RequestPartAnd RequestParam, don't take *@RequestPart(value = "file"@RequestParam(value = "file")
* @param file
* @return* /
@PostMapping(value = "/uploadFile/server", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String fileUpload(@RequestPart(value = "file") MultipartFile file);
}
Copy the code
test
Run the Eureka Server, cloud-OpenFeign – Fileupload-client module and cloud-OpenFeign – Fileupload-server module, using PostMan to test. Finally, the file name is successfully returned, and the file is successfully uploaded to the server.
Resolve the first request failure problem
Because OpenFeign integrates the Ribbon and Hystrix, the first call may fail.
The main reason: Hystrix’s default timeout is 1 second, and if there is no response after that, fallback code will be entered. Because of the Bean’s assembly and lazy loading mechanisms, Feign is slow on the first request. If the response time is longer than 1 second, the fallback will enter and the request will fail. Solution:
-
It is better to increase the timeout of Hystrix
hystrix: command: default: execution: isolation: thread: timeoutInMillseconds: 5000 # 5 seconds Copy the code
-
The timeout period of Hystrix is disabled
hystrix: command: default: execution: timout: enable: false Copy the code
-
Turn off Hystrix when using Feign, this is not recommended
feign: hystrix: enable: false Copy the code
Returns the processing of the image stream
If you return an image, it’s usually a byte array. But Contrller cannot return bytes directly, so the type returned by the called API should use Response.
Add a return image interface to the module created using the file upload above. Take the example of generating a QR code.
Some changes to cloud-openfeign- Fileupload-server
Add new dependencies and quickly generate qr codes using HuTool
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
Copy the code
Controller interface, here is simply generated a TWO-DIMENSIONAL code, two-dimensional code can also add more information. I won’t go into detail here, but there are many methods of QrCodeUtil of Hutool, and those who are interested can study them by themselves.
@GetMapping(value = "/qrcode")
public byte[] image() {
return generateQrCode();
}
/** * first simply generate a URL qr code, pointing to Baidu *@return* /
private byte[] generateQrCode() {
return QrCodeUtil.generatePng("https://www.baidu.cn/".300.300);
}
Copy the code
Some changes to cloud-openfeign- Fileupload-client
Adding a New dependency
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
Copy the code
Feignclient adds a new interface
@GetMapping("/qrcode")
Response getQrCode(a);
Copy the code
For the modification of controller, to display pictures on the front page, the most commonly used is to return a URL to the page, but these are all stored pictures, but every time the verification code and QR code are generated, the server may not store them. So instead of returning a URL address, the return front end Base64 encoding is used for captchas. A QR code may return a byte stream and Base64 image based on HttpServletResponse, produces.
Here, using HttpServletResponse, add the method:
@GetMapping("/qrcode")
public void getQrCode(HttpServletResponse response) {
Response res = fileUploadApiService.getQrCode();
try {
InputStream inputStream = res.body().asInputStream();
response.setContentType(MediaType.IMAGE_PNG_VALUE);
IOUtils.copy(inputStream,response.getOutputStream());
} catch(IOException e) { e.printStackTrace(); }}Copy the code
Browser visit: http://localhost:8011/file/qrcode, and the result
Call passing token
Normally, the system has authentication and authentication function, whether JWT or security, when the external request to service A, is with the token, but this request in service A through Feign call service B, token loss will occur.
The solution is not difficult. When using Feign remote calls, carry the token in the request header. Normally, the token is placed in the request header.
Feign provides a RequestInterceptor that intercepts Feign’s requests by adding a token to the request header. For this part of the code, add it on cloud-OpenFeign-Consumer and cloud-OpenFeign-Provider.
Modify the cloud – openfeign – the provider
Modify the method to make it easier to present the results
@PostMapping(value = "/update")
public String updateOrder(@RequestBody Order order, HttpServletRequest request) {
String token = request.getHeader("token");
return "hello," + order.getName() + "" + "haha! I get a token: " + token;
}
Copy the code
Modify the cloud – openfeign – consumer
Add an interceptor to implement feign.RequestInterceptor
@Component
public class FeignTokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
if (null == getHttpServletRequest()) {
// You can record some logs here
return;
}
// Pass the value corresponding to the obtained Token below
requestTemplate.header("token", getHeaders(getHttpServletRequest()).get("token"));
}
private HttpServletRequest getHttpServletRequest(a) {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (Exception e) {
return null; }}/** * Feign interceptor intercepts the request to get the Token value **@param request
* @return* /
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
returnmap; }}Copy the code
Finally, start the service to start the test, test results: