RestTemplate的缺点
//并不优雅的代码 restTemplate.getForObject("http://book- service/bsn?sn=" + sn,Book.class);
Feign与OpenFeign
◆Feign是一个开源声明式WebService客户端, 用于简化服务通信;
◆Feign采用“接口+注解”方式开发,屏蔽了网络通信的细节;
◆OpenFeign是SpringCloud对Feign的增强, 支持Spring MVC注解;
一、OpenFeign初体验
(1)创建OpenFeign项目
pom.xml如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <parent> 6 <groupId>org.springframework.boot</groupId> 7 <artifactId>spring-boot-starter-parent</artifactId> 8 <version>2.2.2.RELEASE</version> 9 <relativePath/> <!-- lookup parent from repository --> 10 </parent> 11 <groupId>com.angei</groupId> 12 <artifactId>eureka-client-openfeign</artifactId> 13 <version>0.0.1-SNAPSHOT</version> 14 <name>eureka-client-openfeign</name> 15 <description>Demo project for Spring Boot</description> 16 17 <properties> 18 <java.version>1.8</java.version> 19 <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version> 20 </properties> 21 22 <dependencies> 23 <dependency> 24 <groupId>org.springframework.boot</groupId> 25 <artifactId>spring-boot-starter-web</artifactId> 26 </dependency> 27 <dependency> 28 <groupId>org.springframework.cloud</groupId> 29 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 30 </dependency> 31 <dependency> 32 <groupId>org.springframework.cloud</groupId> 33 <artifactId>spring-cloud-starter-openfeign</artifactId> 34 </dependency> 35 36 <dependency> 37 <groupId>org.springframework.boot</groupId> 38 <artifactId>spring-boot-starter-test</artifactId> 39 <scope>test</scope> 40 <exclusions> 41 <exclusion> 42 <groupId>org.junit.vintage</groupId> 43 <artifactId>junit-vintage-engine</artifactId> 44 </exclusion> 45 </exclusions> 46 </dependency> 47 </dependencies> 48 49 <dependencyManagement> 50 <dependencies> 51 <dependency> 52 <groupId>org.springframework.cloud</groupId> 53 <artifactId>spring-cloud-dependencies</artifactId> 54 <version>${spring-cloud.version}</version> 55 <type>pom</type> 56 <scope>import</scope> 57 </dependency> 58 </dependencies> 59 </dependencyManagement> 60 61 <build> 62 <plugins> 63 <plugin> 64 <groupId>org.springframework.boot</groupId> 65 <artifactId>spring-boot-maven-plugin</artifactId> 66 </plugin> 67 </plugins> 68 </build> 69 70 </project>
(2)通过添加@EnableFeignClients的方式开启OpenFeign:
1 import org.springframework.boot.SpringApplication; 2 import org.springframework.boot.autoconfigure.SpringBootApplication; 3 import org.springframework.cloud.openfeign.EnableFeignClients; 4 5 @SpringBootApplication 6 @EnableFeignClients //启用OpenFeign 7 public class EurekaClientOpenfeignApplication { 8 9 public static void main(String[] args) { 10 SpringApplication.run(EurekaClientOpenfeignApplication.class, args); 11 } 12 13 }
(3)配置application.yml:
spring: application: name: eureka-client-openfeign eureka: client: service-url: defaultZone: http://admin:admin@server1:8761/eureka/, http://admin:admin@server2:8762/eureka/, http://admin:admin@server3:8763/eureka/
(4)创建实体类Book:
1 public class Book { 2 3 private String sn; 4 private String name; 5 private String desc; 6 7 public Book() { 8 } 9 10 public Book(String sn, String name, String desc) { 11 this.sn = sn; 12 this.name = name; 13 this.desc = desc; 14 } 15 16 public String getSn() { 17 return sn; 18 } 19 20 public void setSn(String sn) { 21 this.sn = sn; 22 } 23 24 public String getName() { 25 return name; 26 } 27 28 public void setName(String name) { 29 this.name = name; 30 } 31 32 public String getDesc() { 33 return desc; 34 } 35 36 public void setDesc(String desc) { 37 this.desc = desc; 38 } 39 40 public String toString() { 41 return "编号:" + this.sn + "\t书名:" + this.name + "\t描述信息:" + this.desc; 42 } 43 }
(5)相比于使用RestTemplate+@loadBalanced的组合时,通过getForObject()方法中配置url = http://eureka-provider/bsn?sn="+sn的方式获取另一个微服务的信息。
注意对比下面OpenFeign的使用方式:
项目结构图:
创建MemberService接口:
1 import com.angei.eurekaclientopenfeign.entity.Book; 2 import org.springframework.cloud.openfeign.FeignClient; 3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.RequestParam; 5 6 //使用RestTemplate+@loadBalanced时:http://eureka-provider/bsn?sn="+sn 7 @FeignClient(name="eureka-provider") 8 public interface MemberService { 9 10 @GetMapping("/bsn") 11 public Book findBySn(@RequestParam("sn") String sn); 12 }
创建MemberController:
1 import com.angei.eurekaclientopenfeign.Service.MemberService; 2 import com.angei.eurekaclientopenfeign.entity.Book; 3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.RestController; 5 6 import javax.annotation.Resource; 7 8 @RestController 9 public class MemberController { 10 11 @Resource 12 private MemberService service; 13 14 @GetMapping("/borrow") 15 public String borrowBook(String sn) { 16 Book book = service.findBySn(sn); 17 return "【" + book.getName() + "】" + "借阅成功! 服务窗口:" + book.getDesc(); 18 } 19 }
配置服务端口号(10000):
效果展示:
调用Eureka-client-openFeign的相关API:
二、OpenFeign的工作原理及其负载均衡策略
OpenFeign提供了许多负载均衡策略,其中一部分列举如下:
全局配置如下:
1 import com.netflix.loadbalancer.IRule; 2 import com.netflix.loadbalancer.RandomRule; 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cloud.openfeign.EnableFeignClients; 6 import org.springframework.context.annotation.Bean; 7 8 @SpringBootApplication 9 @EnableFeignClients //启用OpenFeign 10 public class EurekaClientOpenfeignApplication { 11 12 @Bean 13 public IRule ribbonRule(){ 14 return new RandomRule(); 15 } 16 17 public static void main(String[] args) { 18 19 SpringApplication.run(EurekaClientOpenfeignApplication.class, args); 20 } 21 22 }
三、OpenFeign通信日志
OpenFeign开启通信日志
◆基于SpringBoot的logback输出,默认为debug级别;
◆设置项: 在配置文件中通过feign.client.config.微服务id.loggerLevel进行配置;
◆微服务id: 若为default代表全局默认配置。
附: 输出级别:日志可以以何种级别输出: logger.trace("=====trace====="); logger.debug("=====debug====="); logger.info("=====info====="); logger.warn("=====warn====="); logger.error("=====error====="); 日志输出级别: TRACE > DEBUG > INFO > WARN > ERROR。
通信日志输出格式
◆NONE:不输出任何通信日志;
◆BASIC:只包含URL、请求方法、状态码、执行时间;
◆HEADERS:在BASIC基础上,额外包含请求与响应头;
◆FULL:包含请求与响应内容最完整的信息;
推荐:在测试的时候使用FULL,但是在线上的时候使用BASIC或者HEADERS即可。
四、替换OpenFeign通信组件
OpenFeign通信组件
◆OpenFeign基于JDK原生URLConnection提供Http通信;
◆OpenFeign支持Apache HttpClient与Square OkHttp;
◆SpringCloud按条件自动加载应用通信组件;
应用条件
◆Maven引入feign-okhttp或者feign-httpclient依赖;
◆设置feign.[httpclient|okhttp].enabled=true;
4.1 使用OkHttp
(1)添加feign-okhttp的依赖
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-okhttp --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
(2)在application.yml进行相关的配置
feign: okhttp: enabled: true
4.2 使用HttpClient
(1)添加feign-httpclient的依赖
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
(2)在application.yml进行相关的配置
1 spring: 2 application: 3 name: eureka-client-openfeign 4 eureka: 5 client: 6 service-url: 7 defaultZone: 8 http://admin:admin@server1:8761/eureka/, http://admin:admin@server2:8762/eureka/, http://admin:admin@server3:8763/eureka/ 9 feign: 10 okhttp: 11 enabled: false 12 httpclient: 13 enabled: true
注意:okhttp和httpClient两者不可兼得!
五、OpenFeign多参数传递
◆ POST方式传递对象使用@RequestBody注解描述参数;
◆ GET方式将对象转换为Map后,利用@RequestParam注解描述;
微服务Eureka-provider的BookController代码更新至如下:
1 import com.angei.eurekaprovider.entity.Book; 2 import org.springframework.web.bind.annotation.GetMapping; 3 import org.springframework.web.bind.annotation.PostMapping; 4 import org.springframework.web.bind.annotation.RequestBody; 5 import org.springframework.web.bind.annotation.RestController; 6 7 import javax.servlet.http.HttpServletRequest; 8 import java.util.ArrayList; 9 import java.util.List; 10 11 @RestController 12 public class BookController { 13 14 15 @GetMapping("/bsn") 16 public Book findBookBySN(String sn, HttpServletRequest request) { 17 Book book = null; 18 if ("111".equals(sn)) { 19 book = new Book("111", "图书1", "服务器端口:" + request.getLocalPort()); 20 } else if ("222".equals(sn)) { 21 book = new Book("222", "图书2", "服务器端口:" + request.getLocalPort()); 22 } else if ("333".equals(sn)) { 23 book = new Book("333", "图书3", "服务器端口:" + request.getLocalPort()); 24 } 25 return book; 26 27 } 28 29 @GetMapping("/get") 30 public List<Book> searchBook(Book book) { 31 //创建结果集对象 32 List ans = new ArrayList<Book>(); 33 if ("08".equals(book.getSn()) && "漫画类".equals(book.getDesc())) { 34 ans.add(new Book("008", "哆啦A梦", "漫画类")); 35 ans.add(new Book("008", "钢铁侠", "漫画类")); 36 ans.add(new Book("008", "千与千寻", "漫画类")); 37 } 38 return ans; 39 } 40 41 @PostMapping("/post") 42 public String createBook(@RequestBody Book book) { 43 return book.getName() + "添加成功!"; 44 } 45 }
5.1 使用Get方式进行多参数传递
微服务Eureka-client的MemberService代码:
1 import com.angei.eurekaclientopenfeign.entity.Book; 2 import org.springframework.cloud.openfeign.FeignClient; 3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.PostMapping; 5 import org.springframework.web.bind.annotation.RequestBody; 6 import org.springframework.web.bind.annotation.RequestParam; 7 8 import java.util.List; 9 import java.util.Map; 10 11 //http://eureka-provider/bsn?sn="+sn 12 @FeignClient(name = "eureka-provider") 13 public interface MemberService { 14 15 @GetMapping("/bsn") 16 public Book findBySn(@RequestParam("sn") String sn); 17 18 @GetMapping("/get") 19 public List<Book> searchBook(@RequestParam Map book); 20 21 }
注意:上面的正确写法就是Map book,因为get请求对Book类型无法识别,所以折中方案就是Map。
微服务Eureka-client的MemberController代码:
1 import com.angei.eurekaclientopenfeign.Service.MemberService; 2 import com.angei.eurekaclientopenfeign.entity.Book; 3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.PostMapping; 5 import org.springframework.web.bind.annotation.RestController; 6 import javax.annotation.Resource; 7 import java.util.HashMap; 8 import java.util.List; 9 import java.util.Map; 10 11 @RestController 12 public class MemberController { 13 14 @Resource 15 private MemberService service; 16 17 @GetMapping("/borrow") 18 public String borrowBook(String sn) { 19 Book book = service.findBySn(sn); 20 return "【" + book.getName() + "】" + "借阅成功! 服务窗口:" + book.getDesc(); 21 } 22 23 @GetMapping("/get") 24 public List<Book> searchBook() { 25 Map param = new HashMap<>(); 26 param.put("sn", "08");//注意:这里的Key值必须和对应实体类的属性名相同 27 param.put("desc", "漫画类");//注意:这里的Key值必须和对应实体类的属性名相同 28 List<Book> ans = service.searchBook(param); 29 return ans; 30 } 31 }
效果显示:
5.2 使用Post方式进行多参数传递
微服务Eureka-client的MemberService代码:
1 import com.angei.eurekaclientopenfeign.entity.Book; 2 import org.springframework.cloud.openfeign.FeignClient; 3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.PostMapping; 5 import org.springframework.web.bind.annotation.RequestBody; 6 import org.springframework.web.bind.annotation.RequestParam; 7 8 import java.util.List; 9 import java.util.Map; 10 11 //http://eureka-provider/bsn?sn="+sn 12 @FeignClient(name = "eureka-provider") 13 public interface MemberService { 14 15 @GetMapping("/bsn") 16 public Book findBySn(@RequestParam("sn") String sn); 17 18 @PostMapping("/post") 19 public String createBook(@RequestBody Book book); 20 21 }
微服务Eureka-client的MemberController代码:
import com.angei.eurekaclientopenfeign.Service.MemberService; import com.angei.eurekaclientopenfeign.entity.Book; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController public class MemberController { @Resource private MemberService service; @GetMapping("/borrow") public String borrowBook(String sn) { Book book = service.findBySn(sn); return "【" + book.getName() + "】" + "借阅成功! 服务窗口:" + book.getDesc(); } @PostMapping("/post") public String createBook() { Book b = new Book(); b.setSn("100"); b.setName("《一千零一夜》"); b.setDesc("经典巨作"); String result = service.createBook(b); return result; } }
效果显示: