一、簡介
在微服務中,服務消費者需要請求服務生產者的接口進行消費,可以使用SpringBoot自帶的RestTemplate或者HttpClient實現,但是都過於麻煩。
這時,就可以使用Feign了,它可以幫助我們更加便捷、優雅地調用HTTP API。
本文代碼全部已上傳至我的github,點擊這里獲取。
二、為服務消費者整合Feign
1.復制項目microservice-consumer-movie,並修改為microservice-consumer-movie-feign
2.pom文件添加依賴:
1 <dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-feign</artifactId>
4 </dependency>
3.創建一個Feign接口,並添加@FeignClient注解

1 package cn.sp.client; 2
3 import cn.sp.bean.User; 4 import org.springframework.cloud.netflix.feign.FeignClient; 5 import org.springframework.web.bind.annotation.GetMapping; 6 import org.springframework.web.bind.annotation.PathVariable; 7
8 /**
9 * @author ship 10 * @Description 11 * @Date: 2018-07-17 13:25 12 */
13 @FeignClient(name = "microservice-provider-user") 14 public interface UserFeignClient { 15
16 @GetMapping("/{id}") 17 User findById(@PathVariable("id") Long id); 18 }
@FeignClient注解中的microservice-provider-user是一個任意的客戶端名稱,用於創建Ribbon負載均衡器。
再這里,由於使用了Eureka,所以Ribbon會把microservice-provider-user解析為Eureka Server服務注冊表中的服務。
4.Controller層代碼
1 /**
2 * 請求用戶微服務的API 3 * Created by 2YSP on 2018/7/8. 4 */
5 @RestController 6 public class MovieController { 7
8 @Autowired 9 private UserFeignClient userFeignClient; 10
11 @GetMapping("/user/{id}") 12 public User findById(@PathVariable Long id){ 13 return userFeignClient.findById(id); 14 } 15 }
5.修改啟動類,添加@EnableFeignClients注解
1 /**
2 * 使用Feign進行聲明式的REST Full API調用 3 */
4 @EnableFeignClients(basePackages = {"cn.sp.client"}) 5 @EnableEurekaClient 6 @SpringBootApplication 7 public class MicroserviceConsumerMovieFeignApplication { 8
9 public static void main(String[] args) { 10 SpringApplication.run(MicroserviceConsumerMovieFeignApplication.class, args); 11 } 12 }
這里我添加了basePackages屬性指定掃描的包,開始沒添加報錯了。
這樣,電影微服務就可以調用用戶微服務的API了。
1.啟動microservice-discovery-eureka
2.啟動生產者microservice-provider-user
3.啟動microservice-consumer-movie-feign
4.訪問http://localhost:8010/user/1獲得返回數據。
三、手動創建Feign
1.復制microservice-provider-user並修改artifactId為microservice-provider-user-with-auth
2.添加依賴
1 <dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-security</artifactId>
4 </dependency>
3.代碼部分

1 package cn.sp.conf; 2
3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 7 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 8 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 import org.springframework.security.core.GrantedAuthority; 12 import org.springframework.security.core.authority.SimpleGrantedAuthority; 13 import org.springframework.security.core.userdetails.UserDetails; 14 import org.springframework.security.core.userdetails.UserDetailsService; 15 import org.springframework.security.core.userdetails.UsernameNotFoundException; 16 import org.springframework.security.crypto.password.NoOpPasswordEncoder; 17 import org.springframework.security.crypto.password.PasswordEncoder; 18 import org.springframework.stereotype.Component; 19
20 import java.util.ArrayList; 21 import java.util.Collection; 22 import java.util.List; 23
24 /**
25 * @author ship 26 * @Description 27 * @Date: 2018-07-18 09:51 28 */
29 @Configuration 30 @EnableWebSecurity 31 @EnableGlobalMethodSecurity(prePostEnabled = true) 32 public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ 33
34 @Autowired 35 CustomUserDetailService userDetailService; 36
37 @Override 38 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 39 super.configure(auth); 40 auth.userDetailsService(userDetailService).passwordEncoder(this.passwordEncoder()); 41 } 42
43 @Override 44 protected void configure(HttpSecurity http) throws Exception { 45 super.configure(http); 46 //所有請求都需要經過HTTP basic認證
47 http.authorizeRequests().anyRequest().authenticated().and().httpBasic(); 48 } 49
50 @Bean 51 public PasswordEncoder passwordEncoder(){ 52 //明文編碼器,這是一個不做任何操作的密碼編碼器,是Spring提供給我們做明文測試的
53 return NoOpPasswordEncoder.getInstance(); 54 } 55
56 } 57
58 @Component 59 class CustomUserDetailService implements UserDetailsService{ 60 /**
61 * 模擬兩個賬號:用戶名user和用戶名admin 62 * @param username 63 * @return
64 * @throws UsernameNotFoundException 65 */
66 @Override 67 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 68 if ("user".equals(username)){ 69 return new SecurityUser("user","password1","user-role"); 70 }else if ("admin".equals(username)){ 71 return new SecurityUser("admin","password2","admin-role"); 72 } 73 return null; 74 } 75 } 76
77 class SecurityUser implements UserDetails{ 78
79 private Long id; 80 private String username; 81 private String password; 82 private String role; 83
84 public SecurityUser(String username,String password,String role){ 85 this.username = username; 86 this.password = password; 87 this.role = role; 88 } 89
90 public Long getId() { 91 return id; 92 } 93
94 public void setId(Long id) { 95 this.id = id; 96 } 97
98 public void setUsername(String username) { 99 this.username = username; 100 } 101
102 public void setPassword(String password) { 103 this.password = password; 104 } 105
106
107 public String getRole() { 108 return role; 109 } 110
111 public void setRole(String role) { 112 this.role = role; 113 } 114
115 @Override 116 public Collection<? extends GrantedAuthority> getAuthorities() { 117 List<GrantedAuthority> authorities = new ArrayList<>(); 118 SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role); 119 authorities.add(authority); 120 return authorities; 121 } 122
123 @Override 124 public String getPassword() { 125 return this.password; 126 } 127
128 @Override 129 public String getUsername() { 130 return this.username; 131 } 132
133 @Override 134 public boolean isAccountNonExpired() { 135 return true; 136 } 137
138 @Override 139 public boolean isAccountNonLocked() { 140 return true; 141 } 142
143 @Override 144 public boolean isCredentialsNonExpired() { 145 return true; 146 } 147
148 @Override 149 public boolean isEnabled() { 150 return true; 151 } 152 }

1 package cn.sp.controller; 2
3 import cn.sp.bean.User; 4 import cn.sp.service.UserService; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.security.core.GrantedAuthority; 9 import org.springframework.security.core.context.SecurityContextHolder; 10 import org.springframework.security.core.userdetails.UserDetails; 11 import org.springframework.web.bind.annotation.GetMapping; 12 import org.springframework.web.bind.annotation.PathVariable; 13 import org.springframework.web.bind.annotation.RestController; 14
15 import java.util.Collection; 16
17 /**
18 * Created by 2YSP on 2018/7/8. 19 */
20 @RestController 21 public class UserController { 22
23 @Autowired 24 private UserService userService; 25
26 private static final Logger log = LoggerFactory.getLogger(UserController.class); 27
28
29
30
31 @GetMapping("/{id}") 32 public User findById(@PathVariable Long id){ 33 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 34 if (principal instanceof UserDetails){ 35 UserDetails user = (UserDetails) principal; 36 Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); 37 for (GrantedAuthority g : authorities){ 38 //打印用戶當前信息
39 log.info("當前用戶是:{},角色是:{}",user.getUsername(),g.getAuthority()); 40 } 41 }else { 42 //do other things
43 } 44 return userService.findById(id); 45 } 46 }
4.測試修改后的用戶服務
先啟動microservice-discovery-eureka,再啟動microservice-provider-user-with-auth,訪問http://localhost:8000/1,即可彈出一個登錄框,輸入用戶名和密碼(user/password1和admin/password)才能獲取結果。
5.修改電影服務,復制microservice-consumer-movie-feign並改為microservice-consumer-movie-feign-manual
6.去掉Feign接口的@FeignClient注解,去掉啟動類的@EnableFeignClients注解
7.controller層代碼修改如下。

/** * 請求用戶微服務的API * Created by 2YSP on 2018/7/8. */ @Import(FeignClientsConfiguration.class) @RestController public class MovieController { private UserFeignClient userUserFeignClient; private UserFeignClient adminUserFeignClient; @Autowired public MovieController(Decoder decoder, Encoder encoder, Client client, Contract contract){ this.userUserFeignClient = Feign.builder().client(client).decoder(decoder).encoder(encoder) .contract(contract).requestInterceptor(new BasicAuthRequestInterceptor("user","password1")) .target(UserFeignClient.class,"http://microservice-provider-user/"); this.adminUserFeignClient = Feign.builder().client(client).decoder(decoder).encoder(encoder) .contract(contract).requestInterceptor(new BasicAuthRequestInterceptor("admin","password2")) .target(UserFeignClient.class,"http://microservice-provider-user/"); } @GetMapping("/user-user/{id}") public User findByIdUser(@PathVariable Long id){ return userUserFeignClient.findById(id); } @GetMapping("/user-admin/{id}") public User findByIdAdmin(@PathVariable Long id){ return adminUserFeignClient.findById(id); } }
8.啟動microservice-consumer-movie-feign-manual,並訪問http://localhost:8010/user-user/4獲取結果同時看到用戶微服務打印日志。
2018-07-18 14:19:06.320 INFO 14256 --- [nio-8000-exec-1] cn.sp.controller.UserController : 當前用戶是:user,角色是:user-role
訪問http://localhost:8010/user-admin/4打印日志:
2018-07-18 14:20:13.772 INFO 14256 --- [nio-8000-exec-2] cn.sp.controller.UserController : 當前用戶是:admin,角色是:admin-role
四、Feign對繼承的支持
Feign還支持繼承,將一些公共操作弄到父接口,從而簡化開發
比如,先寫一個基礎接口:UserService.java
1 public interface UserService { 2 @RequestMapping(method= RequestMethod.GET,value="/user/{id}") 3 User getUser(@PathVariable("id") long id); 4 }
服務提供者Controller:UserResource.java
1 @RestController 2 public class UserResource implements UserService { 3
4 //...
5 }
服務消費者:UserClient.java
1 @FeignClient("users") 2 public interface UserClient extends UserService { 3 }
雖然很方便但是官方不建議這樣做。
五、Feign對壓縮的支持
Feign還可以對傳輸的數據進行壓縮,只需要在appllication.properties文件添加如下配置即可。
feign.compression.request.enable=true
feign.compression.response.enable=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
六、設置Feign的日志
1.復制項目microservice-consumer-movie-feign,修改為microservice-consumer-movie-feign-logging
2.編寫Feign配置類
1 package cn.sp.conf; 2
3 import feign.Logger; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6
7 /**
8 * Created by 2YSP on 2018/7/18. 9 */
10 @Configuration 11 public class FeignLogConfiguration { 12
13 /**
14 * NONE:不記錄任何日志(默認) 15 * BASIC:僅記錄請求方法、URL、響應狀態代碼以及執行時間 16 * HEADERS:記錄BASIC級別的基礎上,記錄請求和響應的header 17 * FULL:記錄請求和響應的header,body和元數據 18 * @return
19 */
20 @Bean 21 Logger.Level feignLoggerLevel(){ 22 return Logger.Level.FULL; 23 } 24 }
3.修改Feign,使用指定配置類
1 @FeignClient(name = "microservice-provider-user",configuration = FeignLogConfiguration.class) 2 public interface UserFeignClient { 3
4 @GetMapping("/{id}") 5 User findById(@PathVariable("id") Long id); 6 }
4.在application.yml中添加如下內容,設置日志級別,注意:Feign的日志打印只會對DEBUG級別做出響應
5測試:啟動microservice-discovery-eureka,microservice-provider-user和microservice-consumer-movie-feign-logging,訪問http://localhost:8010/user/1,可看到日志結果。
七、使用Feign構造多參數請求
當我們用Get請求多參數的URL的時候,比如:http://microservice-provider-user/get?id=1&username=zhangsan,可能會采取如下的方式
1 @FeignClient(name = "microservice-provider-user") 2 public interface UserFeignClient { 3
4 @RequestMapping(value = "/get",method = RequestMethod.GET) 5 User get0(User user); 6
7 }
然而會報錯,盡管指定了GET方法,Feign仍然會使用POST方法發起請求。
正確處理方式一:使用@RequestParam注解
1 @RequestMapping(value = "/get",method = RequestMethod.GET) 2 User get1(@RequestParam("id") Long id,@RequestParam("username") String username);
但是這種方法也有個缺點,如果參數比較多就要寫很長的參數列表。
正確處理方式二:使用map接收
1 @RequestMapping(value = "/get",method = RequestMethod.GET) 2 User get2(Map<String,Object> map);
處理方式三:如果請求方式沒有限制的話,換成POST方式
1 @RequestMapping(value = "/get",method = RequestMethod.POST) 2 User get3(User user);
排版比較亂敬請見諒,參考資料:SpringCloud與Docker微服務架構實戰。