spring cloud 宣告式rest客戶端feign呼叫遠端http服務
在Spring Cloud Netflix棧中,各個微服務都是以HTTP介面的形式暴露自身服務的,因此在呼叫遠端服務時就必須使用HTTP客戶端。Feign就是Spring Cloud提供的一種宣告式REST客戶端。可以通過Feign訪問呼叫遠端微服務提供的REST介面。現在我們就用Feign來呼叫SERVICE-HELLOWORLD暴露的REST介面,以獲取到“Hello World”資訊。在使用Feign時,Spring Cloud集成了Ribbon和Eureka來提供HTTP客戶端的負載均衡。
下面我們就採用Feign的方式來呼叫服務叢集。
1. 建立Maven工程,加入spring-cloud-starter-feign依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
完整的pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pupeiyuan.springcloud</groupId>
<artifactId>spring-Cloud</artifactId>
<version >0.0.1-SNAPSHOT</version>
</parent>
<artifactId>springcloud-moveServer</artifactId>
<dependencies>
<!-- 單元測試 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 支援springWEB web支援: 1、web mvc; 2、restful; 3、jackjson支援; 4、aop ........ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP依賴模組 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.7</version>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- 通用Mapper -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>RELEASE</version>
</dependency>
<!-- 分頁助手 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
<!-- mysql 資料庫驅動. -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 連線池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!-- jstl -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!-- JSP相關 -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!-- httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>
<!-- spring cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- java編譯外掛 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 這是spring boot devtool plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--fork : 如果沒有該項配置,肯呢個devtools不會起作用,即應用不會restart -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. 建立啟動類,需呀加上@EnableFeignClients註解以使用Feign, 使用@EnableDiscoveryClient開啟服務自動發現
mainApplication.java
package com.pupeiyuan.config;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.client.RestTemplate;
import feign.Logger;
@Configuration
//掃描bean
@ComponentScan(basePackages = "com.pupeiyuan.*")
//不用自動配置資料來源
@EnableDiscoveryClient
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnableFeignClients(basePackages="com.pupeiyuan.feignClient")
public class MainApplication extends SpringBootServletInitializer {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
//相當於xml中的bean標籤 用於呼叫當前方法獲取到指定的物件
@Bean(name="remoteRestTemplate")
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(MainApplication.class);
}
}
3. 新增配置檔案application-test.properties, 使用埠8085, 名字定義為springcloud-moveServer, 並註冊到eureka服務中心
application-test.properties
server.port=8085
server.servlet-path=/
#log4j
#logging.level.org.springframework=DEBUG
#SpringMVC
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
#mybatis && Mapper
mybatis.configuration.mapUnderscoreToCamelCase=true
mapper.mappers=com.karle.tk.TkMapper
mapper.identity=MYSQL
#banner
banner.charset= UTF-8
#jsp
server.jsp-servlet.init-parameters.development=true
# pagehelper properties
pagehelper.offsetAsPageNum=true
pagehelper.rowBoundsWithCount=true
pagehelper.pageSizeZero=true
pagehelper.reasonable=false
pagehelper.params=pageNum=pageHelperStart;pageSize=pageHelperRows;
pagehelper.supportMethodsArguments=false
#mq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
management.security.enabled=false
logging.level.com.pupeiyuan.feignClient.FeignClient2=DEBUG
bootstrap.properties
spring.application.name=moveServer
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.service-id=pringcloud-configServer
spring.cloud.config.profile=test
spring.cloud.config.label=master
eureka.client.serviceUrl.defaultZone=http://root:[email protected]:8000/eureka
eureka.instance.prefer-ip-address=true
4. 定義Feign:一個用@FeignClient註解的介面類,UserFeignClient.java
package com.pupeiyuan.feignClient;
import java.util.List;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.pupeiyuan.bean.NhReportStatusHistory;
@FeignClient("MULTIPLE")
public interface UserFeignClient {
@RequestMapping(value = "/getDate/{id}", method = RequestMethod.GET)
public List<NhReportStatusHistory> dataList(@PathVariable("id") Long id); // 兩個坑:1. @GetMapping不支援 2. @PathVariable得設定value
/*@RequestMapping(value = "/user", method = RequestMethod.POST)
public User postUser(@RequestBody User user);
// 該請求不會成功,只要引數是複雜物件,即使指定了是GET方法,feign依然會以POST方法進行傳送請求。可能是我沒找到相應的註解或使用方法錯誤。
// 如勘誤,請@lilizhou2008 [email protected]
@RequestMapping(value = "/get-user", method = RequestMethod.GET)
public User getUser(User user);*/
}
Spring Cloud應用在啟動時,Feign會掃描標有@FeignClient
註解的介面,生成代理,並註冊到Spring容器中。生成代理時Feign會為每個介面方法建立一個RequetTemplate
物件,該物件封裝了HTTP請求需要的全部資訊,請求引數名、請求方法等資訊都是在這個過程中確定的,Feign的模板化就體現在這裡
5. 定義一個WebController。
注入之前通過@FeignClient定義生成的bean,
package com.pupeiyuan.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.pupeiyuan.bean.NhReportStatusHistory;
import com.pupeiyuan.feignClient.FeignClient2;
import com.pupeiyuan.feignClient.UserFeignClient;
@RestController
public class FeignClientController {
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/movie/{id}")
public List<NhReportStatusHistory> list(@PathVariable Long id) {
return this.userFeignClient.dataList(id);
}
}
6. 被呼叫端的WebController--被代理的方法實現
package com.pupeiyuan.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.github.pagehelper.PageHelper;
import com.pupeiyuan.bean.NhReportStatusHistory;
import com.pupeiyuan.core.DataSourceKey;
import com.pupeiyuan.core.DynamicDataSourceContextHolder;
import com.pupeiyuan.services.NhReportService;
@RestController
@RefreshScope
public class ConfigClientController {
//services層注入
@Autowired NhReportService nhReportService;
@GetMapping("/getDate/{id}")
public List<NhReportStatusHistory> getDate(@PathVariable Long id) {
//引數容器
Map<String, Object> params = new HashMap<String, Object>();
PageHelper.startPage(1, 2);
DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE1);
List<NhReportStatusHistory> findList = nhReportService.findList(params);
return findList;
}
}
7. 啟動Feign應用, 訪問http://localhost:8085/movie/1, 多次重新整理,可以看到和前一章Ribbon裡面的應用一樣, 兩個Hello World服務的輸出交替出現。說明通過Feign訪問服務, Spring Cloud已經預設使用了Ribbon負載均衡。
8. 在Feign中使用Apache HTTP Client
Feign在預設情況下使用的是JDK原生的URLConnection
傳送HTTP請求,沒有連線池,但是對每個地址gwai會保持一個長連線,即利用HTTP的persistence connection
。我們可以用Apache的HTTP Client替換Feign原始的http client, 從而獲取連線池、超時時間等與效能息息相關的控制能力。Spring Cloud從Brixtion.SR5
版本開始支援這種替換,首先在專案中宣告Apache HTTP Client和feign-httpclient
依賴:
<!-- 使用Apache HttpClient替換Feign原生httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${feign-httpclient}</version>
</dependency>
然後在application.properties
中新增:
feign.httpclient.enabled=true
7. Feign的Encoder、Decoder和ErrorDecoder
Feign將方法簽名中方法引數物件序列化為請求引數放到HTTP請求中的過程,是由編碼器(Encoder)完成的。同理,將HTTP響應資料反序列化為Java物件是由解碼器(Decoder)完成的。預設情況下,Feign會將標有@RequestParam
註解的引數轉換成字串新增到URL中,將沒有註解的引數通過Jackson轉換成json放到請求體中。注意,如果在@RequetMapping
中的method
將請求方式指定為POST
,那麼所有未標註解的引數將會被忽略,例如:
@RequestMapping(value = "/group/{groupId}", method = RequestMethod.GET)
void update(@PathVariable("groupId") Integer groupId, @RequestParam("groupName") String groupName, DataObject obj);
此時因為宣告的是GET請求沒有請求體,所以obj
引數就會被忽略。
在Spring Cloud環境下,Feign的Encoder只會用來編碼沒有添加註解的引數。如果你自定義了Encoder, 那麼只有在編碼obj
引數時才會呼叫你的Encoder。對於Decoder, 預設會委託給SpringMVC中的MappingJackson2HttpMessageConverter
類進行解碼。只有當狀態碼不在200 ~ 300之間時ErrorDecoder才會被呼叫。ErrorDecoder的作用是可以根據HTTP響應資訊返回一個異常,該異常可以在呼叫Feign介面的地方被捕獲到。我們目前就通過ErrorDecoder來使Feign介面丟擲業務異常以供呼叫者處理。